Audio rewrite (#236)
This replaces SDL_mixer with an internal streaming and mixing system, relying only on basic SDL audio support. It's also a partial refactor of the audio API, most notably BGM-related. The BGM metadata resource type is gone, as well as the `.bgm` files. The metadata is now stored inside the `.opus` files directly, using standard Opus tags.
This commit is contained in:
parent
f0e2eb1118
commit
8480d41b7b
117 changed files with 2729 additions and 1628 deletions
|
@ -17,7 +17,6 @@ Dependencies
|
|||
|
||||
- OpenGL >= 3.3 or OpenGL ES >= 3.0 or OpenGL ES >= 2.0 (with some extensions)
|
||||
- SDL2 >= 2.0.6
|
||||
- SDL2_mixer >= 2.0.4
|
||||
- freetype2
|
||||
- libpng >= 1.5.0
|
||||
- libwebpdecoder >= 0.5 or libwebp >= 0.5
|
||||
|
|
|
@ -138,10 +138,8 @@ endif
|
|||
static = get_option('static') or ['emscripten', 'nx'].contains(host_machine.system())
|
||||
|
||||
dep_freetype = dependency('freetype2', required : true, static : static, fallback : ['freetype', 'freetype_dep'])
|
||||
dep_opusfile = dependency('opusfile', required : false, static : static, fallback : ['opusfile', 'opusfile_dep'])
|
||||
dep_png = dependency('libpng', version : '>=1.5', required : true, static : static, fallback : ['libpng', 'png_dep'])
|
||||
dep_sdl2 = dependency('sdl2', version : '>=2.0.6', required : true, static : static, fallback : ['sdl2', 'sdl2_dep'])
|
||||
dep_sdl2_mixer = dependency('SDL2_mixer', version : '>=2.0.4', required : false, static : static, fallback : ['sdl2_mixer', 'sdl2_mixer_dep'])
|
||||
dep_webp = dependency('libwebp', version : '>=0.5', required : true, static : static, fallback : ['libwebp', 'webpdecoder_dep'])
|
||||
dep_webpdecoder = dependency('libwebpdecoder', version : '>=0.5', required : false, static : static)
|
||||
dep_zip = dependency('libzip', version : '>=1.2', required : false, static : static, fallback : ['libzip', 'libzip_dep'])
|
||||
|
|
|
@ -145,15 +145,15 @@ option(
|
|||
option(
|
||||
'a_default',
|
||||
type : 'combo',
|
||||
choices : ['sdl2mixer', 'null'],
|
||||
choices : ['sdl', 'null'],
|
||||
description : 'Which audio backend to use by default'
|
||||
)
|
||||
|
||||
option(
|
||||
'a_sdl2mixer',
|
||||
'a_sdl',
|
||||
type : 'boolean',
|
||||
value : true,
|
||||
description : 'Build the SDL2_Mixer audio backend'
|
||||
description : 'Build the SDL audio backend'
|
||||
)
|
||||
|
||||
option(
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
title = Divine Correction for Outliers
|
||||
artist = Tuck V
|
||||
loop = res/bgm/bonus0.opus
|
||||
loop_point = 2.909093
|
||||
comment = The decision to arrange a character’s existing theme or to come up with something entirely new is difficult and likely one ZUN himself struggles with.\n\nI know my strengths lie in the latter, which ultimately resulted in the stormy composition usually heard at the top of the tower, but Iku has such an interesting original theme I had to at least accept the challenge.\n\nUltimately, I can’t decide if I did the original justice with this one or not, but I know it holds a special place in my heart.
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = Existential Field
|
||||
artist = Tuck V
|
||||
loop = res/bgm/credits.opus
|
||||
comment = The staff roll theme.\n\nA sense of inspiration and the future is maintained from Excitation Field.\n\nOne that’s a lot gentler than a thunderstorm… perhaps a bit more like a rainbow.
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = Dream Cycle
|
||||
artist = Tuck V
|
||||
loop = res/bgm/ending.opus
|
||||
comment = The ending’s theme.\n\nIn summation, class, what have we learned today?\n\nKnowledge is the forbidden fruit; from it stem the powers of light and darkness. But it’s about time the protagonists have had a snack.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Delta Concision
|
||||
artist = Tuck V
|
||||
loop = res/bgm/menu.opus
|
||||
loop_point = 13.71
|
||||
comment = An introduction to the occident.\n\nMany paths branch from one. Echoes from across the sea yield a new adventure.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Logic Bombardier
|
||||
artist = Tuck V
|
||||
loop = res/bgm/scuttle.opus
|
||||
loop_point = 5.64
|
||||
comment = A secret theme for a most beloved character…\n\nI wanted to play with the concept of Scuttle’s motif as heard in the tunnel of light and try to apply it to a full-length song.\n\nThe results were interesting! Her theme is as much an experiment as her existence.\n\nSupposing she were to appear as a boss, I think it might sound different given a different role in the incident.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = The Fog Invites Unseen Mischief
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage1/stage.opus
|
||||
loop_point = 6.857143
|
||||
comment = The first stage’s theme.\n\nA relaxing mist illustrated with subdued instruments. Familiar shadows stir trouble just out of view. Ripples of times past dance across Misty Lake.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = An Impish Reflection on the Water
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage1/boss.opus
|
||||
loop_point = 3.200000
|
||||
comment = Cirno’s theme.\n\nShe’s feeling just as brave this time around, and acting a fair bit stronger.\n\nHowever, she’s really just a minor annoyance, cold and brittle. The protagonists brush off her pranks and move on.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Treasure the Wager, Treasure the Odds
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage2/stage.opus
|
||||
loop_point = 10.124989
|
||||
comment = The second stage’s theme.\n\nFor such a calm and serene path, there sure are a lot of troublemakers — a deceptively fast-paced theme, to be sure. One excitable example makes a brief appearance. The rhythm of this theme is meant to make you feel lucky.
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = The Cheerful Presence of a Dark God
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage2/boss.opus
|
||||
comment = Hina Kagiyama’s theme.\n\nThis time, it’s all up to chance.\n\nIt’s a little merciful sometimes, but you still have to do your best, no matter what. Gambling might be a poor stress-reliever…
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = Fizeau’s Finding ~ Lightray Vector
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage3/stage.opus
|
||||
comment = The third stage’s theme.\n\nIt’s usually tricky to see in the dark. It pulls you in, engulfs you, like a sense-smothering flame. But this time, it might be best to protect your eyes. A fluttering little bug feels most at home in the dead of night, and most alive in a sea of light.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Lightningbug ~ Lightning Heart
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage3/boss.opus
|
||||
loop_point = 1.500000
|
||||
comment = Wriggle Nightbug’s theme.\n\nA very motivated bug. She’s at the top of the world! — No, someone else is, she’s in the middle of a tunnel. Regardless, she knows she can do anything if she puts her mind to it, right?\n\nSounds a little “beach”-like in the introduction…
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Evidence of Ki Domestication
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage4/stage.opus
|
||||
loop_point = 32.470590
|
||||
comment = The fourth stage’s theme.\n\nAnother trip back into the orient… like a pendulum swinging back and forth.\n\nThis theme draws inspiration from a particularly elusive musician living on the other side of the sea known for the Chinese influences in his music.\n\nSmells like rust.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Marginal Red ~ The Red Word
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage4/boss.opus
|
||||
loop_point = 24.000000
|
||||
comment = Kurumi’s theme.\n\nOld faces and new dreams. She’s taken a new residence, something a little more comfortable than a Lake of Blood, but this confrontation is anything but. A mix of fears make the walls feel like they’re closing in.
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = Speak Not Falsely ~ Excitation Field
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage5/stage.opus
|
||||
comment = The fifth stage’s theme.\n\nAnd back again… how did that song go? “No reason to get excited…”\n\nBut in honesty, this is a combination of Eastern and Western influences now. If it were like a pendulum up to this point, then this theme is like an oscillating electric current.\n\nThose who climb risk a long fall, so look to the top and don’t make a single dishonest step.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = A Prayer Worth Three Hundred Coulombs
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage5/boss.opus
|
||||
loop_point = 12.80
|
||||
comment = Iku Nagae’s theme.\n\nThis song went through the wringer. Perhaps that’s appropriate for what the protagonists have to do. Imagine climbing a tower in the middle of a vicious thunderstorm and try not to feel exhausted.\n\nIku provides the first tribulation.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Summit of Revelations
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage6/stage.opus
|
||||
loop_point = 13.71
|
||||
comment = The sixth stage’s theme.\n\nAt the top of the tower, you can see the world, and at what risk?\n\nA grand architectural marvel may take thousands of years to complete… this song was no different! But the end result feels like a great discovery.
|
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
title = Cosmological Battle ~ The Vacuum Catastrophe
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage6/boss_phase1.opus
|
||||
comment = Elly’s first theme.\n\nAn old friend. I always found myself inspired by the idea of a completely inconsequential character from forgotten times climbing to great heights after fading into obscurity.\n\nShe may have been a lowly gate guard the last time you saw her, but the apple’s fallen on her head. She’s a guard of a different kind of gate now.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Deified Emergent Property ~ Ambivalent Soul
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage6/boss_phase2.opus
|
||||
loop_point = 1.6
|
||||
comment = Elly’s second theme.\n\nThe most frightening and wondrous powers of the world remain just out of reach to our mortal minds. But a couple of truly brilliant people just may have found the bridge between our world and theirs…\n\nElly is a new person now, so an arrange wouldn’t speak the truth.
|
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
title = Immutable Truth
|
||||
artist = Tuck V
|
||||
loop = res/bgm/stage6/boss_phase3.opus
|
||||
loop_point = 35.64
|
||||
comment = Elly’s last spell.\n\nThe most ambitious and impossibly necessary tasks must encapsulate absolutely everything. Many think of science as a body of knowledge, a sealed bubble, within which all “great discoveries” have already been achieved, but the contrary could not be more true. Science is an unending series of dark curtains to pull back, each new finding more magnificent and imperceptible than the last.
|
Binary file not shown.
|
@ -17,9 +17,6 @@
|
|||
|
||||
#define B (_a_backend.funcs)
|
||||
|
||||
CurrentBGM current_bgm = { .name = NULL };
|
||||
|
||||
static char *saved_bgm;
|
||||
static ht_str2int_t sfx_volumes;
|
||||
|
||||
static struct enqueued_sound {
|
||||
|
@ -30,8 +27,12 @@ static struct enqueued_sound {
|
|||
bool replace;
|
||||
} *sound_queue;
|
||||
|
||||
static SoundID play_sound_internal(const char *name, bool is_ui, int cooldown, bool replace, int delay) {
|
||||
if(!audio_output_works() || global.frameskip) {
|
||||
static bool is_skip_mode(void) {
|
||||
return global.frameskip || stage_is_skip_mode();
|
||||
}
|
||||
|
||||
static SFXPlayID play_sound_internal(const char *name, bool is_ui, int cooldown, bool replace, int delay) {
|
||||
if(!audio_output_works() || is_skip_mode()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -45,25 +46,21 @@ static SoundID play_sound_internal(const char *name, bool is_ui, int cooldown, b
|
|||
return 0;
|
||||
}
|
||||
|
||||
if(taisei_is_skip_mode_enabled()) {
|
||||
SFX *sfx = res_sfx(name);
|
||||
|
||||
if(!sfx || (!is_ui && sfx->lastplayframe + 3 + cooldown >= global.frames)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Sound *snd = res_sfx(name);
|
||||
sfx->lastplayframe = global.frames;
|
||||
|
||||
if(!snd || (!is_ui && snd->lastplayframe + 3 + cooldown >= global.frames)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
snd->lastplayframe = global.frames;
|
||||
|
||||
AudioBackendSoundGroup group = is_ui ? SNDGROUP_UI : SNDGROUP_MAIN;
|
||||
SoundID sid;
|
||||
AudioBackendSFXGroup group = is_ui ? SFXGROUP_UI : SFXGROUP_MAIN;
|
||||
SFXPlayID sid;
|
||||
|
||||
if(replace) {
|
||||
sid = B.sound_play_or_restart(snd->impl, group);
|
||||
sid = B.sfx_play_or_restart(sfx->impl, group);
|
||||
} else {
|
||||
sid = B.sound_play(snd->impl, group);
|
||||
sid = B.sfx_play(sfx->impl, group);
|
||||
}
|
||||
|
||||
return sid;
|
||||
|
@ -77,7 +74,7 @@ static void* discard_enqueued_sound(List **queue, List *vsnd, void *arg) {
|
|||
}
|
||||
|
||||
static void* play_enqueued_sound(struct enqueued_sound **queue, struct enqueued_sound *snd, void *arg) {
|
||||
if(!taisei_is_skip_mode_enabled()) {
|
||||
if(!is_skip_mode()) {
|
||||
play_sound_internal(snd->name, false, snd->cooldown, snd->replace, 0);
|
||||
}
|
||||
|
||||
|
@ -86,82 +83,82 @@ static void* play_enqueued_sound(struct enqueued_sound **queue, struct enqueued_
|
|||
return NULL;
|
||||
}
|
||||
|
||||
SoundID play_sound(const char *name) {
|
||||
SFXPlayID play_sfx(const char *name) {
|
||||
return play_sound_internal(name, false, 0, false, 0);
|
||||
}
|
||||
|
||||
SoundID play_sound_ex(const char *name, int cooldown, bool replace) {
|
||||
SFXPlayID play_sfx_ex(const char *name, int cooldown, bool replace) {
|
||||
return play_sound_internal(name, false, cooldown, replace, 0);
|
||||
}
|
||||
|
||||
void play_sound_delayed(const char *name, int cooldown, bool replace, int delay) {
|
||||
void play_sfx_delayed(const char *name, int cooldown, bool replace, int delay) {
|
||||
play_sound_internal(name, false, cooldown, replace, delay);
|
||||
}
|
||||
|
||||
void play_ui_sound(const char *name) {
|
||||
void play_sfx_ui(const char *name) {
|
||||
play_sound_internal(name, true, 0, true, 0);
|
||||
}
|
||||
|
||||
void stop_sound(SoundID sid) {
|
||||
void stop_sound(SFXPlayID sid) {
|
||||
if(sid) {
|
||||
B.sound_stop_id(sid);
|
||||
B.sfx_stop_id(sid);
|
||||
}
|
||||
}
|
||||
|
||||
void replace_sound(SoundID sid, const char *name) {
|
||||
void replace_sfx(SFXPlayID sid, const char *name) {
|
||||
stop_sound(sid);
|
||||
play_sound(name);
|
||||
play_sfx(name);
|
||||
}
|
||||
|
||||
void play_loop(const char *name) {
|
||||
if(!audio_output_works() || global.frameskip) {
|
||||
void play_sfx_loop(const char *name) {
|
||||
if(!audio_output_works() || is_skip_mode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sound *snd = res_sfx(name);
|
||||
SFX *sfx = res_sfx(name);
|
||||
|
||||
if(!snd) {
|
||||
if(!sfx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!snd->islooping) {
|
||||
B.sound_loop(snd->impl, SNDGROUP_MAIN);
|
||||
snd->islooping = true;
|
||||
if(!sfx->islooping) {
|
||||
B.sfx_loop(sfx->impl, SFXGROUP_MAIN);
|
||||
sfx->islooping = true;
|
||||
}
|
||||
|
||||
if(snd->islooping == LS_LOOPING) {
|
||||
snd->lastplayframe = global.frames;
|
||||
if(sfx->islooping == LS_LOOPING) {
|
||||
sfx->lastplayframe = global.frames;
|
||||
}
|
||||
}
|
||||
|
||||
static void* reset_sounds_callback(const char *name, Resource *res, void *arg) {
|
||||
bool reset = (intptr_t)arg;
|
||||
Sound *snd = res->data;
|
||||
SFX *sfx = res->data;
|
||||
|
||||
if(snd) {
|
||||
if(sfx) {
|
||||
if(reset) {
|
||||
snd->lastplayframe = 0;
|
||||
sfx->lastplayframe = 0;
|
||||
}
|
||||
|
||||
if(snd->islooping && (global.frames > snd->lastplayframe + LOOPTIMEOUTFRAMES || reset)) {
|
||||
B.sound_stop_loop(snd->impl);
|
||||
snd->islooping = LS_FADEOUT;
|
||||
if(sfx->islooping && (global.frames > sfx->lastplayframe + LOOPTIMEOUTFRAMES || reset)) {
|
||||
B.sfx_stop_loop(sfx->impl, SFXGROUP_MAIN);
|
||||
sfx->islooping = LS_FADEOUT;
|
||||
}
|
||||
|
||||
if(snd->islooping && (global.frames > snd->lastplayframe + LOOPTIMEOUTFRAMES + LOOPFADEOUT*60/1000. || reset)) {
|
||||
snd->islooping = LS_OFF;
|
||||
if(sfx->islooping && (global.frames > sfx->lastplayframe + LOOPTIMEOUTFRAMES + LOOPFADEOUT*60/1000. || reset)) {
|
||||
sfx->islooping = LS_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void reset_sounds(void) {
|
||||
void reset_all_sfx(void) {
|
||||
resource_for_each(RES_SFX, reset_sounds_callback, (void*)true);
|
||||
list_foreach(&sound_queue, discard_enqueued_sound, NULL);
|
||||
}
|
||||
|
||||
void update_sounds(void) {
|
||||
void update_all_sfx(void) {
|
||||
resource_for_each(RES_SFX, reset_sounds_callback, (void*)false);
|
||||
|
||||
for(struct enqueued_sound *s = sound_queue, *next; s; s = next) {
|
||||
|
@ -173,16 +170,16 @@ void update_sounds(void) {
|
|||
}
|
||||
}
|
||||
|
||||
void pause_sounds(void) {
|
||||
B.sound_pause_all(SNDGROUP_MAIN);
|
||||
void pause_all_sfx(void) {
|
||||
B.sfx_pause_all(SFXGROUP_MAIN);
|
||||
}
|
||||
|
||||
void resume_sounds(void) {
|
||||
B.sound_resume_all(SNDGROUP_MAIN);
|
||||
void resume_all_sfx(void) {
|
||||
B.sfx_resume_all(SFXGROUP_MAIN);
|
||||
}
|
||||
|
||||
void stop_sounds(void) {
|
||||
B.sound_stop_all(SNDGROUP_MAIN);
|
||||
void stop_all_sfx(void) {
|
||||
B.sfx_stop_all(SFXGROUP_MAIN);
|
||||
}
|
||||
|
||||
static bool store_sfx_volume(const char *key, const char *val, void *data) {
|
||||
|
@ -207,118 +204,17 @@ static void load_config_files(void) {
|
|||
parse_keyvalue_file_cb(SFX_PATH_PREFIX "volumes.conf", store_sfx_volume, NULL);
|
||||
}
|
||||
|
||||
static inline char* get_bgm_title(char *name) {
|
||||
MusicMetadata *meta = get_resource_data(RES_BGM_METADATA, name, RESF_OPTIONAL);
|
||||
return meta ? meta->title : NULL;
|
||||
}
|
||||
|
||||
int get_default_sfx_volume(const char *sfx) {
|
||||
return ht_get(&sfx_volumes, sfx, DEFAULT_SFX_VOLUME);
|
||||
}
|
||||
|
||||
void resume_bgm(void) {
|
||||
start_bgm(current_bgm.name); // In most cases it just unpauses existing music.
|
||||
}
|
||||
|
||||
static void stop_bgm_internal(bool pause, double fadetime) {
|
||||
if(!current_bgm.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_bgm.started_at = -1;
|
||||
|
||||
if(!pause) {
|
||||
stralloc(¤t_bgm.name, NULL);
|
||||
}
|
||||
|
||||
if(B.music_is_playing() && !B.music_is_paused()) {
|
||||
if(pause) {
|
||||
B.music_pause();
|
||||
log_debug("BGM paused");
|
||||
} else if(fadetime > 0) {
|
||||
B.music_fade(fadetime);
|
||||
log_debug("BGM fading out");
|
||||
} else {
|
||||
B.music_stop();
|
||||
log_debug("BGM stopped");
|
||||
}
|
||||
} else {
|
||||
log_debug("No BGM was playing");
|
||||
}
|
||||
}
|
||||
|
||||
void stop_bgm(bool force) {
|
||||
stop_bgm_internal(!force, false);
|
||||
}
|
||||
|
||||
void fade_bgm(double fadetime) {
|
||||
stop_bgm_internal(false, fadetime);
|
||||
}
|
||||
|
||||
void save_bgm(void) {
|
||||
// XXX: this is broken
|
||||
stralloc(&saved_bgm, current_bgm.name);
|
||||
}
|
||||
|
||||
void restore_bgm(void) {
|
||||
// XXX: this is broken
|
||||
start_bgm(saved_bgm);
|
||||
free(saved_bgm);
|
||||
saved_bgm = NULL;
|
||||
}
|
||||
|
||||
void start_bgm(const char *name) {
|
||||
if(!name || !*name) {
|
||||
stop_bgm(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// if BGM has changed, change it and start from beginning
|
||||
if(!current_bgm.name || strcmp(name, current_bgm.name)) {
|
||||
B.music_stop();
|
||||
|
||||
stralloc(¤t_bgm.name, name);
|
||||
|
||||
if((current_bgm.music = res_bgm(name)) == NULL) {
|
||||
log_warn("BGM '%s' does not exist", current_bgm.name);
|
||||
stop_bgm(true);
|
||||
free(current_bgm.name);
|
||||
current_bgm.name = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(B.music_is_paused()) {
|
||||
B.music_resume();
|
||||
}
|
||||
|
||||
if(B.music_is_playing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!B.music_play(current_bgm.music->impl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support drawing BGM title in game loop (only when music changed!)
|
||||
if((current_bgm.title = get_bgm_title(current_bgm.name)) != NULL) {
|
||||
current_bgm.started_at = global.frames;
|
||||
// Boss BGM title color may differ from the one at beginning of stage
|
||||
current_bgm.isboss = strendswith(current_bgm.name, "boss");
|
||||
} else {
|
||||
current_bgm.started_at = -1;
|
||||
}
|
||||
|
||||
log_info("Started %s", (current_bgm.title ? current_bgm.title : current_bgm.name));
|
||||
}
|
||||
|
||||
static bool audio_config_updated(SDL_Event *evt, void *arg) {
|
||||
if(config_get_int(CONFIG_MUTE_AUDIO)) {
|
||||
B.sound_set_global_volume(0.0);
|
||||
B.music_set_global_volume(0.0);
|
||||
B.sfx_set_global_volume(0.0);
|
||||
B.bgm_set_global_volume(0.0);
|
||||
} else {
|
||||
B.sound_set_global_volume(config_get_float(CONFIG_SFX_VOLUME));
|
||||
B.music_set_global_volume(config_get_float(CONFIG_BGM_VOLUME));
|
||||
B.sfx_set_global_volume(config_get_float(CONFIG_SFX_VOLUME));
|
||||
B.bgm_set_global_volume(config_get_float(CONFIG_BGM_VOLUME));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -343,6 +239,89 @@ bool audio_output_works(void) {
|
|||
return B.output_works();
|
||||
}
|
||||
|
||||
void audio_music_set_position(double pos) {
|
||||
B.music_set_position(pos);
|
||||
double audioutil_loopaware_position(double rt_pos, double duration, double loop_start) {
|
||||
loop_start = clamp(loop_start, 0, duration);
|
||||
|
||||
if(rt_pos < loop_start) {
|
||||
return fmax(0, rt_pos);
|
||||
}
|
||||
|
||||
return fmod(rt_pos - loop_start, duration - loop_start) + loop_start;
|
||||
}
|
||||
|
||||
bool audio_bgm_play(BGM *bgm, bool loop, double position, double fadein) {
|
||||
BGM *current = B.bgm_current();
|
||||
|
||||
if(current == bgm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return audio_bgm_play_or_restart(bgm, loop, position, fadein);
|
||||
}
|
||||
|
||||
bool audio_bgm_play_or_restart(BGM *bgm, bool loop, double position, double fadein) {
|
||||
if(bgm == NULL) {
|
||||
return B.bgm_stop(0);
|
||||
}
|
||||
|
||||
if(B.bgm_play(bgm, loop, position, fadein)) {
|
||||
assert(B.bgm_current() == bgm);
|
||||
events_emit(TE_AUDIO_BGM_STARTED, 0, bgm, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool audio_bgm_stop(double fadeout) {
|
||||
return B.bgm_stop(fadeout);
|
||||
}
|
||||
|
||||
bool audio_bgm_pause(void) {
|
||||
return B.bgm_pause();
|
||||
}
|
||||
|
||||
bool audio_bgm_resume(void) {
|
||||
return B.bgm_resume();
|
||||
}
|
||||
|
||||
double audio_bgm_seek(double pos) {
|
||||
return B.bgm_seek(pos);
|
||||
}
|
||||
|
||||
double audio_bgm_seek_realtime(double rtpos) {
|
||||
BGM *bgm = audio_bgm_current();
|
||||
|
||||
if(bgm == NULL) {
|
||||
log_warn("No BGM is playing");
|
||||
return -1;
|
||||
}
|
||||
|
||||
double seekpos;
|
||||
double duration = bgm_get_duration(bgm);
|
||||
|
||||
if(audio_bgm_looping()) {
|
||||
double loop_start = bgm_get_loop_start(bgm);
|
||||
seekpos = audioutil_loopaware_position(rtpos, duration, loop_start);
|
||||
} else {
|
||||
seekpos = clamp(rtpos, 0, duration);
|
||||
}
|
||||
|
||||
return audio_bgm_seek(seekpos);
|
||||
}
|
||||
|
||||
double audio_bgm_tell(void) {
|
||||
return B.bgm_tell();
|
||||
}
|
||||
|
||||
BGMStatus audio_bgm_status(void) {
|
||||
return B.bgm_status();
|
||||
}
|
||||
|
||||
bool audio_bgm_looping(void) {
|
||||
return B.bgm_looping();
|
||||
}
|
||||
|
||||
BGM *audio_bgm_current(void) {
|
||||
return B.bgm_current();
|
||||
}
|
||||
|
|
|
@ -13,15 +13,13 @@
|
|||
|
||||
#include "resource/sfx.h"
|
||||
#include "resource/bgm.h"
|
||||
#include "resource/bgm_metadata.h"
|
||||
|
||||
#define LOOPTIMEOUTFRAMES 10
|
||||
#define DEFAULT_SFX_VOLUME 100
|
||||
|
||||
#define LOOPFADEOUT 50
|
||||
|
||||
typedef struct MusicImpl MusicImpl;
|
||||
typedef struct SoundImpl SoundImpl;
|
||||
typedef struct SFXImpl SFXImpl;
|
||||
|
||||
typedef enum {
|
||||
LS_OFF,
|
||||
|
@ -29,57 +27,66 @@ typedef enum {
|
|||
LS_FADEOUT,
|
||||
} LoopState;
|
||||
|
||||
typedef struct Sound {
|
||||
int lastplayframe;
|
||||
typedef struct SFX {
|
||||
SFXImpl *impl;
|
||||
LoopState islooping;
|
||||
void *impl;
|
||||
} Sound;
|
||||
int lastplayframe;
|
||||
} SFX;
|
||||
|
||||
typedef struct Music {
|
||||
MusicImpl *impl;
|
||||
MusicMetadata *meta;
|
||||
} Music;
|
||||
typedef enum BGMStatus {
|
||||
BGM_STOPPED,
|
||||
BGM_PLAYING,
|
||||
BGM_PAUSED,
|
||||
} BGMStatus;
|
||||
|
||||
typedef struct CurrentBGM {
|
||||
char *name;
|
||||
char *title;
|
||||
int isboss;
|
||||
int started_at;
|
||||
Music *music;
|
||||
} CurrentBGM;
|
||||
|
||||
typedef uint64_t SoundID;
|
||||
|
||||
extern CurrentBGM current_bgm;
|
||||
typedef uint64_t SFXPlayID;
|
||||
|
||||
void audio_init(void);
|
||||
void audio_shutdown(void);
|
||||
bool audio_output_works(void);
|
||||
void audio_music_set_position(double pos);
|
||||
|
||||
SoundID play_sound(const char *name) attr_nonnull(1);
|
||||
SoundID play_sound_ex(const char *name, int cooldown, bool replace) attr_nonnull(1);
|
||||
void play_sound_delayed(const char *name, int cooldown, bool replace, int delay) attr_nonnull(1);
|
||||
void play_loop(const char *name) attr_nonnull(1);
|
||||
void play_ui_sound(const char *name) attr_nonnull(1);
|
||||
void stop_sound(SoundID sid);
|
||||
void replace_sound(SoundID sid, const char *name) attr_nonnull(2);
|
||||
void reset_sounds(void);
|
||||
void pause_sounds(void);
|
||||
void resume_sounds(void);
|
||||
void stop_sounds(void);
|
||||
void update_sounds(void); // checks if loops need to be stopped
|
||||
bool audio_bgm_play(BGM *bgm, bool loop, double position, double fadein);
|
||||
bool audio_bgm_play_or_restart(BGM *bgm, bool loop, double position, double fadein);
|
||||
bool audio_bgm_stop(double fadeout);
|
||||
bool audio_bgm_pause(void);
|
||||
bool audio_bgm_resume(void);
|
||||
double audio_bgm_seek(double pos);
|
||||
double audio_bgm_seek_realtime(double rtpos);
|
||||
double audio_bgm_tell(void);
|
||||
BGMStatus audio_bgm_status(void);
|
||||
bool audio_bgm_looping(void);
|
||||
BGM *audio_bgm_current(void);
|
||||
|
||||
// TODO modernize sfx API
|
||||
|
||||
SFXPlayID play_sfx(const char *name) attr_nonnull(1);
|
||||
SFXPlayID play_sfx_ex(const char *name, int cooldown, bool replace) attr_nonnull(1);
|
||||
void play_sfx_delayed(const char *name, int cooldown, bool replace, int delay) attr_nonnull(1);
|
||||
void play_sfx_loop(const char *name) attr_nonnull(1);
|
||||
void play_sfx_ui(const char *name) attr_nonnull(1);
|
||||
void stop_sound(SFXPlayID sid);
|
||||
void replace_sfx(SFXPlayID sid, const char *name) attr_nonnull(2);
|
||||
void reset_all_sfx(void);
|
||||
void pause_all_sfx(void);
|
||||
void resume_all_sfx(void);
|
||||
void stop_all_sfx(void);
|
||||
void update_all_sfx(void); // checks if loops need to be stopped
|
||||
|
||||
int get_default_sfx_volume(const char *sfx);
|
||||
|
||||
DEFINE_DEPRECATED_RESOURCE_GETTER(Sound, get_sound, res_sfx)
|
||||
DEFINE_DEPRECATED_RESOURCE_GETTER(Music, get_music, res_bgm)
|
||||
DEFINE_DEPRECATED_RESOURCE_GETTER(SFX, get_sound, res_sfx)
|
||||
DEFINE_DEPRECATED_RESOURCE_GETTER(BGM, get_music, res_bgm)
|
||||
|
||||
void start_bgm(const char *name);
|
||||
void stop_bgm(bool force);
|
||||
void fade_bgm(double fadetime);
|
||||
void resume_bgm(void);
|
||||
void save_bgm(void); // XXX: this is broken
|
||||
void restore_bgm(void); // XXX: this is broken
|
||||
attr_deprecated("Use play_sfx() instead")
|
||||
INLINE SFXPlayID play_sound(const char *name) {
|
||||
return play_sfx(name);
|
||||
}
|
||||
|
||||
attr_deprecated("Use play_sfx_ex() instead") attr_nonnull(1)
|
||||
INLINE SFXPlayID play_sound_ex(const char *name, int cooldown, bool replace) {
|
||||
return play_sfx_ex(name, cooldown, replace);
|
||||
}
|
||||
|
||||
double audioutil_loopaware_position(double rt_pos, double duration, double loop_start);
|
||||
|
||||
#endif // IGUARD_audio_audio_h
|
||||
|
|
|
@ -14,42 +14,57 @@
|
|||
#include "audio.h"
|
||||
|
||||
typedef enum {
|
||||
SNDGROUP_ALL,
|
||||
SNDGROUP_MAIN,
|
||||
SNDGROUP_UI,
|
||||
} AudioBackendSoundGroup;
|
||||
SFXGROUP_ALL,
|
||||
SFXGROUP_MAIN,
|
||||
SFXGROUP_UI,
|
||||
} AudioBackendSFXGroup;
|
||||
|
||||
typedef struct AudioBGMObjectFuncs {
|
||||
const char *(*get_title)(BGM *bgm);
|
||||
const char *(*get_artist)(BGM *bgm);
|
||||
const char *(*get_comment)(BGM *bgm);
|
||||
double (*get_duration)(BGM *bgm);
|
||||
double (*get_loop_start)(BGM *bgm);
|
||||
} AudioBGMObjectFuncs;
|
||||
|
||||
typedef struct AudioSFXObjectFuncs {
|
||||
bool (*set_volume)(SFXImpl *sfx, double vol);
|
||||
} AudioSFXObjectFuncs;
|
||||
|
||||
typedef struct AudioFuncs {
|
||||
bool (*init)(void);
|
||||
bool (*music_fade)(double fadetime);
|
||||
bool (*music_is_paused)(void);
|
||||
bool (*music_is_playing)(void);
|
||||
bool (*music_pause)(void);
|
||||
bool (*music_play)(MusicImpl *mus);
|
||||
bool (*music_resume)(void);
|
||||
bool (*music_set_global_volume)(double gain);
|
||||
bool (*music_set_loop_point)(MusicImpl *mus, double pos);
|
||||
bool (*music_set_position)(double pos);
|
||||
bool (*music_stop)(void);
|
||||
bool (*bgm_play)(BGM *bgm, bool loop, double position, double fadein);
|
||||
bool (*bgm_stop)(double fadeout);
|
||||
bool (*bgm_pause)(void);
|
||||
bool (*bgm_resume)(void);
|
||||
double (*bgm_seek)(double pos);
|
||||
double (*bgm_tell)(void);
|
||||
BGMStatus (*bgm_status)(void);
|
||||
BGM *(*bgm_current)(void);
|
||||
bool (*bgm_looping)(void);
|
||||
bool (*bgm_set_global_volume)(double gain);
|
||||
bool (*output_works)(void);
|
||||
bool (*set_sfx_volume)(float gain);
|
||||
bool (*shutdown)(void);
|
||||
SoundID (*sound_loop)(SoundImpl *snd, AudioBackendSoundGroup group);
|
||||
bool (*sound_pause_all)(AudioBackendSoundGroup group);
|
||||
SoundID (*sound_play)(SoundImpl *snd, AudioBackendSoundGroup group);
|
||||
SoundID (*sound_play_or_restart)(SoundImpl *snd, AudioBackendSoundGroup group);
|
||||
bool (*sound_resume_all)(AudioBackendSoundGroup group);
|
||||
bool (*sound_set_global_volume)(double gain);
|
||||
bool (*sound_set_volume)(SoundImpl *snd, double vol);
|
||||
bool (*sound_stop_all)(AudioBackendSoundGroup group);
|
||||
bool (*sound_stop_loop)(SoundImpl *snd);
|
||||
bool (*sound_stop_id)(SoundID);
|
||||
const char* const* (*get_supported_music_exts)(uint *out_numexts);
|
||||
const char* const* (*get_supported_sound_exts)(uint *out_numexts);
|
||||
MusicImpl* (*music_load)(const char *vfspath);
|
||||
SoundImpl* (*sound_load)(const char *vfspath);
|
||||
void (*music_unload)(MusicImpl *mus);
|
||||
void (*sound_unload)(SoundImpl *snd);
|
||||
SFXPlayID (*sfx_loop)(SFXImpl *sfx, AudioBackendSFXGroup group);
|
||||
bool (*sfx_pause_all)(AudioBackendSFXGroup group);
|
||||
SFXPlayID (*sfx_play)(SFXImpl *sfx, AudioBackendSFXGroup group);
|
||||
SFXPlayID (*sfx_play_or_restart)(SFXImpl *sfx, AudioBackendSFXGroup group);
|
||||
bool (*sfx_resume_all)(AudioBackendSFXGroup group);
|
||||
bool (*sfx_set_global_volume)(double gain);
|
||||
bool (*sfx_stop_all)(AudioBackendSFXGroup group);
|
||||
bool (*sfx_stop_loop)(SFXImpl *sfx, AudioBackendSFXGroup group);
|
||||
bool (*sfx_stop_id)(SFXPlayID);
|
||||
const char *const *(*get_supported_bgm_exts)(uint *out_numexts);
|
||||
const char *const *(*get_supported_sfx_exts)(uint *out_numexts);
|
||||
BGM *(*bgm_load)(const char *vfspath);
|
||||
SFXImpl *(*sfx_load)(const char *vfspath);
|
||||
void (*bgm_unload)(BGM *bgm);
|
||||
void (*sfx_unload)(SFXImpl *sfx);
|
||||
|
||||
struct {
|
||||
AudioBGMObjectFuncs bgm;
|
||||
AudioSFXObjectFuncs sfx;
|
||||
} object;
|
||||
} AudioFuncs;
|
||||
|
||||
typedef struct AudioBackend {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
|
||||
default_backend = get_option('a_default')
|
||||
|
||||
if default_backend == 'sdl2mixer'
|
||||
error('Your build directory is outdated. Try running the following command:\n\n$ meson --wipe @0@ @1@ -Da_default=sdl\n'.format(meson.source_root(), meson.build_root()))
|
||||
endif
|
||||
|
||||
if not get_option('a_@0@'.format(default_backend))
|
||||
error('Default audio backend \'@0@\' is not enabled. Enable it with -Da_@0@=true, or set a_default to something else.'.format(default_backend))
|
||||
endif
|
||||
|
@ -10,35 +14,32 @@ audio_src = files(
|
|||
'backend.c',
|
||||
)
|
||||
|
||||
audio_deps = []
|
||||
enabled_audio_backends = []
|
||||
|
||||
# NOTE: Order matters here.
|
||||
subdir('sdl2mixer')
|
||||
subdir('null')
|
||||
|
||||
modules = [
|
||||
'sdl2mixer',
|
||||
'sdl',
|
||||
'null',
|
||||
]
|
||||
|
||||
audio_deps = []
|
||||
enabled_audio_backends = []
|
||||
included_deps = []
|
||||
needed_deps = []
|
||||
a_macro = []
|
||||
|
||||
foreach m : modules
|
||||
if get_option('a_@0@'.format(m))
|
||||
audio_src += get_variable('a_@0@_src'.format(m))
|
||||
a_macro += ['A(@0@)'.format(m)]
|
||||
enabled_audio_backends += [m]
|
||||
needed_deps += get_variable('a_@0@_deps'.format(m))
|
||||
subdir(m)
|
||||
included_deps += [m]
|
||||
enabled_audio_backends += [m]
|
||||
a_macro += ['A(@0@)'.format(m)]
|
||||
audio_src += get_variable('a_@0@_src'.format(m))
|
||||
needed_deps += get_variable('a_@0@_deps'.format(m))
|
||||
audio_deps += get_variable('a_@0@_libdeps'.format(m))
|
||||
endif
|
||||
endforeach
|
||||
|
||||
foreach dep : needed_deps
|
||||
if not included_deps.contains(dep)
|
||||
subdir(dep)
|
||||
included_deps += [dep]
|
||||
audio_src += get_variable('a_@0@_src'.format(dep))
|
||||
audio_deps += get_variable('a_@0@_libdeps'.format(dep))
|
||||
|
|
|
@ -19,68 +19,88 @@ static bool audio_null_init(void) {
|
|||
return true;
|
||||
}
|
||||
|
||||
#define FAKE_SOUND_ID ((SoundID)1)
|
||||
#define FAKE_SOUND_ID ((SFXPlayID)1)
|
||||
|
||||
static bool audio_null_music_fade(double fadetime) { return true; }
|
||||
static bool audio_null_music_is_paused(void) { return false; }
|
||||
static bool audio_null_music_is_playing(void) { return false; }
|
||||
static bool audio_null_music_pause(void) { return true; }
|
||||
static bool audio_null_music_play(MusicImpl *impl) { return true; }
|
||||
static bool audio_null_music_resume(void) { return true; }
|
||||
static bool audio_null_music_set_global_volume(double gain) { return true; }
|
||||
static bool audio_null_music_set_loop_point(MusicImpl *impl, double pos) { return true; }
|
||||
static bool audio_null_music_set_position(double pos) { return true; }
|
||||
static bool audio_null_music_stop(void) { return true; }
|
||||
static BGM *audio_null_bgm_current(void) { return NULL; }
|
||||
static double audio_null_bgm_seek(double pos) { return -1; }
|
||||
static double audio_null_bgm_tell(void) { return -1; }
|
||||
static BGMStatus audio_null_bgm_status(void) { return BGM_STOPPED; }
|
||||
static bool audio_null_bgm_looping(void) { return false; }
|
||||
static bool audio_null_bgm_pause(void) { return false; }
|
||||
static bool audio_null_bgm_play(BGM *bgm, bool loop, double pos, double fadein) { return false; }
|
||||
static bool audio_null_bgm_resume(void) { return false; }
|
||||
static bool audio_null_bgm_set_global_volume(double gain) { return false; }
|
||||
static bool audio_null_bgm_stop(double fadeout) { return false; }
|
||||
static bool audio_null_output_works(void) { return false; }
|
||||
static bool audio_null_shutdown(void) { return true; }
|
||||
static SoundID audio_null_sound_loop(SoundImpl *impl, AudioBackendSoundGroup group) { return FAKE_SOUND_ID; }
|
||||
static bool audio_null_sound_pause_all(AudioBackendSoundGroup group) { return true; }
|
||||
static SoundID audio_null_sound_play(SoundImpl *impl, AudioBackendSoundGroup group) { return FAKE_SOUND_ID; }
|
||||
static SoundID audio_null_sound_play_or_restart(SoundImpl *impl, AudioBackendSoundGroup group) { return FAKE_SOUND_ID; }
|
||||
static bool audio_null_sound_resume_all(AudioBackendSoundGroup group) { return true; }
|
||||
static bool audio_null_sound_set_global_volume(double gain) { return true; }
|
||||
static bool audio_null_sound_set_volume(SoundImpl *snd, double gain) { return true; }
|
||||
static bool audio_null_sound_stop_all(AudioBackendSoundGroup group) { return true; }
|
||||
static bool audio_null_sound_stop_loop(SoundImpl *impl) { return true; }
|
||||
static bool audio_null_sound_stop_id(SoundID sid) { return true; }
|
||||
static SFXPlayID audio_null_sfx_loop(SFXImpl *impl, AudioBackendSFXGroup group) { return FAKE_SOUND_ID; }
|
||||
static bool audio_null_sfx_pause_all(AudioBackendSFXGroup group) { return true; }
|
||||
static SFXPlayID audio_null_sfx_play(SFXImpl *impl, AudioBackendSFXGroup group) { return FAKE_SOUND_ID; }
|
||||
static SFXPlayID audio_null_sfx_play_or_restart(SFXImpl *impl, AudioBackendSFXGroup group) { return FAKE_SOUND_ID; }
|
||||
static bool audio_null_sfx_resume_all(AudioBackendSFXGroup group) { return true; }
|
||||
static bool audio_null_sfx_set_global_volume(double gain) { return true; }
|
||||
static bool audio_null_sfx_stop_all(AudioBackendSFXGroup group) { return true; }
|
||||
static bool audio_null_sfx_stop_loop(SFXImpl *impl, AudioBackendSFXGroup group) { return true; }
|
||||
static bool audio_null_sfx_stop_id(SFXPlayID sid) { return true; }
|
||||
|
||||
static const char* const* audio_null_get_supported_exts(uint *out_numexts) { *out_numexts = 0; return NULL; }
|
||||
static MusicImpl* audio_null_music_load(const char *vfspath) { return NULL; }
|
||||
static SoundImpl* audio_null_sound_load(const char *vfspath) { return NULL; }
|
||||
static void audio_null_music_unload(MusicImpl *mus) { }
|
||||
static void audio_null_sound_unload(SoundImpl *snd) { }
|
||||
static BGM *audio_null_bgm_load(const char *vfspath) { return NULL; }
|
||||
static SFXImpl *audio_null_sfx_load(const char *vfspath) { return NULL; }
|
||||
static void audio_null_bgm_unload(BGM *mus) { }
|
||||
static void audio_null_sfx_unload(SFXImpl *snd) { }
|
||||
|
||||
static const char *audio_null_obj_bgm_get_title(BGM *bgm) { return NULL; }
|
||||
static const char *audio_null_obj_bgm_get_artist(BGM *bgm) { return NULL; }
|
||||
static const char *audio_null_obj_bgm_get_comment(BGM *bgm) { return NULL; }
|
||||
static double audio_null_obj_bgm_get_duration(BGM *bgm) { return 1; }
|
||||
static double audio_null_obj_bgm_get_loop_start(BGM *bgm) { return 0; }
|
||||
|
||||
static bool audio_null_obj_sfx_set_volume(SFXImpl *impl, double gain) { return true; }
|
||||
|
||||
AudioBackend _a_backend_null = {
|
||||
.name = "null",
|
||||
.funcs = {
|
||||
.get_supported_music_exts = audio_null_get_supported_exts,
|
||||
.get_supported_sound_exts = audio_null_get_supported_exts,
|
||||
.bgm_current = audio_null_bgm_current,
|
||||
.bgm_load = audio_null_bgm_load,
|
||||
.bgm_looping = audio_null_bgm_looping,
|
||||
.bgm_pause = audio_null_bgm_pause,
|
||||
.bgm_play = audio_null_bgm_play,
|
||||
.bgm_resume = audio_null_bgm_resume,
|
||||
.bgm_seek = audio_null_bgm_seek,
|
||||
.bgm_set_global_volume = audio_null_bgm_set_global_volume,
|
||||
.bgm_status = audio_null_bgm_status,
|
||||
.bgm_stop = audio_null_bgm_stop,
|
||||
.bgm_tell = audio_null_bgm_tell,
|
||||
.bgm_unload = audio_null_bgm_unload,
|
||||
.get_supported_bgm_exts = audio_null_get_supported_exts,
|
||||
.get_supported_sfx_exts = audio_null_get_supported_exts,
|
||||
.init = audio_null_init,
|
||||
.music_fade = audio_null_music_fade,
|
||||
.music_is_paused = audio_null_music_is_paused,
|
||||
.music_is_playing = audio_null_music_is_playing,
|
||||
.music_load = audio_null_music_load,
|
||||
.music_pause = audio_null_music_pause,
|
||||
.music_play = audio_null_music_play,
|
||||
.music_resume = audio_null_music_resume,
|
||||
.music_set_global_volume = audio_null_music_set_global_volume,
|
||||
.music_set_loop_point = audio_null_music_set_loop_point,
|
||||
.music_set_position = audio_null_music_set_position,
|
||||
.music_stop = audio_null_music_stop,
|
||||
.music_unload = audio_null_music_unload,
|
||||
.output_works = audio_null_output_works,
|
||||
.sfx_load = audio_null_sfx_load,
|
||||
.sfx_loop = audio_null_sfx_loop,
|
||||
.sfx_pause_all = audio_null_sfx_pause_all,
|
||||
.sfx_play = audio_null_sfx_play,
|
||||
.sfx_play_or_restart = audio_null_sfx_play_or_restart,
|
||||
.sfx_resume_all = audio_null_sfx_resume_all,
|
||||
.sfx_set_global_volume = audio_null_sfx_set_global_volume,
|
||||
.sfx_stop_all = audio_null_sfx_stop_all,
|
||||
.sfx_stop_id = audio_null_sfx_stop_id,
|
||||
.sfx_stop_loop = audio_null_sfx_stop_loop,
|
||||
.sfx_unload = audio_null_sfx_unload,
|
||||
.shutdown = audio_null_shutdown,
|
||||
.sound_load = audio_null_sound_load,
|
||||
.sound_loop = audio_null_sound_loop,
|
||||
.sound_pause_all = audio_null_sound_pause_all,
|
||||
.sound_play = audio_null_sound_play,
|
||||
.sound_play_or_restart = audio_null_sound_play_or_restart,
|
||||
.sound_resume_all = audio_null_sound_resume_all,
|
||||
.sound_set_global_volume = audio_null_sound_set_global_volume,
|
||||
.sound_set_volume = audio_null_sound_set_volume,
|
||||
.sound_stop_all = audio_null_sound_stop_all,
|
||||
.sound_stop_loop = audio_null_sound_stop_loop,
|
||||
.sound_stop_id = audio_null_sound_stop_id,
|
||||
.sound_unload = audio_null_sound_unload,
|
||||
|
||||
.object = {
|
||||
.bgm = {
|
||||
.get_title = audio_null_obj_bgm_get_title,
|
||||
.get_artist = audio_null_obj_bgm_get_artist,
|
||||
.get_comment = audio_null_obj_bgm_get_comment,
|
||||
.get_duration = audio_null_obj_bgm_get_duration,
|
||||
.get_loop_start = audio_null_obj_bgm_get_loop_start,
|
||||
},
|
||||
|
||||
.sfx = {
|
||||
.set_volume = audio_null_obj_sfx_set_volume,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
731
src/audio/sdl/audio_sdl.c
Normal file
731
src/audio/sdl/audio_sdl.c
Normal file
|
@ -0,0 +1,731 @@
|
|||
/*
|
||||
* This software is licensed under the terms of the MIT License.
|
||||
* See COPYING for further information.
|
||||
* ---
|
||||
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
|
||||
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
|
||||
*/
|
||||
|
||||
#include "taisei.h"
|
||||
|
||||
#include "../backend.h"
|
||||
#include "../stream/stream.h"
|
||||
#include "../stream/stream_pcm.h"
|
||||
#include "../stream/player.h"
|
||||
|
||||
#include "util.h"
|
||||
#include "rwops/rwops_autobuf.h"
|
||||
#include "config.h"
|
||||
|
||||
#define AUDIO_FREQ 48000
|
||||
#define AUDIO_FORMAT AUDIO_F32SYS
|
||||
#define AUDIO_CHANNELS 2
|
||||
|
||||
#define CHAN_BGM 0
|
||||
|
||||
#define NUM_BGM_CHANNELS 1
|
||||
#define NUM_SFX_MAIN_CHANNELS 28
|
||||
#define NUM_SFX_UI_CHANNELS 4
|
||||
#define NUM_SFX_CHANNELS (NUM_SFX_MAIN_CHANNELS + NUM_SFX_UI_CHANNELS)
|
||||
|
||||
enum {
|
||||
FIRST_SFX_GROUP = 0,
|
||||
|
||||
G_SFX_MAIN = FIRST_SFX_GROUP,
|
||||
G_SFX_UI,
|
||||
|
||||
NUM_SFX_GROUPS,
|
||||
|
||||
G_BGM = NUM_SFX_GROUPS,
|
||||
|
||||
NUM_GROUPS,
|
||||
};
|
||||
|
||||
struct SFXImpl {
|
||||
size_t pcm_size;
|
||||
AudioStreamSpec spec;
|
||||
float gain;
|
||||
|
||||
// TODO: maybe implement this tracking in the frontend instead.
|
||||
// It's needed for things like sfx_stop_loop and sfx_play_or_restart to work, which are hacky at best.
|
||||
struct {
|
||||
SFXPlayID last_loop_id;
|
||||
SFXPlayID last_play_id;
|
||||
} per_group[NUM_SFX_GROUPS];
|
||||
|
||||
uint8_t pcm[];
|
||||
};
|
||||
|
||||
struct BGM {
|
||||
AudioStream stream;
|
||||
};
|
||||
|
||||
static_assert(offsetof(BGM, stream) == 0, "");
|
||||
|
||||
static struct {
|
||||
StreamPlayer players[NUM_GROUPS];
|
||||
|
||||
StaticPCMAudioStream sfx_streams[NUM_SFX_CHANNELS];
|
||||
uint32_t chan_play_ids[NUM_SFX_CHANNELS];
|
||||
uint32_t play_counter;
|
||||
|
||||
AudioStreamSpec spec;
|
||||
SDL_AudioDeviceID audio_device;
|
||||
uint8_t silence;
|
||||
} mixer;
|
||||
|
||||
#define IS_VALID_CHANNEL(chan) ((uint)(chan) < NUM_SFX_CHANNELS)
|
||||
|
||||
// BEGIN MISC
|
||||
|
||||
static void SDLCALL mixer_callback(void *ignore, uint8_t *stream, int len) {
|
||||
memset(stream, mixer.silence, len);
|
||||
|
||||
for(int i = 0; i < ARRAY_SIZE(mixer.players); ++i) {
|
||||
StreamPlayer *plr = mixer.players + i;
|
||||
splayer_process(plr, len, stream);
|
||||
}
|
||||
}
|
||||
|
||||
static bool init_sdl_audio(void) {
|
||||
uint num_drivers = SDL_GetNumAudioDrivers();
|
||||
void *buf;
|
||||
SDL_RWops *out = SDL_RWAutoBuffer(&buf, 256);
|
||||
|
||||
SDL_RWprintf(out, "Available audio drivers:");
|
||||
|
||||
for(uint i = 0; i < num_drivers; ++i) {
|
||||
SDL_RWprintf(out, " %s", SDL_GetAudioDriver(i));
|
||||
}
|
||||
|
||||
SDL_WriteU8(out, 0);
|
||||
log_info("%s", (char*)buf);
|
||||
SDL_RWclose(out);
|
||||
|
||||
if(SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
log_sdl_error(LOG_ERROR, "SDL_InitSubSystem");
|
||||
return false;
|
||||
}
|
||||
|
||||
log_info("Using driver '%s'", SDL_GetCurrentAudioDriver());
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool init_audio_device(void) {
|
||||
SDL_AudioSpec want = { 0 }, have;
|
||||
want.callback = mixer_callback;
|
||||
want.channels = AUDIO_CHANNELS;
|
||||
want.format = AUDIO_FORMAT;
|
||||
want.freq = AUDIO_FREQ;
|
||||
want.samples = config_get_int(CONFIG_MIXER_CHUNKSIZE);
|
||||
|
||||
// NOTE: StreamPlayer expects stereo float32 samples.
|
||||
uint allow_changes = SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_SAMPLES_CHANGE;
|
||||
mixer.audio_device = SDL_OpenAudioDevice(NULL, false, &want, &have, allow_changes);
|
||||
|
||||
if(mixer.audio_device == 0) {
|
||||
log_sdl_error(LOG_ERROR, "SDL_OpenAudioDevice");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(have.freq != want.freq || have.format != want.format) {
|
||||
log_warn(
|
||||
"Audio device spec doesn't match our request, "
|
||||
"requested (freq=%i, fmt=0x%x), got (freq=%i, fmt=0x%x). "
|
||||
"Sound may be distorted.",
|
||||
want.freq, want.format, have.freq, have.format
|
||||
);
|
||||
abort();
|
||||
}
|
||||
|
||||
mixer.spec = astream_spec(have.format, have.channels, have.freq);
|
||||
mixer.silence = have.silence;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void shutdown_players(void) {
|
||||
for(int i = 0; i < ARRAY_SIZE(mixer.players); ++i) {
|
||||
StreamPlayer *plr = mixer.players + i;
|
||||
|
||||
if(plr->channels) {
|
||||
splayer_shutdown(plr);
|
||||
memset(plr, 0, sizeof(*plr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool init_players(void) {
|
||||
if(!splayer_init(&mixer.players[G_BGM], NUM_BGM_CHANNELS, &mixer.spec)) {
|
||||
log_error("splayer_init() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!splayer_init(&mixer.players[G_SFX_MAIN], NUM_SFX_MAIN_CHANNELS, &mixer.spec)) {
|
||||
log_error("splayer_init() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!splayer_init(&mixer.players[G_SFX_UI], NUM_SFX_UI_CHANNELS, &mixer.spec)) {
|
||||
log_error("splayer_init() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void init_sfx_streams(void) {
|
||||
for(int i = 0; i < ARRAY_SIZE(mixer.sfx_streams); ++i) {
|
||||
astream_pcm_static_init(mixer.sfx_streams + i);
|
||||
}
|
||||
}
|
||||
|
||||
static bool audio_sdl_init(void) {
|
||||
if(!init_sdl_audio()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!init_audio_device()) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!init_players()) {
|
||||
shutdown_players();
|
||||
SDL_CloseAudioDevice(mixer.audio_device);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return false;
|
||||
}
|
||||
|
||||
init_sfx_streams();
|
||||
|
||||
SDL_PauseAudioDevice(mixer.audio_device, false);
|
||||
log_info("Audio subsystem initialized (SDL)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool audio_sdl_shutdown(void) {
|
||||
SDL_PauseAudioDevice(mixer.audio_device, true);
|
||||
shutdown_players();
|
||||
SDL_CloseAudioDevice(mixer.audio_device);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
|
||||
log_info("Audio subsystem deinitialized (SDL)");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool audio_sdl_output_works(void) {
|
||||
return SDL_GetAudioDeviceStatus(mixer.audio_device) == SDL_AUDIO_PLAYING;
|
||||
}
|
||||
|
||||
static const char *const *audio_sdl_get_supported_exts(uint *numexts) {
|
||||
// TODO figure this out dynamically
|
||||
static const char *const exts[] = { ".opus" };
|
||||
*numexts = ARRAY_SIZE(exts);
|
||||
return exts;
|
||||
}
|
||||
|
||||
INLINE void lock_audio(void) {
|
||||