diff --git a/src/generic/mako.cpp b/src/generic/mako.cpp index 3bb821b..717e96a 100644 --- a/src/generic/mako.cpp +++ b/src/generic/mako.cpp @@ -20,8 +20,6 @@ namespace { -const int SAMPLE_RATE = 44100; - #ifdef _WIN32 // Per-game mapping from music numbers to CD tracks. This is necessary to // forcibly change the sound device with a menu command. @@ -33,66 +31,86 @@ const int8_t RANCE42_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,1 const int8_t DPSALL_tracks[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,1,2,3,-1}; #endif -// The main output stream, opened together with the audio device via -// SDL_OpenAudioDeviceStream(). Its callback mixes fm/music/pcm together. -SDL_AudioStream* g_stream; -SDL_AudioSpec g_device_spec; +// Wraps a MakoYmfm sample generator together with the SDL_AudioStream bound to +// the audio device. +class FmStream { +public: + FmStream(SDL_AudioDeviceID device, std::vector data) + : FmStream(device, std::move(data), device_format(device)) {} + ~FmStream() { SDL_DestroyAudioStream(stream); } // unbinds and stops fill() + + // FM music keeps playing until every channel has looped. + bool is_playing() { int mark, loop; get_mark(&mark, &loop); return !loop; } + + void get_mark(int* mark, int* loop) { + SDL_LockAudioStream(stream); + ymfm.get_mark(mark, loop); + SDL_UnlockAudioStream(stream); + } + +private: + FmStream(SDL_AudioDeviceID device, std::vector data, const SDL_AudioSpec& device_spec) + : ymfm(device_spec.freq, std::move(data)) + { + SDL_AudioSpec src_spec = { SDL_AUDIO_S16, 2, device_spec.freq }; + stream = SDL_CreateAudioStream(&src_spec, &device_spec); + SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) { + static_cast(self)->fill(additional_amount); + }, this); + SDL_BindAudioStream(device, stream); + } + + static SDL_AudioSpec device_format(SDL_AudioDeviceID device) { + SDL_AudioSpec spec; + SDL_GetAudioDeviceFormat(device, &spec, nullptr); + return spec; + } + + // SDL3 stream get-callback: synthesize `additional_amount` bytes of audio. + void fill(int additional_amount) { + const int CHUNK = 4096; // bytes; 1024 stereo S16 frames + int16_t buffer[CHUNK / 2]; + while (additional_amount > 0) { + int len = additional_amount < CHUNK ? additional_amount : CHUNK; + ymfm.Process(buffer, len / 4); + SDL_PutAudioStreamData(stream, buffer, len); + additional_amount -= len; + } + } + + MakoYmfm ymfm; + SDL_AudioStream* stream; +}; + +// The audio device. Each sound source (music, fm, pcm) creates its own +// SDL_AudioStream and binds it to this device. +SDL_AudioDeviceID g_device; std::unique_ptr music; -std::unique_ptr fm; +std::unique_ptr fm; std::unique_ptr midi; -// PCM (sound effect) playback. pcm_loops is the number of remaining plays, -// or -1 for an infinite loop. +// PCM playback. pcm_loops is the number of remaining plays, or -1 for an +// infinite loop. pcm_stream's get-callback re-feeds pcm_src. SDL_AudioStream* pcm_stream; std::vector pcm_src; int pcm_loops; bool pcm_input_finished; -void mix_pcm(Uint8* out, int len) +// Get-callback for the pcm stream: re-feed the decoded sample buffer until the +// remaining loop count is exhausted (pcm_loops == 0). -1 loops forever. +void SDLCALL pcm_audio_callback(void*, SDL_AudioStream* stream, int additional_amount, int /*total_amount*/) { - while (SDL_GetAudioStreamAvailable(pcm_stream) < len && !pcm_input_finished) { + while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !pcm_input_finished) { if (pcm_loops == 0) { - SDL_FlushAudioStream(pcm_stream); + SDL_FlushAudioStream(stream); pcm_input_finished = true; break; } - SDL_PutAudioStreamData(pcm_stream, pcm_src.data(), static_cast(pcm_src.size())); + SDL_PutAudioStreamData(stream, pcm_src.data(), static_cast(pcm_src.size())); if (pcm_loops > 0) pcm_loops--; } - Uint8* tmp = SDL_stack_alloc(Uint8, len); - int got = SDL_GetAudioStreamData(pcm_stream, tmp, len); - if (got > 0) - SDL_MixAudio(out, tmp, SDL_AUDIO_S16, got, 1.0f); - SDL_stack_free(tmp); -} - -// SDL3 audio callback: rather than handing us a buffer to fill, SDL asks us to -// feed `additional_amount` bytes into the stream via SDL_PutAudioStreamData(). -void SDLCALL audio_callback(void*, SDL_AudioStream* stream, int additional_amount, int /*total_amount*/) -{ - // Mix in bounded chunks so the scratch buffers (and the per-source - // SDL_stack_alloc() below) stay small no matter how much SDL requests. - const int CHUNK = 4096; // bytes; 1024 stereo S16 frames - Uint8 buffer[CHUNK]; - while (additional_amount > 0) { - int len = additional_amount < CHUNK ? additional_amount : CHUNK; - SDL_memset(buffer, 0, len); - if (fm) { - int frames = len / 4; - int16_t* tmp = SDL_stack_alloc(int16_t, frames * 2); - fm->Process(tmp, frames); - SDL_MixAudio(buffer, reinterpret_cast(tmp), SDL_AUDIO_S16, len, 1.0f); - SDL_stack_free(tmp); - } - if (music) - music->mix(buffer, len); - if (pcm_stream) - mix_pcm(buffer, len); - SDL_PutAudioStreamData(stream, buffer, len); - additional_amount -= len; - } } } // namespace @@ -123,19 +141,9 @@ MAKO::MAKO(const Config& config, const GameId& game_id) : cd_track[i] = 0; SDL_InitSubSystem(SDL_INIT_AUDIO); - // g_device_spec is the format our callback produces; the resampling streams - // used by MakoMusic/MakoYmfm/pcm convert their sources into this format. - SDL_zero(g_device_spec); - g_device_spec.freq = SAMPLE_RATE; - g_device_spec.format = SDL_AUDIO_S16; - g_device_spec.channels = 2; - g_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &g_device_spec, audio_callback, nullptr); - if (!g_stream) { - WARNING("SDL_OpenAudioDeviceStream failed: %s", SDL_GetError()); - use_fm = false; - } else { - SDL_ResumeAudioStreamDevice(g_stream); - } + g_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); + if (!g_device) + WARNING("Cannot open audio device: %s", SDL_GetError()); midi = std::make_unique(config.midi_device); if (!midi->is_available()) @@ -148,8 +156,8 @@ MAKO::~MAKO() stop_pcm(); midi.reset(); - if (g_stream) - SDL_DestroyAudioStream(g_stream); // also closes the audio device + if (g_device) + SDL_CloseAudioDevice(g_device); SDL_QuitSubSystem(SDL_INIT_AUDIO); } @@ -194,20 +202,16 @@ void MAKO::play_music(int page) return; file = abspath; #endif - auto m = std::make_unique(file, next_loop, g_device_spec); - if (!m->is_open()) + music = std::make_unique(g_device, file, next_loop); + if (!music->is_open()) { + music.reset(); return; - SDL_LockAudioStream(g_stream); - music = std::move(m); - SDL_UnlockAudioStream(g_stream); + } } else if (use_fm) { std::vector data = amus.load(page); if (data.empty()) return; - auto f = std::make_unique(SAMPLE_RATE, std::move(data)); - SDL_LockAudioStream(g_stream); - fm = std::move(f); - SDL_UnlockAudioStream(g_stream); + fm = std::make_unique(g_device, std::move(data)); } else if (midi->is_available()) { if (!midi->play(game_id, amus, mda, page, next_loop)) return; @@ -218,13 +222,8 @@ void MAKO::play_music(int page) void MAKO::stop_music() { - if (music || fm) { - SDL_LockAudioStream(g_stream); - std::unique_ptr old_music = std::move(music); - std::unique_ptr old_fm = std::move(fm); - SDL_UnlockAudioStream(g_stream); - // old_music/old_fm are destroyed here, outside the lock. - } + music.reset(); + fm.reset(); if (midi->is_available()) midi->stop(); current_music = 0; @@ -232,19 +231,10 @@ void MAKO::stop_music() bool MAKO::check_music() { - SDL_LockAudioStream(g_stream); - if (fm) { - int mark, loop; - fm->get_mark(&mark, &loop); - SDL_UnlockAudioStream(g_stream); - return !loop; - } - if (music) { - bool playing = music->is_playing(); - SDL_UnlockAudioStream(g_stream); - return playing; - } - SDL_UnlockAudioStream(g_stream); + if (fm) + return fm->is_playing(); + if (music) + return music->is_playing(); return midi->is_playing(); } @@ -309,13 +299,10 @@ void MAKO::select_sound(BGMDevice dev) void MAKO::get_mark(int* mark, int* loop) { - SDL_LockAudioStream(g_stream); if (fm) { fm->get_mark(mark, loop); - SDL_UnlockAudioStream(g_stream); return; } - SDL_UnlockAudioStream(g_stream); midi->get_mark(mark, loop); } @@ -323,6 +310,9 @@ void MAKO::play_pcm(int page, int loops) { stop_pcm(); + SDL_AudioSpec device_spec; + SDL_GetAudioDeviceFormat(g_device, &device_spec, nullptr); + SDL_AudioStream* stream = nullptr; std::vector src; @@ -338,7 +328,7 @@ void MAKO::play_pcm(int page, int loops) } src.assign(wav, wav + wavlen); SDL_free(wav); - stream = SDL_CreateAudioStream(&spec, &g_device_spec); + stream = SDL_CreateAudioStream(&spec, &device_spec); } else { // AMSE形式 (乙女戦記) data = amse.load(page); @@ -351,39 +341,42 @@ void MAKO::play_pcm(int page, int loops) src.push_back((data[i] & 0x0f) << 4); } SDL_AudioSpec src_spec = { SDL_AUDIO_U8, 1, 8000 }; - stream = SDL_CreateAudioStream(&src_spec, &g_device_spec); + stream = SDL_CreateAudioStream(&src_spec, &device_spec); } if (!stream) { WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError()); return; } - SDL_LockAudioStream(g_stream); - pcm_stream = stream; + pcm_src = std::move(src); pcm_loops = loops ? loops : -1; pcm_input_finished = false; - SDL_UnlockAudioStream(g_stream); + pcm_stream = stream; + SDL_SetAudioStreamGetCallback(pcm_stream, pcm_audio_callback, nullptr); + SDL_BindAudioStream(g_device, pcm_stream); } void MAKO::stop_pcm() { - SDL_LockAudioStream(g_stream); - SDL_AudioStream* old = pcm_stream; - pcm_stream = nullptr; + // Destroy the stream first (unbinds and stops its get-callback), then it is + // safe to drop the source buffer the callback was reading. + if (pcm_stream) { + SDL_DestroyAudioStream(pcm_stream); + pcm_stream = nullptr; + } pcm_src.clear(); pcm_input_finished = false; - SDL_UnlockAudioStream(g_stream); - if (old) - SDL_DestroyAudioStream(old); } bool MAKO::check_pcm() { // 再生中でtrue - SDL_LockAudioStream(g_stream); + if (!pcm_stream) + return false; + SDL_LockAudioStream(pcm_stream); bool playing = pcm_stream && (!pcm_input_finished || SDL_GetAudioStreamAvailable(pcm_stream) > 0); - SDL_UnlockAudioStream(g_stream); + SDL_UnlockAudioStream(pcm_stream); return playing; } diff --git a/src/sys/mako_music.cpp b/src/sys/mako_music.cpp index aaf7973..8e216ec 100644 --- a/src/sys/mako_music.cpp +++ b/src/sys/mako_music.cpp @@ -171,18 +171,24 @@ std::unique_ptr create_decoder(const std::string& path) } // namespace -MakoMusic::MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec) +MakoMusic::MakoMusic(SDL_AudioDeviceID device, const std::string& path, int loops) : decoder(create_decoder(path)), loops_(loops) { if (!decoder) return; const SDL_AudioSpec& src_spec = decoder->spec(); + SDL_AudioSpec device_spec; + SDL_GetAudioDeviceFormat(device, &device_spec, nullptr); stream = SDL_CreateAudioStream(&src_spec, &device_spec); if (!stream) { WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError()); return; } playing = true; + SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) { + static_cast(self)->AudioCallback(additional_amount); + }, this); + SDL_BindAudioStream(device, stream); } MakoMusic::~MakoMusic() @@ -214,19 +220,21 @@ void MakoMusic::decode() } } -void MakoMusic::mix(Uint8* out, int len) +void MakoMusic::AudioCallback(int additional_amount) { - if (!stream || !playing) + if (!playing) return; - while (SDL_GetAudioStreamAvailable(stream) < len && !input_finished) + while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !input_finished) decode(); - - Uint8* tmp = SDL_stack_alloc(Uint8, len); - int got = SDL_GetAudioStreamData(stream, tmp, len); - if (got > 0) - SDL_MixAudio(out, tmp, SDL_AUDIO_S16, got, 1.0f); - SDL_stack_free(tmp); if (input_finished && SDL_GetAudioStreamAvailable(stream) <= 0) playing = false; } + +bool MakoMusic::is_playing() const +{ + SDL_LockAudioStream(stream); + bool result = playing; + SDL_UnlockAudioStream(stream); + return result; +} diff --git a/src/sys/mako_music.h b/src/sys/mako_music.h index d88be05..71cf269 100644 --- a/src/sys/mako_music.h +++ b/src/sys/mako_music.h @@ -7,21 +7,20 @@ class MakoMusicDecoder; -// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension), -// decoding on demand and mixing into the audio callback's output buffer. +// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension). class MakoMusic { public: // loops: number of times to play; 0 means loop forever. - MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec); + MakoMusic(SDL_AudioDeviceID device, const std::string& path, int loops); ~MakoMusic(); bool is_open() const { return stream != nullptr; } - bool is_playing() const { return playing; } - - // Called from the audio callback. - void mix(Uint8* out, int len); + bool is_playing() const; private: + // SDL3 stream get-callback: decode and feed `additional_amount` bytes. + void AudioCallback(int additional_amount); + // Decodes and queues one chunk, handling EOF and decoder errors. void decode();