#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TestUtils.h" using namespace test; namespace { void put16(std::vector& bytes, std::uint16_t value) { bytes.push_back(static_cast(value & 0xFF)); bytes.push_back(static_cast(value >> 8)); } void put32(std::vector& bytes, std::uint32_t value) { for (int shift = 0; shift < 32; shift += 8) { bytes.push_back(static_cast((value >> shift) & 0xFF)); } } void putTag(std::vector& bytes, const char* tag) { bytes.insert(bytes.end(), tag, tag + 4); } std::vector makeWav(std::uint16_t format, std::uint16_t channels, std::uint32_t sampleRate, std::uint16_t bits, const std::vector& data, const std::vector& extraChunk = {}) { std::vector body; putTag(body, "WAVE"); putTag(body, "fmt "); put32(body, 16); put16(body, format); put16(body, channels); put32(body, sampleRate); put32(body, sampleRate * channels * bits / 8u); put16(body, static_cast(channels * bits / 8u)); put16(body, bits); body.insert(body.end(), extraChunk.begin(), extraChunk.end()); putTag(body, "data"); put32(body, static_cast(data.size())); body.insert(body.end(), data.begin(), data.end()); std::vector bytes; putTag(bytes, "RIFF"); put32(bytes, static_cast(body.size())); bytes.insert(bytes.end(), body.begin(), body.end()); return bytes; } void testDecodeWav16Mono() { std::vector data; for (const std::int16_t sample : {std::int16_t{0}, std::int16_t{1000}, std::int16_t{-1000}, std::int16_t{32767}}) { put16(data, static_cast(sample)); } const auto decoded = AudioDecoder::DecodeWav(makeWav(1, 1, 22050, 16, data)); check(decoded.has_value(), "16-bit mono wav decodes"); check(decoded->channels == 1 && decoded->sampleRate == 22050, "wav format is read"); check(decoded->samples == std::vector{0, 1000, -1000, 32767}, "16-bit samples are copied as is"); check(close(decoded->Duration(), 4.0f / 22050.0f), "duration comes from frames and rate"); } void testDecodeWavConvertsFormats() { const auto eightBit = AudioDecoder::DecodeWav(makeWav(1, 2, 8000, 8, {128, 255, 0, 128})); check(eightBit.has_value() && eightBit->channels == 2, "8-bit stereo wav decodes"); check(eightBit->samples == std::vector{0, 127 << 8, -32768, 0}, "8-bit samples are unsigned around 128"); const auto twentyFour = AudioDecoder::DecodeWav(makeWav(1, 1, 48000, 24, {0x00, 0x34, 0x12, 0x00, 0x00, 0x80})); check(twentyFour.has_value(), "24-bit wav decodes"); check(twentyFour->samples == std::vector{0x1234, -32768}, "24-bit keeps the top 16 bits"); std::vector floats; for (const float value : {0.5f, -1.0f, 2.0f}) { std::uint32_t raw = 0; std::memcpy(&raw, &value, sizeof(raw)); put32(floats, raw); } const auto floatWav = AudioDecoder::DecodeWav(makeWav(3, 1, 44100, 32, floats)); check(floatWav.has_value(), "float wav decodes"); check(floatWav->samples == std::vector{16384, -32767, 32767}, "float samples are scaled and clamped"); } void testDecodeWavSkipsPaddedChunks() { std::vector list; putTag(list, "LIST"); put32(list, 3); list.insert(list.end(), {'a', 'b', 'c', 0}); std::vector data; put16(data, 42); const auto decoded = AudioDecoder::DecodeWav(makeWav(1, 1, 8000, 16, data, list)); check(decoded.has_value() && decoded->samples == std::vector{42}, "odd sized chunks are padded"); } void testDecodeRejectsBadFiles() { std::vector data; put16(data, 0); check(!AudioDecoder::DecodeWav(makeWav(1, 6, 48000, 16, data)), "surround wav is rejected"); check(!AudioDecoder::DecodeWav(makeWav(2, 1, 48000, 4, data)), "compressed wav is rejected"); check(!AudioDecoder::DecodeWav(makeWav(1, 1, 0, 16, data)), "wav without a sample rate is rejected"); auto truncated = makeWav(1, 1, 8000, 16, data); truncated.resize(30); check(!AudioDecoder::DecodeWav(truncated), "wav without data is rejected"); const std::vector garbage{'O', 'g', 'g', 'S', 1, 2, 3, 4, 5, 6, 7, 8}; check(!AudioDecoder::DecodeWav(garbage), "non wav bytes are rejected"); check(!AudioDecoder::DecodeOgg(garbage), "broken ogg is rejected"); check(!AudioDecoder::Decode(data, "game://sound.mp3"), "unknown extensions are rejected"); } void testAudioSourceSerialization() { RegisterEngineComponents(); check(ComponentFactory::IsRegistered("AudioSource"), "AudioSource is registered"); check(ComponentFactory::IsRegistered("AudioListener"), "AudioListener is registered"); Scene& scene = SceneManager::GetInstance().CreateScene("audio_serialization"); GameObject* object = scene.CreateGameObject("speaker"); auto* source = object->AddComponent("game://sounds/hum.ogg"); source->SetVolume(0.5f); source->SetPitch(1.25f); source->SetLoop(true); source->SetPlayOnStart(false); source->SetSpatial(false); source->SetDistances(2.0f, 1.0f); check(close(source->GetMaxDistance(), 2.0f), "max distance is never below min distance"); const nlohmann::json data = source->Serialize(); check(data.at("clip").at("path") == "game://sounds/hum.ogg", "clip is saved as an asset reference"); GameObject* copy = scene.CreateGameObject("copy"); auto* loaded = dynamic_cast(ComponentFactory::Create("AudioSource", *copy)); check(loaded != nullptr, "factory creates an AudioSource"); loaded->Deserialize(data); check(loaded->GetClip() == "game://sounds/hum.ogg", "clip round trips"); check(close(loaded->GetVolume(), 0.5f) && close(loaded->GetPitch(), 1.25f), "volume and pitch round trip"); check(loaded->GetLoop() && !loaded->GetPlayOnStart() && !loaded->GetSpatial(), "flags round trip"); check(close(loaded->GetMinDistance(), 2.0f) && close(loaded->GetMaxDistance(), 2.0f), "distances round trip"); loaded->Deserialize({{"clip", nullptr}, {"volume", -3.0f}}); check(loaded->GetClip().empty(), "null clip clears it"); check(close(loaded->GetVolume(), 0.0f), "negative volume is clamped"); SceneManager::GetInstance().Destroy(); } void testVelocitySmoothing() { // 60 Hz physics seen at 120 fps: the object moves 0.2 m every other frame, 12 m/s on average constexpr float dt = 1.0f / 120.0f; glm::vec3 position{0.0f}; glm::vec3 velocity{0.0f}; float lowest = 1000.0f; float highest = 0.0f; for (int frame = 0; frame < 240; ++frame) { const glm::vec3 previous = position; if (frame % 2 == 0) { position.x += 0.2f; } velocity = SmoothAudioVelocity(position, previous, velocity, dt); if (frame >= 120) { lowest = std::min(lowest, velocity.x); highest = std::max(highest, velocity.x); } } check(lowest > 9.0f && highest < 15.0f, "fixed step movement gives a steady velocity, not 0 / 24 m/s"); const glm::vec3 teleported = SmoothAudioVelocity(glm::vec3{50.0f, 0.0f, 0.0f}, glm::vec3{0.0f}, velocity, dt); check(teleported == glm::vec3{0.0f}, "teleports reset the velocity"); } void testWithoutDeviceEverythingIsANoOp() { auto& audio = AudioSystem::GetInstance(); audio.Shutdown(); check(!audio.IsAvailable(), "audio is off after shutdown"); check(audio.LoadClip("game://missing.wav") == nullptr, "no clips without a device"); check(audio.CreateSource() == 0, "no sources without a device"); audio.PlayOneShot("game://missing.wav", glm::vec3{0.0f}); audio.Update(0.016f, false, nullptr); Scene& scene = SceneManager::GetInstance().CreateScene("audio_no_device"); auto* source = scene.CreateGameObject("speaker")->AddComponent("game://missing.wav"); source->Play(); check(!source->IsPlaying(), "playing without a device does nothing"); SceneManager::GetInstance().Destroy(); } void testPlaybackFollowsPause() { auto& audio = AudioSystem::GetInstance(); if (!audio.Init()) { std::cout << " no audio device, skipping playback test\n"; return; } AudioData silence; silence.channels = 1; silence.sampleRate = 8000; silence.samples.assign(8000, 0); auto clip = audio.CreateClip("test://silence.wav", silence); check(clip != nullptr && close(clip->GetDuration(), 1.0f), "clips upload to OpenAL"); check(audio.LoadClip("test://silence.wav") == clip, "created clips are cached by path"); Scene& scene = SceneManager::GetInstance().CreateScene("audio_playback"); GameObject* speaker = scene.CreateGameObject("speaker"); speaker->AddComponent(); auto* source = speaker->AddComponent("test://silence.wav"); source->SetLoop(true); source->Update(0.016f); check(source->IsPlaying(), "play on start plays on the first update"); audio.Update(0.016f, true, nullptr); check(audio.IsPaused() && !source->IsPlaying(), "pausing the simulation pauses sources"); audio.Update(0.016f, false, nullptr); check(source->IsPlaying(), "resuming the simulation resumes sources"); source->Pause(); audio.Update(0.016f, true, nullptr); audio.Update(0.016f, false, nullptr); check(!source->IsPlaying(), "a source paused by hand stays paused after resume"); source->SetEnabled(false); check(!source->IsPlaying(), "disabling stops the source"); AudioData stereo = silence; stereo.channels = 2; check(audio.CreateClip("test://stereo.wav", stereo) != nullptr, "stereo clips upload"); auto* stereoSource = scene.CreateGameObject("stereo")->AddComponent("test://stereo.wav"); stereoSource->SetSpatial(true); stereoSource->Play(); check(stereoSource->IsPlaying(), "stereo clips play as spatial sources"); stereoSource->SetSpatial(false); check(stereoSource->IsPlaying(), "a playing source can switch to flat"); SceneManager::GetInstance().Destroy(); clip.reset(); audio.ReleaseUnusedClips(); audio.Shutdown(); } } void RunAudioTests() { testDecodeWav16Mono(); testDecodeWavConvertsFormats(); testDecodeWavSkipsPaddedChunks(); testDecodeRejectsBadFiles(); testAudioSourceSerialization(); testVelocitySmoothing(); testWithoutDeviceEverythingIsANoOp(); testPlaybackFollowsPause(); }