- Implement ParseWAVData and ParseWAVHeader in AudioLoader to properly parse WAV file headers (sample rate, channels, bits per sample, duration) - Modify Load() to call ParseWAVData for WAV files during loading - Add DecodeAudioData() to AudioSourceComponent to decode PCM bytes to float - Update SetClip() to trigger audio decoding - Fix ProcessAudio() to read from decoded data instead of empty output buffer - Add WAV parsing unit tests (ParseWAV_Mono44100_16bit, ParseWAV_Stereo48000_16bit) Fixes issues: - AudioLoader::ParseWAVData was a stub returning true without parsing - AudioLoader::Load didn't extract audio metadata from WAV headers - AudioSourceComponent::ProcessAudio read from empty m_outputBuffer All 167 tests pass.
325 lines
9.1 KiB
C++
325 lines
9.1 KiB
C++
#include <XCEngine/Components/AudioSourceComponent.h>
|
|
#include <XCEngine/Audio/AudioSystem.h>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
namespace XCEngine {
|
|
namespace Components {
|
|
|
|
AudioSourceComponent::AudioSourceComponent()
|
|
: m_outputBuffer(BufferSize * 2, 0.0f)
|
|
{
|
|
}
|
|
|
|
AudioSourceComponent::~AudioSourceComponent() {
|
|
if (m_playState == Audio::PlayState::Playing) {
|
|
Audio::AudioSystem::Get().UnregisterSource(this);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::Play() {
|
|
if (!m_clip || !m_clip->IsValid()) {
|
|
return;
|
|
}
|
|
|
|
if (m_playState == Audio::PlayState::Paused) {
|
|
m_playState = Audio::PlayState::Playing;
|
|
return;
|
|
}
|
|
|
|
m_samplePosition = 0;
|
|
m_lastingTime = 0.0;
|
|
m_playState = Audio::PlayState::Playing;
|
|
Audio::AudioSystem::Get().RegisterSource(this);
|
|
}
|
|
|
|
void AudioSourceComponent::Pause() {
|
|
if (m_playState == Audio::PlayState::Playing) {
|
|
m_playState = Audio::PlayState::Paused;
|
|
Audio::AudioSystem::Get().UnregisterSource(this);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::Stop(Audio::StopMode mode) {
|
|
if (m_playState != Audio::PlayState::Stopped) {
|
|
m_playState = Audio::PlayState::Stopped;
|
|
m_samplePosition = 0;
|
|
Audio::AudioSystem::Get().UnregisterSource(this);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::SetClip(Resources::AudioClip* clip) {
|
|
m_clip = clip;
|
|
m_isDecoded = false;
|
|
if (clip && clip->IsValid()) {
|
|
DecodeAudioData();
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::SetVolume(float volume) {
|
|
m_volume = std::max(0.0f, std::min(1.0f, volume));
|
|
}
|
|
|
|
void AudioSourceComponent::SetPitch(float pitch) {
|
|
m_pitch = std::max(0.0f, std::min(3.0f, pitch));
|
|
}
|
|
|
|
void AudioSourceComponent::SetPan(float pan) {
|
|
m_pan = std::max(-1.0f, std::min(1.0f, pan));
|
|
}
|
|
|
|
void AudioSourceComponent::SetLooping(bool loop) {
|
|
m_isLooping = loop;
|
|
}
|
|
|
|
void AudioSourceComponent::SetSpatialize(bool spatialize) {
|
|
m_spatialize = spatialize;
|
|
}
|
|
|
|
void AudioSourceComponent::Set3DParams(const Audio::Audio3DParams& params) {
|
|
m_3DParams = params;
|
|
}
|
|
|
|
void AudioSourceComponent::SetDopplerLevel(float level) {
|
|
m_3DParams.dopplerLevel = level;
|
|
}
|
|
|
|
void AudioSourceComponent::SetSpread(float spread) {
|
|
m_3DParams.spread = std::max(0.0f, std::min(1.0f, spread));
|
|
}
|
|
|
|
void AudioSourceComponent::SetReverbZoneMix(float mix) {
|
|
m_3DParams.reverbZoneMix = std::max(0.0f, std::min(1.0f, mix));
|
|
}
|
|
|
|
void AudioSourceComponent::SetOutputMixer(Audio::AudioMixer* mixer) {
|
|
m_outputMixer = mixer;
|
|
}
|
|
|
|
void AudioSourceComponent::SetTime(float seconds) {
|
|
if (!m_clip || !m_clip->IsValid()) {
|
|
return;
|
|
}
|
|
|
|
Audio::uint32 sampleRate = m_clip->GetSampleRate();
|
|
Audio::uint32 channels = m_clip->GetChannels();
|
|
Audio::uint64 sampleOffset = static_cast<Audio::uint64>(seconds * sampleRate * channels);
|
|
m_samplePosition = sampleOffset;
|
|
m_lastingTime = seconds;
|
|
}
|
|
|
|
float AudioSourceComponent::GetTime() const {
|
|
return static_cast<float>(m_lastingTime);
|
|
}
|
|
|
|
float AudioSourceComponent::GetDuration() const {
|
|
if (!m_clip || !m_clip->IsValid()) {
|
|
return 0.0f;
|
|
}
|
|
return m_clip->GetDuration();
|
|
}
|
|
|
|
void AudioSourceComponent::StartEnergyDetect() {
|
|
m_isEnergyDetecting = true;
|
|
m_energyHistory.clear();
|
|
}
|
|
|
|
void AudioSourceComponent::StopEnergyDetect() {
|
|
m_isEnergyDetecting = false;
|
|
}
|
|
|
|
void AudioSourceComponent::Update(float deltaTime) {
|
|
if (m_playState != Audio::PlayState::Playing || !m_clip) {
|
|
return;
|
|
}
|
|
|
|
m_lastingTime += deltaTime * m_pitch;
|
|
|
|
Audio::uint32 channels = m_clip->GetChannels();
|
|
Audio::uint32 sampleRate = m_clip->GetSampleRate();
|
|
Audio::uint64 samplesPerSecond = sampleRate * channels;
|
|
Audio::uint64 samplesToAdvance = static_cast<Audio::uint64>(deltaTime * m_pitch * samplesPerSecond);
|
|
m_samplePosition += samplesToAdvance;
|
|
|
|
Audio::uint64 totalSamples = static_cast<Audio::uint64>(m_clip->GetAudioData().Size()) / (m_clip->GetBitsPerSample() / 8);
|
|
|
|
if (m_samplePosition >= totalSamples) {
|
|
if (m_isLooping) {
|
|
m_samplePosition = m_samplePosition % totalSamples;
|
|
} else {
|
|
Stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::OnEnable() {
|
|
if (m_playState == Audio::PlayState::Playing) {
|
|
Audio::AudioSystem::Get().RegisterSource(this);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::OnDisable() {
|
|
if (m_playState == Audio::PlayState::Playing) {
|
|
Audio::AudioSystem::Get().UnregisterSource(this);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::OnDestroy() {
|
|
Stop();
|
|
}
|
|
|
|
void AudioSourceComponent::DecodeAudioData() {
|
|
if (!m_clip || !m_clip->IsValid()) {
|
|
return;
|
|
}
|
|
|
|
if (m_isDecoded) {
|
|
return;
|
|
}
|
|
|
|
const auto& audioData = m_clip->GetAudioData();
|
|
if (audioData.Empty()) {
|
|
return;
|
|
}
|
|
|
|
Audio::uint32 channels = m_clip->GetChannels();
|
|
Audio::uint32 bitsPerSample = m_clip->GetBitsPerSample();
|
|
uint32_t bytesPerSample = bitsPerSample / 8;
|
|
uint32_t totalSamples = static_cast<uint32_t>(audioData.Size()) / bytesPerSample;
|
|
|
|
m_decodedData.resize(totalSamples);
|
|
|
|
const uint8_t* rawData = audioData.Data();
|
|
|
|
if (bitsPerSample == 16) {
|
|
const int16_t* samples16 = reinterpret_cast<const int16_t*>(rawData);
|
|
for (uint32_t i = 0; i < totalSamples; ++i) {
|
|
m_decodedData[i] = samples16[i] / 32768.0f;
|
|
}
|
|
} else if (bitsPerSample == 8) {
|
|
for (uint32_t i = 0; i < totalSamples; ++i) {
|
|
m_decodedData[i] = (rawData[i] - 128) / 128.0f;
|
|
}
|
|
} else if (bitsPerSample == 24) {
|
|
for (uint32_t i = 0; i < totalSamples; ++i) {
|
|
int32_t sample = (rawData[i * 3] | (rawData[i * 3 + 1] << 8) | (rawData[i * 3 + 2] << 16));
|
|
if (sample & 0x800000) {
|
|
sample |= 0xFF000000;
|
|
}
|
|
m_decodedData[i] = sample / 8388608.0f;
|
|
}
|
|
} else if (bitsPerSample == 32) {
|
|
const int32_t* samples32 = reinterpret_cast<const int32_t*>(rawData);
|
|
for (uint32_t i = 0; i < totalSamples; ++i) {
|
|
m_decodedData[i] = samples32[i] / 2147483648.0f;
|
|
}
|
|
}
|
|
|
|
m_isDecoded = true;
|
|
}
|
|
|
|
void AudioSourceComponent::ProcessAudio(float* buffer, Audio::uint32 sampleCount, Audio::uint32 channels,
|
|
const Math::Vector3& listenerPosition,
|
|
const Math::Quaternion& listenerRotation) {
|
|
if (m_playState != Audio::PlayState::Playing || !m_clip || !m_isDecoded) {
|
|
return;
|
|
}
|
|
|
|
if (channels == 0 || sampleCount == 0) {
|
|
return;
|
|
}
|
|
|
|
if (m_decodedData.empty()) {
|
|
return;
|
|
}
|
|
|
|
float volume = m_volume;
|
|
if (m_spatialize) {
|
|
Apply3DAttenuation(listenerPosition);
|
|
volume *= m_volume;
|
|
}
|
|
|
|
Audio::uint32 clipChannels = m_clip->GetChannels();
|
|
Audio::uint64 totalSamples = static_cast<Audio::uint64>(m_decodedData.size());
|
|
Audio::uint64 samplesPerFrame = sampleCount * channels;
|
|
|
|
for (Audio::uint32 i = 0; i < sampleCount; ++i) {
|
|
for (Audio::uint32 ch = 0; ch < channels; ++ch) {
|
|
Audio::uint64 outputIndex = m_samplePosition + i * channels + ch;
|
|
|
|
if (outputIndex >= totalSamples) {
|
|
if (m_isLooping && totalSamples > 0) {
|
|
outputIndex = outputIndex % totalSamples;
|
|
} else {
|
|
buffer[i * channels + ch] += 0.0f;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Audio::uint64 decodedChannel = (ch < clipChannels) ? ch : (clipChannels - 1);
|
|
Audio::uint64 decodedIndex = (outputIndex / channels) * clipChannels + decodedChannel;
|
|
float sample = m_decodedData[decodedIndex];
|
|
|
|
buffer[i * channels + ch] += sample * volume;
|
|
}
|
|
}
|
|
|
|
m_samplePosition += samplesPerFrame;
|
|
|
|
if (m_samplePosition >= totalSamples) {
|
|
if (m_isLooping) {
|
|
m_samplePosition = m_samplePosition % totalSamples;
|
|
} else {
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
if (m_isEnergyDetecting) {
|
|
UpdateEnergy(buffer, sampleCount * channels);
|
|
}
|
|
}
|
|
|
|
void AudioSourceComponent::Apply3DAttenuation(const Math::Vector3& listenerPosition) {
|
|
if (!m_gameObject) {
|
|
return;
|
|
}
|
|
|
|
Math::Vector3 sourcePosition = transform().GetPosition();
|
|
Math::Vector3 direction = sourcePosition - listenerPosition;
|
|
float distance = direction.Magnitude();
|
|
|
|
if (distance > m_3DParams.maxDistance) {
|
|
m_volume = 0.0f;
|
|
return;
|
|
}
|
|
|
|
float normalizedDistance = distance / m_3DParams.maxDistance;
|
|
normalizedDistance = std::max(0.0f, std::min(1.0f, normalizedDistance));
|
|
|
|
float attenuation = 1.0f - normalizedDistance;
|
|
attenuation = std::pow(attenuation, 2.0f);
|
|
|
|
m_volume *= attenuation;
|
|
}
|
|
|
|
void AudioSourceComponent::UpdateEnergy(const float* buffer, Audio::uint32 sampleCount) {
|
|
if (!buffer || sampleCount == 0) {
|
|
return;
|
|
}
|
|
|
|
float sumSquares = 0.0f;
|
|
for (Audio::uint32 i = 0; i < sampleCount; ++i) {
|
|
sumSquares += buffer[i] * buffer[i];
|
|
}
|
|
|
|
m_energy = std::sqrt(sumSquares / static_cast<float>(sampleCount));
|
|
|
|
m_energyHistory.push_back(m_energy);
|
|
if (m_energyHistory.size() > 10) {
|
|
m_energyHistory.pop_front();
|
|
}
|
|
}
|
|
|
|
} // namespace Components
|
|
} // namespace XCEngine
|