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 | } |