diff --git a/assets/data/sadie_h12.amir b/assets/data/sadie_h12.amir new file mode 100644 index 00000000..e4d15e97 Binary files /dev/null and b/assets/data/sadie_h12.amir differ diff --git a/include/SparkyStudios/Audio/Amplitude/Core/Common.h b/include/SparkyStudios/Audio/Amplitude/Core/Common.h index ac9bf1ea..7b2f9043 100644 --- a/include/SparkyStudios/Audio/Amplitude/Core/Common.h +++ b/include/SparkyStudios/Audio/Amplitude/Core/Common.h @@ -329,6 +329,26 @@ namespace SparkyStudios::Audio::Amplitude ePanningMode_BinauralHighQuality = 3, }; + /** + * @brief Defines how the HRIR sphere is sampled when doing Ambisonics binauralization. + * + * @ingroup core + */ + enum eHRIRSphereSamplingMode : AmUInt8 + { + /** + * @brief Provides the most accurate binauralization, as the HRIR data are smoothly transitioned between sphere points. + * + * See more info about bilinear sampling [here](http://www02.smt.ufrj.br/~diniz/conf/confi117.pdf). + */ + eHRIRSphereSamplingMode_Bilinear = 0, + + /** + * @brief Provides a more efficient binauralization, as the HRIR data are interpolated using only the nearest neighbors. + */ + eHRIRSphereSamplingMode_NearestNeighbor = 1, + }; + /** * @brief Describe the format of an audio sample. * diff --git a/include/SparkyStudios/Audio/Amplitude/Core/Engine.h b/include/SparkyStudios/Audio/Amplitude/Core/Engine.h index c10897ca..bbbb9c78 100644 --- a/include/SparkyStudios/Audio/Amplitude/Core/Engine.h +++ b/include/SparkyStudios/Audio/Amplitude/Core/Engine.h @@ -32,6 +32,8 @@ #include #include +#include + #include #include @@ -75,7 +77,7 @@ namespace SparkyStudios::Audio::Amplitude public: virtual ~Engine() = default; -#pragma region Miscalaneous +#pragma region Miscellaneous /** * @brief Gets the version structure. @@ -1215,7 +1217,7 @@ namespace SparkyStudios::Audio::Amplitude #pragma endregion -#pragma region Engine State& Configuration +#pragma region Engine State and Configuration /** * @brief Get the current speed of sound. @@ -1296,6 +1298,25 @@ namespace SparkyStudios::Audio::Amplitude */ [[nodiscard]] virtual ePanningMode GetPanningMode() const = 0; + /** + * Gets the HRIR sphere sampling mode defined in the loaded engine configuration. + * + * @return The HRIR sphere sampling mode. + */ + [[nodiscard]] virtual eHRIRSphereSamplingMode GetHRIRSphereSamplingMode() const = 0; + + /** + * Gets the HRIR sphere defined in the loaded engine configuration. + * + * @return The HRIR sphere. If no HRIR sphere is defined, returns nullptr. + * + * @note The HRIR sphere is optional and can be null in some cases. If the + * engine does not have an HRIR sphere defined, this function will return `nullptr`. + * + * @see HRIRSphere + */ + [[nodiscard]] virtual const HRIRSphere* GetHRIRSphere() const = 0; + #pragma endregion #pragma region Plugins Management diff --git a/include/SparkyStudios/Audio/Amplitude/HRTF/HRIRSphere.h b/include/SparkyStudios/Audio/Amplitude/HRTF/HRIRSphere.h index 8212a846..2220bb8b 100644 --- a/include/SparkyStudios/Audio/Amplitude/HRTF/HRIRSphere.h +++ b/include/SparkyStudios/Audio/Amplitude/HRTF/HRIRSphere.h @@ -149,24 +149,25 @@ namespace SparkyStudios::Audio::Amplitude [[nodiscard]] virtual AmUInt32 GetIRLength() const = 0; /** - * @brief Samples the HRIR sphere for the given direction using bilinear interpolation. + * @brief Sets the sampling mode for the HRIR sphere. * - * See more info about bilinear sampling [here](http://www02.smt.ufrj.br/~diniz/conf/confi117.pdf). - * - * @param[in] direction The sound to listener direction. - * @param[out] leftHRIR The left HRIR data. - * @param[out] rightHRIR The right HRIR data. + * @param[in] mode The sampling mode to use. + */ + virtual void SetSamplingMode(eHRIRSphereSamplingMode mode) = 0; + + /** + * @brief Gets the sampling mode for the HRIR sphere. */ - virtual void SampleBilinear(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const = 0; + [[nodiscard]] virtual eHRIRSphereSamplingMode GetSamplingMode() const = 0; /** - * @brief Samples the HRIR sphere for the given direction using nearest neighbor interpolation. + * @brief Samples the HRIR sphere for the given direction. * * @param[in] direction The sound to listener direction. * @param[out] leftHRIR The left HRIR data. * @param[out] rightHRIR The right HRIR data. */ - virtual void SampleNearestNeighbor(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const = 0; + virtual void Sample(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const = 0; virtual void Transform(const AmMat4& matrix) = 0; diff --git a/samples/rawassets/pc.config.json b/samples/rawassets/pc.config.json index db78a5f9..51a26268 100644 --- a/samples/rawassets/pc.config.json +++ b/samples/rawassets/pc.config.json @@ -1,7 +1,7 @@ { "driver": "miniaudio", "output": { - "frequency": 44100, + "frequency": 48000, "buffer_size": 4096, "format": "Float32" }, @@ -11,6 +11,10 @@ "virtual_channels": 100, "pipeline": "default.ampipeline" }, + "hrtf": { + "amir_file": "data/sadie_h12.amir", + "hrir_sampling": "Bilinear" + }, "game": { "listener_fetch_mode": "Nearest", "track_environments": true, diff --git a/schemas/engine_config_definition.fbs b/schemas/engine_config_definition.fbs index 1b6fb47b..eaeb5432 100644 --- a/schemas/engine_config_definition.fbs +++ b/schemas/engine_config_definition.fbs @@ -53,9 +53,9 @@ enum eListenerFetchMode : ushort { /// that only one listener is used everytime. /// /// The default listener should be set using Engine::SetDefaultListener(). - /// If no listener is set to default, the behavior is the same as ListenerFetchMode::None. + /// If no listener is set to default, the behavior is the same as eListenerFetchMode::None. /// If the listener set as default is next removed, the behavior will be the same as - /// ListenerFetchMode::None, until another default listener is set. + /// eListenerFetchMode::None, until another default listener is set. Default, /// Fetches for the first available listener. This ensures @@ -67,6 +67,18 @@ enum eListenerFetchMode : ushort { Last, } +/// Defines how the HRIR sphere is sampled when doing Ambisonics +/// binauralization. +enum HRIRSphereSamplingMode : ushort { + /// Provides the most accurate binauralization, as the HRIR data are smoothly + /// transitioned between sphere points. + Bilinear, + + /// Provides the fastest binauralization, as the closest sphere point to the + /// current direction is always picked. + NearestNeighbor, +} + /// Playback device configuration table PlaybackOutputConfig { /// Output sampling frequency in samples per second. @@ -91,9 +103,9 @@ table AudioMixerConfig { /// still tracked. virtual_channels:uint; - /// Defines the panning mode to use. If Stereo mode is set, all the sound sources - /// playing with HRTF spatialization will fallback to PositionOrientation spatialization. - panning_mode:PanningMode; + /// Defines the panning mode to use. Using any valy other than Stereo + /// will apply Ambisonics binauralization with the defined HRIR sphere file. + panning_mode:PanningMode = Stereo; /// The name of the pipeline asset file to load. pipeline:string (required); @@ -129,7 +141,7 @@ table GameSyncConfig { rooms:uint = 1024; /// The maximum speed of sound in the game. - sound_speed:float = 333.0; + sound_speed:float = 343.0; /// The Doppler factor. Set 1.0 to normal Doppler effect, /// set 0.0 to disable Doppler effect. @@ -148,6 +160,16 @@ table GameSyncConfig { track_environments:bool = true; } +/// HRTF and Ambisonics binauralization configuration +table HRTFConfig { + /// The path to the AMIR asset file (generated using the amit tool). + amir_file:string (required); + + /// The HRIR sampling mode. + hrir_sampling: +HRIRSphereSamplingMode = NearestNeighbor; +} + table EngineConfigDefinition { /// Configures the playback device. output:PlaybackOutputConfig (required); @@ -155,6 +177,9 @@ table EngineConfigDefinition { /// Configures the audio mixer. mixer:AudioMixerConfig (required); + /// Configures HRTF processing. + hrtf:HRTFConfig; + /// Configures the game sync. game:GameSyncConfig (required); diff --git a/src/Ambisonics/AmbisonicBinauralizer.cpp b/src/Ambisonics/AmbisonicBinauralizer.cpp index c68d45cd..535ae90d 100644 --- a/src/Ambisonics/AmbisonicBinauralizer.cpp +++ b/src/Ambisonics/AmbisonicBinauralizer.cpp @@ -64,11 +64,11 @@ namespace SparkyStudios::Audio::Amplitude hrir[0].Clear(); hrir[1].Clear(); - _hrir->SampleBilinear(position.ToCartesian(), hrir[0].GetBuffer(), hrir[1].GetBuffer()); + _hrir->Sample(position.ToCartesian(), hrir[0].GetBuffer(), hrir[1].GetBuffer()); // Scale the HRTFs by the coefficient of the current channel/component - // The spherical harmonic coefficients are multiplied by (2*order + 1) to provide the correct decoder - // for SN3D normalized Ambisonic inputs. + // The spherical harmonic coefficients are multiplied by (2*order + 1) to provide the correct decoder + // for SN3D normalized Ambisonic inputs. const AmReal32 coefficient = _decoder.GetSpeakerCoefficient(i, c) * (2.f * std::floor(std::sqrt(static_cast(c))) + 1.f); @@ -111,8 +111,8 @@ namespace SparkyStudios::Audio::Amplitude for (AmUInt32 c = 0; c < m_channelCount; c++) { - _convL[c].Init(kInterpolationBlockSize, _accumulatedHRIR[0][c].begin(), _hrir->GetIRLength()); - _convR[c].Init(kInterpolationBlockSize, _accumulatedHRIR[1][c].begin(), _hrir->GetIRLength()); + _convL[c].Init(_hrir->GetIRLength(), _accumulatedHRIR[0][c].begin(), _hrir->GetIRLength()); + _convR[c].Init(_hrir->GetIRLength(), _accumulatedHRIR[1][c].begin(), _hrir->GetIRLength()); } return true; diff --git a/src/Core/Engine.cpp b/src/Core/Engine.cpp index d54bba99..0996efe9 100644 --- a/src/Core/Engine.cpp +++ b/src/Core/Engine.cpp @@ -688,9 +688,30 @@ namespace SparkyStudios::Audio::Amplitude return false; } - // Load the panning mode + // Store the panning mode _state->panning_mode = static_cast(config->mixer()->panning_mode()); + const auto* hrtfConfig = config->hrtf(); + + if (hrtfConfig != nullptr) + { + // Store the HRIR sampling mode + _state->hrir_sampling_mode = static_cast(config->hrtf()->hrir_sampling()); + + // Load the HRIR sphere + _state->hrir_sphere = ampoolnew(MemoryPoolKind::Engine, HRIRSphereImpl); + _state->hrir_sphere->SetResource(AM_STRING_TO_OS_STRING(config->hrtf()->amir_file()->c_str())); + _state->hrir_sphere->SetSamplingMode(_state->hrir_sampling_mode); + _state->hrir_sphere->Load(GetFileSystem()); + } + else if (_state->panning_mode != ePanningMode_Stereo) + { + amLogCritical("The HRTF configuration is missing, but the panning mode is not stereo. Please provide an HRTF configuration, or " + "set the panning mode to Stereo."); + Deinitialize(); + return false; + } + // Initialize audio mixer if (!_state->mixer.Init(config)) { @@ -819,6 +840,13 @@ namespace SparkyStudios::Audio::Amplitude // Unload sound banks UnloadSoundBanks(); + // Release HRIR sphere + if (_state->hrir_sphere != nullptr) + { + ampooldelete(MemoryPoolKind::Engine, HRIRSphereImpl, _state->hrir_sphere); + _state->hrir_sphere = nullptr; + } + ampooldelete(MemoryPoolKind::Engine, EngineInternalState, _state); _state = nullptr; @@ -2359,6 +2387,16 @@ namespace SparkyStudios::Audio::Amplitude return _state->panning_mode; } + eHRIRSphereSamplingMode EngineImpl::GetHRIRSphereSamplingMode() const + { + return _state->hrir_sampling_mode; + } + + const HRIRSphere* EngineImpl::GetHRIRSphere() const + { + return _state->hrir_sphere; + } + #pragma endregion Channel EngineImpl::PlayScopedSwitchContainer( diff --git a/src/Core/Engine.h b/src/Core/Engine.h index 84099f15..6a8cfa10 100644 --- a/src/Core/Engine.h +++ b/src/Core/Engine.h @@ -189,6 +189,8 @@ namespace SparkyStudios::Audio::Amplitude [[nodiscard]] const Curve& GetObstructionCoefficientCurve() const override; [[nodiscard]] const Curve& GetObstructionGainCurve() const override; [[nodiscard]] ePanningMode GetPanningMode() const override; + [[nodiscard]] eHRIRSphereSamplingMode GetHRIRSphereSamplingMode() const override; + [[nodiscard]] const HRIRSphere* GetHRIRSphere() const override; private: Channel PlayScopedSwitchContainer( diff --git a/src/Core/EngineInternalState.h b/src/Core/EngineInternalState.h index a1d76655..14fcd6b6 100644 --- a/src/Core/EngineInternalState.h +++ b/src/Core/EngineInternalState.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -154,7 +155,7 @@ namespace SparkyStudios::Audio::Amplitude , current_frame(0) , total_time(0.0) , listener_fetch_mode(eListenerFetchMode_None) - , sound_speed(333.0) + , sound_speed(343.0) , doppler_factor(1.0) , obstruction_config() , occlusion_config() @@ -163,13 +164,15 @@ namespace SparkyStudios::Audio::Amplitude , track_environments(false) , samples_per_stream(512) , panning_mode(ePanningMode_Stereo) + , hrir_sampling_mode(eHRIRSphereSamplingMode_NearestNeighbor) + , hrir_sphere(nullptr) , version(nullptr) {} AmplimixImpl mixer; // Hold the audio buses definition file contents. - std::string buses_source; + AmString buses_source; // The state of the buses. std::vector buses; @@ -250,7 +253,7 @@ namespace SparkyStudios::Audio::Amplitude PipelineImpl pipeline; // Hold the audio buses definition file contents. - std::string pipeline_source; + AmString pipeline_source; // The pre-allocated pool of all ChannelInternalState objects ChannelStateVector channel_state_memory; @@ -303,6 +306,10 @@ namespace SparkyStudios::Audio::Amplitude ePanningMode panning_mode; + eHRIRSphereSamplingMode hrir_sampling_mode; + + HRIRSphereImpl* hrir_sphere; + const struct Version* version; }; @@ -341,7 +348,7 @@ namespace SparkyStudios::Audio::Amplitude // listener, respectively. AmVec2 CalculatePan(const AmVec3& listenerSpaceLocation); - bool LoadFile(const std::shared_ptr& file, std::string* dest); + bool LoadFile(const std::shared_ptr& file, AmString* dest); AmUInt32 GetMaxNumberOfChannels(const EngineConfigDefinition* config); } // namespace SparkyStudios::Audio::Amplitude diff --git a/src/HRTF/HRIRSphere.cpp b/src/HRTF/HRIRSphere.cpp index 464a00d5..aa78f5d9 100644 --- a/src/HRTF/HRIRSphere.cpp +++ b/src/HRTF/HRIRSphere.cpp @@ -141,6 +141,38 @@ namespace SparkyStudios::Audio::Amplitude return _header.m_IRLength; } + eHRIRSphereSamplingMode HRIRSphereImpl::GetSamplingMode() const + { + return _samplingMode; + } + + void HRIRSphereImpl::SetSamplingMode(eHRIRSphereSamplingMode mode) + { + _samplingMode = mode; + } + + void HRIRSphereImpl::Sample(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const + { + switch (_samplingMode) + { + case eHRIRSphereSamplingMode_Bilinear: + return SampleBilinear(direction, leftHRIR, rightHRIR); + case eHRIRSphereSamplingMode_NearestNeighbor: + return SampleNearestNeighbor(direction, leftHRIR, rightHRIR); + } + } + + void HRIRSphereImpl::Transform(const AmMat4& matrix) + { + for (auto& vertex : _vertices) + vertex.m_Position = AM_Mul(matrix, AM_V4V(vertex.m_Position, 1.0f)).XYZ; + } + + bool HRIRSphereImpl::IsLoaded() const + { + return _loaded; + } + void HRIRSphereImpl::SampleBilinear(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const { const auto& dir = AM_Mul(direction, 10.0f); @@ -248,17 +280,6 @@ namespace SparkyStudios::Audio::Amplitude } } - void HRIRSphereImpl::Transform(const AmMat4& matrix) - { - for (auto& vertex : _vertices) - vertex.m_Position = AM_Mul(matrix, AM_V4V(vertex.m_Position, 1.0f)).XYZ; - } - - bool HRIRSphereImpl::IsLoaded() const - { - return _loaded; - } - const HRIRSphereVertex* HRIRSphereImpl::GetClosestVertex(const AmVec3& position, const Face* face) const { const auto& vertexA = _vertices[face->m_A]; diff --git a/src/HRTF/HRIRSphere.h b/src/HRTF/HRIRSphere.h index 6c8a63c4..0137d5dc 100644 --- a/src/HRTF/HRIRSphere.h +++ b/src/HRTF/HRIRSphere.h @@ -52,14 +52,18 @@ namespace SparkyStudios::Audio::Amplitude [[nodiscard]] AmUInt32 GetFaceCount() const override; [[nodiscard]] AmUInt32 GetSampleRate() const override; [[nodiscard]] AmUInt32 GetIRLength() const override; - void SampleBilinear(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const override; - void SampleNearestNeighbor(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const override; + void SetSamplingMode(eHRIRSphereSamplingMode mode) override; + [[nodiscard]] eHRIRSphereSamplingMode GetSamplingMode() const override; + void Sample(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const override; void Transform(const AmMat4& matrix) override; [[nodiscard]] bool IsLoaded() const override; private: + void SampleBilinear(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const; + void SampleNearestNeighbor(const AmVec3& direction, AmReal32* leftHRIR, AmReal32* rightHRIR) const; const HRIRSphereVertex* GetClosestVertex(const AmVec3& position, const Face* face) const; + eHRIRSphereSamplingMode _samplingMode; HRIRSphereFileHeaderDescription _header; std::vector _vertices; std::vector _faces; diff --git a/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.cpp b/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.cpp index db280298..4d71bc84 100644 --- a/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.cpp +++ b/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.cpp @@ -20,11 +20,16 @@ namespace SparkyStudios::Audio::Amplitude { - AmbisonicBinauralDecoderNodeInstance::AmbisonicBinauralDecoderNodeInstance(const HRIRSphere* hrirSphere) - : _hrirSphere(hrirSphere) + AmbisonicBinauralDecoderNodeInstance::AmbisonicBinauralDecoderNodeInstance() + : ProcessorNodeInstance(false) { - const ePanningMode mode = Engine::GetInstance()->GetPanningMode(); - const AmUInt32 order = AM_MAX(static_cast(mode), 1); + _hrirSphere = Engine::GetInstance()->GetHRIRSphere(); + ePanningMode mode = Engine::GetInstance()->GetPanningMode(); + + if (mode != ePanningMode_Stereo && _hrirSphere == nullptr) + mode = ePanningMode_Stereo; + + const AmUInt32 order = AM_MAX(static_cast(mode), 1u); if (mode == ePanningMode_Stereo) _decoder.Configure(order, true, eSpeakersPreset_Stereo); @@ -40,7 +45,7 @@ namespace SparkyStudios::Audio::Amplitude const auto* layer = GetLayer(); const ePanningMode mode = Engine::GetInstance()->GetPanningMode(); - const AmUInt32 order = AM_MAX(static_cast(mode), 1); + const AmUInt32 order = AM_MAX(static_cast(mode), 1u); BFormat soundField; soundField.Configure(order, true, input->GetFrameCount()); @@ -60,12 +65,5 @@ namespace SparkyStudios::Audio::Amplitude AmbisonicBinauralDecoderNode::AmbisonicBinauralDecoderNode() : Node("AmbisonicBinauralDecoder") - { - if (!_hrirSphere.IsLoaded()) - { - // TODO: Get the HRIR file to load from settings. - _hrirSphere.SetResource(AM_OS_STRING("./data/mit.amir")); - _hrirSphere.Load(Engine::GetInstance()->GetFileSystem()); - } - } + {} } // namespace SparkyStudios::Audio::Amplitude diff --git a/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.h b/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.h index 384170c3..d065f88e 100644 --- a/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.h +++ b/src/Mixer/Nodes/AmbisonicBinauralDecoderNode.h @@ -29,7 +29,7 @@ namespace SparkyStudios::Audio::Amplitude class AmbisonicBinauralDecoderNodeInstance final : public ProcessorNodeInstance { public: - AmbisonicBinauralDecoderNodeInstance(const HRIRSphere* hrir); + AmbisonicBinauralDecoderNodeInstance(); const AudioBuffer* Process(const AudioBuffer* input) override; @@ -48,16 +48,13 @@ namespace SparkyStudios::Audio::Amplitude [[nodiscard]] AM_INLINE NodeInstance* CreateInstance() const override { - return ampoolnew(MemoryPoolKind::Amplimix, AmbisonicBinauralDecoderNodeInstance, &_hrirSphere); + return ampoolnew(MemoryPoolKind::Amplimix, AmbisonicBinauralDecoderNodeInstance); } void DestroyInstance(NodeInstance* instance) const override { ampooldelete(MemoryPoolKind::Amplimix, AmbisonicBinauralDecoderNodeInstance, (AmbisonicBinauralDecoderNodeInstance*)instance); } - - private: - HRIRSphereImpl _hrirSphere; }; } // namespace SparkyStudios::Audio::Amplitude