Audio: Use SDL3's automatic stream mixing

Each sound source creates its own SDL_AudioStream, feeds it via a get-
callback, and binds it to the device so SDL mixes the bound streams.
This commit is contained in:
kichikuou
2026-09-10 20:49:59 +09:00
parent b9664c28bf
commit cba87935f6
3 changed files with 122 additions and 122 deletions
+98 -105
View File
@@ -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<uint8_t> 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<uint8_t> 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<FmStream*>(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<MakoMusic> music;
std::unique_ptr<MakoYmfm> fm;
std::unique_ptr<FmStream> fm;
std::unique_ptr<MAKOMidi> 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<uint8_t> 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<int>(pcm_src.size()));
SDL_PutAudioStreamData(stream, pcm_src.data(), static_cast<int>(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<Uint8*>(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<MAKOMidi>(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<MakoMusic>(file, next_loop, g_device_spec);
if (!m->is_open())
music = std::make_unique<MakoMusic>(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<uint8_t> data = amus.load(page);
if (data.empty())
return;
auto f = std::make_unique<MakoYmfm>(SAMPLE_RATE, std::move(data));
SDL_LockAudioStream(g_stream);
fm = std::move(f);
SDL_UnlockAudioStream(g_stream);
fm = std::make_unique<FmStream>(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<MakoMusic> old_music = std::move(music);
std::unique_ptr<MakoYmfm> 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<uint8_t> 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;
}
+18 -10
View File
@@ -171,18 +171,24 @@ std::unique_ptr<MakoMusicDecoder> 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<MakoMusic*>(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;
}
+6 -7
View File
@@ -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();