#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);
}
}