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, ¤t_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 | } |