Files
FlaxEngine/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs
Ari Vuollet f09fd7ad34 Use exact value comparison in caching related functions
(cherry picked from commit 9d7c6b26422e127719836944d8d473910190e7d4)
2025-05-02 14:19:55 +03:00

790 lines
26 KiB
C#

// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FlaxEditor.Utilities;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEditor.Viewport.Previews;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.GUI.Timeline.Tracks
{
/// <summary>
/// The timeline media that represents an audio clip media event.
/// </summary>
/// <seealso cref="FlaxEditor.GUI.Timeline.Media" />
public class AudioMedia : Media
{
/// <summary>
/// True if loop track, otherwise audio clip will stop on the end.
/// </summary>
public bool Loop
{
get => Track.Loop;
set
{
if (Loop == value)
return;
Track.Loop = value;
Preview.DrawMode = value ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single;
}
}
/// <summary>
/// Playback offset of the audio (in seconds).
/// </summary>
public float Offset
{
get => Preview.ViewOffset;
set
{
if (Preview.ViewOffset == value)
return;
Preview.ViewOffset = value;
Timeline?.MarkAsEdited();
}
}
private sealed class Proxy : ProxyBase<AudioTrack, AudioMedia>
{
[EditorDisplay("General"), EditorOrder(10), Tooltip("The audio clip to play.")]
public AudioClip Audio
{
get => Track.Asset;
set => Track.Asset = value;
}
[EditorDisplay("General"), EditorOrder(20), Tooltip("If checked, the audio clip will loop when playback exceeds its duration. Otherwise it will stop play.")]
public bool Loop
{
get => Media.Loop;
set => Media.Loop = value;
}
[EditorDisplay("General"), EditorOrder(30), Tooltip("Playback offset of the audio (in seconds).")]
[Limit(0, float.MaxValue, 0.01f)]
public float Offset
{
get => Media.Offset;
set => Media.Offset = value;
}
public Proxy(AudioTrack track, AudioMedia media)
: base(track, media)
{
}
}
/// <summary>
/// The audio clip preview.
/// </summary>
public AudioClipPreview Preview;
/// <inheritdoc />
public AudioMedia()
{
CanSplit = true;
CanDelete = true;
Preview = new AudioClipPreview
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
DrawMode = AudioClipPreview.DrawModes.Single,
Parent = this,
};
}
/// <inheritdoc />
protected override void OnStartFrameChanged()
{
base.OnStartFrameChanged();
if (Track != null && Track.SubTracks.Count != 0 && Track.SubTracks[0] is AudioVolumeTrack volumeTrack)
volumeTrack.UpdateCurve();
}
/// <inheritdoc />
protected override void OnDurationFramesChanged()
{
base.OnDurationFramesChanged();
if (Track != null && Track.SubTracks.Count != 0 && Track.SubTracks[0] is AudioVolumeTrack volumeTrack)
volumeTrack.UpdateCurve();
}
/// <inheritdoc />
public override void OnTimelineChanged(Track track)
{
base.OnTimelineChanged(track);
PropertiesEditObject = track != null ? new Proxy((AudioTrack)track, this) : null;
}
/// <inheritdoc />
public override void OnTimelineZoomChanged()
{
base.OnTimelineZoomChanged();
Preview.ViewScale = Timeline.UnitsPerSecond / AudioClipPreview.UnitsPerSecond * Timeline.Zoom;
}
/// <inheritdoc />
public override Media Split(int frame)
{
var offset = Offset + ((float)(frame - StartFrame) / DurationFrames) * Duration;
var clone = (AudioMedia)base.Split(frame);
clone.Preview.ViewOffset = offset;
clone.Preview.Asset = Preview.Asset;
clone.Preview.DrawMode = Preview.DrawMode;
return clone;
}
}
/// <summary>
/// The timeline track that represents an audio clip playback.
/// </summary>
/// <seealso cref="FlaxEditor.GUI.Timeline.Track" />
public class AudioTrack : SingleMediaAssetTrack<AudioClip, AudioMedia>
{
/// <summary>
/// Gets the archetype.
/// </summary>
/// <returns>The archetype.</returns>
public static TrackArchetype GetArchetype()
{
return new TrackArchetype
{
TypeId = 5,
Name = "Audio",
Create = options => new AudioTrack(ref options),
Load = LoadTrack,
Save = SaveTrack,
};
}
private static void LoadTrack(int version, Track track, BinaryReader stream)
{
var e = (AudioTrack)track;
Guid id = stream.ReadGuid();
e.Asset = FlaxEngine.Content.LoadAsync<AudioClip>(id);
if (version <= 3)
{
// [Deprecated on 03.09.2021 expires on 03.09.2023]
var m = e.TrackMedia;
m.StartFrame = stream.ReadInt32();
m.DurationFrames = stream.ReadInt32();
m.Preview.ViewOffset = 0.0f;
m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single;
}
else
{
var count = stream.ReadInt32();
while (e.Media.Count > count)
e.RemoveMedia(e.Media.Last());
while (e.Media.Count < count)
e.AddMedia(new AudioMedia());
for (int i = 0; i < count; i++)
{
var m = (AudioMedia)e.Media[i];
m.StartFrame = stream.ReadInt32();
m.DurationFrames = stream.ReadInt32();
m.Preview.ViewOffset = stream.ReadSingle();
m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single;
m.Preview.Asset = e.Asset;
}
}
}
private static void SaveTrack(Track track, BinaryWriter stream)
{
var e = (AudioTrack)track;
stream.WriteGuid(ref e.AssetID);
var count = e.Media.Count;
stream.Write(count);
for (int i = 0; i < count; i++)
{
var m = (AudioMedia)e.Media[i];
stream.Write(m.StartFrame);
stream.Write(m.DurationFrames);
stream.Write(m.Offset);
}
}
private Button _addButton;
/// <inheritdoc />
public AudioTrack(ref TrackCreateOptions options)
: base(ref options)
{
MinMediaCount = 1;
// Add button
const float buttonSize = 14;
_addButton = new Button
{
Text = "+",
TooltipText = "Add sub-tracks",
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
Offsets = new Margin(-buttonSize - 2 + _muteCheckbox.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
Parent = this,
};
_addButton.Clicked += OnAddButtonClicked;
_picker.Location = new Float2(_addButton.Left - _picker.Width - 2, 2);
}
private void OnAddButtonClicked()
{
var cm = new ContextMenu.ContextMenu();
cm.AddButton("Volume", OnAddVolumeTrack);
cm.Show(_addButton.Parent, _addButton.BottomLeft);
}
private void OnAddVolumeTrack()
{
var track = Timeline.NewTrack(AudioVolumeTrack.GetArchetype());
track.ParentTrack = this;
track.TrackIndex = TrackIndex + 1;
track.Name = Guid.NewGuid().ToString("N");
Timeline.AddTrack(track);
Expand();
}
/// <inheritdoc />
protected override void OnSubTracksChanged()
{
base.OnSubTracksChanged();
_addButton.Enabled = SubTracks.Count == 0;
}
/// <inheritdoc />
protected override void OnAssetChanged()
{
base.OnAssetChanged();
foreach (AudioMedia m in Media)
m.Preview.Asset = Asset;
}
}
/// <summary>
/// The child volume track for audio track. Used to animate audio volume over time.
/// </summary>
/// <seealso cref="FlaxEditor.GUI.Timeline.Track" />
class AudioVolumeTrack : Track, IKeyframesEditorContext
{
/// <summary>
/// Gets the archetype.
/// </summary>
/// <returns>The archetype.</returns>
public static TrackArchetype GetArchetype()
{
return new TrackArchetype
{
TypeId = 6,
Name = "Audio Volume",
DisableSpawnViaGUI = true,
Create = options => new AudioVolumeTrack(ref options),
Load = LoadTrack,
Save = SaveTrack,
};
}
private static void LoadTrack(int version, Track track, BinaryReader stream)
{
var e = (AudioVolumeTrack)track;
int count = stream.ReadInt32();
var keyframes = new BezierCurve<float>.Keyframe[count];
for (int i = 0; i < count; i++)
{
keyframes[i] = new BezierCurve<float>.Keyframe
{
Time = stream.ReadSingle(),
Value = stream.ReadSingle(),
TangentIn = stream.ReadSingle(),
TangentOut = stream.ReadSingle(),
};
}
e.Curve.SetKeyframes(keyframes);
}
private static void SaveTrack(Track track, BinaryWriter stream)
{
var e = (AudioVolumeTrack)track;
var keyframes = e.Curve.Keyframes;
int count = keyframes.Count;
stream.Write(count);
for (int i = 0; i < count; i++)
{
var keyframe = keyframes[i];
stream.Write(keyframe.Time);
stream.Write(keyframe.Value);
stream.Write(keyframe.TangentIn);
stream.Write(keyframe.TangentOut);
}
}
private const float CollapsedHeight = 20.0f;
private const float ExpandedHeight = 64.0f;
private Label _previewValue;
private byte[] _curveEditingStartData;
/// <summary>
/// The volume curve. Values can be in range 0-1 to animate volume intensity and the track playback starts at the parent audio track media beginning. This curve does not loop.
/// </summary>
public BezierCurveEditor<float> Curve;
/// <inheritdoc />
public AudioVolumeTrack(ref TrackCreateOptions options)
: base(ref options)
{
Title = "Volume";
Height = CollapsedHeight;
// Curve editor
Curve = new BezierCurveEditor<float>
{
Visible = false,
EnableZoom = CurveEditorBase.UseMode.Off,
EnablePanning = CurveEditorBase.UseMode.Off,
ScrollBars = ScrollBars.None,
DefaultValue = 1.0f,
ShowStartEndLines = true,
ShowBackground = false,
};
Curve.Edited += OnCurveEdited;
Curve.EditingStart += OnCurveEditingStart;
Curve.EditingEnd += OnCurveEditingEnd;
Curve.UnlockChildrenRecursive();
// Navigation buttons
const float keySize = 18;
const float addSize = 20;
var icons = Editor.Instance.Icons;
var rightKey = new Image
{
TooltipText = "Sets the time to the next key",
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
Color = Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(icons.Right32),
Offsets = new Margin(-keySize - 2 + _muteCheckbox.Offsets.Left, keySize, keySize * -0.5f, keySize),
Parent = this,
};
rightKey.Clicked += OnRightKeyClicked;
var addKey = new Image
{
TooltipText = "Adds a new key at the current time",
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
Color = Style.Current.ForegroundGrey,
Margin = new Margin(3),
Brush = new SpriteBrush(icons.Add32),
Offsets = new Margin(-addSize - 2 + rightKey.Offsets.Left, addSize, addSize * -0.5f, addSize),
Parent = this,
};
addKey.Clicked += OnAddKeyClicked;
var leftKey = new Image
{
TooltipText = "Sets the time to the previous key",
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
Color = Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(icons.Left32),
Offsets = new Margin(-keySize - 2 + addKey.Offsets.Left, keySize, keySize * -0.5f, keySize),
Parent = this,
};
leftKey.Clicked += OnLeftKeyClicked;
// Value preview
var previewWidth = 50.0f;
_previewValue = new Label
{
AutoFocus = true,
AnchorPreset = AnchorPresets.MiddleRight,
IsScrollable = false,
HorizontalAlignment = TextAlignment.Near,
TextColor = Style.Current.ForegroundGrey,
Margin = new Margin(1),
Offsets = new Margin(-previewWidth - 2 + leftKey.Offsets.Left, previewWidth, TextBox.DefaultHeight * -0.5f, TextBox.DefaultHeight),
Parent = this,
};
}
private void OnRightKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetNextKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
private bool GetRangeFrames(out int startFrame, out int endFrame)
{
if (ParentTrack != null && ParentTrack.Media.Count != 0)
{
startFrame = ParentTrack.Media[0].StartFrame;
endFrame = ParentTrack.Media[0].EndFrame;
for (int i = 1; i < ParentTrack.Media.Count; i++)
{
endFrame = Mathf.Max(endFrame, ParentTrack.Media[i].EndFrame);
}
return true;
}
startFrame = endFrame = 0;
return false;
}
private bool GetRangeMedia(out Media startMedia, out Media endMedia)
{
if (ParentTrack != null && ParentTrack.Media.Count != 0)
{
startMedia = endMedia = ParentTrack.Media[0];
for (int i = 1; i < ParentTrack.Media.Count; i++)
{
if (ParentTrack.Media[i].EndFrame >= endMedia.EndFrame)
endMedia = ParentTrack.Media[i];
}
return true;
}
startMedia = endMedia = null;
return false;
}
/// <inheritdoc />
protected override void OnContextMenu(ContextMenu.ContextMenu menu)
{
base.OnContextMenu(menu);
if (!GetRangeFrames(out var startFrame, out _) || Curve == null)
return;
menu.AddSeparator();
menu.AddButton("Copy Preview Value", () =>
{
var time = (Timeline.CurrentFrame - startFrame) / Timeline.FramesPerSecond;
Curve.Evaluate(out var value, time, false);
Clipboard.Text = FlaxEngine.Utils.RoundTo2DecimalPlaces(Mathf.Saturate(value)).ToString("0.00");
}).LinkTooltip("Copies the current track value to the clipboard").Enabled = Timeline.ShowPreviewValues;
}
/// <inheritdoc />
public override bool GetNextKeyframeFrame(float time, out int result)
{
if (GetRangeFrames(out var startFrame, out var endFrame))
{
var mediaTime = time - startFrame / Timeline.FramesPerSecond;
for (int i = 0; i < Curve.Keyframes.Count; i++)
{
var k = Curve.Keyframes[i];
if (k.Time > mediaTime)
{
result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame;
return true;
}
}
}
return base.GetNextKeyframeFrame(time, out result);
}
private void OnAddKeyClicked(Image image, MouseButton button)
{
var currentFrame = Timeline.CurrentFrame;
if (button == MouseButton.Left && GetRangeFrames(out var startFrame, out var endFrame) && currentFrame >= startFrame && currentFrame < endFrame)
{
var time = (currentFrame - startFrame) / Timeline.FramesPerSecond;
for (int i = Curve.Keyframes.Count - 1; i >= 0; i--)
{
var k = Curve.Keyframes[i];
var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame;
if (frame == Timeline.CurrentFrame)
{
// Already added
return;
}
}
using (new TrackUndoBlock(this))
Curve.AddKeyframe(new BezierCurve<float>.Keyframe(time, 1.0f));
}
}
private void OnLeftKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetPreviousKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
/// <inheritdoc />
public override bool GetPreviousKeyframeFrame(float time, out int result)
{
if (GetRangeFrames(out var startFrame, out _))
{
var mediaTime = time - startFrame / Timeline.FramesPerSecond;
for (int i = Curve.Keyframes.Count - 1; i >= 0; i--)
{
var k = Curve.Keyframes[i];
if (k.Time < mediaTime)
{
result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame;
return true;
}
}
}
return base.GetPreviousKeyframeFrame(time, out result);
}
private void UpdatePreviewValue()
{
if (!GetRangeFrames(out var startFrame, out _) || Curve == null || Timeline == null)
return;
var time = (Timeline.CurrentFrame - startFrame) / Timeline.FramesPerSecond;
Curve.Evaluate(out var value, time, false);
_previewValue.Text = FlaxEngine.Utils.RoundTo2DecimalPlaces(Mathf.Saturate(value)).ToString("0.00");
}
internal void UpdateCurve()
{
if (!GetRangeMedia(out var startMedia, out var endMedia) || Curve == null || Timeline == null)
return;
bool wasVisible = Curve.Visible;
Curve.Visible = Visible;
if (!Visible)
{
if (wasVisible)
Curve.ClearSelection();
return;
}
Curve.KeyframesEditorContext = Timeline;
Curve.CustomViewPanning = Timeline.OnKeyframesViewPanning;
Curve.Bounds = new Rectangle(startMedia.X, Y + 1.0f, endMedia.Right - startMedia.Left, Height - 2.0f);
var expanded = IsExpanded;
if (expanded)
{
Curve.ViewScale = new Float2(Timeline.Zoom, 0.7f);
Curve.ViewOffset = new Float2(0.0f, 35.0f);
}
else
{
Curve.ViewScale = new Float2(Timeline.Zoom, 1.0f);
Curve.ViewOffset = Float2.Zero;
}
Curve.ShowCollapsed = !expanded;
Curve.ShowAxes = expanded ? CurveEditorBase.UseMode.Horizontal : CurveEditorBase.UseMode.Off;
Curve.UpdateKeyframes();
}
private void OnCurveEdited()
{
UpdatePreviewValue();
Timeline.MarkAsEdited();
}
private void OnCurveEditingStart()
{
_curveEditingStartData = EditTrackAction.CaptureData(this);
}
private void OnCurveEditingEnd()
{
var after = EditTrackAction.CaptureData(this);
if (!FlaxEngine.Utils.ArraysEqual(_curveEditingStartData, after))
Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _curveEditingStartData, after));
_curveEditingStartData = null;
}
/// <inheritdoc />
public override bool CanDrag => false;
/// <inheritdoc />
public override bool CanRename => false;
/// <inheritdoc />
public override bool CanCopyPaste => false;
/// <inheritdoc />
public override bool CanExpand => true;
/// <inheritdoc />
public override void OnParentTrackChanged(Track parent)
{
base.OnParentTrackChanged(parent);
if (parent != null)
{
UpdateCurve();
UpdatePreviewValue();
}
}
/// <inheritdoc />
protected override void OnExpandedChanged()
{
Height = IsExpanded ? ExpandedHeight : CollapsedHeight;
UpdateCurve();
base.OnExpandedChanged();
}
/// <inheritdoc />
protected override void OnVisibleChanged()
{
base.OnVisibleChanged();
Curve.Visible = Visible;
}
/// <inheritdoc />
public override void OnTimelineChanged(Timeline timeline)
{
base.OnTimelineChanged(timeline);
Curve.Parent = timeline?.MediaPanel;
Curve.FPS = timeline?.FramesPerSecond;
UpdateCurve();
UpdatePreviewValue();
}
/// <inheritdoc />
public override void OnUndo()
{
base.OnUndo();
if (Curve != null)
{
// TODO: fix this hack: curve UI doesn't update properly after undo that modified keyframes
IsExpanded = !IsExpanded;
IsExpanded = !IsExpanded;
}
UpdatePreviewValue();
}
/// <inheritdoc />
public override void OnTimelineZoomChanged()
{
base.OnTimelineZoomChanged();
UpdateCurve();
}
/// <inheritdoc />
public override void OnTimelineArrange()
{
base.OnTimelineArrange();
UpdateCurve();
}
/// <inheritdoc />
public override void OnTimelineFpsChanged(float before, float after)
{
base.OnTimelineFpsChanged(before, after);
Curve.FPS = after;
UpdatePreviewValue();
}
/// <inheritdoc />
public override void OnTimelineCurrentFrameChanged(int frame)
{
base.OnTimelineCurrentFrameChanged(frame);
UpdatePreviewValue();
}
/// <inheritdoc />
public override void OnDuplicated(Track clone)
{
base.OnDuplicated(clone);
clone.Name = Guid.NewGuid().ToString("N");
}
/// <inheritdoc />
public override void OnDestroy()
{
if (Curve != null)
{
Curve.Dispose();
Curve = null;
}
_previewValue = null;
base.OnDestroy();
}
/// <inheritdoc />
public void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDeselect(editor);
}
/// <inheritdoc />
public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesSelection(editor, control, selection);
}
/// <inheritdoc />
public int OnKeyframesSelectionCount()
{
return Curve != null && Curve.Visible ? Curve.OnKeyframesSelectionCount() : 0;
}
/// <inheritdoc />
public void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDelete(editor);
}
/// <inheritdoc />
public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesMove(editor, control, location, start, end);
}
/// <inheritdoc />
public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, System.Text.StringBuilder data)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesCopy(editor, timeOffset, data);
}
/// <inheritdoc />
public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
/// <inheritdoc />
public void OnKeyframesGet(Action<string, float, object> get)
{
Curve?.OnKeyframesGet(Name, get);
}
/// <inheritdoc />
public void OnKeyframesSet(List<KeyValuePair<float, object>> keyframes)
{
Curve?.OnKeyframesSet(keyframes);
}
}
}