132 lines
5.3 KiB
C#
132 lines
5.3 KiB
C#
#nullable enable
|
|
using T3.Core.Animation;
|
|
using T3.Core.Audio;
|
|
|
|
namespace Lib.io.audio;
|
|
|
|
/// <summary>
|
|
/// A timeline audio clip as an operator. Placed via its <see cref="TimeSlot"/> (so it gets all the
|
|
/// standard op-clip interactions — drag / trim / split / snap / delete), it plays through the
|
|
/// <see cref="AudioEngine"/> while the playhead is inside its <c>TimeRange</c>. With
|
|
/// <see cref="AutoPlay"/> on (default) the clip registers itself for playback with no
|
|
/// <c>[AudioClipPlayer]</c> required; otherwise a player drives it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Mirrors <c>[VideoClip]</c> on the audio side. Reuses the existing playback machinery: it owns a
|
|
/// <see cref="TimelineAudioClip"/> data carrier synced from its inputs, and the engine plays it via
|
|
/// <see cref="AudioClipResourceHandle"/> + <c>SoundtrackClipStream</c>. The op never starts/stops the
|
|
/// stream — registration (heartbeat) is the engine's start/stop signal (see <see cref="AudioClipCollector"/>).
|
|
/// </remarks>
|
|
[Guid("f0008b50-091d-4e9f-91eb-baa212acfa20")]
|
|
internal sealed class AudioClip : Instance<AudioClip>, IAudioClipProvider, IContentTimeClip, IStatusProvider, IDescriptiveFilename
|
|
{
|
|
[Output(Guid = "5fb7a174-9ab2-4688-89a0-7fbcbf831dcf", DirtyFlagTrigger = DirtyFlagTrigger.Always)]
|
|
public readonly TimeClipSlot<Command> TimeSlot = new();
|
|
|
|
public AudioClip()
|
|
{
|
|
TimeSlot.UpdateAction += Update;
|
|
}
|
|
|
|
private void Update(EvaluationContext context)
|
|
{
|
|
// Command passthrough so the op can sit in a command chain (e.g. wired into a player).
|
|
Command.GetValue(context);
|
|
|
|
// When the op is actually evaluated (wired / driven by a player), sync from the live context
|
|
// so animated Volume / Mute take effect. The AutoPlay registrar covers the unwired case via
|
|
// GetResourceHandle() reading static input values.
|
|
SyncClip(context.Playback, Path.GetValue(context), Volume.GetValue(context), Mute.GetValue(context));
|
|
|
|
TimeSlot.DirtyFlag.Clear();
|
|
}
|
|
|
|
TimeClip? IAudioClipProvider.TimeClip => TimeSlot.TimeClip;
|
|
|
|
bool IAudioClipProvider.AutoPlay => AutoPlay.TypedInputValue.Value;
|
|
|
|
double IAudioClipProvider.SourceLengthInSeconds => _clip.LengthInSeconds;
|
|
|
|
AudioClipResourceHandle IAudioClipProvider.GetResourceHandle()
|
|
{
|
|
// Registrar path: no EvaluationContext, so read static input values. Animated inputs on an
|
|
// unwired clip aren't evaluated
|
|
SyncClip(Playback.Current, Path.TypedInputValue.Value, Volume.TypedInputValue.Value, Mute.TypedInputValue.Value);
|
|
return _handle ??= new AudioClipResourceHandle(_clip, this);
|
|
}
|
|
|
|
// Drives the timeline clip label (filename, or "RenamedName (file)") and the waveform body renderer.
|
|
InputSlot<string> IDescriptiveFilename.SourcePathSlot => Path;
|
|
|
|
void IAudioClipProvider.MarkManaged() => _lastManagedFrame = Playback.FrameCount;
|
|
private bool IsManaged => Playback.FrameCount - _lastManagedFrame <= ManagedFrameSlack;
|
|
private const int ManagedFrameSlack = 2;
|
|
|
|
private void SyncClip(Playback? playback, string? path, float volume, bool mute)
|
|
{
|
|
_clip.AssetPath = path ?? string.Empty;
|
|
_clip.Volume = volume;
|
|
_clip.IsMuted = mute;
|
|
|
|
var timeClip = TimeSlot.TimeClip;
|
|
_clip.TimeRange = timeClip.TimeRange;
|
|
_clip.LayerIndex = timeClip.LayerIndex;
|
|
|
|
// Source trim: SourceRange is file-time in bars; the engine seeks in seconds → map via BPM.
|
|
// Interim mapping — refine when start-handle source trim editing lands.
|
|
if (playback != null)
|
|
_clip.SourceOffsetSecs = playback.SecondsFromBars(timeClip.SourceRange.Start);
|
|
|
|
_syncedClip = true;
|
|
}
|
|
|
|
public IStatusProvider.StatusLevel GetStatusLevel()
|
|
{
|
|
if (!_syncedClip)
|
|
return IStatusProvider.StatusLevel.Success;
|
|
|
|
if (string.IsNullOrEmpty(_clip.AssetPath))
|
|
return IStatusProvider.StatusLevel.Warning;
|
|
|
|
// AutoPlay clips drive themselves via the registrar; otherwise they rely on an [AudioClipPlayer].
|
|
if (AutoPlay.TypedInputValue.Value || IsManaged)
|
|
return IStatusProvider.StatusLevel.Success;
|
|
|
|
return IStatusProvider.StatusLevel.Notice;
|
|
}
|
|
|
|
public string? GetStatusMessage()
|
|
{
|
|
if (!_syncedClip)
|
|
return string.Empty;
|
|
|
|
if (string.IsNullOrEmpty(_clip.AssetPath))
|
|
return "No audio file set.";
|
|
|
|
if (AutoPlay.TypedInputValue.Value || IsManaged)
|
|
return null;
|
|
|
|
return "Not played — enable AutoPlay or add an [AudioClipPlayer] that collects this clip.";
|
|
}
|
|
|
|
private readonly TimelineAudioClip _clip = new() { IsMainSoundtrack = false };
|
|
private AudioClipResourceHandle? _handle;
|
|
private int _lastManagedFrame;
|
|
private bool _syncedClip;
|
|
|
|
[Input(Guid = "97948c5e-10d5-4e18-824e-aea17eb4eb2a")]
|
|
public readonly InputSlot<Command> Command = new();
|
|
|
|
[Input(Guid = "625951af-5f99-4171-b5b0-c97413121f56")]
|
|
public readonly InputSlot<string> Path = new();
|
|
|
|
[Input(Guid = "06b8b927-ec47-4392-bb67-b9a140cc852b")]
|
|
public readonly InputSlot<float> Volume = new();
|
|
|
|
[Input(Guid = "4ad8fba6-6e13-4698-b3c6-bd5c808724ab")]
|
|
public readonly InputSlot<bool> Mute = new();
|
|
|
|
[Input(Guid = "260b61ae-7605-4f06-a3fb-793ae5a23646")]
|
|
public readonly InputSlot<bool> AutoPlay = new();
|
|
}
|