using T3.Core.Audio; // ReSharper disable MemberCanBePrivate.Global namespace Lib.io.audio { [Guid("65e95f77-4743-437f-ab31-f34b831d28d7")] internal sealed class AudioPlayer : Instance { [Input(Guid = "505139a0-71ce-4297-8440-5bf84488902e")] public readonly InputSlot AudioFile = new(); [Input(Guid = "726bc4d3-df8b-4abe-a38e-2e09cf44ca10")] public readonly InputSlot PlayAudio = new(); [Input(Guid = "59b659c6-ca1f-4c2b-8dff-3a1da9abd352")] public readonly InputSlot StopAudio = new(); [Input(Guid = "7e42f2a8-3c5d-4f6e-9b8a-1d2e3f4a5b6c")] public readonly InputSlot PauseAudio = new(); [Input(Guid = "c0645e37-db4e-4658-9d65-96478851f6f6")] public readonly InputSlot Volume = new(); [Input(Guid = "1a3f4b7c-12d3-4a5b-9c7d-8e1f2a3b4c5d")] public readonly InputSlot Mute = new(); [Input(Guid = "53d1622e-b1d5-4b1c-acd0-ebceb7064043")] public readonly InputSlot Panning = new(); [Input(Guid = "d1a11c4c-9526-4f6b-873e-1798b9dd2b48")] public readonly InputSlot Speed = new(); [Input(Guid = "a5de0d72-5924-4f3a-a02f-d5de7c03f07f")] public readonly InputSlot Seek = new(); [Input(Guid = "905d9e01-b1fb-47c0-801c-fc920ed36884", MappedType = typeof(AdsrCalculator.TriggerMode))] public readonly InputSlot TriggerMode = new(); [Input(Guid = "f7a8b9c0-d1e2-4f3a-5b6c-7d8e9f0a1b2c")] public readonly InputSlot Duration = new(); [Input(Guid = "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e")] public readonly InputSlot UseEnvelope = new(); [Input(Guid = "3dbcbbe6-a8b4-4b83-a2c0-e22b24b91b42")] public readonly InputSlot Envelope = new(); [Output(Guid = "2433f838-a8ba-4f3a-809e-2d41c404bb84")] public readonly Slot Result = new(); [Output(Guid = "960aa0a3-89b4-4eff-8b52-36ff6965cf8f")] public readonly Slot IsPlaying = new(); [Output(Guid = "b09d215a-bcf0-479a-a649-56f9c698ecb1")] public readonly Slot GetLevel = new(); private Guid _operatorId; private bool _wasPausedLastFrame; private bool _previousPlayTrigger; private readonly AdsrCalculator _calculator = new(); public AudioPlayer() { Result.UpdateAction += UpdatePlayback; IsPlaying.UpdateAction += UpdateStatus; GetLevel.UpdateAction += UpdateStatus; } private void EnsureOperatorId() { if (_operatorId != Guid.Empty) return; _operatorId = AudioPlayerUtils.ComputeInstanceGuid(InstancePath); Log.Gated.Audio($"[AudioPlayer] Initialized: {_operatorId}"); } private void UpdateStatus(EvaluationContext context) { EnsureOperatorId(); IsPlaying.Value = AudioEngine.IsOperatorStreamPlaying(_operatorId); GetLevel.Value = AudioEngine.GetOperatorLevel(_operatorId); } 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 panning = Panning.GetValue(context); var speed = Speed.GetValue(context); var seek = Seek.GetValue(context); var triggerMode = (AdsrCalculator.TriggerMode)TriggerMode.GetValue(context); var duration = Duration.GetValue(context); var useEnvelope = UseEnvelope.GetValue(context); var envelope = Envelope.GetValue(context); // Apply defaults if (duration <= 0) duration = float.MaxValue; // Extract ADSR from Vector4: X=Attack, Y=Decay, Z=Sustain, W=Release var attack = envelope.X > 0 ? envelope.X : 0.01f; var decay = envelope.Y > 0 ? envelope.Y : 0.1f; var sustain = envelope.Z >= 0 ? Math.Clamp(envelope.Z, 0f, 1f) : 0.7f; var release = envelope.W > 0 ? envelope.W : 0.3f; // Update ADSR calculator parameters _calculator.SetParameters(attack, decay, sustain, release); _calculator.SetMode(triggerMode); _calculator.SetDuration(duration); // Detect play trigger edges for ADSR var risingEdge = shouldPlay && !_previousPlayTrigger; var fallingEdge = !shouldPlay && _previousPlayTrigger; _previousPlayTrigger = shouldPlay; if (useEnvelope) { if (risingEdge) { _calculator.TriggerAttack(); } else if (fallingEdge && triggerMode == AdsrCalculator.TriggerMode.Gate) { _calculator.TriggerRelease(); } // Update envelope (frame-based for UI display) _calculator.Update(shouldPlay, context.LocalFxTime, attack, decay, sustain, release, triggerMode, duration); } // Apply envelope to volume only if UseEnvelope is enabled var envelopeModulatedVolume = useEnvelope ? volume * _calculator.Value : volume; // Handle pause/resume transitions if (shouldPause != _wasPausedLastFrame) { if (shouldPause) AudioEngine.PauseOperator(_operatorId); else AudioEngine.ResumeOperator(_operatorId); } _wasPausedLastFrame = shouldPause; AudioEngine.UpdateStereoOperatorPlayback( operatorId: _operatorId, filePath: filePath, shouldPlay: shouldPlay, shouldStop: shouldStop, volume: envelopeModulatedVolume, mute: mute, panning: panning, speed: speed, seek: seek); IsPlaying.Value = AudioEngine.IsOperatorStreamPlaying(_operatorId); GetLevel.Value = AudioEngine.GetOperatorLevel(_operatorId); } ~AudioPlayer() { if (_operatorId != Guid.Empty) AudioEngine.UnregisterOperator(_operatorId); } } }