feat(audio): formalize runtime effects and wav loading

This commit is contained in:
2026-04-19 00:26:28 +08:00
parent 8164baba0a
commit d2017b251c
13 changed files with 518 additions and 254 deletions

View File

@@ -11,7 +11,7 @@ public:
Equalizer();
~Equalizer() override;
void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override;
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override;
void SetBandCount(uint32 count);
uint32 GetBandCount() const { return m_bandCount; }
@@ -30,13 +30,15 @@ public:
void SetWetMix(float wetMix) override;
float GetWetMix() const override { return m_wetMix; }
void SetSampleRate(uint32 sampleRate) override;
void ResetState() override;
private:
void ProcessBand(float* buffer, uint32 sampleCount, uint32 channel, uint32 band);
void EnsureStateCapacity(uint32 channels);
void ComputeCoefficients(uint32 band, float frequency, float q, float gainDb);
private:
uint32 m_bandCount = 4;
uint32 m_bandCount = 0;
std::vector<float> m_frequencies;
std::vector<float> m_gains;
std::vector<float> m_qs;
@@ -54,11 +56,8 @@ private:
};
std::vector<BandState> m_bandStates;
uint32 m_stateChannels = 0;
float m_wetMix = 1.0f;
bool m_enabled = true;
uint32 m_sampleRate = 48000;
};
} // namespace Audio

View File

@@ -12,13 +12,14 @@ public:
explicit FFTFilter(uint32 fftSize);
~FFTFilter() override;
void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override;
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override;
void SetFFTSize(uint32 size);
uint32 GetFFTSize() const { return m_fftSize; }
void SetSmoothingFactor(float factor);
float GetSmoothingFactor() const { return m_smoothingFactor; }
void ResetState() override;
const float* GetSpectrumData() const { return m_spectrumData.data(); }
size_t GetSpectrumSize() const { return m_spectrumData.size(); }

View File

@@ -24,7 +24,7 @@ public:
HRTF();
~HRTF();
void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels,
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels,
const Math::Vector3& sourcePosition,
const Math::Vector3& listenerPosition,
const Math::Quaternion& listenerRotation);
@@ -46,6 +46,9 @@ public:
void SetSpeedOfSound(float speed);
float GetSpeedOfSound() const { return m_speedOfSound; }
void SetSampleRate(uint32 sampleRate);
uint32 GetSampleRate() const { return m_sampleRate; }
void ResetState();
private:
void ComputeDirection(const Math::Vector3& sourcePosition,
@@ -56,8 +59,8 @@ private:
void ComputeILD(float azimuth, float elevation);
float ComputePinnaEffect(float azimuth, float elevation);
void ApplyHRTF(float* buffer, uint32 sampleCount, uint32 channels);
void ApplySimplePanning(float* buffer, uint32 sampleCount, uint32 channels, float pan);
void ApplyHRTF(float* buffer, uint32 frameCount, uint32 channels);
void ApplySimplePanning(float* buffer, uint32 frameCount, uint32 channels, float pan);
private:
bool m_enabled = true;

View File

@@ -9,7 +9,7 @@ class IAudioEffect {
public:
virtual ~IAudioEffect() = default;
virtual void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) = 0;
virtual void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) = 0;
virtual void SetEnabled(bool enabled) { m_enabled = enabled; }
virtual bool IsEnabled() const { return m_enabled; }
@@ -17,9 +17,17 @@ public:
virtual void SetWetMix(float wetMix) { m_wetMix = wetMix; }
virtual float GetWetMix() const { return m_wetMix; }
virtual void SetSampleRate(uint32 sampleRate) {
m_sampleRate = sampleRate > 0 ? sampleRate : 1u;
}
virtual uint32 GetSampleRate() const { return m_sampleRate; }
virtual void ResetState() {}
protected:
bool m_enabled = true;
float m_wetMix = 1.0f;
uint32 m_sampleRate = 48000;
};
} // namespace Audio

View File

@@ -11,7 +11,7 @@ public:
Reverbation();
~Reverbation() override;
void ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) override;
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override;
void SetRoomSize(float size);
float GetRoomSize() const { return m_roomSize; }
@@ -30,19 +30,10 @@ public:
void SetFreeze(bool freeze);
bool IsFreeze() const { return m_freeze; }
void SetSampleRate(uint32 sampleRate) override;
void ResetState() override;
private:
void ProcessCombFilter(float* buffer, uint32 sampleCount, uint32 channel, uint32 combIndex);
void ProcessAllPassFilter(float* buffer, uint32 sampleCount, uint32 channel);
private:
float m_roomSize = 0.5f;
float m_damping = 0.5f;
float m_wetMix = 0.3f;
float m_dryMix = 0.7f;
float m_width = 1.0f;
bool m_freeze = false;
static constexpr uint32 CombCount = 8;
static constexpr uint32 AllPassCount = 4;
static constexpr uint32 MaxDelayLength = 2000;
@@ -61,13 +52,28 @@ private:
std::vector<float> buffer;
uint32 bufferSize = 0;
uint32 writeIndex = 0;
float feedback = 0.0f;
float feedback = 0.5f;
};
CombFilter m_combFilters[CombCount];
AllPassFilter m_allPassFilters[AllPassCount];
struct ChannelState {
CombFilter combFilters[CombCount];
AllPassFilter allPassFilters[AllPassCount];
};
uint32 m_sampleRate = 48000;
void EnsureChannelState(uint32 channels);
void InitializeChannelState(ChannelState& state, uint32 channelIndex);
void UpdateCombParameters();
float ProcessChannelSample(uint32 channelIndex, float inputSample);
private:
float m_roomSize = 0.5f;
float m_damping = 0.5f;
float m_dryMix = 0.7f;
float m_width = 1.0f;
bool m_freeze = false;
std::vector<ChannelState> m_channelStates;
uint32 m_stateChannels = 0;
};
} // namespace Audio

View File

@@ -6,7 +6,6 @@ namespace XCEngine {
namespace Audio {
Equalizer::Equalizer()
: m_sampleRate(48000)
{
SetBandCount(4);
@@ -25,8 +24,8 @@ Equalizer::Equalizer()
Equalizer::~Equalizer() {
}
void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || sampleCount == 0) {
void Equalizer::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || frameCount == 0 || m_wetMix <= 0.0f) {
return;
}
@@ -34,6 +33,15 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels)
return;
}
EnsureStateCapacity(channels);
const size_t sampleCount = static_cast<size_t>(frameCount) * channels;
std::vector<float> dryBuffer;
std::vector<float> wetBuffer(buffer, buffer + sampleCount);
if (m_wetMix < 1.0f) {
dryBuffer.assign(buffer, buffer + sampleCount);
}
for (uint32 ch = 0; ch < channels; ++ch) {
for (uint32 band = 0; band < m_bandCount; ++band) {
uint32 stateIndex = ch * m_bandCount + band;
@@ -45,9 +53,9 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels)
float b1 = m_b1[band];
float b2 = m_b2[band];
for (uint32 i = 0; i < sampleCount; ++i) {
for (uint32 i = 0; i < frameCount; ++i) {
uint32 sampleIndex = i * channels + ch;
float x0 = buffer[sampleIndex];
float x0 = wetBuffer[sampleIndex];
float y0 = a0 * x0 + a1 * state.x1 + a2 * state.x2 - b1 * state.y1 - b2 * state.y2;
@@ -56,10 +64,20 @@ void Equalizer::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels)
state.y2 = state.y1;
state.y1 = y0;
buffer[sampleIndex] = y0;
wetBuffer[sampleIndex] = y0;
}
}
}
if (m_wetMix >= 1.0f) {
std::copy(wetBuffer.begin(), wetBuffer.end(), buffer);
return;
}
const float dryMix = 1.0f - m_wetMix;
for (size_t i = 0; i < sampleCount; ++i) {
buffer[i] = dryBuffer[i] * dryMix + wetBuffer[i] * m_wetMix;
}
}
void Equalizer::SetBandCount(uint32 count) {
@@ -67,17 +85,18 @@ void Equalizer::SetBandCount(uint32 count) {
return;
}
m_bandCount = count;
m_frequencies.resize(count, 1000.0f);
m_gains.resize(count, 0.0f);
m_qs.resize(count, 1.0f);
m_a0.resize(count, 1.0f);
m_a1.resize(count, 0.0f);
m_a2.resize(count, 0.0f);
m_b1.resize(count, 0.0f);
m_b2.resize(count, 0.0f);
m_bandCount = std::max(1u, count);
m_frequencies.resize(m_bandCount, 1000.0f);
m_gains.resize(m_bandCount, 0.0f);
m_qs.resize(m_bandCount, 1.0f);
m_a0.resize(m_bandCount, 1.0f);
m_a1.resize(m_bandCount, 0.0f);
m_a2.resize(m_bandCount, 0.0f);
m_b1.resize(m_bandCount, 0.0f);
m_b2.resize(m_bandCount, 0.0f);
m_bandStates.resize(count * 2);
m_bandStates.clear();
m_stateChannels = 0;
}
void Equalizer::SetBandFrequency(uint32 band, float frequency) {
@@ -136,6 +155,34 @@ void Equalizer::SetWetMix(float wetMix) {
m_wetMix = std::max(0.0f, std::min(1.0f, wetMix));
}
void Equalizer::SetSampleRate(uint32 sampleRate) {
const uint32 clampedSampleRate = sampleRate > 0 ? sampleRate : 1u;
if (m_sampleRate == clampedSampleRate) {
return;
}
IAudioEffect::SetSampleRate(clampedSampleRate);
for (uint32 i = 0; i < m_bandCount; ++i) {
ComputeCoefficients(i, m_frequencies[i], m_qs[i], m_gains[i]);
}
ResetState();
}
void Equalizer::ResetState() {
for (BandState& state : m_bandStates) {
state = {};
}
}
void Equalizer::EnsureStateCapacity(uint32 channels) {
if (m_stateChannels == channels && m_bandStates.size() == static_cast<size_t>(channels) * m_bandCount) {
return;
}
m_stateChannels = channels;
m_bandStates.assign(static_cast<size_t>(channels) * m_bandCount, BandState{});
}
void Equalizer::ComputeCoefficients(uint32 band, float frequency, float q, float gainDb) {
if (band >= m_bandCount) {
return;

View File

@@ -33,8 +33,8 @@ FFTFilter::~FFTFilter() {
}
}
void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || sampleCount == 0) {
void FFTFilter::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || frameCount == 0) {
return;
}
@@ -42,10 +42,6 @@ void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels)
return;
}
if (sampleCount < m_fftSize) {
return;
}
uint32 binCount = m_fftSize / 2;
if (m_spectrumData.size() != binCount) {
m_spectrumData.resize(binCount);
@@ -53,9 +49,13 @@ void FFTFilter::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels)
}
std::vector<float> monoBuffer(m_fftSize, 0.0f);
for (uint32 i = 0; i < m_fftSize; ++i) {
uint32 channelIndex = (channels == 1) ? 0 : (i % channels);
monoBuffer[i] = buffer[i * channels + channelIndex];
const uint32 framesToCopy = std::min(frameCount, m_fftSize);
for (uint32 i = 0; i < framesToCopy; ++i) {
float monoSample = 0.0f;
for (uint32 ch = 0; ch < channels; ++ch) {
monoSample += buffer[i * channels + ch];
}
monoBuffer[i] = monoSample / static_cast<float>(channels);
}
ComputeFFT(monoBuffer.data(), m_fftSize);
@@ -66,16 +66,21 @@ void FFTFilter::SetFFTSize(uint32 size) {
return;
}
m_fftSize = size;
m_spectrumData.resize(size / 2);
m_prevSpectrum.resize(size / 2);
InitializeFFT(size);
m_fftSize = std::max(2u, size);
m_spectrumData.assign(m_fftSize / 2, 0.0f);
m_prevSpectrum.assign(m_fftSize / 2, 0.0f);
InitializeFFT(m_fftSize);
}
void FFTFilter::SetSmoothingFactor(float factor) {
m_smoothingFactor = std::max(0.0f, std::min(1.0f, factor));
}
void FFTFilter::ResetState() {
std::fill(m_spectrumData.begin(), m_spectrumData.end(), 0.0f);
std::fill(m_prevSpectrum.begin(), m_prevSpectrum.end(), 0.0f);
}
void FFTFilter::InitializeFFT(uint32 size) {
if (m_fftConfig) {
kiss_fft_free(static_cast<kiss_fft_cfg>(m_fftConfig));

View File

@@ -1,10 +1,22 @@
#include <XCEngine/Audio/HRTF.h>
#include <algorithm>
#include <cmath>
#include <numeric>
namespace XCEngine {
namespace Audio {
namespace {
static constexpr float PI_F = 3.14159265f;
static constexpr float HeadRadiusMeters = 0.075f;
float DegreesToRadians(float degrees) {
return degrees * PI_F / 180.0f;
}
} // namespace
HRTF::HRTF()
: m_leftDelayLine(MaxDelaySamples, 0.0f)
, m_rightDelayLine(MaxDelaySamples, 0.0f)
@@ -14,11 +26,11 @@ HRTF::HRTF()
HRTF::~HRTF() {
}
void HRTF::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels,
const Math::Vector3& sourcePosition,
const Math::Vector3& listenerPosition,
const Math::Quaternion& listenerRotation) {
if (!m_enabled || buffer == nullptr || sampleCount == 0) {
void HRTF::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels,
const Math::Vector3& sourcePosition,
const Math::Vector3& listenerPosition,
const Math::Quaternion& listenerRotation) {
if (!m_enabled || buffer == nullptr || frameCount == 0 || channels < 2) {
return;
}
@@ -28,16 +40,21 @@ void HRTF::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels,
ComputeDirection(sourcePosition, listenerPosition, listenerRotation, azimuth, elevation);
ComputeITD(azimuth, elevation);
ComputeILD(azimuth, elevation);
m_params.headShadowing = std::min(1.0f, std::abs(azimuth) / 90.0f);
m_params.pinnaCues = ComputePinnaEffect(azimuth, elevation);
m_params.azimuth = azimuth;
m_params.elevation = elevation;
if (m_hrtfEnabled) {
ApplyHRTF(buffer, sampleCount, channels);
ApplyHRTF(buffer, frameCount, channels);
} else {
float pan = azimuth / 180.0f;
ApplySimplePanning(buffer, sampleCount, channels, pan);
const float pan = std::clamp(azimuth / 90.0f, -1.0f, 1.0f);
ApplySimplePanning(buffer, frameCount, channels, pan);
}
m_prevSourcePosition = sourcePosition;
m_prevListenerPosition = listenerPosition;
}
void HRTF::SetQualityLevel(uint32 level) {
@@ -52,12 +69,24 @@ void HRTF::SetSpeedOfSound(float speed) {
m_speedOfSound = std::max(1.0f, speed);
}
void HRTF::SetSampleRate(uint32 sampleRate) {
m_sampleRate = std::max(1u, sampleRate);
}
void HRTF::ResetState() {
std::fill(m_leftDelayLine.begin(), m_leftDelayLine.end(), 0.0f);
std::fill(m_rightDelayLine.begin(), m_rightDelayLine.end(), 0.0f);
m_leftDelayIndex = 0;
m_rightDelayIndex = 0;
m_prevDopplerShift = 1.0f;
}
void HRTF::ComputeDirection(const Math::Vector3& sourcePosition,
const Math::Vector3& listenerPosition,
const Math::Quaternion& listenerRotation,
float& azimuth, float& elevation) {
const Math::Vector3& listenerPosition,
const Math::Quaternion& listenerRotation,
float& azimuth, float& elevation) {
Math::Vector3 direction = sourcePosition - listenerPosition;
float distance = Math::Vector3::Magnitude(direction);
const float distance = Math::Vector3::Magnitude(direction);
if (distance < 0.001f) {
azimuth = 0.0f;
@@ -66,111 +95,117 @@ void HRTF::ComputeDirection(const Math::Vector3& sourcePosition,
}
direction = Math::Vector3::Normalize(direction);
const Math::Vector3 localDirection = listenerRotation.Inverse() * direction;
Math::Quaternion conjugateRotation = listenerRotation.Inverse();
Math::Vector3 localDirection;
localDirection.x = conjugateRotation.x * direction.x + conjugateRotation.y * direction.y + conjugateRotation.z * direction.z;
localDirection.y = conjugateRotation.x * direction.y - conjugateRotation.y * direction.x + conjugateRotation.w * direction.z;
localDirection.z = conjugateRotation.x * direction.z - conjugateRotation.y * direction.y - conjugateRotation.w * direction.x;
azimuth = std::atan2(localDirection.x, localDirection.z) * 180.0f / PI_F;
elevation = std::asin(std::clamp(localDirection.y, -1.0f, 1.0f)) * 180.0f / PI_F;
Math::Vector3 forward(0.0f, 0.0f, 1.0f);
Math::Vector3 up(0.0f, 1.0f, 0.0f);
float dotForward = Math::Vector3::Dot(forward, localDirection);
float dotUp = Math::Vector3::Dot(up, localDirection);
azimuth = std::atan2(localDirection.x, localDirection.z) * 180.0f / 3.14159265f;
elevation = std::asin(dotUp) * 180.0f / 3.14159265f;
azimuth = std::max(-180.0f, std::min(180.0f, azimuth));
elevation = std::max(-90.0f, std::min(90.0f, elevation));
azimuth = std::clamp(azimuth, -180.0f, 180.0f);
elevation = std::clamp(elevation, -90.0f, 90.0f);
}
void HRTF::ComputeITD(float azimuth, float elevation) {
float headRadius = 0.075f;
(void)elevation;
float cosAzimuth = std::cos(azimuth * 3.14159265f / 180.0f);
float itd = (headRadius / m_speedOfSound) * (cosAzimuth - 1.0f);
m_params.interauralTimeDelay = itd * m_sampleRate;
const float azimuthRadians = DegreesToRadians(azimuth);
const float itdSeconds = (HeadRadiusMeters / m_speedOfSound) * std::sin(azimuthRadians);
m_params.interauralTimeDelay = itdSeconds * static_cast<float>(m_sampleRate);
}
void HRTF::ComputeILD(float azimuth, float elevation) {
float absAzimuth = std::abs(azimuth);
(void)elevation;
float ild = (absAzimuth / 90.0f) * 20.0f;
m_params.interauralLevelDifference = ild;
const float absAzimuth = std::abs(azimuth);
m_params.interauralLevelDifference = (absAzimuth / 90.0f) * 18.0f;
}
float HRTF::ComputePinnaEffect(float azimuth, float elevation) {
float pinnaGain = 1.0f;
if (elevation > 0.0f) {
pinnaGain += elevation / 90.0f * 3.0f;
} else if (elevation < -30.0f) {
pinnaGain -= 3.0f;
}
pinnaGain = std::max(0.5f, std::min(2.0f, pinnaGain));
return pinnaGain;
const float frontalBias = 1.0f - std::min(1.0f, std::abs(azimuth) / 180.0f);
const float elevationBoost = elevation > 0.0f ? (elevation / 90.0f) * 0.25f : 0.0f;
return std::clamp(0.85f + frontalBias * 0.15f + elevationBoost, 0.5f, 1.25f);
}
void HRTF::ApplyHRTF(float* buffer, uint32 sampleCount, uint32 channels) {
if (channels < 2) {
return;
}
void HRTF::ApplyHRTF(float* buffer, uint32 frameCount, uint32 channels) {
const float normalizedAzimuth = std::clamp(m_params.azimuth / 90.0f, -1.0f, 1.0f);
const float panAngle = (normalizedAzimuth + 1.0f) * PI_F * 0.25f;
float leftGain = std::cos(panAngle);
float rightGain = std::sin(panAngle);
float leftGain = 1.0f;
float rightGain = 1.0f;
if (m_params.azimuth < 0.0f) {
rightGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f);
const float ildAttenuation =
std::pow(10.0f, -std::max(0.0f, m_params.interauralLevelDifference) / 20.0f);
if (m_params.azimuth >= 0.0f) {
leftGain *= ildAttenuation;
} else {
leftGain *= (1.0f - std::abs(m_params.azimuth) / 90.0f);
rightGain *= ildAttenuation;
}
float levelReduction = m_params.interauralLevelDifference / 20.0f;
rightGain *= std::pow(10.0f, -levelReduction / 20.0f);
const float pinnaGain = m_params.pinnaCues;
leftGain *= pinnaGain;
rightGain *= pinnaGain;
for (uint32 i = 0; i < sampleCount; ++i) {
uint32 sampleIndex = i * channels;
const uint32 delaySamples = std::min(
static_cast<uint32>(std::abs(std::lround(m_params.interauralTimeDelay))),
MaxDelaySamples - 1);
const bool delayLeftEar = m_params.interauralTimeDelay > 0.0f;
float leftSample = buffer[sampleIndex];
float rightSample = buffer[sampleIndex + 1];
for (uint32 frame = 0; frame < frameCount; ++frame) {
const uint32 sampleIndex = frame * channels;
float delayedLeft = m_leftDelayLine[m_leftDelayIndex];
float delayedRight = m_rightDelayLine[m_rightDelayIndex];
float monoSample = 0.0f;
for (uint32 ch = 0; ch < channels; ++ch) {
monoSample += buffer[sampleIndex + ch];
}
monoSample /= static_cast<float>(channels);
buffer[sampleIndex] = delayedLeft * leftGain;
buffer[sampleIndex + 1] = delayedRight * rightGain;
m_leftDelayLine[m_leftDelayIndex] = monoSample;
m_rightDelayLine[m_rightDelayIndex] = monoSample;
m_leftDelayLine[m_leftDelayIndex] = leftSample;
m_rightDelayLine[m_rightDelayIndex] = rightSample;
const uint32 delayedLeftIndex =
(m_leftDelayIndex + MaxDelaySamples - (delayLeftEar ? delaySamples : 0)) % MaxDelaySamples;
const uint32 delayedRightIndex =
(m_rightDelayIndex + MaxDelaySamples - (delayLeftEar ? 0 : delaySamples)) % MaxDelaySamples;
float leftSample = m_leftDelayLine[delayedLeftIndex] * leftGain;
float rightSample = m_rightDelayLine[delayedRightIndex] * rightGain;
if (m_crossFeed > 0.0f) {
const float cross = std::clamp(m_crossFeed, 0.0f, 1.0f) * 0.5f;
const float mixedLeft = leftSample * (1.0f - cross) + rightSample * cross;
const float mixedRight = rightSample * (1.0f - cross) + leftSample * cross;
leftSample = mixedLeft;
rightSample = mixedRight;
}
buffer[sampleIndex] = leftSample;
buffer[sampleIndex + 1] = rightSample;
for (uint32 ch = 2; ch < channels; ++ch) {
buffer[sampleIndex + ch] = (leftSample + rightSample) * 0.5f;
}
m_leftDelayIndex = (m_leftDelayIndex + 1) % MaxDelaySamples;
m_rightDelayIndex = (m_rightDelayIndex + 1) % MaxDelaySamples;
}
}
void HRTF::ApplySimplePanning(float* buffer, uint32 sampleCount, uint32 channels, float pan) {
if (channels < 2) {
return;
}
void HRTF::ApplySimplePanning(float* buffer, uint32 frameCount, uint32 channels, float pan) {
pan = std::clamp(pan, -1.0f, 1.0f);
const float panAngle = (pan + 1.0f) * PI_F * 0.25f;
const float leftGain = std::cos(panAngle);
const float rightGain = std::sin(panAngle);
pan = std::max(-1.0f, std::min(1.0f, pan));
for (uint32 frame = 0; frame < frameCount; ++frame) {
const uint32 sampleIndex = frame * channels;
float monoSample = 0.0f;
for (uint32 ch = 0; ch < channels; ++ch) {
monoSample += buffer[sampleIndex + ch];
}
monoSample /= static_cast<float>(channels);
float leftGain = (1.0f - pan) / 2.0f;
float rightGain = (1.0f + pan) / 2.0f;
for (uint32 i = 0; i < sampleCount; ++i) {
uint32 sampleIndex = i * channels;
float sample = (buffer[sampleIndex] + buffer[sampleIndex + 1]) / 2.0f;
buffer[sampleIndex] = sample * leftGain;
buffer[sampleIndex + 1] = sample * rightGain;
buffer[sampleIndex] = monoSample * leftGain;
buffer[sampleIndex + 1] = monoSample * rightGain;
for (uint32 ch = 2; ch < channels; ++ch) {
buffer[sampleIndex + ch] = monoSample;
}
}
}

View File

@@ -1,33 +1,27 @@
#include <XCEngine/Audio/Reverbation.h>
#include <algorithm>
#include <cstring>
#include <cmath>
namespace XCEngine {
namespace Audio {
static const uint32 CombTuning[] = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 };
static const uint32 AllPassTuning[] = { 556, 441, 341, 225 };
namespace {
Reverbation::Reverbation()
: m_sampleRate(48000)
{
for (uint32 i = 0; i < CombCount; ++i) {
m_combFilters[i].bufferSize = CombTuning[i];
m_combFilters[i].buffer.resize(m_combFilters[i].bufferSize, 0.0f);
m_combFilters[i].writeIndex = 0;
m_combFilters[i].feedback = 0.0f;
m_combFilters[i].damp1 = 0.0f;
m_combFilters[i].damp2 = 0.0f;
m_combFilters[i].filterStore = 0.0f;
}
static const uint32 CombTuning[] = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
static const uint32 AllPassTuning[] = {556, 441, 341, 225};
static constexpr uint32 StereoCombSpread = 23;
static constexpr uint32 StereoAllPassSpread = 11;
static constexpr float WetScale = 0.015f;
for (uint32 i = 0; i < AllPassCount; ++i) {
m_allPassFilters[i].bufferSize = AllPassTuning[i];
m_allPassFilters[i].buffer.resize(m_allPassFilters[i].bufferSize, 0.0f);
m_allPassFilters[i].writeIndex = 0;
m_allPassFilters[i].feedback = 0.5f;
}
uint32 ScaleDelayLength(uint32 tuning, uint32 sampleRate) {
const float scaled = static_cast<float>(tuning) * static_cast<float>(sampleRate) / 48000.0f;
return std::max(1u, static_cast<uint32>(std::lround(scaled)));
}
} // namespace
Reverbation::Reverbation() {
SetWetMix(0.3f);
SetRoomSize(0.5f);
SetDamping(0.5f);
}
@@ -35,72 +29,54 @@ Reverbation::Reverbation()
Reverbation::~Reverbation() {
}
void Reverbation::ProcessAudio(float* buffer, uint32 sampleCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || sampleCount == 0) {
void Reverbation::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) {
if (!m_enabled || buffer == nullptr || frameCount == 0 || channels == 0) {
return;
}
if (channels == 0) {
if (m_wetMix <= 0.0f && m_dryMix >= 1.0f) {
return;
}
for (uint32 i = 0; i < sampleCount; ++i) {
float input = buffer[i * channels];
EnsureChannelState(channels);
float wet = 0.0f;
for (uint32 c = 0; c < CombCount; ++c) {
float output = m_combFilters[c].buffer[m_combFilters[c].writeIndex];
m_combFilters[c].filterStore = output * m_combFilters[c].damp2 + m_combFilters[c].filterStore * m_combFilters[c].damp1;
m_combFilters[c].buffer[m_combFilters[c].writeIndex] = input + m_combFilters[c].filterStore * m_combFilters[c].feedback;
m_combFilters[c].writeIndex++;
if (m_combFilters[c].writeIndex >= m_combFilters[c].bufferSize) {
m_combFilters[c].writeIndex = 0;
}
wet += output;
}
for (uint32 a = 0; a < AllPassCount; ++a) {
float output = m_allPassFilters[a].buffer[m_allPassFilters[a].writeIndex];
float temp = output;
m_allPassFilters[a].buffer[m_allPassFilters[a].writeIndex] = wet + output * m_allPassFilters[a].feedback;
wet = -wet + output;
wet += temp;
m_allPassFilters[a].writeIndex++;
if (m_allPassFilters[a].writeIndex >= m_allPassFilters[a].bufferSize) {
m_allPassFilters[a].writeIndex = 0;
}
}
float outSample = input * m_dryMix + wet * m_wetMix;
const size_t sampleCount = static_cast<size_t>(frameCount) * channels;
std::vector<float> dryBuffer(buffer, buffer + sampleCount);
std::vector<float> wetBuffer(sampleCount, 0.0f);
for (uint32 frame = 0; frame < frameCount; ++frame) {
const size_t frameOffset = static_cast<size_t>(frame) * channels;
for (uint32 ch = 0; ch < channels; ++ch) {
buffer[i * channels + ch] = outSample;
wetBuffer[frameOffset + ch] = ProcessChannelSample(ch, dryBuffer[frameOffset + ch]);
}
for (uint32 ch = 0; ch + 1 < channels; ch += 2) {
const size_t leftIndex = frameOffset + ch;
const size_t rightIndex = leftIndex + 1;
const float leftWet = wetBuffer[leftIndex];
const float rightWet = wetBuffer[rightIndex];
const float width = std::clamp(m_width, 0.0f, 1.0f);
const float leftMix = 0.5f + 0.5f * width;
const float rightMix = 0.5f - 0.5f * width;
wetBuffer[leftIndex] = leftWet * leftMix + rightWet * rightMix;
wetBuffer[rightIndex] = rightWet * leftMix + leftWet * rightMix;
}
}
for (size_t i = 0; i < sampleCount; ++i) {
buffer[i] = dryBuffer[i] * m_dryMix + wetBuffer[i] * m_wetMix;
}
}
void Reverbation::SetRoomSize(float size) {
m_roomSize = std::max(0.0f, std::min(1.0f, size));
float roomScale = 0.28f + 0.7f * m_roomSize;
for (uint32 i = 0; i < CombCount; ++i) {
m_combFilters[i].feedback = roomScale;
}
UpdateCombParameters();
}
void Reverbation::SetDamping(float damping) {
m_damping = std::max(0.0f, std::min(1.0f, damping));
for (uint32 i = 0; i < CombCount; ++i) {
m_combFilters[i].damp1 = m_damping * 0.4f;
m_combFilters[i].damp2 = 1.0f - m_damping * 0.4f;
}
UpdateCombParameters();
}
void Reverbation::SetWetMix(float wetMix) {
@@ -119,5 +95,122 @@ void Reverbation::SetFreeze(bool freeze) {
m_freeze = freeze;
}
void Reverbation::SetSampleRate(uint32 sampleRate) {
const uint32 clampedSampleRate = sampleRate > 0 ? sampleRate : 1u;
if (m_sampleRate == clampedSampleRate) {
return;
}
IAudioEffect::SetSampleRate(clampedSampleRate);
if (m_stateChannels == 0) {
return;
}
const uint32 channels = m_stateChannels;
m_stateChannels = 0;
EnsureChannelState(channels);
}
void Reverbation::ResetState() {
for (ChannelState& state : m_channelStates) {
for (uint32 i = 0; i < CombCount; ++i) {
std::fill(state.combFilters[i].buffer.begin(), state.combFilters[i].buffer.end(), 0.0f);
state.combFilters[i].writeIndex = 0;
state.combFilters[i].filterStore = 0.0f;
}
for (uint32 i = 0; i < AllPassCount; ++i) {
std::fill(state.allPassFilters[i].buffer.begin(), state.allPassFilters[i].buffer.end(), 0.0f);
state.allPassFilters[i].writeIndex = 0;
}
}
}
void Reverbation::EnsureChannelState(uint32 channels) {
if (m_stateChannels == channels && m_channelStates.size() == channels) {
return;
}
m_channelStates.assign(channels, ChannelState{});
m_stateChannels = channels;
for (uint32 ch = 0; ch < channels; ++ch) {
InitializeChannelState(m_channelStates[ch], ch);
}
UpdateCombParameters();
ResetState();
}
void Reverbation::InitializeChannelState(ChannelState& state, uint32 channelIndex) {
const uint32 combSpread = (channelIndex % 2 == 1) ? StereoCombSpread : 0u;
const uint32 allPassSpread = (channelIndex % 2 == 1) ? StereoAllPassSpread : 0u;
for (uint32 i = 0; i < CombCount; ++i) {
CombFilter& comb = state.combFilters[i];
comb.bufferSize = std::min(
MaxDelayLength,
ScaleDelayLength(CombTuning[i] + combSpread, m_sampleRate));
comb.buffer.assign(comb.bufferSize, 0.0f);
comb.writeIndex = 0;
comb.filterStore = 0.0f;
}
for (uint32 i = 0; i < AllPassCount; ++i) {
AllPassFilter& allPass = state.allPassFilters[i];
allPass.bufferSize = std::min(
MaxDelayLength,
ScaleDelayLength(AllPassTuning[i] + allPassSpread, m_sampleRate));
allPass.buffer.assign(allPass.bufferSize, 0.0f);
allPass.writeIndex = 0;
allPass.feedback = 0.5f;
}
}
void Reverbation::UpdateCombParameters() {
const float roomScale = 0.28f + 0.7f * m_roomSize;
const float damp1 = m_damping * 0.4f;
const float damp2 = 1.0f - damp1;
for (ChannelState& state : m_channelStates) {
for (uint32 i = 0; i < CombCount; ++i) {
state.combFilters[i].feedback = roomScale;
state.combFilters[i].damp1 = damp1;
state.combFilters[i].damp2 = damp2;
}
}
}
float Reverbation::ProcessChannelSample(uint32 channelIndex, float inputSample) {
if (channelIndex >= m_channelStates.size()) {
return 0.0f;
}
ChannelState& state = m_channelStates[channelIndex];
const float effectiveInput = m_freeze ? 0.0f : inputSample;
float wet = 0.0f;
for (uint32 i = 0; i < CombCount; ++i) {
CombFilter& comb = state.combFilters[i];
const float output = comb.buffer[comb.writeIndex];
const float damp1 = m_freeze ? 0.0f : comb.damp1;
const float damp2 = m_freeze ? 1.0f : comb.damp2;
const float feedback = m_freeze ? 1.0f : comb.feedback;
comb.filterStore = output * damp2 + comb.filterStore * damp1;
comb.buffer[comb.writeIndex] = effectiveInput + comb.filterStore * feedback;
comb.writeIndex = (comb.writeIndex + 1) % comb.bufferSize;
wet += output;
}
for (uint32 i = 0; i < AllPassCount; ++i) {
AllPassFilter& allPass = state.allPassFilters[i];
const float buffered = allPass.buffer[allPass.writeIndex];
allPass.buffer[allPass.writeIndex] = wet + buffered * allPass.feedback;
wet = buffered - wet;
allPass.writeIndex = (allPass.writeIndex + 1) % allPass.bufferSize;
}
return wet * WetScale;
}
} // namespace Audio
} // namespace XCEngine

View File

@@ -3,6 +3,7 @@
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/IO/ResourceFileSystem.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
#include <XCEngine/Resources/AudioClip/AudioLoader.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Resources/Model/ModelLoader.h>
@@ -47,6 +48,7 @@ void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) {
}
GaussianSplatLoader g_gaussianSplatLoader;
AudioLoader g_audioLoader;
MaterialLoader g_materialLoader;
ModelLoader g_modelLoader;
MeshLoader g_meshLoader;
@@ -93,6 +95,7 @@ void ResourceManager::EnsureInitialized() {
asyncLoader->Initialize(2);
RegisterBuiltinLoader(*this, g_gaussianSplatLoader);
RegisterBuiltinLoader(*this, g_audioLoader);
RegisterBuiltinLoader(*this, g_materialLoader);
RegisterBuiltinLoader(*this, g_modelLoader);
RegisterBuiltinLoader(*this, g_meshLoader);

View File

@@ -13,52 +13,89 @@ struct WAVHeader {
uint32_t channels = 2;
uint32_t bitsPerSample = 16;
uint32_t dataSize = 0;
uint32_t dataOffset = 44;
uint32_t dataOffset = 0;
};
uint16_t ReadLE16(const uint8_t* data) {
return static_cast<uint16_t>(data[0]) |
(static_cast<uint16_t>(data[1]) << 8);
}
uint32_t ReadLE32(const uint8_t* data) {
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}
bool ParseWAVHeader(const uint8_t* data, size_t size, WAVHeader& header) {
if (size < 44) {
if (size < 12) {
return false;
}
if (data[0] != 'R' || data[1] != 'I' || data[2] != 'F' || data[3] != 'F') {
if (std::memcmp(data, "RIFF", 4) != 0) {
return false;
}
if (data[8] != 'W' || data[9] != 'A' || data[10] != 'V' || data[11] != 'E') {
if (std::memcmp(data + 8, "WAVE", 4) != 0) {
return false;
}
if (data[12] != 'f' || data[13] != 'm' || data[14] != 't' || data[15] != ' ') {
bool foundFormatChunk = false;
bool foundDataChunk = false;
size_t offset = 12;
while (offset + 8 <= size) {
const uint8_t* chunk = data + offset;
const uint32_t chunkSize = ReadLE32(chunk + 4);
const size_t chunkDataOffset = offset + 8;
if (chunkDataOffset > size || chunkSize > size - chunkDataOffset) {
return false;
}
if (std::memcmp(chunk, "fmt ", 4) == 0) {
if (chunkSize < 16) {
return false;
}
const uint16_t audioFormat = ReadLE16(chunk + 8);
if (audioFormat != 1) {
return false;
}
header.channels = ReadLE16(chunk + 10);
header.sampleRate = ReadLE32(chunk + 12);
header.bitsPerSample = ReadLE16(chunk + 22);
foundFormatChunk = true;
} else if (std::memcmp(chunk, "data", 4) == 0) {
header.dataOffset = static_cast<uint32_t>(chunkDataOffset);
header.dataSize = chunkSize;
foundDataChunk = true;
}
offset = chunkDataOffset + chunkSize + (chunkSize & 1u);
}
if (!foundFormatChunk || !foundDataChunk) {
return false;
}
uint32_t subchunk1Size = *reinterpret_cast<const uint32_t*>(&data[16]);
if (subchunk1Size < 16) {
if (header.channels == 0 || header.sampleRate == 0 || header.bitsPerSample == 0) {
return false;
}
uint16_t audioFormat = *reinterpret_cast<const uint16_t*>(&data[20]);
if (audioFormat != 1) {
if ((header.bitsPerSample % 8u) != 0u) {
return false;
}
header.channels = *reinterpret_cast<const uint16_t*>(&data[22]);
header.sampleRate = *reinterpret_cast<const uint32_t*>(&data[24]);
header.bitsPerSample = *reinterpret_cast<const uint16_t*>(&data[34]);
if (data[36] != 'd' || data[37] != 'a' || data[38] != 't' || data[39] != 'a') {
const uint32_t bytesPerFrame =
static_cast<uint32_t>(header.channels) * (header.bitsPerSample / 8u);
if (bytesPerFrame == 0 || (header.dataSize % bytesPerFrame) != 0u) {
return false;
}
header.dataSize = *reinterpret_cast<const uint32_t*>(&data[40]);
header.dataOffset = 44;
if (header.dataOffset + header.dataSize > size) {
return false;
}
return true;
return header.dataOffset + header.dataSize <= size;
}
} // namespace
@@ -70,18 +107,12 @@ AudioLoader::~AudioLoader() = default;
Containers::Array<Containers::String> AudioLoader::GetSupportedExtensions() const {
Containers::Array<Containers::String> extensions;
extensions.PushBack("wav");
extensions.PushBack("ogg");
extensions.PushBack("mp3");
extensions.PushBack("flac");
extensions.PushBack("aiff");
extensions.PushBack("aif");
return extensions;
}
bool AudioLoader::CanLoad(const Containers::String& path) const {
Containers::String ext = GetExtension(path);
return ext == "wav" || ext == "ogg" || ext == "mp3" ||
ext == "flac" || ext == "aiff" || ext == "aif";
return ext == "wav";
}
LoadResult AudioLoader::Load(const Containers::String& path, const ImportSettings* settings) {
@@ -96,18 +127,21 @@ LoadResult AudioLoader::Load(const Containers::String& path, const ImportSetting
audioClip->m_guid = ResourceGUID::Generate(path);
AudioFormat format = DetectAudioFormat(path, data);
if (format == AudioFormat::WAV) {
if (!ParseWAVData(data, audioClip)) {
delete audioClip;
return LoadResult("Failed to parse WAV data");
}
if (format != AudioFormat::WAV) {
delete audioClip;
return LoadResult("Unsupported audio format: " + path);
}
if (!ParseWAVData(data, audioClip)) {
delete audioClip;
return LoadResult("Failed to parse WAV data");
}
audioClip->SetAudioFormat(format);
audioClip->SetAudioData(data);
audioClip->m_isValid = true;
audioClip->m_memorySize = sizeof(AudioClip) + audioClip->m_name.Length() +
audioClip->m_path.Length() + audioClip->GetAudioData().Size();
audioClip->m_path.Length() + audioClip->GetPCMData().Size();
return LoadResult(audioClip);
}
@@ -117,6 +151,10 @@ ImportSettings* AudioLoader::GetDefaultSettings() const {
}
bool AudioLoader::ParseWAVData(const Containers::Array<Core::uint8>& data, AudioClip* audioClip) {
if (audioClip == nullptr) {
return false;
}
WAVHeader header;
if (!ParseWAVHeader(data.Data(), data.Size(), header)) {
return false;
@@ -126,10 +164,15 @@ bool AudioLoader::ParseWAVData(const Containers::Array<Core::uint8>& data, Audio
audioClip->SetChannels(header.channels);
audioClip->SetBitsPerSample(header.bitsPerSample);
uint32_t bytesPerSample = header.bitsPerSample / 8;
uint32_t totalSamples = header.dataSize / (bytesPerSample * header.channels);
float duration = static_cast<float>(totalSamples) / (header.sampleRate * header.channels);
audioClip->SetDuration(duration);
Containers::Array<Core::uint8> pcmData;
pcmData.ResizeUninitialized(header.dataSize);
if (header.dataSize > 0) {
std::memcpy(
pcmData.Data(),
data.Data() + header.dataOffset,
header.dataSize);
}
audioClip->SetPCMData(pcmData);
return true;
}

View File

@@ -4,7 +4,11 @@ project(XCEngine_ComponentsTests)
set(COMPONENTS_TEST_SOURCES
test_component.cpp
test_audio_mixer.cpp
test_audio_system.cpp
test_windows_audio_backend.cpp
test_component_factory_registry.cpp
test_audio_source_component.cpp
test_transform_component.cpp
test_game_object.cpp
test_camera_light_component.cpp

View File

@@ -65,7 +65,10 @@ TEST(AudioLoader, ParseWAV_Mono44100_16bit) {
EXPECT_EQ(clip->GetSampleRate(), 44100u);
EXPECT_EQ(clip->GetChannels(), 1u);
EXPECT_EQ(clip->GetBitsPerSample(), 16u);
EXPECT_GT(clip->GetDuration(), 0.0f);
EXPECT_EQ(clip->GetPCMData().Size(), 44100u * sizeof(int16_t));
EXPECT_EQ(clip->GetFrameCount(), 44100u);
EXPECT_EQ(clip->GetSampleCount(), 44100u);
EXPECT_FLOAT_EQ(clip->GetDuration(), 1.0f);
}
std::remove(testPath);
@@ -84,6 +87,10 @@ TEST(AudioLoader, ParseWAV_Stereo48000_16bit) {
EXPECT_EQ(clip->GetSampleRate(), 48000u);
EXPECT_EQ(clip->GetChannels(), 2u);
EXPECT_EQ(clip->GetBitsPerSample(), 16u);
EXPECT_EQ(clip->GetPCMData().Size(), 4800u * 2u * sizeof(int16_t));
EXPECT_EQ(clip->GetFrameCount(), 4800u);
EXPECT_EQ(clip->GetSampleCount(), 9600u);
EXPECT_FLOAT_EQ(clip->GetDuration(), 0.1f);
}
std::remove(testPath);
@@ -97,14 +104,15 @@ TEST(AudioLoader, GetResourceType) {
TEST(AudioLoader, GetSupportedExtensions) {
AudioLoader loader;
auto extensions = loader.GetSupportedExtensions();
EXPECT_GE(extensions.Size(), 1u);
ASSERT_EQ(extensions.Size(), 1u);
EXPECT_EQ(extensions[0], XCEngine::Containers::String("wav"));
}
TEST(AudioLoader, CanLoad) {
AudioLoader loader;
EXPECT_TRUE(loader.CanLoad("test.wav"));
EXPECT_TRUE(loader.CanLoad("test.mp3"));
EXPECT_TRUE(loader.CanLoad("test.ogg"));
EXPECT_FALSE(loader.CanLoad("test.mp3"));
EXPECT_FALSE(loader.CanLoad("test.ogg"));
EXPECT_FALSE(loader.CanLoad("test.txt"));
EXPECT_FALSE(loader.CanLoad("test.png"));
}
@@ -115,4 +123,13 @@ TEST(AudioLoader, LoadInvalidPath) {
EXPECT_FALSE(result);
}
TEST(AudioLoader, ResourceManagerRegistersAudioLoader) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
EXPECT_NE(manager.GetLoader(ResourceType::AudioClip), nullptr);
manager.Shutdown();
}
} // namespace