From 07ee64818196cbe91528e90b51ce8172d990120a Mon Sep 17 00:00:00 2001 From: Clownacy Date: Sun, 19 Apr 2020 19:03:00 +0100 Subject: [PATCH] Add Wii U software audio mixer The hardware-accelerated one is suffering from a bizarre-ass bug that I can't fix for the life of me. --- CMakeLists.txt | 4 +- src/Backends/Audio/WiiU-Software.cpp | 340 +++++++++++++++++++++++++++ 2 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/Backends/Audio/WiiU-Software.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e983ea7..471b1ade 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -332,7 +332,9 @@ elseif(BACKEND_AUDIO MATCHES "miniaudio") target_link_libraries(CSE2 PRIVATE ${CMAKE_DL_LIBS}) elseif(BACKEND_AUDIO MATCHES "WiiU") target_sources(CSE2 PRIVATE - "src/Backends/Audio/WiiU.cpp" + "src/Backends/Audio/WiiU-Software.cpp" + "src/Backends/Audio/SoftwareMixer.cpp" + "src/Backends/Audio/SoftwareMixer.h" ) elseif(BACKEND_AUDIO MATCHES "Null") target_sources(CSE2 PRIVATE diff --git a/src/Backends/Audio/WiiU-Software.cpp b/src/Backends/Audio/WiiU-Software.cpp new file mode 100644 index 00000000..ba1a0064 --- /dev/null +++ b/src/Backends/Audio/WiiU-Software.cpp @@ -0,0 +1,340 @@ +#include "../Audio.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "SoftwareMixer.h" + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define CLAMP(x, y, z) MIN(MAX((x), (y)), (z)) + +static void (*organya_callback)(void); +static unsigned int organya_callback_milliseconds; + +static unsigned long ticks_per_second; + +static OSMutex sound_list_mutex; +static OSMutex organya_mutex; + +static AXVoice *voice; + +static short *stream_buffer; +static float *stream_buffer_float; +static size_t buffer_length; +static unsigned long output_frequency; + +static unsigned long GetTicksMilliseconds(void) +{ + static uint64_t accumulator; + + static unsigned long last_tick; + + unsigned long current_tick = OSGetTick(); + + accumulator += current_tick - last_tick; + + last_tick = current_tick; + + return (accumulator * 1000) / ticks_per_second; +} + +static void Callback(void *output_stream, size_t frames_total) +{ + + float *stream = (float*)output_stream; + + OSLockMutex(&organya_mutex); + + if (organya_callback_milliseconds == 0) + { + OSLockMutex(&sound_list_mutex); + Mixer_MixSounds(stream, frames_total); + OSUnlockMutex(&sound_list_mutex); + } + 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); + + OSLockMutex(&sound_list_mutex); + Mixer_MixSounds(stream + frames_done * 2, frames_to_do); + OSUnlockMutex(&sound_list_mutex); + + frames_done += frames_to_do; + organya_countdown -= frames_to_do; + } + } + + OSUnlockMutex(&organya_mutex); +} + +static int ThreadFunction(int argc, const char *argv[]) +{ + for (;;) + { + OSTestThreadCancel(); + + static int last_half = 1; + + unsigned int half; + + AXVoiceOffsets offsets; + + AXGetVoiceOffsets(voice, &offsets); + + if (offsets.currentOffset > (buffer_length / 2)) + { + half = 1; + } + else + { + half = 0; + } + + if (half != last_half) + { + for (unsigned int i = 0; i < buffer_length / 2; ++i) + { + stream_buffer_float[i * 2 + 0] = 0.0f; + stream_buffer_float[i * 2 + 1] = 0.0f; + } + + Callback(stream_buffer_float, buffer_length / 2); + + for (unsigned int i = 0; i < buffer_length / 2; ++i) + { + float sample = stream_buffer_float[i * 2]; + + if (sample < -1.0f) + sample = -1.0f; + else if (sample > 1.0f) + sample = 1.0f; + + stream_buffer[((buffer_length / 2) * last_half) + i] = sample * 32767.0f; + } + + DCStoreRange(&stream_buffer[(buffer_length / 2) * last_half], buffer_length / 2 * sizeof(short)); + + last_half = half; + } + } + + return 0; +} + +bool AudioBackend_Init(void) +{ + if (!AXIsInit()) + { + AXInitParams initparams = { + .renderer = AX_INIT_RENDERER_48KHZ, + .pipeline = AX_INIT_PIPELINE_SINGLE, + }; + + AXInitWithParams(&initparams); + } + + OSInitMutex(&sound_list_mutex); + OSInitMutex(&organya_mutex); + + output_frequency = AXGetInputSamplesPerSec(); + + Mixer_Init(output_frequency); + + voice = AXAcquireVoice(31, NULL, NULL); + + if (voice != NULL) + { + AXVoiceBegin(voice); + + AXSetVoiceType(voice, 0); + + AXVoiceVeData vol = {.volume = 0x8000}; + AXSetVoiceVe(voice, &vol); + + AXVoiceDeviceMixData mix_data[6]; + memset(mix_data, 0, sizeof(mix_data)); + mix_data[0].bus[0].volume = 0x8000; + mix_data[1].bus[0].volume = 0x8000; + + AXSetVoiceDeviceMix(voice, AX_DEVICE_TYPE_DRC, 0, mix_data); + AXSetVoiceDeviceMix(voice, AX_DEVICE_TYPE_TV, 0, mix_data); + + AXSetVoiceSrcRatio(voice, 1.0f); // We use the native sample rate + AXSetVoiceSrcType(voice, AX_VOICE_SRC_TYPE_NONE); + + buffer_length = output_frequency / 100; // 10ms buffer + + stream_buffer = (short*)malloc(buffer_length * sizeof(short)); + stream_buffer_float = (float*)malloc(buffer_length * sizeof(float) * 2); + + AXVoiceOffsets offs; + offs.dataType = AX_VOICE_FORMAT_LPCM16; + offs.endOffset = buffer_length; + offs.loopingEnabled = AX_VOICE_LOOP_ENABLED; + offs.loopOffset = 0; + offs.currentOffset = 0; + offs.data = stream_buffer; + AXSetVoiceOffsets(voice, &offs); + + AXSetVoiceState(voice, AX_VOICE_STATE_PLAYING); + + AXVoiceEnd(voice); + + //AXRegisterAppFrameCallback(FrameCallback); + + OSRunThread(OSGetDefaultThread(0), ThreadFunction, 0, NULL); + + return true; + } + + return false; +} + +void AudioBackend_Deinit(void) +{ + OSCancelThread(OSGetDefaultThread(0)); + + OSJoinThread(OSGetDefaultThread(0), NULL); + + AXFreeVoice(voice); + + AXQuit(); +} + +AudioBackend_Sound* AudioBackend_CreateSound(unsigned int frequency, const unsigned char *samples, size_t length) +{ + OSLockMutex(&sound_list_mutex); + + Mixer_Sound *sound = Mixer_CreateSound(frequency, samples, length); + + OSUnlockMutex(&sound_list_mutex); + + return (AudioBackend_Sound*)sound; +} + +void AudioBackend_DestroySound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_DestroySound((Mixer_Sound*)sound); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_PlaySound(AudioBackend_Sound *sound, bool looping) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_PlaySound((Mixer_Sound*)sound, looping); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_StopSound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_StopSound((Mixer_Sound*)sound); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_RewindSound(AudioBackend_Sound *sound) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_RewindSound((Mixer_Sound*)sound); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_SetSoundFrequency(AudioBackend_Sound *sound, unsigned int frequency) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_SetSoundFrequency((Mixer_Sound*)sound, frequency); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_SetSoundVolume(AudioBackend_Sound *sound, long volume) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_SetSoundVolume((Mixer_Sound*)sound, volume); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_SetSoundPan(AudioBackend_Sound *sound, long pan) +{ + if (sound == NULL) + return; + + OSLockMutex(&sound_list_mutex); + + Mixer_SetSoundPan((Mixer_Sound*)sound, pan); + + OSUnlockMutex(&sound_list_mutex); +} + +void AudioBackend_SetOrganyaCallback(void (*callback)(void)) +{ + // As far as thread-safety goes - this is guarded by + // `organya_milliseconds`, which is guarded by `organya_mutex`. + organya_callback = callback; +} + +void AudioBackend_SetOrganyaTimer(unsigned int milliseconds) +{ + OSLockMutex(&organya_mutex); + + organya_callback_milliseconds = milliseconds; + + OSUnlockMutex(&organya_mutex); +}