131 lines
5.5 KiB
C#
131 lines
5.5 KiB
C#
#nullable enable
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using ImGuiNET;
|
|
using SharpDX.Direct3D11;
|
|
using T3.Core.Animation;
|
|
using T3.Core.Audio;
|
|
using T3.Core.DataTypes;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Operator;
|
|
using T3.Core.Operator.Interfaces;
|
|
using T3.Core.Resource;
|
|
using T3.Editor.Gui.Audio;
|
|
using T3.Editor.Gui.Styling;
|
|
using Texture2D = T3.Core.DataTypes.Texture2D;
|
|
|
|
namespace T3.Editor.Gui.Windows.TimeLine.TimeClips;
|
|
|
|
/// <summary>
|
|
/// Draws the waveform image inside an <c>[AudioClip]</c> op-clip body. Mirrors
|
|
/// <see cref="DataClipBodyRenderer"/>: <see cref="TimeClipItem"/> calls <see cref="TryDraw"/> for
|
|
/// every op-clip, and this no-ops unless the op is an <see cref="IAudioClipProvider"/> with a file set.
|
|
/// </summary>
|
|
internal static class AudioClipBodyRenderer
|
|
{
|
|
public static void TryDraw(Instance instance,
|
|
TimeClip timeClip,
|
|
Vector2 bodyMin,
|
|
Vector2 bodyMax,
|
|
ImDrawListPtr drawList)
|
|
{
|
|
// Op-type gate: only [AudioClip] (and only when it exposes a file path).
|
|
if (instance is not IAudioClipProvider provider)
|
|
return;
|
|
if (instance is not IDescriptiveFilename descriptive)
|
|
return;
|
|
|
|
var path = descriptive.SourcePathSlot.TypedInputValue.Value;
|
|
if (string.IsNullOrEmpty(path))
|
|
return;
|
|
|
|
var bodyWidth = bodyMax.X - bodyMin.X;
|
|
var bodyHeight = bodyMax.Y - bodyMin.Y;
|
|
if (bodyWidth < 3 || bodyHeight < 4)
|
|
return;
|
|
|
|
if (!TryGetWaveformSrv(path, instance, out var srv))
|
|
return;
|
|
|
|
drawList.PushClipRect(bodyMin, bodyMax, true);
|
|
|
|
// The body maps linearly to the clip's source window [SourceRange.Start, SourceRange.End]
|
|
// (converted to seconds via BPM); the waveform image spans the whole file [0, length]. Draw
|
|
// the image only across the body sub-rect where file content actually exists — so a clip
|
|
// longer than its source isn't stretched, and a trimmed clip shows just its used slice.
|
|
// File length comes from the played clip when available, else an async probe (so the crop
|
|
// works immediately, not only after the clip plays once). Full-stretch fallback until known.
|
|
var lengthSecs = provider.SourceLengthInSeconds;
|
|
if (lengthSecs <= 0.0001)
|
|
AudioClipDurationCache.TryGetDurationSecs(path, instance, out lengthSecs);
|
|
|
|
var playback = Playback.Current;
|
|
var drawn = false;
|
|
if (lengthSecs > 0.0001 && playback != null)
|
|
{
|
|
var sourceStartSecs = playback.SecondsFromBars(timeClip.SourceRange.Start);
|
|
var sourceEndSecs = playback.SecondsFromBars(timeClip.SourceRange.End);
|
|
var sourceSpanSecs = sourceEndSecs - sourceStartSecs;
|
|
var visibleStartSecs = Math.Max(sourceStartSecs, 0.0);
|
|
var visibleEndSecs = Math.Min(sourceEndSecs, lengthSecs);
|
|
|
|
if (sourceSpanSecs > 0.0001 && visibleEndSecs > visibleStartSecs)
|
|
{
|
|
var x0 = (float)((visibleStartSecs - sourceStartSecs) / sourceSpanSecs);
|
|
var x1 = (float)((visibleEndSecs - sourceStartSecs) / sourceSpanSecs);
|
|
var u0 = (float)(visibleStartSecs / lengthSecs);
|
|
var u1 = (float)(visibleEndSecs / lengthSecs);
|
|
|
|
drawList.AddImage((IntPtr)srv,
|
|
new Vector2(bodyMin.X + x0 * bodyWidth + 1, bodyMin.Y + 1),
|
|
new Vector2(bodyMin.X + x1 * bodyWidth - 1, bodyMax.Y - 1),
|
|
new Vector2(u0, 0), new Vector2(u1, 1),
|
|
UiColors.ForegroundFull.Fade(1f));
|
|
drawn = true;
|
|
}
|
|
}
|
|
|
|
if (!drawn)
|
|
{
|
|
drawList.AddImage((IntPtr)srv,
|
|
bodyMin + new Vector2(1, 1),
|
|
bodyMax - new Vector2(1, 1),
|
|
Vector2.Zero, Vector2.One,
|
|
UiColors.ForegroundFull.Fade(0.5f));
|
|
}
|
|
|
|
drawList.PopClipRect();
|
|
}
|
|
|
|
/// <summary>
|
|
/// (Cached) shader-resource view of the file's waveform image. First call per asset kicks off a
|
|
/// background generation via <see cref="AudioImageFactory"/>; later calls return the loaded SRV.
|
|
/// </summary>
|
|
private static bool TryGetWaveformSrv(string assetPath, Instance owner, [NotNullWhen(true)] out ShaderResourceView? srv)
|
|
{
|
|
srv = null;
|
|
var handle = new AudioClipResourceHandle(new TimelineAudioClip { AssetPath = assetPath }, owner);
|
|
if (!AudioImageFactory.TryGetOrCreateImagePathForClip(handle, out var imagePath))
|
|
return false;
|
|
|
|
if (_imageCache.TryGetValue(imagePath, out var entry))
|
|
{
|
|
srv = entry.Srv;
|
|
return srv is { IsDisposed: false };
|
|
}
|
|
|
|
var resource = ResourceManager.CreateTextureResource(imagePath, owner);
|
|
ShaderResourceView? newSrv = null;
|
|
resource.Value?.CreateShaderResourceView(ref newSrv, imagePath);
|
|
|
|
_imageCache[imagePath] = new CachedImage(resource, newSrv);
|
|
srv = newSrv;
|
|
return srv is { IsDisposed: false };
|
|
}
|
|
|
|
private readonly record struct CachedImage(Resource<Texture2D> Resource, ShaderResourceView? Srv);
|
|
|
|
// Lifetime: entries live for the editor session (same as the waveform cache in the old item renderer).
|
|
private static readonly Dictionary<string, CachedImage> _imageCache = new();
|
|
}
|