#nullable enable using ManagedBass; using T3.Core.Audio; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Local namespace Lib.io.audio { /// /// A spatial audio player operator that provides 3D positional audio playback capabilities. /// Supports sound positioning, listener orientation, distance-based attenuation, and directional sound cones. /// [Guid("8a3c9f2e-4b7d-4e1a-9c5f-7d2e8b1a6c3f")] internal sealed class SpatialAudioPlayer : Instance, ITransformable, ISpatialAudioPropertiesProvider { #region ITransformable Implementation IInputSlot ITransformable.TranslationInput => SourcePosition; IInputSlot ITransformable.RotationInput => SourceRotation; IInputSlot? ITransformable.ScaleInput => null; public Action? TransformCallback { get; set; } #endregion #region ISpatialAudioPropertiesProvider Implementation Vector3 ISpatialAudioPropertiesProvider.SourcePosition => SourcePosition.Value; Vector3 ISpatialAudioPropertiesProvider.SourceRotation => SourceRotation.Value; Vector3 ISpatialAudioPropertiesProvider.ListenerPosition => ListenerPosition.Value; Vector3 ISpatialAudioPropertiesProvider.ListenerRotation => ListenerRotation.Value; float ISpatialAudioPropertiesProvider.MinDistance => MinDistance.Value; float ISpatialAudioPropertiesProvider.MaxDistance => MaxDistance.Value; float ISpatialAudioPropertiesProvider.InnerConeAngle => InnerConeAngle.Value; float ISpatialAudioPropertiesProvider.OuterConeAngle => OuterConeAngle.Value; GizmoVisibility ISpatialAudioPropertiesProvider.GizmoVisibility => Visibility.Value; #endregion [Input(Guid = "2aa976e3-a63c-4c7e-80a5-a459b3240388")] public readonly InputSlot AudioFile = new(); [Input(Guid = "454a65fa-c0a3-4055-85ec-ac3771b2524c")] public readonly InputSlot PlayAudio = new(); [Input(Guid = "1800cdcb-8863-423e-a2ec-901cf54e014c")] public readonly InputSlot StopAudio = new(); [Input(Guid = "d0c660d7-f90b-4e61-95ff-0ce7c76d9d67")] public readonly InputSlot PauseAudio = new(); [Input(Guid = "7110399e-fd85-4053-9a49-cc09760dceb6")] public readonly InputSlot Volume = new(); [Input(Guid = "ad8822bc-939b-4152-b253-bf01c5aa4d73")] public readonly InputSlot Mute = new(); [Input(Guid = "9c0b519b-45b1-4b28-8c0e-2eba7035a999")] public readonly InputSlot Speed = new(); [Input(Guid = "b921cbd4-4803-48e8-9eb9-f05901cc5802")] public readonly InputSlot Seek = new(); [Input(Guid = "f3730e3b-a335-4a13-9b2a-87b8e75cddeb")] public readonly InputSlot SourcePosition = new(); [Input(Guid = "3cf1b579-a1f2-4e9f-a54a-0e9e3580a6bf")] public readonly InputSlot SourceRotation = new(); [Input(Guid = "732be517-a58e-4f21-8395-7761a95905ea")] public readonly InputSlot MinDistance = new(); [Input(Guid = "7eb612cb-1dbf-4336-b91d-1042573bd7ff")] public readonly InputSlot MaxDistance = new(); [Input(Guid = "159c7342-a8c8-4ffb-8a69-da3ffa0d3b71")] public readonly InputSlot ListenerPosition = new(); [Input(Guid = "db9a0aa1-95db-4da4-99fc-31c385773ca8")] public readonly InputSlot ListenerRotation = new(); [Input(Guid = "785bb240-2020-4937-b132-b46f542e8986")] public readonly InputSlot InnerConeAngle = new(); [Input(Guid = "73971807-98d8-4b8b-b90f-41d673398b2b")] public readonly InputSlot OuterConeAngle = new(); [Input(Guid = "b4d639d4-d96b-42e1-a250-efe57d7e9c5e")] public readonly InputSlot OuterConeVolume = new(); [Input(Guid = "42d8337a-d419-4038-a132-0e3934d25a2a", MappedType = typeof(Audio3DModes))] public readonly InputSlot Audio3DMode = new(); /// /// Controls the visibility of the transform gizmo. /// [Input(Guid = "77d9309e-0c9d-407f-b9b1-dd4b734fa51e")] public readonly InputSlot Visibility = new(); // Outputs /// /// Command output for chaining with other operators. /// [Output(Guid = "117c0d2e-c14b-471a-8a6d-a9f983e48908")] public readonly TransformCallbackSlot Result = new(); /// /// Indicates whether the audio is currently playing. /// [Output(Guid = "022b0246-73d7-4a99-9c89-3e199171fd6c")] public readonly Slot IsPlaying = new(); /// /// Indicates whether the audio is currently paused. /// [Output(Guid = "52e624ca-508c-44c1-8583-946260e9f660")] public readonly Slot IsPaused = new(); /// /// Returns the current audio level/amplitude. /// [Output(Guid = "ccbff254-1090-4ba2-8341-08fa3cb1540c")] public readonly Slot GetLevel = new(); private Guid _operatorId; private bool _wasPausedLastFrame; /// /// Gets the current audio file path being played. /// public string CurrentFilePath { get; private set; } = string.Empty; /// /// Initializes a new instance of the class. /// public SpatialAudioPlayer() { Result.TransformableOp = this; Result.UpdateAction += UpdatePlayback; IsPlaying.UpdateAction += UpdateStatus; IsPaused.UpdateAction += UpdateStatus; GetLevel.UpdateAction += UpdateStatus; } private void EnsureOperatorId() { if (_operatorId != Guid.Empty) return; _operatorId = AudioPlayerUtils.ComputeInstanceGuid(InstancePath); Log.Gated.Audio($"[SpatialAudioPlayer] Initialized: {_operatorId}"); } private void UpdateStatus(EvaluationContext context) { EnsureOperatorId(); IsPlaying.Value = AudioEngine.IsSpatialOperatorStreamPlaying(_operatorId); IsPaused.Value = AudioEngine.IsSpatialOperatorPaused(_operatorId); GetLevel.Value = AudioEngine.GetSpatialOperatorLevel(_operatorId); } /// /// Updates the spatial audio player state and processes playback. /// /// The evaluation context for the current frame. private void UpdatePlayback(EvaluationContext context) { EnsureOperatorId(); string? filePath = AudioFile.GetValue(context); bool shouldPlay = PlayAudio.GetValue(context); var shouldStop = StopAudio.GetValue(context) || !shouldPlay; var shouldPause = PauseAudio.GetValue(context); var volume = Volume.GetValue(context); var mute = Mute.GetValue(context); var sourcePosition = SourcePosition.GetValue(context); var listenerPosition = ListenerPosition.GetValue(context); var listenerRotation = ListenerRotation.GetValue(context); // Convert Euler angles (in degrees) to forward and up vectors var rotationMatrix = Matrix4x4.CreateFromYawPitchRoll( listenerRotation.Y * (MathF.PI / 180f), listenerRotation.X * (MathF.PI / 180f), listenerRotation.Z * (MathF.PI / 180f)); var listenerForward = Vector3.Transform(new Vector3(0, 0, 1), rotationMatrix); var listenerUp = Vector3.Transform(new Vector3(0, 1, 0), rotationMatrix); var minDistance = MinDistance.GetValue(context); if (minDistance <= 0) minDistance = 1.0f; var maxDistance = MaxDistance.GetValue(context); if (maxDistance <= minDistance) maxDistance = minDistance + 10.0f; var speed = Speed.GetValue(context); var seek = Seek.GetValue(context); var sourceRotation = SourceRotation.GetValue(context); // Convert source rotation (Euler angles in degrees) to orientation vector var sourceRotationMatrix = Matrix4x4.CreateFromYawPitchRoll( sourceRotation.Y * (MathF.PI / 180f), sourceRotation.X * (MathF.PI / 180f), sourceRotation.Z * (MathF.PI / 180f)); var sourceOrientation = Vector3.Transform(new Vector3(0, 0, 1), sourceRotationMatrix); var innerConeAngle = Math.Clamp(InnerConeAngle.GetValue(context), 0f, 360f); var outerConeAngle = Math.Clamp(OuterConeAngle.GetValue(context), 0f, 360f); var outerConeVolume = Math.Clamp(OuterConeVolume.GetValue(context), 0f, 1f); var audio3DMode = Math.Clamp(Audio3DMode.GetValue(context), 0, 2); AudioEngine.Set3DListenerPosition(listenerPosition, listenerForward, listenerUp); // Handle pause/resume transitions if (shouldPause != _wasPausedLastFrame) { if (shouldPause) AudioEngine.PauseSpatialOperator(_operatorId); else AudioEngine.ResumeSpatialOperator(_operatorId); } _wasPausedLastFrame = shouldPause; AudioEngine.UpdateSpatialOperatorPlayback( operatorId: _operatorId, filePath: filePath, shouldPlay: shouldPlay, shouldStop: shouldStop, volume: volume, mute: mute, position: sourcePosition, minDistance: minDistance, maxDistance: maxDistance, speed: speed, seek: seek, orientation: sourceOrientation, innerConeAngle: innerConeAngle, outerConeAngle: outerConeAngle, outerConeVolume: outerConeVolume, mode3D: audio3DMode); IsPlaying.Value = AudioEngine.IsSpatialOperatorStreamPlaying(_operatorId); IsPaused.Value = AudioEngine.IsSpatialOperatorPaused(_operatorId); GetLevel.Value = AudioEngine.GetSpatialOperatorLevel(_operatorId); } /// /// Render audio for export. This is called by AudioRendering during export. /// /// The start time in seconds. /// The duration to render in seconds. /// The buffer to write audio samples to. /// The number of samples written to the buffer. public int RenderAudio(double startTime, double duration, float[] buffer) { if (AudioEngine.TryGetSpatialOperatorStream(_operatorId, out var stream) && stream != null) return stream.RenderAudio(startTime, duration, buffer, AudioConfig.MixerFrequency, 2); Array.Clear(buffer, 0, buffer.Length); return buffer.Length; } /// /// Restores the volume level after an audio export operation. /// public void RestoreVolumeAfterExport() { if (AudioEngine.TryGetSpatialOperatorStream(_operatorId, out var stream) && stream != null) Bass.ChannelSetAttribute(stream.StreamHandle, ChannelAttribute.Volume, Volume.Value); } /// /// Finalizer that unregisters the operator from the audio engine. /// ~SpatialAudioPlayer() { if (_operatorId != Guid.Empty) AudioEngine.UnregisterOperator(_operatorId); } /// /// Defines the 3D audio processing modes. /// private enum Audio3DModes { /// /// Normal 3D audio processing with absolute positioning. /// Normal = 0, /// /// Relative 3D audio processing where the source position is relative to the listener. /// Example: footsteps, breathing, or equipment sounds attached to the listener's position. /// Relative = 1, /// /// Disables 3D audio processing. /// Off = 2 } } }