mirror of
https://github.com/izzy2lost/libultraship.git
synced 2026-06-19 01:16:13 -07:00
Sound Matrix Decoder (#986)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -20,6 +20,11 @@ class Audio {
|
||||
std::shared_ptr<std::vector<AudioBackend>> 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();
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#include "stdint.h"
|
||||
#include "stddef.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#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<SoundMatrixDecoder> mSoundMatrixDecoder;
|
||||
|
||||
AudioSettings mAudioSettings;
|
||||
bool mInitialized = false;
|
||||
};
|
||||
} // namespace Ship
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
|
||||
#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<const uint8_t*, int> 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<float, gMaxDelay> 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<int16_t> mSurroundBuffer;
|
||||
};
|
||||
|
||||
} // namespace Ship
|
||||
@@ -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<IMMDeviceEnumerator> mDeviceEnumerator;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,18 @@ std::shared_ptr<std::vector<AudioBackend>> 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
|
||||
|
||||
@@ -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<SoundMatrixDecoder>(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<SoundMatrixDecoder>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<float>(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<double>(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<float>(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<int16_t>(value);
|
||||
}
|
||||
|
||||
std::tuple<const uint8_t*, int> SoundMatrixDecoder::Process(const uint8_t* buf, size_t len) {
|
||||
const int16_t* stereoInput = reinterpret_cast<const int16_t*>(buf);
|
||||
int samplePairs = len / (2 * sizeof(int16_t));
|
||||
|
||||
// Resize output buffer if needed
|
||||
size_t samplesNeeded = static_cast<size_t>(samplePairs) * 6;
|
||||
if (mSurroundBuffer.size() < samplesNeeded) {
|
||||
mSurroundBuffer.resize(samplesNeeded);
|
||||
}
|
||||
|
||||
for (int i = 0; i < samplePairs; ++i) {
|
||||
float inL = static_cast<float>(stereoInput[i * 2]);
|
||||
float inR = static_cast<float>(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<const uint8_t*>(mSurroundBuffer.data()), samplePairs * 6 * sizeof(int16_t) };
|
||||
}
|
||||
|
||||
} // namespace Ship
|
||||
@@ -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;
|
||||
|
||||
@@ -261,11 +261,13 @@ AudioBackend Config::GetCurrentAudioBackend() {
|
||||
}
|
||||
|
||||
AudioChannelsSetting Config::GetCurrentAudioChannelsSetting() {
|
||||
int32_t surround =
|
||||
int32_t channelsSetting =
|
||||
GetInt("CVars." CVAR_AUDIO_CHANNELS_SETTING, static_cast<int32_t>(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:
|
||||
|
||||
Reference in New Issue
Block a user