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
 * This is an alternate backend for the music system.
9
 * It uses SDL_mixer to provide a more reliable playback,
10
 * and allow processing of multiple audio formats.
11
 *
12
 *  -- MD2211 (2006-04-24)
13
 */
14
 
15
#include <SDL.h>
16
#include <SDL_mixer.h>
17
#include <string.h>
18
#include <stdlib.h>
19
#include <unistd.h> // Pierre-Marie Baty -- for getuid()
20
#include <pwd.h> // Pierre-Marie Baty -- for getpwuid()
21
 
22
#include "args.h"
23
#include "hmp.h"
24
#include "adlmidi_dynamic.h"
25
#include "digi_mixer_music.h"
26
#include "strutil.h"
27
#include "u_mem.h"
28
#include "config.h"
29
#include "console.h"
30
#include "physfsrwops.h"
31
 
32
namespace dcx {
33
 
34
namespace {
35
 
36
#if SDL_MIXER_MAJOR_VERSION == 2
37
#define DXX_SDL_MIXER_MANAGES_RWOPS     1
38
#else
39
#define DXX_SDL_MIXER_MANAGES_RWOPS     0
40
#endif
41
 
42
#if DXX_SDL_MIXER_MANAGES_RWOPS
43
#define DXX_SDL_MIXER_Mix_LoadMUS_MANAGE_RWOPS  , SDL_TRUE
44
#define DXX_SDL_MIXER_Mix_LoadMUS_PASS_RWOPS(rw)
45
#else
46
#define DXX_SDL_MIXER_Mix_LoadMUS_MANAGE_RWOPS
47
#define DXX_SDL_MIXER_Mix_LoadMUS_PASS_RWOPS(rw)        , rw
48
#endif
49
 
50
struct Music_delete
51
{
52
        void operator()(Mix_Music *m) const
53
        {
54
                Mix_FreeMusic(m);
55
        }
56
};
57
 
58
class current_music_t : std::unique_ptr<Mix_Music, Music_delete>
59
{
60
#if !DXX_SDL_MIXER_MANAGES_RWOPS
61
        using rwops_pointer = std::unique_ptr<SDL_RWops, RWops_delete>;
62
        rwops_pointer m_ops;
63
#endif
64
        using music_pointer = std::unique_ptr<Mix_Music, Music_delete>;
65
public:
66
#if DXX_SDL_MIXER_MANAGES_RWOPS
67
        using music_pointer::reset;
68
#else
69
        void reset(
70
                Mix_Music *const music = nullptr
71
                , SDL_RWops *const rwops = nullptr
72
                ) noexcept
73
        {
74
                /* Clear music first in case it needs the old ops
75
                 * Clear old ops
76
                 * If no new music, clear new ops immediately.  This only
77
                 * happens if the new music fails to load.
78
                 */
79
                this->music_pointer::reset(music);
80
                m_ops.reset(rwops);
81
                if (!music)
82
                        m_ops.reset();
83
        }
84
#endif
85
        using music_pointer::operator bool;
86
        using music_pointer::get;
87
};
88
 
89
}
90
 
91
static current_music_t current_music;
92
static std::vector<uint8_t> current_music_hndlbuf;
93
 
94
#if DXX_USE_ADLMIDI
95
static ADL_MIDIPlayer_t current_adlmidi;
96
static ADL_MIDIPlayer *get_adlmidi()
97
{
98
        if (!CGameCfg.ADLMIDI_enabled)
99
                return nullptr;
100
        ADL_MIDIPlayer *adlmidi = current_adlmidi.get();
101
        if (!adlmidi)
102
        {
103
                int sample_rate;
104
                Mix_QuerySpec(&sample_rate, nullptr, nullptr);
105
                adlmidi = adl_init(sample_rate);
106
                if (adlmidi)
107
                {
108
                        adl_switchEmulator(adlmidi, ADLMIDI_EMU_DOSBOX);
109
                        adl_setNumChips(adlmidi, CGameCfg.ADLMIDI_num_chips);
110
                        adl_setBank(adlmidi, CGameCfg.ADLMIDI_bank);
111
                        adl_setSoftPanEnabled(adlmidi, 1);
112
                        current_adlmidi.reset(adlmidi);
113
                }
114
        }
115
        return adlmidi;
116
}
117
 
118
static void mix_adlmidi(void *udata, Uint8 *stream, int len);
119
#endif
120
 
121
enum class CurrentMusicType
122
{
123
        None,
124
#if DXX_USE_ADLMIDI
125
        ADLMIDI,
126
#endif
127
        SDLMixer,
128
};
129
 
130
static CurrentMusicType current_music_type = CurrentMusicType::None;
131
 
132
static CurrentMusicType load_mus_data(const uint8_t *data, size_t size);
133
static CurrentMusicType load_mus_file(const char *filename);
134
 
135
/*
136
 *  Plays a music file from an absolute path or a relative path
137
 */
138
 
139
int mix_play_file(const char *filename, int loop, void (*hook_finished_track)())
140
{
141
        std::array<char, PATH_MAX> full_path;
142
        const char *fptr;
143
        unsigned int bufsize = 0;
144
 
145
        mix_free_music();       // stop and free what we're already playing, if anything
146
 
147
        fptr = strrchr(filename, '.');
148
 
149
        if (fptr == NULL)
150
                return 0;
151
 
152
        // It's a .hmp!
153
        if (!d_stricmp(fptr, ".hmp"))
154
        {
155
                hmp2mid(filename, current_music_hndlbuf);
156
                current_music_type = load_mus_data(current_music_hndlbuf.data(), current_music_hndlbuf.size());
157
        }
158
 
159
        // try loading music via given filename
160
        if (current_music_type == CurrentMusicType::None)
161
                current_music_type = load_mus_file(filename);
162
 
163
        // allow the shell convention tilde character to mean the user's home folder
164
        // chiefly used for default jukebox level song music referenced in 'descent.m3u' for Mac OS X
165
        if (current_music_type == CurrentMusicType::None && *filename == '~')
166
        {
167
                const auto sep = PHYSFS_getDirSeparator();
168
                const auto lensep = strlen(sep);
169
                snprintf(full_path.data(), PATH_MAX, "%s/%s", /*PHYSFS_getUserDir()*/getpwuid(getuid())->pw_dir, // Pierre-Marie Baty -- work around PHYSFS_getUserDir() deprecation
170
                                 &filename[1 + (!strncmp(&filename[1], sep, lensep)
171
                        ? lensep
172
                        : 0)]);
173
                current_music_type = load_mus_file(full_path.data());
174
                if (current_music_type != CurrentMusicType::None)
175
                        filename = full_path.data();    // used later for possible error reporting
176
        }
177
 
178
        // no luck. so it might be in Searchpath. So try to build absolute path
179
        if (current_music_type == CurrentMusicType::None)
180
        {
181
                PHYSFSX_getRealPath(filename, full_path);
182
                current_music_type = load_mus_file(full_path.data());
183
                if (current_music_type != CurrentMusicType::None)
184
                        filename = full_path.data();    // used later for possible error reporting
185
        }
186
 
187
        // still nothin'? Let's open via PhysFS in case it's located inside an archive
188
        if (current_music_type == CurrentMusicType::None)
189
        {
190
                if (RAIIPHYSFS_File filehandle{PHYSFS_openRead(filename)})
191
                {
192
                        unsigned len = PHYSFS_fileLength(filehandle);
193
                        current_music_hndlbuf.resize(len);
194
                        bufsize = PHYSFS_read(filehandle, &current_music_hndlbuf[0], sizeof(char), len);
195
                        current_music_type = load_mus_data(current_music_hndlbuf.data(), bufsize*sizeof(char));
196
                }
197
        }
198
 
199
        switch (current_music_type)
200
        {
201
 
202
#if DXX_USE_ADLMIDI
203
        case CurrentMusicType::ADLMIDI:
204
        {
205
                ADL_MIDIPlayer *adlmidi = get_adlmidi();
206
                adl_setLoopEnabled(adlmidi, loop);
207
                Mix_HookMusic(&mix_adlmidi, nullptr);
208
                Mix_HookMusicFinished(hook_finished_track ? hook_finished_track : mix_free_music);
209
                return 1;
210
        }
211
#endif
212
 
213
        case CurrentMusicType::SDLMixer:
214
        {
215
                Mix_PlayMusic(current_music.get(), (loop ? -1 : 1));
216
                Mix_HookMusicFinished(hook_finished_track ? hook_finished_track : mix_free_music);
217
                return 1;
218
        }
219
 
220
        default:
221
        {
222
                con_printf(CON_CRITICAL,"Music %s could not be loaded: %s", filename, Mix_GetError());
223
                mix_stop_music();
224
        }
225
 
226
        }
227
 
228
        return 0;
229
}
230
 
231
// What to do when stopping song playback
232
void mix_free_music()
233
{
234
        Mix_HaltMusic();
235
#if DXX_USE_ADLMIDI
236
        /* Only ADLMIDI can set a hook, so if ADLMIDI is compiled out, there is no
237
         * need to clear the hook.
238
         *
239
         * When ADLMIDI is supported, clear unconditionally, instead of checking
240
         * whether the music type requires it.
241
         */
242
        Mix_HookMusic(nullptr, nullptr);
243
#endif
244
        current_music.reset();
245
        current_music_hndlbuf.clear();
246
        current_music_type = CurrentMusicType::None;
247
}
248
 
249
void mix_set_music_volume(int vol)
250
{
251
        vol *= MIX_MAX_VOLUME/8;
252
        Mix_VolumeMusic(vol);
253
}
254
 
255
void mix_stop_music()
256
{
257
        Mix_HaltMusic();
258
        current_music_hndlbuf.clear();
259
}
260
 
261
void mix_pause_music()
262
{
263
        Mix_PauseMusic();
264
}
265
 
266
void mix_resume_music()
267
{
268
        Mix_ResumeMusic();
269
}
270
 
271
void mix_pause_resume_music()
272
{
273
        if (Mix_PausedMusic())
274
                Mix_ResumeMusic();
275
        else if (Mix_PlayingMusic())
276
                Mix_PauseMusic();
277
}
278
 
279
static CurrentMusicType load_mus_data(const uint8_t *data, size_t size)
280
{
281
        CurrentMusicType type = CurrentMusicType::None;
282
#if DXX_USE_ADLMIDI
283
        const auto adlmidi = get_adlmidi();
284
        if (adlmidi && adl_openData(adlmidi, data, size) == 0)
285
                type = CurrentMusicType::ADLMIDI;
286
        else
287
#endif
288
        {
289
                const auto rw = SDL_RWFromConstMem(data, size);
290
                current_music.reset(Mix_LoadMUS_RW(rw DXX_SDL_MIXER_Mix_LoadMUS_MANAGE_RWOPS) DXX_SDL_MIXER_Mix_LoadMUS_PASS_RWOPS(rw));
291
                if (current_music)
292
                        type = CurrentMusicType::SDLMixer;
293
        }
294
 
295
        return type;
296
}
297
 
298
static CurrentMusicType load_mus_file(const char *filename)
299
{
300
        CurrentMusicType type = CurrentMusicType::None;
301
#if DXX_USE_ADLMIDI
302
        const auto adlmidi = get_adlmidi();
303
        if (adlmidi && adl_openFile(adlmidi, filename) == 0)
304
                type = CurrentMusicType::ADLMIDI;
305
        else
306
#endif
307
        {
308
                current_music.reset(Mix_LoadMUS(filename));
309
                if (current_music)
310
                        type = CurrentMusicType::SDLMixer;
311
        }
312
 
313
        return type;
314
}
315
 
316
#if DXX_USE_ADLMIDI
317
static int16_t sat16(int32_t x)
318
{
319
        x = (x < INT16_MIN) ? INT16_MIN : x;
320
        x = (x > INT16_MAX) ? INT16_MAX : x;
321
        return x;
322
}
323
 
324
static void mix_adlmidi(void *, Uint8 *stream, int len)
325
{
326
        ADLMIDI_AudioFormat format;
327
        format.containerSize = sizeof(int16_t);
328
        format.sampleOffset = 2 * format.containerSize;
329
        format.type = ADLMIDI_SampleType_S16;
330
 
331
        ADL_MIDIPlayer *adlmidi = get_adlmidi();
332
        int sampleCount = len / format.containerSize;
333
        adl_playFormat(adlmidi, sampleCount, stream, stream + format.containerSize, &format);
334
 
335
        const auto samples = reinterpret_cast<int16_t *>(stream);
336
        const auto amplify = [](int16_t i) { return sat16(2 * i); };
337
        std::transform(samples, samples + sampleCount, samples, amplify);
338
}
339
#endif
340
 
341
}