diff --git a/assets/data/tests/test_sound_01.wav b/assets/data/tests/test_sound_01.wav new file mode 100644 index 00000000..bf38f0cd Binary files /dev/null and b/assets/data/tests/test_sound_01.wav differ diff --git a/assets/data/tests/test_sound_02.wav b/assets/data/tests/test_sound_02.wav new file mode 100644 index 00000000..9dfcb705 Binary files /dev/null and b/assets/data/tests/test_sound_02.wav differ diff --git a/assets/data/tests/test_sound_03.wav b/assets/data/tests/test_sound_03.wav new file mode 100644 index 00000000..29f43b61 Binary files /dev/null and b/assets/data/tests/test_sound_03.wav differ diff --git a/sample_project/collections/test_collection.json b/sample_project/collections/test_collection.json new file mode 100644 index 00000000..3d8f2808 --- /dev/null +++ b/sample_project/collections/test_collection.json @@ -0,0 +1,53 @@ +{ + "id": 1999, + "name": "test_collection", + "priority": { + "kind": "Static", + "value": 1.0 + }, + "gain": { + "kind": "Static", + "value": 1.0 + }, + "bus": 2, + "sounds_type": [ + "Sequence", + "Sequence", + "Sequence" + ], + "sounds": [ + { + "sound": 9991, + "gain": { + "kind": "Static", + "value": 1.0 + } + }, + { + "sound": 9992, + "gain": { + "kind": "Static", + "value": 1.0 + } + }, + { + "sound": 9993, + "gain": { + "kind": "Static", + "value": 1.0 + } + } + ], + "spatialization": "None", + "attenuation": 0, + "play_mode": "PlayAll", + "scheduler": { + "mode": "Sequence", + "config_type": "Sequence", + "config": { + "end_behavior": "Reverse" + } + }, + "scope": "World", + "fader": "Linear" +} \ No newline at end of file diff --git a/sample_project/soundbanks/tests.init.json b/sample_project/soundbanks/tests.init.json new file mode 100644 index 00000000..d0b49e19 --- /dev/null +++ b/sample_project/soundbanks/tests.init.json @@ -0,0 +1,66 @@ +{ + "id": 1, + "name": "init", + "sounds": [ + "background/AMBForst_Forest_ID_0100_BSB.wav.amsound", + "footsteps/snow/a.mp3.amsound", + "footsteps/snow/b.mp3.amsound", + "footsteps/metal/a.mp3.amsound", + "footsteps/metal/b.mp3.amsound", + "footsteps/grass/a.mp3.amsound", + "footsteps/grass/b.mp3.amsound", + "footsteps/grass/c.mp3.amsound", + "footsteps/grass/d.mp3.amsound", + "footsteps/grass/e.mp3.amsound", + "footsteps/grass/f.mp3.amsound", + "footsteps/grass/g.mp3.amsound", + "music/symphony.ogg.amsound", + "tests/test_sound_01.amsound", + "tests/test_sound_02.amsound", + "tests/test_sound_03.amsound", + "throw_01.ogg.amsound", + "throw_02.ogg.amsound", + "throw_03.ogg.amsound", + "throw_04.ogg.amsound", + "throw_05.ogg.amsound", + "throw_06.ogg.amsound", + "throw_07.ogg.amsound", + "throw_08.ogg.amsound" + ], + "effects": [ + "bassboost.amfx", + "delay.amfx", + "flanger.amfx", + "lpf.amfx", + "robotize.amfx" + ], + "collections": [ + "grass_footsteps.amcollection", + "metal_footsteps.amcollection", + "snow_footsteps.amcollection", + "test_collection.amcollection", + "throw_collection_1.amcollection", + "throw_collection_2.amcollection" + ], + "events": [ + "mute_bg_bus.amevent", + "play_throw.amevent", + "player_footstep.amevent", + "stop_throw.amevent" + ], + "attenuators": [ + "impact.amattenuation", + "room.amattenuation", + "pipe.amattenuation" + ], + "switches": [ + "env.amswitch", + "surface_type.amswitch" + ], + "switch_containers": [ + "footsteps.amswitchcontainer" + ], + "rtpc": [ + "rtpc_player_height.amrtpc" + ] +} \ No newline at end of file diff --git a/sample_project/sounds/tests/test_sound_01.json b/sample_project/sounds/tests/test_sound_01.json new file mode 100644 index 00000000..e710be33 --- /dev/null +++ b/sample_project/sounds/tests/test_sound_01.json @@ -0,0 +1,22 @@ +{ + "id": 9991, + "name": "test_sound_01", + "gain": { + "kind": "Static", + "value": 1.0 + }, + "bus": 1, + "priority": { + "kind": "Static", + "value": 1.0 + }, + "stream": false, + "loop": { + "enabled": false, + "loop_count": 5 + }, + "spatialization": "None", + "attenuation": 0, + "fader": "Linear", + "path": "tests/test_sound_01.wav" +} \ No newline at end of file diff --git a/sample_project/sounds/tests/test_sound_02.json b/sample_project/sounds/tests/test_sound_02.json new file mode 100644 index 00000000..3d34748f --- /dev/null +++ b/sample_project/sounds/tests/test_sound_02.json @@ -0,0 +1,22 @@ +{ + "id": 9992, + "name": "test_sound_02", + "gain": { + "kind": "Static", + "value": 1.0 + }, + "bus": 1, + "priority": { + "kind": "Static", + "value": 1.0 + }, + "stream": false, + "loop": { + "enabled": false, + "loop_count": 5 + }, + "spatialization": "None", + "attenuation": 0, + "fader": "Linear", + "path": "tests/test_sound_02.wav" +} \ No newline at end of file diff --git a/sample_project/sounds/tests/test_sound_03.json b/sample_project/sounds/tests/test_sound_03.json new file mode 100644 index 00000000..1f597e49 --- /dev/null +++ b/sample_project/sounds/tests/test_sound_03.json @@ -0,0 +1,22 @@ +{ + "id": 9993, + "name": "test_sound_03", + "gain": { + "kind": "Static", + "value": 1.0 + }, + "bus": 1, + "priority": { + "kind": "Static", + "value": 1.0 + }, + "stream": false, + "loop": { + "enabled": false, + "loop_count": 5 + }, + "spatialization": "None", + "attenuation": 0, + "fader": "Linear", + "path": "tests/test_sound_03.wav" +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ecdfec9a..cd557abe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,7 +28,8 @@ add_executable(${PROJECT_NAME} entity.cpp engine.cpp environment.cpp - common.cpp) + common.cpp + math.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE Catch2::Catch2 Shared) add_custom_command( @@ -41,9 +42,17 @@ add_dependencies(${PROJECT_NAME} ss_amplitude_audio_sample_project amac ampk + amir ) include(Catch) catch_discover_tests(${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) + +target_compile_definitions( + ${PROJECT_NAME} + PRIVATE + AM_SDK_PLATFORM="${VCPKG_TARGET_TRIPLET}" + AM_SDK_PATH="$ENV{AM_SDK_PATH}" +) diff --git a/tests/engine.cpp b/tests/engine.cpp index ec5805c8..6aa87c5c 100644 --- a/tests/engine.cpp +++ b/tests/engine.cpp @@ -203,14 +203,18 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(amEngine->FindBus(120198434).Valid()); } - THEN("it cannot unload an unloaded soundbank") + THEN("it cannot unload an unloaded sound bank") { - // REQUIRE_THROWS(amEngine->UnloadSoundBank("init.ambank")); + // REQUIRE_THROWS(amEngine->UnloadSoundBank("tests.init.ambank")); } - WHEN("engine has loaded a soundbank") + WHEN("engine has loaded a sound bank") { - REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("init.ambank"))); + REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("tests.init.ambank"))); + + const auto& listener = amEngine->AddListener(1); + amEngine->SetDefaultListener(1); + REQUIRE(amEngine->GetDefaultListener().GetState() == listener.GetState()); THEN("it can load sound files") { @@ -253,7 +257,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(e4.Valid()); REQUIRE_FALSE(e5.Valid()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } THEN("it can register listeners") @@ -285,7 +289,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(l4.Valid()); REQUIRE_FALSE(l5.Valid()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } THEN("it can register environments") @@ -317,7 +321,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(e4.Valid()); REQUIRE_FALSE(e5.Valid()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } THEN("it can access sound assets by names") @@ -326,7 +330,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE(amEngine->GetSoundHandle("AMB_Forest") != nullptr); REQUIRE(amEngine->GetSoundHandle("throw_01") != nullptr); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } THEN("it can access sound assets by IDs") @@ -335,7 +339,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE(amEngine->GetSoundHandle(100) != nullptr); REQUIRE(amEngine->GetSoundHandle(1) != nullptr); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } THEN("it accesses the same sound assets when fetching by name or ID") @@ -348,18 +352,18 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE(amEngine->GetSoundHandle(name) == amEngine->GetSoundHandle(id)); } - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } - THEN("it can load the same soundbank again") + THEN("it can load the same sound bank again") { - REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("init.ambank"))); - amEngine->UnloadSoundBank("init.ambank"); + REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("tests.init.ambank"))); + amEngine->UnloadSoundBank("tests.init.ambank"); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } - THEN("it can load other soundbanks") + THEN("it can load other sound banks") { REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("sample_01.ambank"))); REQUIRE(amEngine->LoadSoundBank(AM_OS_STRING("sample_02.ambank"))); @@ -367,35 +371,95 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") amEngine->UnloadSoundBanks(); } - THEN("engine can play a sound") + THEN("engine can play a sound using its handle") + { + SoundHandle test_sound_01 = amEngine->GetSoundHandle("test_sound_01"); + + Channel channel = amEngine->Play(test_sound_01); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(1000); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a sound using its ID") + { + Channel channel = amEngine->Play(9992); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(1000); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a sound using its name") { - SoundHandle throw01 = amEngine->GetSoundHandle("throw_01"); + Channel channel = amEngine->Play("test_sound_03"); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame - Channel channel = amEngine->Play(throw01); REQUIRE(channel.Valid()); REQUIRE(channel.Playing()); Thread::Sleep(1000); // wait for the sound to finish playing REQUIRE_FALSE(channel.Playing()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a collection using its handle") + { + CollectionHandle test_collection = amEngine->GetCollectionHandle("test_collection"); + + Channel channel = amEngine->Play(test_collection); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(kAmSecond * 3); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a collection using its ID") + { + Channel channel = amEngine->Play(1999); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(kAmSecond * 3); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); } - THEN("engine can play a collection") + THEN("engine can play a collection using its name") { - CollectionHandle throw_collection = amEngine->GetCollectionHandle("throw_collection_1"); + Channel channel = amEngine->Play("test_collection"); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame - Channel channel = amEngine->Play(throw_collection); REQUIRE(channel.Valid()); REQUIRE(channel.Playing()); - Thread::Sleep(8000); // wait for the sound to finish playing + Thread::Sleep(kAmSecond * 3); // wait for the sound to finish playing REQUIRE_FALSE(channel.Playing()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } - THEN("engine can play a switch container") + THEN("engine can play a switch container using its handle") { Entity entity = amEngine->AddEntity(100); SwitchContainerHandle footsteps = amEngine->GetSwitchContainerHandle("footsteps"); @@ -404,20 +468,61 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(channel.Valid()); // switch container is entity scoped channel = amEngine->Play(footsteps, entity); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(1000); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a switch container using its ID") + { + Entity entity = amEngine->AddEntity(100); + + Channel channel = amEngine->Play(200); + REQUIRE_FALSE(channel.Valid()); // switch container is entity scoped + + channel = amEngine->Play(200, entity); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + + REQUIRE(channel.Valid()); + REQUIRE(channel.Playing()); + + Thread::Sleep(1000); // wait for the sound to finish playing + REQUIRE_FALSE(channel.Playing()); + + amEngine->UnloadSoundBank("tests.init.ambank"); + } + + THEN("engine can play a switch container using its name") + { + Entity entity = amEngine->AddEntity(100); + + Channel channel = amEngine->Play("footsteps"); + REQUIRE_FALSE(channel.Valid()); // switch container is entity scoped + + channel = amEngine->Play("footsteps", entity); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame + REQUIRE(channel.Valid()); REQUIRE(channel.Playing()); Thread::Sleep(1000); // wait for the sound to finish playing REQUIRE_FALSE(channel.Playing()); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } GIVEN("a playing channel") { AmVec3 location = { 10.0f, 20.0f, 30.0f }; AmReal32 userGain = 0.36f; - Channel channel = amEngine->Play(101, location, userGain); + Channel channel = amEngine->Play(100, location, userGain); + amEngine->WaitUntilNextFrame(); // Playing is done in the next frame REQUIRE(channel.Valid()); REQUIRE(channel.Playing()); @@ -435,12 +540,12 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") channel.Pause(); REQUIRE(channel.GetPlaybackState() == ChannelPlaybackState::FadingOut); REQUIRE_FALSE(channel.Playing()); - Thread::Sleep(kAmSecond); // wait for sixty frames + amEngine->WaitUntilFrames(2); REQUIRE(channel.GetPlaybackState() == ChannelPlaybackState::Paused); channel.Resume(); REQUIRE(channel.GetPlaybackState() == ChannelPlaybackState::FadingIn); - Thread::Sleep(kAmSecond); // wait for sixty frames + amEngine->WaitUntilFrames(2); REQUIRE(channel.GetPlaybackState() == ChannelPlaybackState::Playing); REQUIRE(channel.Playing()); @@ -545,7 +650,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") if (channel.Valid()) channel.Stop(0); - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } GIVEN("a registered bus") @@ -613,7 +718,7 @@ TEST_CASE("Engine Tests", "[engine][core][amplitude]") REQUIRE_FALSE(bus.Valid()); } - amEngine->UnloadSoundBank("init.ambank"); + amEngine->UnloadSoundBank("tests.init.ambank"); } } } diff --git a/tests/entity.cpp b/tests/entity.cpp index 368603a8..463ecc0d 100644 --- a/tests/entity.cpp +++ b/tests/entity.cpp @@ -115,6 +115,19 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") { REQUIRE(state.GetEnvironmentFactor(12345) == 0.0f); } + + WHEN("the directivity changes") + { + constexpr auto directivity = 0.5f; + constexpr auto sharpness = 1.5f; + state.SetDirectivity(directivity, sharpness); + + THEN("it returns the new directivity and sharpness") + { + REQUIRE(state.GetDirectivity() == directivity); + REQUIRE(state.GetDirectivitySharpness() == sharpness); + } + } } SECTION("can be used with a wrapper") @@ -125,6 +138,8 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") SECTION("can return the correct ID") { REQUIRE(wrapper.GetId() == 1); + + REQUIRE(wrapper.GetId() == state.GetId()); } WHEN("the location changes") @@ -136,6 +151,8 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") THEN("it returns the new location") { REQUIRE(AM_EqV3(wrapper.GetLocation(), location)); + + REQUIRE(AM_EqV3(wrapper.GetLocation(), state.GetLocation())); } AND_WHEN("an update occurs") @@ -147,6 +164,8 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") const auto& velocity = location - lastLocation; REQUIRE(AM_EqV3(wrapper.GetVelocity(), velocity)); + + REQUIRE(AM_EqV3(wrapper.GetVelocity(), state.GetVelocity())); } } } @@ -155,12 +174,16 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") { const auto direction = AM_V3(1, 0, 0); const auto up = AM_V3(0, 0, 1); - wrapper.SetOrientation(Orientation(direction, up)); + const auto orientation = Orientation(direction, up); + wrapper.SetOrientation(orientation); THEN("it returns the new orientation") { REQUIRE(AM_EqV3(wrapper.GetDirection(), direction)); REQUIRE(AM_EqV3(wrapper.GetUp(), up)); + + REQUIRE(AM_EqV3(wrapper.GetDirection(), state.GetDirection())); + REQUIRE(AM_EqV3(wrapper.GetUp(), state.GetUp())); } } @@ -172,6 +195,8 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") THEN("it returns the new obstruction") { REQUIRE(wrapper.GetObstruction() == obstruction); + + REQUIRE(wrapper.GetObstruction() == state.GetObstruction()); } } @@ -183,16 +208,8 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") THEN("it returns the new obstruction") { REQUIRE(wrapper.GetOcclusion() == occlusion); - } - } - - WHEN("the internal state wrapper is cleared") - { - wrapper.Clear(); - THEN("it is no longer valid") - { - REQUIRE_FALSE(wrapper.Valid()); + REQUIRE(wrapper.GetOcclusion() == state.GetOcclusion()); } } @@ -205,18 +222,51 @@ TEST_CASE("Entity Tests", "[entity][core][amplitude]") THEN("it returns the new environment factor") { REQUIRE(wrapper.GetEnvironmentFactor(environment) == factor); + + REQUIRE(wrapper.GetEnvironmentFactor(environment) == state.GetEnvironmentFactor(environment)); } THEN("the list of environment factors is updated") { REQUIRE(wrapper.GetEnvironments().size() == 1); REQUIRE(wrapper.GetEnvironments().at(environment) == factor); + + REQUIRE(wrapper.GetEnvironments().size() == state.GetEnvironments().size()); + REQUIRE(wrapper.GetEnvironments().at(environment) == state.GetEnvironmentFactor(environment)); } } SECTION("returns 0 as the environment factor for an unregistered environment ID") { REQUIRE(wrapper.GetEnvironmentFactor(12345) == 0.0f); + + REQUIRE(wrapper.GetEnvironmentFactor(12345) == state.GetEnvironmentFactor(12345)); + } + + WHEN("the directivity changes") + { + constexpr auto directivity = 0.5f; + constexpr auto sharpness = 1.5f; + wrapper.SetDirectivity(directivity, sharpness); + + THEN("it returns the new directivity and sharpness") + { + REQUIRE(wrapper.GetDirectivity() == directivity); + REQUIRE(wrapper.GetDirectivitySharpness() == sharpness); + + REQUIRE(wrapper.GetDirectivity() == state.GetDirectivity()); + REQUIRE(wrapper.GetDirectivitySharpness() == state.GetDirectivitySharpness()); + } + } + + WHEN("the internal state wrapper is cleared") + { + wrapper.Clear(); + + THEN("it is no longer valid") + { + REQUIRE_FALSE(wrapper.Valid()); + } } } diff --git a/tests/environment.cpp b/tests/environment.cpp index 9c494638..e0e6c6a7 100644 --- a/tests/environment.cpp +++ b/tests/environment.cpp @@ -54,6 +54,8 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") THEN("it returns the new location") { REQUIRE(AM_EqV3(state.GetLocation(), location)); + + REQUIRE(AM_EqV3(zone.GetLocation(), location)); } AND_WHEN("an update occurs") @@ -63,6 +65,8 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") THEN("the location stays the same") { REQUIRE(AM_EqV3(state.GetLocation(), location)); + + REQUIRE(AM_EqV3(zone.GetLocation(), location)); } } } @@ -83,6 +87,9 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") { REQUIRE(AM_EqV3(state.GetDirection(), direction)); REQUIRE(AM_EqV3(state.GetUp(), up)); + + REQUIRE(AM_EqV3(zone.GetDirection(), direction)); + REQUIRE(AM_EqV3(zone.GetUp(), up)); } } @@ -99,6 +106,46 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") REQUIRE(state.GetZone() == &zone); } } + + WHEN("the effect changes") + { + SphereShape inner(10); + SphereShape outer(20); + SphereZone zone(&inner, &outer); + + state.SetZone(&zone); + + WHEN("an effect is set by ID") + { + state.SetEffect(2); + + THEN("it returns the new effect") + { + REQUIRE(state.GetEffect() == amEngine->GetEffectHandle(2)); + } + } + + WHEN("an effect is set by name") + { + state.SetEffect("lpf"); + + THEN("it returns the new effect") + { + REQUIRE(state.GetEffect() == amEngine->GetEffectHandle("lpf")); + } + } + + WHEN("an effect is set by handle") + { + auto* effect = amEngine->GetEffectHandle("equalizer"); + state.SetEffect(effect); + + THEN("it returns the new effect") + { + REQUIRE(state.GetEffect() == effect); + } + } + } } SECTION("can be used with a wrapper") @@ -109,6 +156,8 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") SECTION("can return the correct ID") { REQUIRE(wrapper.GetId() == 1); + + REQUIRE(state.GetId() == 1); } WHEN("the location changes") @@ -125,6 +174,10 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") THEN("it returns the new location") { REQUIRE(AM_EqV3(wrapper.GetLocation(), location)); + + REQUIRE(AM_EqV3(wrapper.GetLocation(), state.GetLocation())); + + REQUIRE(AM_EqV3(state.GetLocation(), zone.GetLocation())); } AND_WHEN("an update occurs") @@ -134,6 +187,10 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") THEN("the location stays the same") { REQUIRE(AM_EqV3(wrapper.GetLocation(), location)); + + REQUIRE(AM_EqV3(wrapper.GetLocation(), state.GetLocation())); + + REQUIRE(AM_EqV3(state.GetLocation(), zone.GetLocation())); } } } @@ -154,6 +211,12 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") { REQUIRE(AM_EqV3(wrapper.GetDirection(), direction)); REQUIRE(AM_EqV3(wrapper.GetUp(), up)); + + REQUIRE(AM_EqV3(wrapper.GetDirection(), state.GetDirection())); + REQUIRE(AM_EqV3(wrapper.GetUp(), state.GetUp())); + + REQUIRE(AM_EqV3(state.GetDirection(), zone.GetDirection())); + REQUIRE(AM_EqV3(state.GetUp(), zone.GetUp())); } } @@ -168,6 +231,54 @@ TEST_CASE("Environment Tests", "[entity][core][amplitude]") THEN("it returns the new zone") { REQUIRE(wrapper.GetZone() == &zone); + + REQUIRE(wrapper.GetZone() == state.GetZone()); + } + } + + WHEN("the effect changes") + { + SphereShape inner(10); + SphereShape outer(20); + SphereZone zone(&inner, &outer); + + wrapper.SetZone(&zone); + + WHEN("an effect is set by ID") + { + wrapper.SetEffect(2); + + THEN("it returns the new effect") + { + REQUIRE(wrapper.GetEffect() == amEngine->GetEffectHandle(2)); + + REQUIRE(wrapper.GetEffect() == state.GetEffect()); + } + } + + WHEN("an effect is set by name") + { + wrapper.SetEffect("lpf"); + + THEN("it returns the new effect") + { + REQUIRE(wrapper.GetEffect() == amEngine->GetEffectHandle("lpf")); + + REQUIRE(wrapper.GetEffect() == state.GetEffect()); + } + } + + WHEN("an effect is set by handle") + { + auto* effect = amEngine->GetEffectHandle("equalizer"); + wrapper.SetEffect(effect); + + THEN("it returns the new effect") + { + REQUIRE(wrapper.GetEffect() == effect); + + REQUIRE(wrapper.GetEffect() == state.GetEffect()); + } } } diff --git a/tests/listener.cpp b/tests/listener.cpp index c773af45..a27dbe19 100644 --- a/tests/listener.cpp +++ b/tests/listener.cpp @@ -76,6 +76,19 @@ TEST_CASE("Listener Tests", "[listener][core][amplitude]") REQUIRE(AM_EqV3(state.GetUp(), up)); } } + + WHEN("the directivity changes") + { + constexpr auto directivity = 0.5f; + constexpr auto sharpness = 1.5f; + state.SetDirectivity(directivity, sharpness); + + THEN("it returns the new directivity and sharpness") + { + REQUIRE(state.GetDirectivity() == directivity); + REQUIRE(state.GetDirectivitySharpness() == sharpness); + } + } } SECTION("can be used with a wrapper") @@ -85,6 +98,8 @@ TEST_CASE("Listener Tests", "[listener][core][amplitude]") SECTION("can return the correct ID") { REQUIRE(wrapper.GetId() == 1); + + REQUIRE(wrapper.GetId() == state.GetId()); } WHEN("the location changes") @@ -96,6 +111,8 @@ TEST_CASE("Listener Tests", "[listener][core][amplitude]") THEN("it returns the new location") { REQUIRE(AM_EqV3(wrapper.GetLocation(), location)); + + REQUIRE(AM_EqV3(wrapper.GetLocation(), state.GetLocation())); } AND_WHEN("an update occurs") @@ -107,6 +124,8 @@ TEST_CASE("Listener Tests", "[listener][core][amplitude]") const auto& velocity = location - lastLocation; REQUIRE(AM_EqV3(wrapper.GetVelocity(), velocity)); + + REQUIRE(AM_EqV3(wrapper.GetVelocity(), state.GetVelocity())); } } } @@ -119,8 +138,27 @@ TEST_CASE("Listener Tests", "[listener][core][amplitude]") THEN("it returns the new orientation") { - REQUIRE(AM_EqV3(state.GetDirection(), direction)); - REQUIRE(AM_EqV3(state.GetUp(), up)); + REQUIRE(AM_EqV3(wrapper.GetDirection(), direction)); + REQUIRE(AM_EqV3(wrapper.GetUp(), up)); + + REQUIRE(AM_EqV3(wrapper.GetDirection(), state.GetDirection())); + REQUIRE(AM_EqV3(wrapper.GetUp(), state.GetUp())); + } + } + + WHEN("the directivity changes") + { + constexpr auto directivity = 0.5f; + constexpr auto sharpness = 1.5f; + wrapper.SetDirectivity(directivity, sharpness); + + THEN("it returns the new directivity and sharpness") + { + REQUIRE(wrapper.GetDirectivity() == directivity); + REQUIRE(wrapper.GetDirectivitySharpness() == sharpness); + + REQUIRE(wrapper.GetDirectivity() == state.GetDirectivity()); + REQUIRE(wrapper.GetDirectivitySharpness() == state.GetDirectivitySharpness()); } } diff --git a/tests/main.cpp b/tests/main.cpp index d978edff..74daa331 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -21,16 +21,16 @@ using namespace SparkyStudios::Audio::Amplitude; -struct MyListener : Catch::EventListenerBase +struct AmTestListener : Catch::EventListenerBase { using EventListenerBase::EventListenerBase; // inherit constructor - // Get rid of Wweak-tables - ~MyListener() override = default; + // Get rid of weak-tables + ~AmTestListener() override = default; static void run(AmVoidPtr listener) { - const auto* self = static_cast(listener); + const auto* self = static_cast(listener); while (self->running) { @@ -39,6 +39,8 @@ struct MyListener : Catch::EventListenerBase amEngine->AdvanceFrame(delta); Thread::Sleep(static_cast(delta)); } + + amLogDebug("Test run ended"); } // The whole test run starting @@ -59,16 +61,10 @@ struct MyListener : Catch::EventListenerBase const auto sdkPath = std::filesystem::path(std::getenv("AM_SDK_PATH")); Engine::AddPluginSearchPath(AM_OS_STRING("./assets/plugins")); -#if defined(AM_WINDOWS_VERSION) - Engine::AddPluginSearchPath(sdkPath / AM_OS_STRING("lib/win/plugins")); -#elif defined(AM_LINUX_VERSION) - Engine::AddPluginSearchPath(sdkPath / AM_OS_STRING("lib/linux/plugins")); -#elif defined(AM_OSX_VERSION) - Engine::AddPluginSearchPath(sdkPath / AM_OS_STRING("lib/osx/plugins")); -#endif + Engine::AddPluginSearchPath(sdkPath / AM_OS_STRING("lib/" AM_SDK_PLATFORM "/plugins")); - Engine::LoadPlugin(AM_OS_STRING("AmplitudeVorbisCodecPlugin_d")); - Engine::LoadPlugin(AM_OS_STRING("AmplitudeFlacCodecPlugin_d")); + // Engine::LoadPlugin(AM_OS_STRING("AmplitudeVorbisCodecPlugin_d")); + // Engine::LoadPlugin(AM_OS_STRING("AmplitudeFlacCodecPlugin_d")); running = true; @@ -106,10 +102,13 @@ struct MyListener : Catch::EventListenerBase bool running = false; }; -CATCH_REGISTER_LISTENER(MyListener) +CATCH_REGISTER_LISTENER(AmTestListener) int main(int argc, char* argv[]) { + ConsoleLogger logger; + Logger::SetLogger(&logger); + MemoryManager::Initialize({}); const auto res = Catch::Session().run(argc, argv); diff --git a/tests/math.cpp b/tests/math.cpp new file mode 100644 index 00000000..2342772f --- /dev/null +++ b/tests/math.cpp @@ -0,0 +1,357 @@ +// Copyright (c) 2024-present Sparky Studios. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include + +using namespace SparkyStudios::Audio::Amplitude; + +TEST_CASE("Barycentric Coordinate Tests", "[barycentric_coordinates][math][amplitude]") +{ + const auto point1 = AM_V3(1, 2, 1); + const auto point2 = AM_V3(0, 0, 0); + const auto point3 = AM_V3(2, 1, 2); + + const auto center = AM_V3(1, 1, 1); + const auto triangle = std::array{ point1, point2, point3 }; + + const auto i = point1 - center; + const auto j = point2 - center; + const auto k = AM_Cross(i, j); + + const auto rayOrigin = center; + const auto rayDirection = k; + + SECTION("can compute the barycentric coordinates of a point inside the triangle") + { + BarycentricCoordinates coordinates(center, triangle); + + REQUIRE(coordinates.IsValid()); + + REQUIRE(coordinates.m_U + coordinates.m_V + coordinates.m_W == 1.0f); + + REQUIRE((coordinates.m_U - 1.0f / 3.0f) < kEpsilon); + REQUIRE((coordinates.m_V - 1.0f / 3.0f) < kEpsilon); + REQUIRE((coordinates.m_W - 1.0f / 3.0f) < kEpsilon); + } + + SECTION("can compute the barycentric coordinates of a ray-triangle intersection") + { + BarycentricCoordinates coordinates; + REQUIRE(BarycentricCoordinates::RayTriangleIntersection(rayOrigin, rayDirection, triangle, coordinates)); + + REQUIRE(coordinates.IsValid()); + + REQUIRE(coordinates.m_U + coordinates.m_V + coordinates.m_W == 1.0f); + + REQUIRE((coordinates.m_U - 1.0f / 3.0f) < kEpsilon); + REQUIRE((coordinates.m_V - 1.0f / 3.0f) < kEpsilon); + REQUIRE((coordinates.m_W - 1.0f / 3.0f) < kEpsilon); + } +} + +TEST_CASE("Cartesian Coordinate System Tests", "[cartesian_coordinate_system][math][amplitude]") +{ + GIVEN("a right-handed z-up Cartesian coordinate system") + { + const CartesianCoordinateSystem coordinateSystem = CartesianCoordinateSystem::RightHandedZUp(); + + THEN("it have the correct axes") + { + REQUIRE(AM_EqV3(coordinateSystem.GetRightVector(), AM_V3(1, 0, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetUpVector(), AM_V3(0, 0, 1))); + REQUIRE(AM_EqV3(coordinateSystem.GetForwardVector(), AM_V3(0, 1, 0))); + } + } + + GIVEN("a right-handed y-up Cartesian coordinate system") + { + const CartesianCoordinateSystem coordinateSystem = CartesianCoordinateSystem::RightHandedYUp(); + + THEN("it have the correct axes") + { + REQUIRE(AM_EqV3(coordinateSystem.GetRightVector(), AM_V3(1, 0, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetUpVector(), AM_V3(0, 1, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetForwardVector(), AM_V3(0, 0, -1))); + } + } + + GIVEN("a left-handed z-up Cartesian coordinate system") + { + const CartesianCoordinateSystem coordinateSystem = CartesianCoordinateSystem::LeftHandedZUp(); + + THEN("it have the correct axes") + { + REQUIRE(AM_EqV3(coordinateSystem.GetRightVector(), AM_V3(1, 0, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetUpVector(), AM_V3(0, 0, 1))); + REQUIRE(AM_EqV3(coordinateSystem.GetForwardVector(), AM_V3(0, -1, 0))); + } + } + + GIVEN("a left-handed y-up Cartesian coordinate system") + { + const CartesianCoordinateSystem coordinateSystem = CartesianCoordinateSystem::LeftHandedYUp(); + + THEN("it have the correct axes") + { + REQUIRE(AM_EqV3(coordinateSystem.GetRightVector(), AM_V3(1, 0, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetUpVector(), AM_V3(0, 1, 0))); + REQUIRE(AM_EqV3(coordinateSystem.GetForwardVector(), AM_V3(0, 0, 1))); + } + } + + GIVEN("a Cartesian coordinate system with arbitrary axes") + { + constexpr auto rightVector = CartesianCoordinateSystem::Axis::NegativeX; + constexpr auto upVector = CartesianCoordinateSystem::Axis::PositiveY; + constexpr auto forwardVector = CartesianCoordinateSystem::Axis::NegativeZ; + + const CartesianCoordinateSystem coordinateSystem(rightVector, forwardVector, upVector); + + THEN("it have the correct axes") + { + REQUIRE(AM_EqV3(coordinateSystem.GetRightVector(), CartesianCoordinateSystem::GetVector(rightVector))); + REQUIRE(AM_EqV3(coordinateSystem.GetUpVector(), CartesianCoordinateSystem::GetVector(upVector))); + REQUIRE(AM_EqV3(coordinateSystem.GetForwardVector(), CartesianCoordinateSystem::GetVector(forwardVector))); + } + } + + SECTION("coordinate system conversion") + { + const auto from = CartesianCoordinateSystem::Default(); + const auto to = CartesianCoordinateSystem::AmbiX(); + + GIVEN("a point in the default coordinate system") + { + const auto point = AM_V3(1, 2, 3); + + WHEN("converted to AmbiX coordinate system") + { + const auto convertedPoint = CartesianCoordinateSystem::Convert(point, from, to); + + THEN("it should have the same position in the AmbiX coordinate system") + { + REQUIRE(AM_EqV3(convertedPoint, AM_V3(2, -1, 3))); + } + + THEN("it should convert back to the original coordinate system") + { + const auto convertedBackPoint = CartesianCoordinateSystem::Convert(convertedPoint, to, from); + + REQUIRE(AM_EqV3(convertedBackPoint, point)); + } + + THEN("it should match the point using the Converter API") + { + const auto converter = CartesianCoordinateSystem::Converter(from, to); + const auto convertedPoint2 = converter.Forward(point); + + REQUIRE(AM_EqV3(convertedPoint2, convertedPoint)); + + AND_THEN("it should convert back to the original coordinate system using the Converter API") + { + const auto convertedBackPoint2 = converter.Backward(convertedPoint); + + REQUIRE(AM_EqV3(convertedBackPoint2, point)); + } + } + } + } + + GIVEN("a quaternion rotation in the default coordinate system") + { + const auto rotation = AM_QFromAxisAngle_RH(AM_V3(0, 1, 0), AM_DegToRad * 45.0f); + + WHEN("converted to AmbiX coordinate system") + { + const auto convertedRotation = CartesianCoordinateSystem::Convert(rotation, from, to); + + THEN("it should have the same rotation in the AmbiX coordinate system") + { + REQUIRE(AM_EqV3(convertedRotation.XYZ, AM_V3(rotation.Y, -rotation.X, rotation.Z))); + REQUIRE(convertedRotation.W == rotation.W); + } + + THEN("it should convert back to the original coordinate system") + { + const auto convertedBackRotation = CartesianCoordinateSystem::Convert(convertedRotation, to, from); + + REQUIRE(AM_EqV3(convertedBackRotation.XYZ, rotation.XYZ)); + REQUIRE(convertedBackRotation.W == rotation.W); + } + + THEN("it should match the rotation using the Converter API") + { + const auto converter = CartesianCoordinateSystem::Converter(from, to); + const auto convertedRotation2 = converter.Forward(rotation); + + REQUIRE(AM_EqV3(convertedRotation2.XYZ, convertedRotation.XYZ)); + REQUIRE(convertedRotation2.W == convertedRotation.W); + + AND_THEN("it should convert back to the original coordinate system using the Converter API") + { + const auto convertedBackRotation2 = converter.Backward(convertedRotation); + + REQUIRE(AM_EqV3(convertedBackRotation2.XYZ, rotation.XYZ)); + REQUIRE(convertedBackRotation2.W == rotation.W); + } + } + } + } + + GIVEN("a scalar in the default coordinate system") + { + constexpr auto scalar = 5.0f; + + WHEN("converted to AmbiX coordinate system") + { + const auto convertedScalar = CartesianCoordinateSystem::Convert(scalar, from, to); + + THEN("it should have the same scalar in the AmbiX coordinate system") + { + REQUIRE(convertedScalar == scalar); + } + + THEN("it should convert back to the original coordinate system") + { + const auto convertedBackScalar = CartesianCoordinateSystem::Convert(convertedScalar, to, from); + + REQUIRE(convertedBackScalar == scalar); + } + + THEN("it should match the scalar using the Converter API") + { + const auto converter = CartesianCoordinateSystem::Converter(from, to); + const auto convertedScalar2 = converter.Forward(scalar); + + REQUIRE(convertedScalar2 == convertedScalar); + + AND_THEN("it should convert back to the original coordinate system using the Converter API") + { + const auto convertedBackScalar2 = converter.Backward(convertedScalar); + + REQUIRE(convertedBackScalar2 == scalar); + } + } + } + } + } +} + +TEST_CASE("Spherical Position Tests", "[spherical_position][math][amplitude]") +{ + GIVEN("a spherical position") + { + const auto position = SphericalPosition(AM_DegToRad * 45.0f, AM_DegToRad * 30.0f, 5.0f); + + THEN("it should store the correct spherical coordinates") + { + REQUIRE(position.GetAzimuth() == AM_DegToRad * 45.0f); + REQUIRE(position.GetElevation() == AM_DegToRad * 30.0f); + REQUIRE(position.GetRadius() == 5.0f); + } + + THEN("it can convert to Cartesian coordinates") + { + const auto cartesianPosition = position.ToCartesian(); + + REQUIRE(cartesianPosition.X == +5.0f * std::cos(position.GetElevation()) * std::cos(position.GetAzimuth())); + REQUIRE(cartesianPosition.Y == -5.0f * std::cos(position.GetElevation()) * std::sin(position.GetAzimuth())); + REQUIRE(cartesianPosition.Z == +5.0f * std::sin(position.GetElevation())); + } + + THEN("it can flip azimuth") + { + const auto flippedPosition = position.FlipAzimuth(); + + REQUIRE(flippedPosition.GetAzimuth() == -45.0f * AM_DegToRad); + REQUIRE(flippedPosition.GetElevation() == position.GetElevation()); + REQUIRE(flippedPosition.GetRadius() == position.GetRadius()); + } + + THEN("it can be rotated") + { + const auto rotation = AM_QFromAxisAngle_RH(AM_V3(0, 0, 1), AM_DegToRad * 90.0f); + const auto rotatedPosition = position.Rotate(rotation); + + const auto rotatedPosition2 = SphericalPosition::FromWorldSpace(AM_RotateV3Q(position.ToCartesian(), rotation)); + + REQUIRE(rotatedPosition.GetAzimuth() == rotatedPosition2.GetAzimuth()); + REQUIRE(rotatedPosition.GetElevation() == rotatedPosition2.GetElevation()); + REQUIRE(rotatedPosition.GetRadius() == rotatedPosition2.GetRadius()); + } + + SECTION("equality comparison") + { + const auto otherPosition = SphericalPosition(AM_DegToRad * 45.0f, AM_DegToRad * 30.0f, 5.0f); + + THEN("it should be equal to itself") + { + REQUIRE(position == position); + } + + THEN("it should be equal to another spherical position with the same coordinates") + { + REQUIRE(position == otherPosition); + } + + THEN("it should not be equal to another spherical position with different coordinates") + { + const auto differentPosition = SphericalPosition(AM_DegToRad * 60.0f, AM_DegToRad * 45.0f, 5.0f); + + REQUIRE(position != differentPosition); + } + } + } + + GIVEN("a cartesian position") + { + const auto cartesianPosition = AM_V3(5.0f, 3.0f, 4.0f); + + THEN("it can convert to spherical coordinates in world space") + { + const auto sphericalPosition = SphericalPosition::FromWorldSpace(cartesianPosition); + + REQUIRE(sphericalPosition.GetAzimuth() == -std::atan2(cartesianPosition.Y, cartesianPosition.X)); + REQUIRE(sphericalPosition.GetElevation() == std::atan2(cartesianPosition.Z, AM_Len(cartesianPosition.XY))); + REQUIRE(sphericalPosition.GetRadius() == AM_Len(cartesianPosition)); + } + + THEN("it can convert to spherical coordinates in AmbiX space") + { + const auto sphericalPosition = SphericalPosition::ForHRTF(cartesianPosition); + + REQUIRE(sphericalPosition.GetAzimuth() == 90.0f * AM_DegToRad - std::atan2(cartesianPosition.Y, cartesianPosition.X)); + REQUIRE(sphericalPosition.GetElevation() == std::atan2(cartesianPosition.Z, AM_Len(cartesianPosition.XY))); + REQUIRE(sphericalPosition.GetRadius() == AM_Len(cartesianPosition)); + } + } + + GIVEN("values in degrees") + { + constexpr auto azimuth = 45.0f; + constexpr auto elevation = 30.0f; + constexpr auto radius = 5.0f; + + THEN("it should store the correct spherical coordinates") + { + const auto sphericalPosition = SphericalPosition::FromDegrees(azimuth, elevation, radius); + + REQUIRE(sphericalPosition.GetAzimuth() == azimuth * AM_DegToRad); + REQUIRE(sphericalPosition.GetElevation() == elevation * AM_DegToRad); + REQUIRE(sphericalPosition.GetRadius() == radius); + } + } +} \ No newline at end of file