diff --git a/include/libultraship/bridge/audiobridge.h b/include/libultraship/bridge/audiobridge.h index 93fca70..153ff61 100644 --- a/include/libultraship/bridge/audiobridge.h +++ b/include/libultraship/bridge/audiobridge.h @@ -14,6 +14,10 @@ AudioChannelsSetting GetAudioChannels(); int32_t GetNumAudioChannels(); void AudioPlayerPlayFrame(const uint8_t* buf, size_t len); +// Set audio channels configuration at runtime (stereo or 5.1 surround) +// This will reinitialize the audio backend without requiring a game restart +void SetAudioChannels(AudioChannelsSetting channels); + #ifdef __cplusplus }; #endif diff --git a/include/ship/audio/Audio.h b/include/ship/audio/Audio.h index 3190664..1f393b5 100644 --- a/include/ship/audio/Audio.h +++ b/include/ship/audio/Audio.h @@ -20,6 +20,11 @@ class Audio { std::shared_ptr> GetAvailableAudioBackends(); void SetCurrentAudioBackend(AudioBackend backend); + // Set audio channels configuration and reinitialize audio player + // This can be called at runtime without restarting the game + void SetAudioChannels(AudioChannelsSetting channels); + AudioChannelsSetting GetAudioChannels() const; + protected: void InitAudioPlayer(); diff --git a/include/ship/audio/AudioChannelsSetting.h b/include/ship/audio/AudioChannelsSetting.h index 853a699..2504d20 100644 --- a/include/ship/audio/AudioChannelsSetting.h +++ b/include/ship/audio/AudioChannelsSetting.h @@ -1,3 +1,16 @@ #pragma once -typedef enum AudioChannelsSetting { audioStereo, audioSurround51, audioMax } AudioChannelsSetting; +typedef enum AudioChannelsSetting { audioStereo, audioMatrix51, audioRaw51, audioMax } AudioChannelsSetting; + +inline const char* AudioChannelsSettingName(AudioChannelsSetting setting) { + switch (setting) { + case audioStereo: + return "Stereo"; + case audioMatrix51: + return "5.1 Matrix"; + case audioRaw51: + return "5.1 Raw"; + default: + return "Unknown"; + } +} diff --git a/include/ship/audio/AudioPlayer.h b/include/ship/audio/AudioPlayer.h index 1d8b1c4..af40368 100644 --- a/include/ship/audio/AudioPlayer.h +++ b/include/ship/audio/AudioPlayer.h @@ -2,7 +2,9 @@ #include "stdint.h" #include "stddef.h" #include +#include #include "ship/audio/AudioChannelsSetting.h" +#include "ship/audio/SoundMatrixDecoder.h" namespace Ship { @@ -10,7 +12,7 @@ struct AudioSettings { int32_t SampleRate = 44100; int32_t SampleLength = 1024; int32_t DesiredBuffered = 2480; - AudioChannelsSetting AudioSurround = AudioChannelsSetting::audioStereo; + AudioChannelsSetting ChannelSetting = AudioChannelsSetting::audioStereo; }; class AudioPlayer { @@ -22,7 +24,11 @@ class AudioPlayer { bool Init(); virtual int32_t Buffered() = 0; - virtual void Play(const uint8_t* buf, size_t len) = 0; + + // Play audio + // buf: interleaved samples in either stereo: (L, R, L, R, ...), or surround: (FL, FR, C, LFE, SL, SR, ...) + // len: length in bytes + void Play(const uint8_t* buf, size_t len); bool IsInitialized(); @@ -40,14 +46,29 @@ class AudioPlayer { void SetDesiredBuffered(int32_t size); - void SetAudioChannels(AudioChannelsSetting surround); + // Change audio channels and reinitialize the audio device + // Returns true if successful + bool SetAudioChannels(AudioChannelsSetting channels); + + // Get the number of output channels (2 for stereo, 6 for surround) + int32_t GetNumOutputChannels() const; protected: + // Initialize the audio device. virtual bool DoInit() = 0; + // Close the current audio device. + virtual void DoClose() = 0; + + // Internal play method - receives audio in the output format (stereo or surround) + virtual void DoPlay(const uint8_t* buf, size_t len) = 0; + private: - bool mInitialized = false; + // Sound matrix decoder for surround mode + std::unique_ptr mSoundMatrixDecoder; + AudioSettings mAudioSettings; + bool mInitialized = false; }; } // namespace Ship diff --git a/include/ship/audio/NullAudioPlayer.h b/include/ship/audio/NullAudioPlayer.h index e215ba3..d7ab5c7 100644 --- a/include/ship/audio/NullAudioPlayer.h +++ b/include/ship/audio/NullAudioPlayer.h @@ -8,10 +8,11 @@ class NullAudioPlayer final : public AudioPlayer { } ~NullAudioPlayer(); - int Buffered(); - void Play(const uint8_t* buf, size_t len); + int Buffered() override; protected: - bool DoInit(); + bool DoInit() override; + void DoClose() override; + void DoPlay(const uint8_t* buf, size_t len) override; }; } // namespace Ship diff --git a/include/ship/audio/SDLAudioPlayer.h b/include/ship/audio/SDLAudioPlayer.h index fb82faf..8d9a08e 100644 --- a/include/ship/audio/SDLAudioPlayer.h +++ b/include/ship/audio/SDLAudioPlayer.h @@ -9,14 +9,15 @@ class SDLAudioPlayer final : public AudioPlayer { } ~SDLAudioPlayer(); - int Buffered(); - void Play(const uint8_t* buf, size_t len); + int Buffered() override; protected: - bool DoInit(); + bool DoInit() override; + void DoClose() override; + void DoPlay(const uint8_t* buf, size_t len) override; private: - SDL_AudioDeviceID mDevice; + SDL_AudioDeviceID mDevice = 0; int32_t mNumChannels = 2; }; } // namespace Ship diff --git a/include/ship/audio/SoundMatrixDecoder.h b/include/ship/audio/SoundMatrixDecoder.h new file mode 100644 index 0000000..40402e4 --- /dev/null +++ b/include/ship/audio/SoundMatrixDecoder.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace Ship { + +/** + * Passive matrix decoder for stereo to 5.1 surround upmixing. + * Implements standard audio matrix decoding techniques using: + * - Linkwitz-Riley crossover filters for frequency band separation + * - All-pass filters for phase manipulation + * - Delay lines for surround channel timing + */ +class SoundMatrixDecoder { + public: + /** + * Construct and initialize the decoder with a specific sample rate. + * @param sampleRate The audio sample rate in Hz + */ + SoundMatrixDecoder(int32_t sampleRate); + ~SoundMatrixDecoder() = default; + + /** + * Reset filter states without recomputing coefficients. + * Useful when audio is interrupted to prevent clicks. + */ + void ResetState(); + + /** + * Decode stereo to 5.1 surround + * @param stereoInput Interleaved stereo samples [L0, R0, L1, R1, ...] + * @param samplePairs Number of stereo sample pairs to process + * @return Pointer to internal buffer with interleaved 5.1 samples [FL, FR, C, LFE, SL, SR, ...] + */ + std::tuple Process(const uint8_t* buf, size_t len); + + private: + // 4th-order IIR filter (Linkwitz-Riley) for 24dB/octave slopes + struct BiquadCascade { + double X[4] = {}; // Input history + double Y[4] = {}; // Output history + }; + + struct FilterCoefficients { + double A[5]; // Feedforward (numerator) + double B[4]; // Feedback (denominator, excluding b0=1) + }; + + // Sweeping all-pass for phase decorrelation + struct AllPassChain { + double Freq = 0; + double FreqMin = 0; + double FreqMax = 0; + double SweepRate = 0; + double XHist[4] = {}; + double YHist[4] = {}; + bool Ready = false; + }; + + // Circular delay buffer + static constexpr int gMaxDelay = 1024; + struct CircularDelay { + std::array Data = {}; + int Head = 0; + int Length = 0; + }; + + // Filter design + FilterCoefficients DesignLowPass(double frequency, int32_t sampleRate); + FilterCoefficients DesignHighPass(double frequency, int32_t sampleRate); + + // Signal processing + float ProcessFilter(float sample, BiquadCascade& state, const FilterCoefficients& coef); + void PrepareAllPass(AllPassChain& chain, int32_t sampleRate); + float ProcessAllPass(float sample, AllPassChain& chain, bool negate); + float ProcessDelay(float sample, CircularDelay& buffer); + + static int16_t Saturate(float value); + + int32_t mDelayLength = 0; + double mAllPassBaseRate = 1.0; // Precomputed for ProcessAllPass + + // Filter coefficients (computed once per sample rate) + FilterCoefficients mCoefCenterHP; + FilterCoefficients mCoefCenterLP; + FilterCoefficients mCoefSurroundHP; + FilterCoefficients mCoefSubLP; + + // Per-channel filter states + BiquadCascade mCenterHighPass; + BiquadCascade mCenterLowPass; + BiquadCascade mSurrLeftMainHP; + BiquadCascade mSurrLeftCrossHP; + BiquadCascade mSurrRightMainHP; + BiquadCascade mSurrRightCrossHP; + BiquadCascade mSubLowPass; + + // Phase processing + AllPassChain mPhaseLeftMain; + AllPassChain mPhaseLeftCross; + AllPassChain mPhaseRightMain; + AllPassChain mPhaseRightCross; + + // Timing + CircularDelay mDelaySurrLeft; + CircularDelay mDelaySurrRight; + + // Output buffer + std::vector mSurroundBuffer; +}; + +} // namespace Ship diff --git a/include/ship/audio/WasapiAudioPlayer.h b/include/ship/audio/WasapiAudioPlayer.h index 6f0dacb..7d372b7 100644 --- a/include/ship/audio/WasapiAudioPlayer.h +++ b/include/ship/audio/WasapiAudioPlayer.h @@ -15,10 +15,13 @@ class WasapiAudioPlayer : public AudioPlayer, public IMMNotificationClient { WasapiAudioPlayer(AudioSettings settings) : AudioPlayer(settings) { } - int Buffered(); - void Play(const uint8_t* buf, size_t len); + int Buffered() override; protected: + bool DoInit() override; + void DoClose() override; + void DoPlay(const uint8_t* buf, size_t len) override; + virtual HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState); virtual HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId); virtual HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId); @@ -29,7 +32,6 @@ class WasapiAudioPlayer : public AudioPlayer, public IMMNotificationClient { virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface); void ThrowIfFailed(HRESULT res); bool SetupStream(); - bool DoInit(); private: ComPtr mDeviceEnumerator; diff --git a/src/libultraship/bridge/audiobridge.cpp b/src/libultraship/bridge/audiobridge.cpp index bfe60a3..c1d433f 100644 --- a/src/libultraship/bridge/audiobridge.cpp +++ b/src/libultraship/bridge/audiobridge.cpp @@ -41,12 +41,13 @@ AudioChannelsSetting GetAudioChannels() { } int32_t GetNumAudioChannels() { - switch (GetAudioChannels()) { - case audioSurround51: - return 6; - default: - return 2; + auto audio = Ship::Context::GetInstance()->GetAudio()->GetAudioPlayer(); + + if (audio == nullptr) { + return 2; } + + return audio->GetNumOutputChannels(); } void AudioPlayerPlayFrame(const uint8_t* buf, size_t len) { @@ -61,4 +62,13 @@ void AudioPlayerPlayFrame(const uint8_t* buf, size_t len) { audio->Play(buf, len); } + +void SetAudioChannels(AudioChannelsSetting channels) { + auto audio = Ship::Context::GetInstance()->GetAudio(); + if (audio == nullptr) { + return; + } + + audio->SetAudioChannels(channels); +} } diff --git a/src/ship/audio/Audio.cpp b/src/ship/audio/Audio.cpp index 6efce2c..832689a 100644 --- a/src/ship/audio/Audio.cpp +++ b/src/ship/audio/Audio.cpp @@ -62,4 +62,18 @@ std::shared_ptr> Audio::GetAvailableAudioBackends() { return mAvailableAudioBackends; } +void Audio::SetAudioChannels(AudioChannelsSetting channels) { + if (mAudioSettings.ChannelSetting != channels) { + mAudioSettings.ChannelSetting = channels; + // Reinitialize the existing audio player with the new channel configuration + if (mAudioPlayer) { + mAudioPlayer->SetAudioChannels(channels); + } + } +} + +AudioChannelsSetting Audio::GetAudioChannels() const { + return mAudioSettings.ChannelSetting; +} + } // namespace Ship diff --git a/src/ship/audio/AudioPlayer.cpp b/src/ship/audio/AudioPlayer.cpp index 380878f..037f1b4 100644 --- a/src/ship/audio/AudioPlayer.cpp +++ b/src/ship/audio/AudioPlayer.cpp @@ -2,11 +2,17 @@ #include "spdlog/spdlog.h" namespace Ship { + AudioPlayer::~AudioPlayer() { SPDLOG_TRACE("destruct audio player"); } bool AudioPlayer::Init() { + // Initialize sound matrix decoder if matrix surround mode is enabled + if (mAudioSettings.ChannelSetting == AudioChannelsSetting::audioMatrix51) { + SPDLOG_INFO("Initializing sound matrix decoder for surround"); + mSoundMatrixDecoder = std::make_unique(mAudioSettings.SampleRate); + } mInitialized = DoInit(); return IsInitialized(); } @@ -28,7 +34,7 @@ int32_t AudioPlayer::GetDesiredBuffered() const { } AudioChannelsSetting AudioPlayer::GetAudioChannels() const { - return mAudioSettings.AudioSurround; + return mAudioSettings.ChannelSetting; } void AudioPlayer::SetSampleRate(int32_t rate) { @@ -43,7 +49,60 @@ void AudioPlayer::SetDesiredBuffered(int32_t size) { mAudioSettings.DesiredBuffered = size; } -void AudioPlayer::SetAudioChannels(AudioChannelsSetting surround) { - mAudioSettings.AudioSurround = surround; +bool AudioPlayer::SetAudioChannels(AudioChannelsSetting channels) { + if (mAudioSettings.ChannelSetting == channels) { + return true; // No change needed + } + + SPDLOG_INFO("Changing audio channels from {} to {}", AudioChannelsSettingName(mAudioSettings.ChannelSetting), + AudioChannelsSettingName(channels)); + + // Close current audio device + DoClose(); + + // Update channel setting + mAudioSettings.ChannelSetting = channels; + + // Setup or teardown sound matrix decoder + if (channels == AudioChannelsSetting::audioMatrix51) { + if (!mSoundMatrixDecoder) { + mSoundMatrixDecoder = std::make_unique(mAudioSettings.SampleRate); + } + } else { + // When switching away from matrix mode, release the decoder + mSoundMatrixDecoder.reset(); + } + + return DoInit(); +} + +int32_t AudioPlayer::GetNumOutputChannels() const { + switch (mAudioSettings.ChannelSetting) { + case AudioChannelsSetting::audioMatrix51: + case AudioChannelsSetting::audioRaw51: + return 6; + case AudioChannelsSetting::audioStereo: + default: + return 2; + } +} + +void AudioPlayer::Play(const uint8_t* buf, size_t len) { + if (mAudioSettings.ChannelSetting != AudioChannelsSetting::audioMatrix51) { + // Stereo or Raw 5.1 passthrough + DoPlay(buf, len); + return; + } + + if (!mSoundMatrixDecoder) { + SPDLOG_ERROR("AudioPlayer: Matrix 5.1 mode enabled but SoundMatrixDecoder is not initialized"); + return; + } + + // Decode stereo to surround using sound matrix decoder + const auto [surroundOut, surroundLen] = mSoundMatrixDecoder->Process(buf, len); + + // Play the audio + DoPlay(surroundOut, surroundLen); } } // namespace Ship diff --git a/src/ship/audio/NullAudioPlayer.cpp b/src/ship/audio/NullAudioPlayer.cpp index 4aaf6b1..b778dbb 100644 --- a/src/ship/audio/NullAudioPlayer.cpp +++ b/src/ship/audio/NullAudioPlayer.cpp @@ -11,10 +11,14 @@ bool NullAudioPlayer::DoInit() { return true; } +void NullAudioPlayer::DoClose() { + // Nothing to close for null player +} + int NullAudioPlayer::Buffered() { return 0; } -void NullAudioPlayer::Play(const uint8_t* buf, size_t len) { +void NullAudioPlayer::DoPlay(const uint8_t* buf, size_t len) { } } // namespace Ship diff --git a/src/ship/audio/SDLAudioPlayer.cpp b/src/ship/audio/SDLAudioPlayer.cpp index c74c884..cf8a133 100644 --- a/src/ship/audio/SDLAudioPlayer.cpp +++ b/src/ship/audio/SDLAudioPlayer.cpp @@ -5,15 +5,30 @@ namespace Ship { SDLAudioPlayer::~SDLAudioPlayer() { SPDLOG_TRACE("destruct SDL audio player"); + DoClose(); SDL_QuitSubSystem(SDL_INIT_AUDIO); } +void SDLAudioPlayer::DoClose() { + if (mDevice != 0) { + // Pause playback first + SDL_PauseAudioDevice(mDevice, 1); + // Clear any queued audio to prevent glitches when reopening + SDL_ClearQueuedAudio(mDevice); + SDL_CloseAudioDevice(mDevice); + mDevice = 0; + } +} + bool SDLAudioPlayer::DoInit() { if (SDL_Init(SDL_INIT_AUDIO) != 0) { SPDLOG_ERROR("SDL init error: %s\n", SDL_GetError()); return false; } - mNumChannels = this->GetAudioChannels() == AudioChannelsSetting::audioSurround51 ? 6 : 2; + + // Always open with the correct number of output channels + mNumChannels = this->GetNumOutputChannels(); + SDL_AudioSpec want, have; SDL_zero(want); want.freq = this->GetSampleRate(); @@ -21,11 +36,15 @@ bool SDLAudioPlayer::DoInit() { want.channels = mNumChannels; want.samples = this->GetSampleLength(); want.callback = NULL; + mDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); if (mDevice == 0) { SPDLOG_ERROR("SDL_OpenAudio error: {}", SDL_GetError()); return false; } + + SPDLOG_INFO("SDL Audio initialized: {} channels, {} Hz", mNumChannels, this->GetSampleRate()); + SDL_PauseAudioDevice(mDevice, 0); return true; } @@ -34,7 +53,7 @@ int SDLAudioPlayer::Buffered() { return SDL_GetQueuedAudioSize(mDevice) / (sizeof(int16_t) * mNumChannels); } -void SDLAudioPlayer::Play(const uint8_t* buf, size_t len) { +void SDLAudioPlayer::DoPlay(const uint8_t* buf, size_t len) { if (Buffered() < 6000) { // Don't fill the audio buffer too much in case this happens SDL_QueueAudio(mDevice, buf, len); diff --git a/src/ship/audio/SoundMatrixDecoder.cpp b/src/ship/audio/SoundMatrixDecoder.cpp new file mode 100644 index 0000000..7eeeffe --- /dev/null +++ b/src/ship/audio/SoundMatrixDecoder.cpp @@ -0,0 +1,284 @@ +#include "ship/audio/SoundMatrixDecoder.h" + +namespace Ship { + +// Standard matrix decoding gains derived from psychoacoustic principles +namespace Gains { +constexpr float gCenter = 0.7071067811865476f * 0.5f; // -3dB (1/sqrt(2)) * 0.5 +constexpr float gFront = 0.5f; // -6dB +constexpr float gSurroundPrimary = 0.4359f; // Primary surround contribution +constexpr float gSurroundSecondary = 0.2449f; // Cross-feed surround contribution +} // namespace Gains + +// Timing constants +namespace Timing { +constexpr int gSurroundDelayMs = 10; // ITU-R BS.775 recommends 10-25ms +} + +SoundMatrixDecoder::SoundMatrixDecoder(int32_t sampleRate) { + // Compute delay length from sample rate + mDelayLength = (sampleRate * Timing::gSurroundDelayMs) / 1000; + if (mDelayLength > gMaxDelay) { + mDelayLength = gMaxDelay; + } + + // Precompute base rate for all-pass sweep + mAllPassBaseRate = std::pow(std::pow(2.0, 4.0), 0.1 / (sampleRate / 2.0)); + + // Design filters for this sample rate + mCoefCenterHP = DesignHighPass(70.0, sampleRate); // Remove rumble from center + mCoefCenterLP = DesignLowPass(20000.0, sampleRate); // Anti-alias center + mCoefSurroundHP = DesignHighPass(100.0, sampleRate); // Surround channels high-passed + mCoefSubLP = DesignLowPass(120.0, sampleRate); // LFE low-pass + + // Initialize phase chains with sample rate + mPhaseLeftMain = {}; + mPhaseLeftCross = {}; + mPhaseRightMain = {}; + mPhaseRightCross = {}; + PrepareAllPass(mPhaseLeftMain, sampleRate); + PrepareAllPass(mPhaseLeftCross, sampleRate); + PrepareAllPass(mPhaseRightMain, sampleRate); + PrepareAllPass(mPhaseRightCross, sampleRate); + + // Reset filter states + ResetState(); +} + +void SoundMatrixDecoder::ResetState() { + // Clear all filter states + mCenterHighPass = {}; + mCenterLowPass = {}; + mSurrLeftMainHP = {}; + mSurrLeftCrossHP = {}; + mSurrRightMainHP = {}; + mSurrRightCrossHP = {}; + mSubLowPass = {}; + + // Reset delay lines + mDelaySurrLeft = {}; + mDelaySurrLeft.Length = mDelayLength; + mDelaySurrRight = {}; + mDelaySurrRight.Length = mDelayLength; +} + +SoundMatrixDecoder::FilterCoefficients SoundMatrixDecoder::DesignLowPass(double frequency, int32_t sampleRate) { + FilterCoefficients coef = {}; + + // Clamp to safe range for bilinear transform stability + double maxFreq = sampleRate * 0.475; + if (frequency > maxFreq) { + frequency = maxFreq; + } + + // Linkwitz-Riley 4th order: two cascaded Butterworth 2nd order + double omega = 2.0 * M_PI * frequency; + double omega2 = omega * omega; + double omega3 = omega2 * omega; + double omega4 = omega2 * omega2; + + // Bilinear transform warping + double kVal = omega / std::tan(M_PI * frequency / sampleRate); + double k2 = kVal * kVal; + double k3 = k2 * kVal; + double k4 = k2 * k2; + + double rt2 = std::sqrt(2.0); + double term1 = rt2 * omega3 * kVal; + double term2 = rt2 * omega * k3; + double norm = 4.0 * omega2 * k2 + 2.0 * term1 + k4 + 2.0 * term2 + omega4; + + // Feedback coefficients + coef.B[0] = (4.0 * (omega4 + term1 - k4 - term2)) / norm; + coef.B[1] = (6.0 * omega4 - 8.0 * omega2 * k2 + 6.0 * k4) / norm; + coef.B[2] = (4.0 * (omega4 - term1 + term2 - k4)) / norm; + coef.B[3] = (k4 - 2.0 * term1 + omega4 - 2.0 * term2 + 4.0 * omega2 * k2) / norm; + + // Feedforward coefficients (low-pass response) + coef.A[0] = omega4 / norm; + coef.A[1] = 4.0 * omega4 / norm; + coef.A[2] = 6.0 * omega4 / norm; + coef.A[3] = coef.A[1]; + coef.A[4] = coef.A[0]; + + return coef; +} + +SoundMatrixDecoder::FilterCoefficients SoundMatrixDecoder::DesignHighPass(double frequency, int32_t sampleRate) { + FilterCoefficients coef = {}; + + double omega = 2.0 * M_PI * frequency; + double omega2 = omega * omega; + double omega3 = omega2 * omega; + double omega4 = omega2 * omega2; + + double kVal = omega / std::tan(M_PI * frequency / sampleRate); + double k2 = kVal * kVal; + double k3 = k2 * kVal; + double k4 = k2 * k2; + + double rt2 = std::sqrt(2.0); + double term1 = rt2 * omega3 * kVal; + double term2 = rt2 * omega * k3; + double norm = 4.0 * omega2 * k2 + 2.0 * term1 + k4 + 2.0 * term2 + omega4; + + coef.B[0] = (4.0 * (omega4 + term1 - k4 - term2)) / norm; + coef.B[1] = (6.0 * omega4 - 8.0 * omega2 * k2 + 6.0 * k4) / norm; + coef.B[2] = (4.0 * (omega4 - term1 + term2 - k4)) / norm; + coef.B[3] = (k4 - 2.0 * term1 + omega4 - 2.0 * term2 + 4.0 * omega2 * k2) / norm; + + // Feedforward coefficients (high-pass response) + coef.A[0] = k4 / norm; + coef.A[1] = -4.0 * k4 / norm; + coef.A[2] = 6.0 * k4 / norm; + coef.A[3] = coef.A[1]; + coef.A[4] = coef.A[0]; + + return coef; +} + +float SoundMatrixDecoder::ProcessFilter(float sample, BiquadCascade& state, const FilterCoefficients& coef) { + double in = sample; + double out = coef.A[0] * in + coef.A[1] * state.X[0] + coef.A[2] * state.X[1] + coef.A[3] * state.X[2] + + coef.A[4] * state.X[3] - coef.B[0] * state.Y[0] - coef.B[1] * state.Y[1] - coef.B[2] * state.Y[2] - + coef.B[3] * state.Y[3]; + + // Shift history + state.X[3] = state.X[2]; + state.X[2] = state.X[1]; + state.X[1] = state.X[0]; + state.X[0] = in; + state.Y[3] = state.Y[2]; + state.Y[2] = state.Y[1]; + state.Y[1] = state.Y[0]; + state.Y[0] = out; + + return static_cast(out); +} + +void SoundMatrixDecoder::PrepareAllPass(AllPassChain& chain, int32_t sampleRate) { + // Sweeping all-pass parameters for decorrelation + constexpr double depth = 4.0; + constexpr double baseDelay = 100.0; + constexpr double sweepSpeed = 0.1; + + chain.FreqMin = (M_PI * baseDelay) / sampleRate; + chain.Freq = chain.FreqMin; + + double range = std::pow(2.0, depth); + chain.FreqMax = (M_PI * baseDelay * range) / sampleRate; + chain.SweepRate = std::pow(range, sweepSpeed / (sampleRate / 2.0)); + chain.Ready = true; +} + +float SoundMatrixDecoder::ProcessAllPass(float sample, AllPassChain& chain, bool negate) { + // First-order all-pass coefficient + double c = (1.0 - chain.Freq) / (1.0 + chain.Freq); + double input = static_cast(sample); + + // Cascade of 4 first-order all-pass sections + chain.YHist[0] = c * (chain.YHist[0] + input) - chain.XHist[0]; + chain.XHist[0] = input; + + chain.YHist[1] = c * (chain.YHist[1] + chain.YHist[0]) - chain.XHist[1]; + chain.XHist[1] = chain.YHist[0]; + + chain.YHist[2] = c * (chain.YHist[2] + chain.YHist[1]) - chain.XHist[2]; + chain.XHist[2] = chain.YHist[1]; + + chain.YHist[3] = c * (chain.YHist[3] + chain.YHist[2]) - chain.XHist[3]; + chain.XHist[3] = chain.YHist[2]; + + double result = negate ? -chain.YHist[3] : chain.YHist[3]; + + // Sweep the frequency for time-varying decorrelation + chain.Freq *= chain.SweepRate; + + if (chain.Freq > chain.FreqMax) { + chain.SweepRate = 1.0 / mAllPassBaseRate; + } else if (chain.Freq < chain.FreqMin) { + chain.SweepRate = mAllPassBaseRate; + } + + return static_cast(result); +} + +float SoundMatrixDecoder::ProcessDelay(float sample, CircularDelay& buffer) { + float output = buffer.Data[buffer.Head]; + buffer.Data[buffer.Head] = sample; + buffer.Head = (buffer.Head + 1) % buffer.Length; + return output; +} + +int16_t SoundMatrixDecoder::Saturate(float value) { + if (value > 32767.0f) { + return 32767; + } + if (value < -32768.0f) { + return -32768; + } + return static_cast(value); +} + +std::tuple SoundMatrixDecoder::Process(const uint8_t* buf, size_t len) { + const int16_t* stereoInput = reinterpret_cast(buf); + int samplePairs = len / (2 * sizeof(int16_t)); + + // Resize output buffer if needed + size_t samplesNeeded = static_cast(samplePairs) * 6; + if (mSurroundBuffer.size() < samplesNeeded) { + mSurroundBuffer.resize(samplesNeeded); + } + + for (int i = 0; i < samplePairs; ++i) { + float inL = static_cast(stereoInput[i * 2]); + float inR = static_cast(stereoInput[i * 2 + 1]); + + // Center: sum of L+R, band-limited + float ctr = (inL + inR) * Gains::gCenter; + ctr = ProcessFilter(ctr, mCenterHighPass, mCoefCenterHP); + ctr = ProcessFilter(ctr, mCenterLowPass, mCoefCenterLP); + + // Front channels: attenuated direct signal + float frontL = inL * Gains::gFront; + float frontR = inR * Gains::gFront; + + // Surround Left: L primary (inverted phase) + R secondary (shifted phase) + float slMain = inL * Gains::gSurroundPrimary; + slMain = ProcessFilter(slMain, mSurrLeftMainHP, mCoefSurroundHP); + slMain = ProcessAllPass(slMain, mPhaseLeftMain, true); + + float slCross = inR * Gains::gSurroundSecondary; + slCross = ProcessFilter(slCross, mSurrLeftCrossHP, mCoefSurroundHP); + slCross = ProcessAllPass(slCross, mPhaseLeftCross, false); + + float surrL = ProcessDelay(slMain + slCross, mDelaySurrLeft); + + // Surround Right: R primary (shifted phase) + L secondary (inverted phase) + float srMain = inR * Gains::gSurroundPrimary; + srMain = ProcessFilter(srMain, mSurrRightMainHP, mCoefSurroundHP); + srMain = ProcessAllPass(srMain, mPhaseRightMain, false); + + float srCross = inL * Gains::gSurroundSecondary; + srCross = ProcessFilter(srCross, mSurrRightCrossHP, mCoefSurroundHP); + srCross = ProcessAllPass(srCross, mPhaseRightCross, true); + + float surrR = ProcessDelay(srMain + srCross, mDelaySurrRight); + + // LFE: low-passed sum + float lfe = (inL + inR) * Gains::gCenter; + lfe = ProcessFilter(lfe, mSubLowPass, mCoefSubLP); + + // Output: FL, FR, C, LFE, SL, SR + mSurroundBuffer[i * 6 + 0] = Saturate(frontL); + mSurroundBuffer[i * 6 + 1] = Saturate(frontR); + mSurroundBuffer[i * 6 + 2] = Saturate(ctr); + mSurroundBuffer[i * 6 + 3] = Saturate(lfe); + mSurroundBuffer[i * 6 + 4] = Saturate(surrL); + mSurroundBuffer[i * 6 + 5] = Saturate(surrR); + } + + return { reinterpret_cast(mSurroundBuffer.data()), samplePairs * 6 * sizeof(int16_t) }; +} + +} // namespace Ship diff --git a/src/ship/audio/WasapiAudioPlayer.cpp b/src/ship/audio/WasapiAudioPlayer.cpp index 81a7ee9..69fa381 100644 --- a/src/ship/audio/WasapiAudioPlayer.cpp +++ b/src/ship/audio/WasapiAudioPlayer.cpp @@ -28,31 +28,31 @@ bool WasapiAudioPlayer::SetupStream() { ThrowIfFailed(mDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &mDevice)); ThrowIfFailed(mDevice->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, IID_PPV_ARGS_Helper(&mClient))); - auto audioSurround = this->GetAudioChannels(); - if (audioSurround == AudioChannelsSetting::audioStereo) { - mNumChannels = 2; + // Use GetNumOutputChannels() to determine stereo vs surround + mNumChannels = this->GetNumOutputChannels(); + + if (mNumChannels == 2) { WAVEFORMATEX desired; desired.wFormatTag = WAVE_FORMAT_PCM; desired.nChannels = mNumChannels; // Stereo audio desired.wBitsPerSample = 16; // 16-bit audio desired.nSamplesPerSec = this->GetSampleRate(); desired.nBlockAlign = desired.nChannels * desired.wBitsPerSample / 8; - desired.nAvgBytesPerSec = desired.nSamplesPerSec * desired.nBlockAlign; // 2 bytes per sample (16-bit audio) + desired.nAvgBytesPerSec = desired.nSamplesPerSec * desired.nBlockAlign; desired.cbSize = 0; ThrowIfFailed(mClient->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, 2000000, 0, &desired, nullptr)); - } else if (audioSurround == AudioChannelsSetting::audioSurround51) { - mNumChannels = 6; + } else if (mNumChannels == 6) { + // 5.1 surround (6 channels) WAVEFORMATEXTENSIBLE desired; desired.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; desired.Format.nChannels = mNumChannels; // 6 channels for 5.1 audio desired.Format.wBitsPerSample = 16; // 16-bit audio desired.Format.nSamplesPerSec = this->GetSampleRate(); desired.Format.nBlockAlign = desired.Format.nChannels * desired.Format.wBitsPerSample / 8; - desired.Format.nAvgBytesPerSec = - desired.Format.nSamplesPerSec * desired.Format.nBlockAlign; // 2 bytes per sample (16-bit audio) + desired.Format.nAvgBytesPerSec = desired.Format.nSamplesPerSec * desired.Format.nBlockAlign; desired.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); desired.dwChannelMask = KSAUDIO_SPEAKER_5POINT1; desired.Samples.wValidBitsPerSample = 16; @@ -75,15 +75,27 @@ bool WasapiAudioPlayer::SetupStream() { bool WasapiAudioPlayer::DoInit() { try { - ThrowIfFailed( - CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&mDeviceEnumerator))); + if (!mDeviceEnumerator) { + ThrowIfFailed( + CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&mDeviceEnumerator))); + ThrowIfFailed(mDeviceEnumerator->RegisterEndpointNotificationCallback(this)); + } } catch (HRESULT res) { return false; } - ThrowIfFailed(mDeviceEnumerator->RegisterEndpointNotificationCallback(this)); - return true; } +void WasapiAudioPlayer::DoClose() { + if (mClient) { + mClient->Stop(); + } + mRenderClient.Reset(); + mClient.Reset(); + mDevice.Reset(); + mInitialized = false; + mStarted = false; +} + int WasapiAudioPlayer::Buffered() { if (!mInitialized) { if (!SetupStream()) { @@ -97,7 +109,7 @@ int WasapiAudioPlayer::Buffered() { } catch (HRESULT res) { return 0; } } -void WasapiAudioPlayer::Play(const uint8_t* buf, size_t len) { +void WasapiAudioPlayer::DoPlay(const uint8_t* buf, size_t len) { if (!mInitialized) { if (!SetupStream()) { return; diff --git a/src/ship/config/Config.cpp b/src/ship/config/Config.cpp index f5b220c..14b9796 100644 --- a/src/ship/config/Config.cpp +++ b/src/ship/config/Config.cpp @@ -261,11 +261,13 @@ AudioBackend Config::GetCurrentAudioBackend() { } AudioChannelsSetting Config::GetCurrentAudioChannelsSetting() { - int32_t surround = + int32_t channelsSetting = GetInt("CVars." CVAR_AUDIO_CHANNELS_SETTING, static_cast(AudioChannelsSetting::audioMax)); - switch (surround) { - case AudioChannelsSetting::audioSurround51: - return AudioChannelsSetting::audioSurround51; + switch (channelsSetting) { + case AudioChannelsSetting::audioMatrix51: + return AudioChannelsSetting::audioMatrix51; + case AudioChannelsSetting::audioRaw51: + return AudioChannelsSetting::audioRaw51; case AudioChannelsSetting::audioStereo: case AudioChannelsSetting::audioMax: default: