// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Ronald "Eidolon" Kinard // Copyright (C) 2024 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- #include #include #include #include #include #include "../audio/chunk_load.hpp" #include "../audio/gain.hpp" #include "../audio/mixer.hpp" #include "../audio/music_player.hpp" #include "../audio/resample.hpp" #include "../audio/sound_chunk.hpp" #include "../audio/sound_effect_player.hpp" #include "../cxxutil.hpp" #include "../io/streams.hpp" #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #include "../m_avrecorder.hpp" #endif #include "../doomdef.h" #include "../i_sound.h" #include "../s_sound.h" #include "../sounds.h" #include "../w_wad.h" #include "../z_zone.h" using std::make_shared; using std::make_unique; using std::shared_ptr; using std::unique_ptr; using std::vector; using srb2::audio::Gain; using srb2::audio::Mixer; using srb2::audio::MusicPlayer; using srb2::audio::Resampler; using srb2::audio::Sample; using srb2::audio::SoundChunk; using srb2::audio::SoundEffectPlayer; using srb2::audio::Source; using namespace srb2; using namespace srb2::io; // extern in i_sound.h UINT8 sound_started = false; static unique_ptr> master_gain; static shared_ptr> master; static shared_ptr> mixer_sound_effects; static shared_ptr> mixer_music; static shared_ptr music_player; static shared_ptr> resample_music_player; static shared_ptr> gain_sound_effects; static shared_ptr> gain_music_player; static shared_ptr> gain_music_channel; static vector> sound_effect_channels; #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES static shared_ptr av_recorder; #endif static void (*music_fade_callback)(); void* I_GetSfx(sfxinfo_t* sfx) { if (sfx->lumpnum == LUMPERROR) sfx->lumpnum = S_GetSfxLumpNum(sfx); sfx->length = W_LumpLength(sfx->lumpnum); std::byte* lump = static_cast(W_CacheLumpNum(sfx->lumpnum, PU_SOUND)); auto _ = srb2::finally([lump]() { Z_Free(lump); }); tcb::span data_span(lump, sfx->length); std::optional chunk = srb2::audio::try_load_chunk(data_span); if (!chunk) return nullptr; SoundChunk* heap_chunk = new SoundChunk {std::move(*chunk)}; return heap_chunk; } void I_FreeSfx(sfxinfo_t* sfx) { if (sfx->data) { SoundChunk* chunk = static_cast(sfx->data); auto _ = srb2::finally([chunk]() { delete chunk; }); // Stop any channels playing this chunk for (auto& player : sound_effect_channels) { if (player->is_playing_chunk(chunk)) { player->reset(); } } } sfx->data = nullptr; sfx->lumpnum = LUMPERROR; } namespace { class SdlAudioLockHandle { public: SdlAudioLockHandle() { SDL_LockAudio(); } ~SdlAudioLockHandle() { SDL_UnlockAudio(); } }; #ifdef TRACY_ENABLE static const char* kAudio = "Audio"; #endif void audio_callback(void* userdata, Uint8* buffer, int len) { tracy::SetThreadName("SDL Audio Thread"); FrameMarkStart(kAudio); ZoneScoped; // The SDL Audio lock is implied to be held during callback. try { Sample<2>* float_buffer = reinterpret_cast*>(buffer); size_t float_len = len / 8; for (size_t i = 0; i < float_len; i++) { float_buffer[i] = Sample<2> {0.f, 0.f}; } if (!master_gain) return; master_gain->generate(tcb::span {float_buffer, float_len}); for (size_t i = 0; i < float_len; i++) { float_buffer[i] = { std::clamp(float_buffer[i].amplitudes[0], -1.f, 1.f), std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f), }; } #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES if (av_recorder) av_recorder->push_audio_samples(tcb::span {float_buffer, float_len}); #endif } catch (...) { } FrameMarkEnd(kAudio); return; } void initialize_sound() { if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { CONS_Alert(CONS_ERROR, "Error initializing SDL Audio: %s\n", SDL_GetError()); return; } SDL_AudioSpec desired; desired.format = AUDIO_F32SYS; desired.channels = 2; desired.samples = 1024; desired.freq = 44100; desired.callback = audio_callback; if (SDL_OpenAudio(&desired, NULL) < 0) { CONS_Alert(CONS_ERROR, "Failed to open SDL Audio device: %s\n", SDL_GetError()); SDL_QuitSubSystem(SDL_INIT_AUDIO); return; } SDL_PauseAudio(SDL_FALSE); { SdlAudioLockHandle _; master_gain = make_unique>(); master = make_shared>(); master_gain->bind(master); mixer_sound_effects = make_shared>(); mixer_music = make_shared>(); music_player = make_shared(); resample_music_player = make_shared>(music_player, 1.f); gain_sound_effects = make_shared>(); gain_music_player = make_shared>(); gain_music_channel = make_shared>(); gain_sound_effects->bind(mixer_sound_effects); gain_music_player->bind(resample_music_player); gain_music_channel->bind(mixer_music); master->add_source(gain_sound_effects); master->add_source(gain_music_channel); mixer_music->add_source(gain_music_player); for (size_t i = 0; i < static_cast(cv_numChannels.value); i++) { shared_ptr player = make_shared(); sound_effect_channels.push_back(player); mixer_sound_effects->add_source(player); } } sound_started = true; } } // namespace void I_StartupSound(void) { if (!sound_started) initialize_sound(); } void I_ShutdownSound(void) { SdlAudioLockHandle _; for (auto& channel : sound_effect_channels) { if (channel) *channel = audio::SoundEffectPlayer(); } } void I_UpdateSound(void) { // The SDL audio lock is re-entrant, so it is safe to lock twice // for the "fade to stop music" callback later. SdlAudioLockHandle _; if (music_fade_callback && !music_player->fading()) { auto old_callback = music_fade_callback; music_fade_callback = nullptr; (old_callback()); } return; } // // SFX I/O // INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel) { (void) pitch; (void) priority; SdlAudioLockHandle _; if (channel >= 0 && static_cast(channel) >= sound_effect_channels.size()) return -1; shared_ptr player_channel; if (channel < 0) { // find a free sfx channel for (size_t i = 0; i < sound_effect_channels.size(); i++) { if (sound_effect_channels[i]->finished()) { player_channel = sound_effect_channels[i]; channel = i; break; } } } else { player_channel = sound_effect_channels[channel]; } if (!player_channel) return -1; SoundChunk* chunk = static_cast(S_sfx[id].data); if (chunk == nullptr) return -1; float vol_float = static_cast(vol) / 255.f; float sep_float = static_cast(sep) / 127.f - 1.f; player_channel->start(chunk, vol_float, sep_float); return channel; } void I_StopSound(INT32 handle) { SdlAudioLockHandle _; if (sound_effect_channels.empty()) return; if (handle < 0) return; size_t index = handle; if (index >= sound_effect_channels.size()) return; sound_effect_channels[index]->reset(); } boolean I_SoundIsPlaying(INT32 handle) { SdlAudioLockHandle _; // Handle is channel index if (sound_effect_channels.empty()) return 0; if (handle < 0) return 0; size_t index = handle; if (index >= sound_effect_channels.size()) return 0; return sound_effect_channels[index]->finished() ? 0 : 1; } void I_UpdateSoundParams(INT32 handle, UINT8 vol, UINT8 sep, UINT8 pitch) { (void) pitch; SdlAudioLockHandle _; if (sound_effect_channels.empty()) return; if (handle < 0) return; size_t index = handle; if (index >= sound_effect_channels.size()) return; shared_ptr& channel = sound_effect_channels[index]; if (!channel->finished()) { float vol_float = static_cast(vol) / 255.f; float sep_float = static_cast(sep) / 127.f - 1.f; channel->update(vol_float, sep_float); } } void I_SetSfxVolume(int volume) { SdlAudioLockHandle _; float vol = static_cast(volume) / 100.f; if (gain_sound_effects) { gain_sound_effects->gain(std::clamp(vol * vol * vol, 0.f, 1.f)); } } void I_SetMasterVolume(int volume) { SdlAudioLockHandle _; float vol = static_cast(volume) / 100.f; if (master_gain) { master_gain->gain(std::clamp(vol * vol * vol, 0.f, 1.f)); } } /// ------------------------ // MUSIC SYSTEM /// ------------------------ void I_InitMusic(void) { if (!sound_started) initialize_sound(); SdlAudioLockHandle _; if (music_player != nullptr) *music_player = audio::MusicPlayer(); } void I_ShutdownMusic(void) { SdlAudioLockHandle _; if (music_player) *music_player = audio::MusicPlayer(); } /// ------------------------ // MUSIC PROPERTIES /// ------------------------ const char* I_SongType(void) { if (!music_player) return nullptr; SdlAudioLockHandle _; std::optional music_type = music_player->music_type(); if (music_type == std::nullopt) { return nullptr; } switch (*music_type) { case audio::MusicType::kOgg: return "OGG"; case audio::MusicType::kMod: return "Mod"; default: return nullptr; } } boolean I_SongPlaying(void) { if (!music_player) return false; SdlAudioLockHandle _; return music_player->music_type().has_value(); } boolean I_SongPaused(void) { if (!music_player) return false; SdlAudioLockHandle _; return !music_player->playing(); } /// ------------------------ // MUSIC EFFECTS /// ------------------------ boolean I_SetSongSpeed(float speed) { if (resample_music_player) { resample_music_player->ratio(speed); return true; } return false; } /// ------------------------ // MUSIC SEEKING /// ------------------------ UINT32 I_GetSongLength(void) { if (!music_player) return 0; SdlAudioLockHandle _; std::optional duration = music_player->duration_seconds(); if (!duration) return 0; return static_cast(std::round(*duration * 1000.f)); } boolean I_SetSongLoopPoint(UINT32 looppoint) { if (!music_player) return 0; SdlAudioLockHandle _; if (music_player->music_type() == audio::MusicType::kOgg) { music_player->loop_point_seconds(looppoint / 1000.f); return true; } return false; } UINT32 I_GetSongLoopPoint(void) { if (!music_player) return 0; SdlAudioLockHandle _; std::optional loop_point_seconds = music_player->loop_point_seconds(); if (!loop_point_seconds) return 0; return static_cast(std::round(*loop_point_seconds * 1000.f)); } boolean I_SetSongPosition(UINT32 position) { if (!music_player) return false; SdlAudioLockHandle _; music_player->seek(position / 1000.f); return true; } UINT32 I_GetSongPosition(void) { if (!music_player) return 0; SdlAudioLockHandle _; std::optional position_seconds = music_player->position_seconds(); if (!position_seconds) return 0; return static_cast(std::round(*position_seconds * 1000.f)); } void I_UpdateSongLagThreshold(void) { } void I_UpdateSongLagConditions(void) { } /// ------------------------ // MUSIC PLAYBACK /// ------------------------ namespace { void print_walk_ex_stack(const std::exception& ex) { CONS_Alert(CONS_WARNING, " Caused by: %s\n", ex.what()); try { std::rethrow_if_nested(ex); } catch (const std::exception& ex) { print_walk_ex_stack(ex); } } void print_ex(const std::exception& ex) { CONS_Alert(CONS_WARNING, "Exception loading music: %s\n", ex.what()); try { std::rethrow_if_nested(ex); } catch (const std::exception& ex) { print_walk_ex_stack(ex); } } } // namespace boolean I_LoadSong(char* data, size_t len) { if (!music_player) return false; tcb::span data_span(reinterpret_cast(data), len); audio::MusicPlayer new_player; try { new_player = audio::MusicPlayer {data_span}; } catch (const std::exception& ex) { print_ex(ex); return false; } if (music_fade_callback && music_player->fading()) { auto old_callback = music_fade_callback; music_fade_callback = nullptr; (old_callback)(); } SdlAudioLockHandle _; try { *music_player = std::move(new_player); } catch (const std::exception& ex) { print_ex(ex); return false; } if (gain_music_player) { // Reset song volume to 1.0 for newly loaded songs. gain_music_player->gain(1.0); } return true; } void I_UnloadSong(void) { if (!music_player) return; if (music_fade_callback && music_player->fading()) { auto old_callback = music_fade_callback; music_fade_callback = nullptr; (old_callback)(); } SdlAudioLockHandle _; *music_player = audio::MusicPlayer(); } boolean I_PlaySong(boolean looping) { if (!music_player) return false; SdlAudioLockHandle _; music_player->play(looping); return true; } void I_StopSong(void) { if (!music_player) return; SdlAudioLockHandle _; music_player->stop(); } void I_PauseSong(void) { if (!music_player) return; SdlAudioLockHandle _; music_player->pause(); } void I_ResumeSong(void) { if (!music_player) return; SdlAudioLockHandle _; music_player->unpause(); } void I_SetMusicVolume(int volume) { float vol = static_cast(volume) / 100.f; if (gain_music_channel) { // Music channel volume is interpreted as logarithmic rather than linear. // We approximate by cubing the gain level so vol 50 roughly sounds half as loud. gain_music_channel->gain(std::clamp(vol * vol * vol, 0.f, 1.f)); } } void I_SetCurrentSongVolume(int volume) { float vol = static_cast(volume) / 100.f; if (gain_music_player) { // However, different from music channel volume, musicdef volumes are explicitly linear. gain_music_player->gain(std::max(vol, 0.f)); } } boolean I_SetSongTrack(int track) { (void) track; return false; } /// ------------------------ // MUSIC FADING /// ------------------------ void I_SetInternalMusicVolume(UINT8 volume) { if (!music_player) return; SdlAudioLockHandle _; float gain = volume / 100.f; music_player->internal_gain(gain); } void I_StopFadingSong(void) { if (!music_player) return; SdlAudioLockHandle _; music_player->stop_fade(); } boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void)) { if (!music_player) return false; SdlAudioLockHandle _; float source_gain = source_volume / 100.f; float target_gain = target_volume / 100.f; float seconds = ms / 1000.f; music_player->fade_from_to(source_gain, target_gain, seconds); if (music_fade_callback) music_fade_callback(); music_fade_callback = callback; return true; } boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void)) { if (!music_player) return false; SdlAudioLockHandle _; float target_gain = target_volume / 100.f; float seconds = ms / 1000.f; music_player->fade_to(target_gain, seconds); if (music_fade_callback) music_fade_callback(); music_fade_callback = callback; return true; } static void stop_song_cb(void) { if (!music_player) return; SdlAudioLockHandle _; music_player->stop(); } boolean I_FadeOutStopSong(UINT32 ms) { return I_FadeSong(0.f, ms, stop_song_cb); } boolean I_FadeInPlaySong(UINT32 ms, boolean looping) { if (I_PlaySong(looping)) return I_FadeSongFromVolume(100, 0, ms, nullptr); else return false; } void I_UpdateAudioRecorder(void) { #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES // must be locked since av_recorder is used by audio_callback SdlAudioLockHandle _; av_recorder = g_av_recorder; #endif }