mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-10-11 08:48:11 +03:00
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:
+98
-105
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user