Subversion Repositories Games.Descent

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 pmbaty 1
/*
2
 * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
3
 * It is copyright by its individual contributors, as recorded in the
4
 * project's Git history.  See COPYING.txt at the top level for license
5
 * terms and a link to the Git history.
6
 */
7
/*
8
 * DXX Rebirth "jukebox" code
9
 * MD 2211 <md2211@users.sourceforge.net>, 2007
10
 */
11
 
12
#include <stdlib.h>
13
#include <stdio.h>
14
#include <string.h>
15
 
16
#include "hudmsg.h"
17
#include "songs.h"
18
#include "jukebox.h"
19
#include "dxxerror.h"
20
#include "console.h"
21
#include "config.h"
22
#include "strutil.h"
23
#include "u_mem.h"
24
#include "physfs_list.h"
25
#include "digi.h"
26
 
27
#include "partial_range.h"
28
#include <memory>
29
 
30
namespace dcx {
31
 
32
extern bool isDirectory(const char *fname); // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
33
 
34
#define MUSIC_HUDMSG_MAXLEN 40
35
#define JUKEBOX_HUDMSG_PLAYING "Now playing:"
36
#define JUKEBOX_HUDMSG_STOPPED "Jukebox stopped"
37
 
38
namespace {
39
 
40
struct m3u_bytes
41
{
42
        using range_type = partial_range_t<char *>;
43
        using ptr_range_type = partial_range_t<char **>;
44
        using alloc_type = std::unique_ptr<char *[]>;
45
        range_type range = {nullptr, nullptr};
46
        ptr_range_type ptr_range = {nullptr, nullptr};
47
        alloc_type alloc;
48
        m3u_bytes() = default;
49
        m3u_bytes(m3u_bytes &&) = default;
50
        m3u_bytes(range_type &&r, ptr_range_type &&p, alloc_type &&b) :
51
                range(std::move(r)),
52
                ptr_range(std::move(p)),
53
                alloc(std::move(b))
54
        {
55
        }
56
};
57
 
58
class FILE_deleter
59
{
60
public:
61
        void operator()(FILE *const p) const
62
        {
63
                fclose(p);
64
        }
65
};
66
 
67
class list_deleter : PHYSFS_list_deleter
68
{
69
public:
70
        /* When `list_pointers` is a PHYSFS allocation, `buf` is nullptr.
71
         * When `list_pointers` is a new[char *[]] allocation, `buf`
72
         * points to the same location as `list_pointers`.
73
         */
74
        std::unique_ptr<char *[]> buf;
75
        void operator()(char **list)
76
        {
77
                if (buf)
78
                {
79
                        assert(buf.get() == list);
80
                        buf.reset();
81
                }
82
                else
83
                        this->PHYSFS_list_deleter::operator()(list);
84
        }
85
};
86
 
87
class list_pointers : public PHYSFSX_uncounted_list_template<list_deleter>
88
{
89
        typedef PHYSFSX_uncounted_list_template<list_deleter> base_ptr;
90
public:
91
        using base_ptr::reset;
92
        void set_combined(std::unique_ptr<char *[]> &&buf)
93
                noexcept(
94
                        noexcept(std::declval<base_ptr>().reset(buf.get())) &&
95
                        noexcept(std::declval<list_deleter>().buf = std::move(buf))
96
                )
97
        {
98
                this->base_ptr::reset(buf.get());
99
                get_deleter().buf = std::move(buf);
100
        }
101
        void reset(PHYSFSX_uncounted_list list)
102
                noexcept(noexcept(std::declval<base_ptr>().reset(list.release())))
103
        {
104
                this->base_ptr::reset(list.release());
105
        }
106
};
107
 
108
class jukebox_songs
109
{
110
public:
111
        void unload();
112
        list_pointers list;     // the actual list
113
        unsigned num_songs;     // number of jukebox songs
114
        static const std::size_t max_songs = 1024;      // maximum number of pointers that 'list' can hold, i.e. size of list / size of one pointer
115
};
116
 
117
}
118
 
119
static jukebox_songs JukeboxSongs;
120
 
121
void jukebox_songs::unload()
122
{
123
        num_songs = 0;
124
        list.reset();
125
}
126
 
127
void jukebox_unload()
128
{
129
        JukeboxSongs.unload();
130
}
131
 
132
const std::array<file_extension_t, 5> jukebox_exts{{
133
        SONG_EXT_HMP,
134
        SONG_EXT_MID,
135
        SONG_EXT_OGG,
136
        SONG_EXT_FLAC,
137
        SONG_EXT_MP3
138
}};
139
 
140
/* Open an m3u using fopen, not PHYSFS.  If the path seems to be under
141
 * PHYSFS, that will be preferred over a raw filesystem path.
142
 */
143
static std::unique_ptr<FILE, FILE_deleter> open_m3u_from_disk(const char *const cfgpath)
144
{
145
        std::array<char, PATH_MAX> absbuf;
146
        return std::unique_ptr<FILE, FILE_deleter>(fopen(
147
        // it's a child of Sharepath, build full path
148
                (PHYSFSX_exists(cfgpath, 0)
149
                        ? (PHYSFSX_getRealPath(cfgpath, absbuf), absbuf.data())
150
                        : cfgpath), "rb")
151
        );
152
}
153
 
154
static m3u_bytes read_m3u_bytes_from_disk(const char *const cfgpath)
155
{
156
        const auto &&f = open_m3u_from_disk(cfgpath);
157
        if (!f)
158
                return {};
159
        const auto fp = f.get();
160
        fseek(fp, -1, SEEK_END);
161
        const std::size_t length = ftell(fp) + 1;
162
        const auto juke_max_songs = JukeboxSongs.max_songs;
163
        if (length >= PATH_MAX * juke_max_songs)
164
                return {};
165
        fseek(fp, 0, SEEK_SET);
166
        /* A file consisting only of single character records and newline
167
         * separators, with no junk newlines, comments, or final terminator,
168
         * will need one pointer per two bytes of file, rounded up.  Any
169
         * file that uses longer records, which most will use, will need
170
         * fewer pointers.  This expression usually overestimates, sometimes
171
         * substantially.  However, it is still more conservative than the
172
         * previous expression, which was to allocate exactly
173
         * `JukeboxSongs.max_songs` pointers without regard to the file size
174
         * or contents.
175
         */
176
        const auto required_alloc_size = 1 + (length / 2);
177
        const auto max_songs = std::min(required_alloc_size, juke_max_songs);
178
        /* Use T=`char*[]` to ensure alignment.  Place pointers before file
179
         * contents to keep the pointer array aligned.
180
         */
181
        auto &&list_buf = std::make_unique<char*[]>(max_songs + 1 + (length / sizeof(char *)));
182
        const auto p = reinterpret_cast<char *>(list_buf.get() + max_songs);
183
        p[length] = '\0';       // make sure the last string is terminated
184
        return fread(p, length, 1, fp)
185
                ? m3u_bytes(
186
                        unchecked_partial_range(p, length),
187
                        unchecked_partial_range(list_buf.get(), max_songs),
188
                        std::move(list_buf)
189
                )
190
                : m3u_bytes();
191
}
192
 
193
static int read_m3u(void)
194
{
195
        auto &&m3u = read_m3u_bytes_from_disk(CGameCfg.CMLevelMusicPath.data());
196
        auto &list_buf = m3u.alloc;
197
        if (!list_buf)
198
                return 0;
199
 
200
        // The growing string list is allocated last, hopefully reducing memory fragmentation when it grows
201
        const auto eol = [](char c) {
202
                return c == '\n' || c == '\r' || !c;
203
        };
204
        JukeboxSongs.list.set_combined(std::move(list_buf));
205
        const auto &range = m3u.range;
206
        auto pp = m3u.ptr_range.begin();
207
        for (auto buf = range.begin(); buf != range.end(); ++buf)
208
        {
209
                for (; buf != range.end() && eol(*buf);)        // find new line - support DOS, Unix and Mac line endings
210
                        buf++;
211
                if (buf == range.end())
212
                        break;
213
                if (*buf != '#')        // ignore comments / extra info
214
                {
215
                        *pp++ = buf;
216
                        if (pp == m3u.ptr_range.end())
217
                                break;
218
                }
219
                for (; buf != range.end(); ++buf)       // find end of line
220
                        if (eol(*buf))
221
                        {
222
                                *buf = 0;
223
                                break;
224
                        }
225
                if (buf == range.end())
226
                        break;
227
        }
228
        JukeboxSongs.num_songs = std::distance(m3u.ptr_range.begin(), pp);
229
        return 1;
230
}
231
 
232
/* Loads music file names from a given directory or M3U playlist */
233
void jukebox_load()
234
{
235
        jukebox_unload();
236
 
237
        // Check if it's an M3U file
238
        auto &cfgpath = CGameCfg.CMLevelMusicPath;
239
        size_t musiclen = strlen(cfgpath.data());
240
        if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u"))
241
                read_m3u();
242
        else    // a directory
243
        {
244
                class PHYSFS_path_deleter
245
                {
246
                public:
247
                        void operator()(const char *const p) const noexcept
248
                        {
249
                                PHYSFS_unmount(p); // Pierre-Marie Baty -- work around PHYSFS_removeFromSearchPath() deprecation
250
                        }
251
                };
252
                std::unique_ptr<const char, PHYSFS_path_deleter> new_path;
253
                const char *sep = PHYSFS_getDirSeparator();
254
                size_t seplen = strlen(sep);
255
 
256
                // stick a separator on the end if necessary.
257
                if (musiclen >= seplen)
258
                {
259
                        auto p = &cfgpath[musiclen - seplen];
260
                        if (strcmp(p, sep))
261
                                cfgpath.copy_if(musiclen, sep, seplen);
262
                }
263
 
264
                const auto p = cfgpath.data();
265
                // Read directory using PhysicsFS
266
                if (/*PHYSFS_*/isDirectory(p))  // find files in relative directory // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
267
                        JukeboxSongs.list.reset(PHYSFSX_findFiles(p, jukebox_exts));
268
                else
269
                {
270
                        if (PHYSFSX_isNewPath(p))
271
                                new_path.reset(p);
272
                        PHYSFS_mount(p, NULL, 0); // Pierre-Marie Baty -- work around PHYSFS_addToSearchPath() deprecation
273
 
274
                        // as mountpoints are no option (yet), make sure only files originating from GameCfg.CMLevelMusicPath are aded to the list.
275
                        JukeboxSongs.list.reset(PHYSFSX_findabsoluteFiles("", p, jukebox_exts));
276
                }
277
 
278
                if (!JukeboxSongs.list)
279
                {
280
                        return;
281
                }
282
                JukeboxSongs.num_songs = std::distance(JukeboxSongs.list.begin(), JukeboxSongs.list.end());
283
        }
284
 
285
        if (JukeboxSongs.num_songs)
286
        {
287
                con_printf(CON_DEBUG,"Jukebox: %d music file(s) found in %s", JukeboxSongs.num_songs, cfgpath.data());
288
                if (CGameCfg.CMLevelMusicTrack[1] != JukeboxSongs.num_songs)
289
                {
290
                        CGameCfg.CMLevelMusicTrack[1] = JukeboxSongs.num_songs;
291
                        CGameCfg.CMLevelMusicTrack[0] = 0; // number of songs changed so start from beginning.
292
                }
293
        }
294
        else
295
        {
296
                CGameCfg.CMLevelMusicTrack[0] = -1;
297
                CGameCfg.CMLevelMusicTrack[1] = -1;
298
                con_puts(CON_DEBUG,"Jukebox music could not be found!");
299
        }
300
}
301
 
302
// To proceed tru our playlist. Usually used for continous play, but can loop as well.
303
static void jukebox_hook_next()
304
{
305
        if (!JukeboxSongs.list || CGameCfg.CMLevelMusicTrack[0] == -1)
306
                return;
307
 
308
        if (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Random)
309
                CGameCfg.CMLevelMusicTrack[0] = d_rand() % CGameCfg.CMLevelMusicTrack[1]; // simply a random selection - no check if this song has already been played. But that's how I roll!
310
        else
311
                CGameCfg.CMLevelMusicTrack[0]++;
312
        if (CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
313
                CGameCfg.CMLevelMusicTrack[0] = 0;
314
 
315
        jukebox_play();
316
}
317
 
318
}
319
 
320
namespace dsx {
321
 
322
// Play tracks from Jukebox directory. Play track specified in GameCfg.CMLevelMusicTrack[0] and loop depending on CGameCfg.CMLevelMusicPlayOrder
323
int jukebox_play()
324
{
325
        const char *music_filename;
326
        uint_fast32_t size_full_filename = 0;
327
 
328
        if (!JukeboxSongs.list)
329
                return 0;
330
 
331
        if (CGameCfg.CMLevelMusicTrack[0] < 0 ||
332
                CGameCfg.CMLevelMusicTrack[0] + 1 > CGameCfg.CMLevelMusicTrack[1])
333
                return 0;
334
 
335
        music_filename = JukeboxSongs.list[CGameCfg.CMLevelMusicTrack[0]];
336
        if (!music_filename)
337
                return 0;
338
 
339
        size_t size_music_filename = strlen(music_filename);
340
        auto &cfgpath = CGameCfg.CMLevelMusicPath;
341
        size_t musiclen = strlen(cfgpath.data());
342
        size_full_filename = musiclen + size_music_filename + 1;
343
        RAIIdmem<char[]> full_filename;
344
        CALLOC(full_filename, char[], size_full_filename);
345
        const char *LevelMusicPath;
346
        if (musiclen > 4 && !d_stricmp(&cfgpath[musiclen - 4], ".m3u")) // if it's from an M3U playlist
347
                LevelMusicPath = "";
348
        else                                                                                    // if it's from a specified path
349
                LevelMusicPath = cfgpath.data();
350
        snprintf(full_filename.get(), size_full_filename, "%s%s", LevelMusicPath, music_filename);
351
 
352
        int played = songs_play_file(full_filename.get(), (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level ? 1 : 0), (CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level ? nullptr : jukebox_hook_next));
353
        full_filename.reset();
354
        if (!played)
355
        {
356
                return 0;       // whoops, got an error
357
        }
358
 
359
        // Formatting a pretty message
360
        const char *prefix = "...";
361
        if (size_music_filename >= MUSIC_HUDMSG_MAXLEN) {
362
                music_filename += size_music_filename - MUSIC_HUDMSG_MAXLEN;
363
        } else {
364
                prefix += 3;
365
        }
366
 
367
        HUD_init_message(HM_DEFAULT, "%s %s%s", JUKEBOX_HUDMSG_PLAYING, prefix, music_filename);
368
 
369
        return 1;
370
}
371
 
372
}