640 lines
21 KiB
C#
640 lines
21 KiB
C#
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.IO;
|
|
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 : SingleMediaAssetMedia
|
|
{
|
|
/// <summary>
|
|
/// True if loop track, otherwise audio clip will stop on the end.
|
|
/// </summary>
|
|
public bool Loop
|
|
{
|
|
get => Track.Loop;
|
|
set
|
|
{
|
|
if (Track.Loop != value)
|
|
{
|
|
Track.Loop = value;
|
|
Preview.DrawMode = value ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single;
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class Proxy : ProxyBase<AudioTrack, AudioMedia>
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the audio clip to play.
|
|
/// </summary>
|
|
[EditorDisplay("General"), EditorOrder(10), Tooltip("The audio clip to play.")]
|
|
public AudioClip Audio
|
|
{
|
|
get => Track.Asset;
|
|
set => Track.Asset = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the audio clip looping mode.
|
|
/// </summary>
|
|
[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 => Track.TrackLoop;
|
|
set => Track.TrackLoop = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Proxy(AudioTrack track, AudioMedia media)
|
|
: base(track, media)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The audio clip preview.
|
|
/// </summary>
|
|
public AudioClipPreview Preview;
|
|
|
|
/// <inheritdoc />
|
|
public AudioMedia()
|
|
{
|
|
Preview = new AudioClipPreview
|
|
{
|
|
AnchorPreset = AnchorPresets.StretchAll,
|
|
Offsets = Margin.Zero,
|
|
DrawMode = AudioClipPreview.DrawModes.Single,
|
|
Parent = this,
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnTimelineChanged(Track track)
|
|
{
|
|
base.OnTimelineChanged(track);
|
|
|
|
PropertiesEditObject = new Proxy(Track as AudioTrack, this);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnTimelineZoomChanged()
|
|
{
|
|
base.OnTimelineZoomChanged();
|
|
|
|
Preview.ViewScale = Timeline.UnitsPerSecond / AudioClipPreview.UnitsPerSecond * Timeline.Zoom;
|
|
}
|
|
}
|
|
|
|
/// <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 = new Guid(stream.ReadBytes(16));
|
|
e.Asset = FlaxEngine.Content.LoadAsync<AudioClip>(id);
|
|
var m = e.TrackMedia;
|
|
m.StartFrame = stream.ReadInt32();
|
|
m.DurationFrames = stream.ReadInt32();
|
|
m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single;
|
|
}
|
|
|
|
private static void SaveTrack(Track track, BinaryWriter stream)
|
|
{
|
|
var e = (AudioTrack)track;
|
|
var assetId = e.Asset?.ID ?? Guid.Empty;
|
|
|
|
stream.Write(assetId.ToByteArray());
|
|
|
|
if (e.Media.Count != 0)
|
|
{
|
|
var m = e.TrackMedia;
|
|
stream.Write(m.StartFrame);
|
|
stream.Write(m.DurationFrames);
|
|
}
|
|
else
|
|
{
|
|
stream.Write(0);
|
|
stream.Write(track.Timeline.DurationFrames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the audio clip looping mode.
|
|
/// </summary>
|
|
public bool TrackLoop
|
|
{
|
|
get => TrackMedia.Loop;
|
|
set
|
|
{
|
|
AudioMedia media = TrackMedia;
|
|
if (media.Loop == value)
|
|
return;
|
|
|
|
media.Loop = value;
|
|
Timeline?.MarkAsEdited();
|
|
}
|
|
}
|
|
|
|
private Button _addButton;
|
|
|
|
/// <inheritdoc />
|
|
public AudioTrack(ref TrackCreateOptions options)
|
|
: base(ref options)
|
|
{
|
|
// 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 Vector2(_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();
|
|
|
|
TrackMedia.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
|
|
{
|
|
/// <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 AudioMedia _audioMedia;
|
|
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,
|
|
};
|
|
Curve.Edited += OnCurveEdited;
|
|
Curve.EditingStart += OnCurveEditingStart;
|
|
Curve.EditingEnd += OnCurveEditingEnd;
|
|
Curve.UnlockChildrenRecursive();
|
|
|
|
// Navigation buttons
|
|
const float buttonSize = 14;
|
|
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.ArrowRight32),
|
|
Offsets = new Margin(-buttonSize - 2 + _muteCheckbox.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
|
|
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.Add48),
|
|
Offsets = new Margin(-buttonSize - 2 + rightKey.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
|
|
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.ArrowLeft32),
|
|
Offsets = new Margin(-buttonSize - 2 + addKey.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnContextMenu(ContextMenu.ContextMenu menu)
|
|
{
|
|
base.OnContextMenu(menu);
|
|
|
|
if (_audioMedia == null || Curve == null)
|
|
return;
|
|
menu.AddSeparator();
|
|
menu.AddButton("Copy Preview Value", () =>
|
|
{
|
|
var time = (Timeline.CurrentFrame - _audioMedia.StartFrame) / Timeline.FramesPerSecond;
|
|
Curve.Evaluate(out var value, time, false);
|
|
Clipboard.Text = 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 (_audioMedia != null)
|
|
{
|
|
var mediaTime = time - _audioMedia.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) + _audioMedia.StartFrame;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return base.GetNextKeyframeFrame(time, out result);
|
|
}
|
|
|
|
private void OnAddKeyClicked(Image image, MouseButton button)
|
|
{
|
|
var currentFrame = Timeline.CurrentFrame;
|
|
if (button == MouseButton.Left && _audioMedia != null && currentFrame >= _audioMedia.StartFrame && currentFrame < _audioMedia.StartFrame + _audioMedia.DurationFrames)
|
|
{
|
|
var time = (currentFrame - _audioMedia.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) + _audioMedia.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 (_audioMedia != null)
|
|
{
|
|
var mediaTime = time - _audioMedia.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) + _audioMedia.StartFrame;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return base.GetPreviousKeyframeFrame(time, out result);
|
|
}
|
|
|
|
private void UpdatePreviewValue()
|
|
{
|
|
if (_audioMedia == null || Curve == null || Timeline == null)
|
|
return;
|
|
|
|
var time = (Timeline.CurrentFrame - _audioMedia.StartFrame) / Timeline.FramesPerSecond;
|
|
Curve.Evaluate(out var value, time, false);
|
|
_previewValue.Text = Utils.RoundTo2DecimalPlaces(Mathf.Saturate(value)).ToString("0.00");
|
|
}
|
|
|
|
private void UpdateCurve()
|
|
{
|
|
if (_audioMedia == null || Curve == null || Timeline == null)
|
|
return;
|
|
|
|
Curve.Bounds = new Rectangle(_audioMedia.X, Y + 1.0f, _audioMedia.Width, Height - 2.0f);
|
|
|
|
var expanded = IsExpanded;
|
|
if (expanded)
|
|
{
|
|
//Curve.ViewScale = new Vector2(1.0f, CurveEditor<float>.UnitsPerSecond / Curve.Height);
|
|
Curve.ViewScale = new Vector2(Timeline.Zoom, 0.4f);
|
|
Curve.ViewOffset = new Vector2(0.0f, 30.0f);
|
|
}
|
|
else
|
|
{
|
|
Curve.ViewScale = new Vector2(Timeline.Zoom, 1.0f);
|
|
Curve.ViewOffset = Vector2.Zero;
|
|
}
|
|
Curve.ShowCollapsed = !expanded;
|
|
Curve.ShowBackground = expanded;
|
|
Curve.ShowAxes = expanded;
|
|
Curve.Visible = Visible;
|
|
Curve.UpdateKeyframes();
|
|
}
|
|
|
|
private void OnCurveEdited()
|
|
{
|
|
UpdatePreviewValue();
|
|
Timeline.MarkAsEdited();
|
|
}
|
|
|
|
private void OnCurveEditingStart()
|
|
{
|
|
_curveEditingStartData = EditTrackAction.CaptureData(this);
|
|
}
|
|
|
|
private void OnCurveEditingEnd()
|
|
{
|
|
var after = EditTrackAction.CaptureData(this);
|
|
if (!Utils.ArraysEqual(_curveEditingStartData, after))
|
|
Timeline.Undo.AddAction(new EditTrackAction(Timeline, this, _curveEditingStartData, after));
|
|
_curveEditingStartData = null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override bool CanDrag => false;
|
|
|
|
/// <inheritdoc />
|
|
protected override bool CanRename => false;
|
|
|
|
/// <inheritdoc />
|
|
protected override bool CanExpand => true;
|
|
|
|
/// <inheritdoc />
|
|
public override void OnParentTrackChanged(Track parent)
|
|
{
|
|
base.OnParentTrackChanged(parent);
|
|
|
|
if (_audioMedia != null)
|
|
{
|
|
_audioMedia.StartFrameChanged -= UpdateCurve;
|
|
_audioMedia.DurationFramesChanged -= UpdateCurve;
|
|
_audioMedia = null;
|
|
}
|
|
|
|
if (parent is AudioTrack audioTrack)
|
|
{
|
|
var media = audioTrack.TrackMedia;
|
|
media.StartFrameChanged += UpdateCurve;
|
|
media.DurationFramesChanged += UpdateCurve;
|
|
_audioMedia = media;
|
|
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 OnDestroy()
|
|
{
|
|
if (Curve != null)
|
|
{
|
|
Curve.Dispose();
|
|
Curve = null;
|
|
}
|
|
_previewValue = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
}
|