diff --git a/CMakeLists.txt b/CMakeLists.txt index b062ccd3..229b60b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ option(LANCZOS_RESAMPLER "Use Lanczos filtering for audio resampling instead of option(FREETYPE_FONTS "Use FreeType2 to render the DejaVu Mono (English) or Migu1M (Japanese) fonts, instead of using pre-rendered copies of Courier New (English) and MS Gothic (Japanese)" OFF) set(BACKEND_RENDERER "SDLTexture" CACHE STRING "Which renderer the game should use: 'OpenGL3' for an OpenGL 3.2 renderer, 'OpenGLES2' for an OpenGL ES 2.0 renderer, 'SDLTexture' for SDL2's hardware-accelerated Texture API, 'SDLSurface' for SDL2's software-rendered Surface API, 'Wii U' for the Wii U's hardware-accelerated GX2 API, '3DS' for the 3DS's hardware accelerated Citro2D/Citro3D API, or 'Software' for a handwritten software renderer") -set(BACKEND_AUDIO "SDL2" CACHE STRING "Which audio backend the game should use: 'SDL2', 'miniaudio', 'WiiU-Hardware', 'WiiU-Software', '3DS-Hardware', '3DS-Software', or 'Null'") +set(BACKEND_AUDIO "SDL2" CACHE STRING "Which audio backend the game should use: 'SDL2', 'SDL1', 'miniaudio', 'WiiU-Hardware', 'WiiU-Software', '3DS-Hardware', '3DS-Software', or 'Null'") set(BACKEND_PLATFORM "SDL2" CACHE STRING "Which platform backend the game should use: 'SDL2', 'SDL1', 'GLFW3', 'WiiU', '3DS', or 'Null'") option(LTO "Enable link-time optimisation" OFF) @@ -336,6 +336,12 @@ if(BACKEND_AUDIO MATCHES "SDL2") "src/Backends/Audio/SoftwareMixer/Backend.h" "src/Backends/Audio/SoftwareMixer/SDL2.cpp" ) +elseif(BACKEND_AUDIO MATCHES "SDL1") + target_sources(CSE2 PRIVATE + "src/Backends/Audio/SDL1.cpp" + "src/Backends/Audio/SoftwareMixer.cpp" + "src/Backends/Audio/SoftwareMixer.h" + ) elseif(BACKEND_AUDIO MATCHES "miniaudio") target_sources(CSE2 PRIVATE "src/Backends/Audio/SoftwareMixer.cpp" @@ -607,7 +613,11 @@ if(BACKEND_PLATFORM MATCHES "SDL2" OR BACKEND_AUDIO MATCHES "SDL2") endif() endif() -if(BACKEND_PLATFORM MATCHES "SDL1") +if(BACKEND_PLATFORM MATCHES "SDL1" OR BACKEND_AUDIO MATCHES "SDL1") + if(BACKEND_PLATFORM MATCHES "SDL2" OR BACKEND_AUDIO MATCHES "SDL2") + message(FATAL_ERROR "SDL1 and SDL2 cannot be used simultaneously!") + endif() + find_package(SDL 1.2.15) if (PKG_CONFIG_FOUND) diff --git a/src/Backends/Audio/SDL1.cpp b/src/Backends/Audio/SDL1.cpp new file mode 100644 index 00000000..b750a4c2 --- /dev/null +++ b/src/Backends/Audio/SDL1.cpp @@ -0,0 +1,243 @@ +#include "../Audio.h" + +#include +#include +#include +#include + +#include "SDL.h" + +#include "../Misc.h" + +#include "SoftwareMixer.h" + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +static unsigned long output_frequency; + +static void (*organya_callback)(void); +static unsigned int organya_callback_milliseconds; + +static void MixSoundsAndUpdateOrganya(long *stream, size_t frames_total) +{ + if (organya_callback_milliseconds == 0) + { + Mixer_MixSounds(stream, frames_total); + } + else + { + // Synchronise audio generation with Organya. + // In the original game, Organya ran asynchronously in a separate thread, + // firing off commands to DirectSound in realtime. To match that, we'd + // need a very low-latency buffer, otherwise we'd get mistimed instruments. + // Instead, we can just do this. + unsigned int frames_done = 0; + + while (frames_done != frames_total) + { + static unsigned long organya_countdown; + + if (organya_countdown == 0) + { + organya_countdown = (organya_callback_milliseconds * output_frequency) / 1000; // organya_timer is in milliseconds, so convert it to audio frames + organya_callback(); + } + + const unsigned int frames_to_do = MIN(organya_countdown, frames_total - frames_done); + + Mixer_MixSounds(stream + frames_done * 2, frames_to_do); + + frames_done += frames_to_do; + organya_countdown -= frames_to_do; + } + } +} + +static void Callback(void *user_data, Uint8 *stream_uint8, int len) +{ + (void)user_data; + + short *stream = (short*)stream_uint8; + const size_t frames_total = len / sizeof(short) / 2; + + size_t frames_done = 0; + + while (frames_done != frames_total) + { + long mix_buffer[0x800 * 2]; // 2 because stereo + + size_t subframes = MIN(0x800, frames_total - frames_done); + + memset(mix_buffer, 0, subframes * sizeof(long) * 2); + + MixSoundsAndUpdateOrganya(mix_buffer, subframes); + + for (size_t i = 0; i < subframes * 2; ++i) + { + if (mix_buffer[i] > 0x7FFF) + *stream++ = 0x7FFF; + else if (mix_buffer[i] < -0x7FFF) + *stream++ = -0x7FFF; + else + *stream++ = mix_buffer[i]; + } + + frames_done += subframes; + } +} + +bool AudioBackend_Init(void) +{ + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) + { + std::string errorMessage = std::string("'SDL_InitSubSystem(SDL_INIT_AUDIO)' failed: ") + SDL_GetError(); + Backend_ShowMessageBox("Fatal error (SDL1 audio backend)", errorMessage.c_str()); + return false; + } + + SDL_AudioSpec specification; + specification.freq = 48000; + specification.format = AUDIO_S16; + specification.channels = 2; + specification.samples = 0x400; // Roughly 10 milliseconds for 48000Hz + specification.callback = Callback; + specification.userdata = NULL; + + SDL_AudioSpec obtained_specification; + if (SDL_OpenAudio(&specification, &obtained_specification) != 0) + { + std::string error_message = std::string("'SDL_OpenAudio' failed: ") + SDL_GetError(); + Backend_ShowMessageBox("Fatal error (SDL1 audio backend)", error_message.c_str()); + return false; + } + + output_frequency = obtained_specification.freq; + Mixer_Init(obtained_specification.freq); + + SDL_PauseAudio(0); + + char driver[20]; + Backend_PrintInfo("Selected SDL audio driver: %s", SDL_AudioDriverName(driver, 20)); + + return true; +} + +void AudioBackend_Deinit(void) +{ + SDL_CloseAudio(); + + SDL_QuitSubSystem(SDL_INIT_AUDIO); +} + +AudioBackend_Sound* AudioBackend_CreateSound(unsigned int frequency, const unsigned char *samples, size_t length) +{ + SDL_LockAudio(); + + Mixer_Sound *sound = Mixer_CreateSound(frequency, samples, length); + + SDL_UnlockAudio(); + + return (AudioBackend_Sound*)sound; +} + +void AudioBackend_DestroySound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_DestroySound((Mixer_Sound*)sound); + + SDL_UnlockAudio(); +} + +void AudioBackend_PlaySound(AudioBackend_Sound *sound, bool looping) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_PlaySound((Mixer_Sound*)sound, looping); + + SDL_UnlockAudio(); +} + +void AudioBackend_StopSound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_StopSound((Mixer_Sound*)sound); + + SDL_UnlockAudio(); +} + +void AudioBackend_RewindSound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_RewindSound((Mixer_Sound*)sound); + + SDL_UnlockAudio(); +} + +void AudioBackend_SetSoundFrequency(AudioBackend_Sound *sound, unsigned int frequency) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_SetSoundFrequency((Mixer_Sound*)sound, frequency); + + SDL_UnlockAudio(); +} + +void AudioBackend_SetSoundVolume(AudioBackend_Sound *sound, long volume) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_SetSoundVolume((Mixer_Sound*)sound, volume); + + SDL_UnlockAudio(); +} + +void AudioBackend_SetSoundPan(AudioBackend_Sound *sound, long pan) +{ + if (sound == NULL) + return; + + SDL_LockAudio(); + + Mixer_SetSoundPan((Mixer_Sound*)sound, pan); + + SDL_UnlockAudio(); +} + +void AudioBackend_SetOrganyaCallback(void (*callback)(void)) +{ + SDL_LockAudio(); + + organya_callback = callback; + + SDL_UnlockAudio(); +} + +void AudioBackend_SetOrganyaTimer(unsigned int milliseconds) +{ + SDL_LockAudio(); + + organya_callback_milliseconds = milliseconds; + + SDL_UnlockAudio(); +}