#nullable enable using System.Collections.Generic; using T3.Core.Operator; namespace T3.Core.Audio; /// /// Per-frame registrar for op-backed audio clips. Scans a composition's children for /// s with AutoPlay on and registers the ones whose /// TimeRange contains the playhead with the — the /// graph-independent "drop a clip and hear it" path that doesn't need an [AudioClipPlayer]. /// /// /// Registration is a heartbeat: creates the BASS stream /// on first call, seeks / pauses / volumes it via SoundtrackClipStream, and frees it the first /// frame it isn't called again (stale eviction). So this method only declares "active now" — it never /// starts, stops, seeks, or frees streams itself. Called from the editor playback update and the /// player main loop. /// public static class AudioClipCollector { public static void RegisterAutoPlayClips(Instance? composition, double timeInBars, double timeInSecs) { if (composition == null) return; // Rebuild the cached provider list only when the composition instance changes (focus switch / // hot-reload → a new instance) or the graph structure changes (Symbol.StructureVersion). This // avoids the per-frame Children.Values walk + locked per-child lookup, which is a frame-drop // source on large graphs (InstanceChildren's own TODO flags it). if (!ReferenceEquals(_cachedComposition, composition) || _cachedStructureVersion != composition.Symbol.VersionCounter) { _cachedComposition = composition; _cachedStructureVersion = composition.Symbol.VersionCounter; _cachedProviders.Clear(); foreach (var child in composition.Children.Values) { if (child is IAudioClipProvider provider) _cachedProviders.Add(provider); } } // AutoPlay is a per-frame input value, so it's read each frame (not cached). for (var i = 0; i < _cachedProviders.Count; i++) { var provider = _cachedProviders[i]; if (provider.AutoPlay) RegisterIfActive(provider, timeInBars, timeInSecs); } } private static Instance? _cachedComposition; private static int _cachedStructureVersion = -1; private static readonly List _cachedProviders = new(); /// /// Registers a single clip with the engine for this frame if the playhead is inside its TimeRange. /// Shared by the AutoPlay registrar and [AudioClipPlayer]. Does not mark the clip as managed — /// the player does that separately (for all clips it drives, active or not) to feed its status hint. /// public static void RegisterIfActive(IAudioClipProvider provider, double timeInBars, double timeInSecs) { var timeClip = provider.TimeClip; if (timeClip == null) return; // Exclusive end matches TimeClipSlot's own range test, so adjacent clips sharing a cut // boundary don't both register on that frame. if (timeInBars < timeClip.TimeRange.Start || timeInBars >= timeClip.TimeRange.End) return; AudioEngine.UseSoundtrackClip(provider.GetResourceHandle(), timeInSecs); } }