feat(audio): formalize runtime effects and wav loading
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user