@pro-gram/flux 0.0.0-stage → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/NOTICE.txt +33 -0
- package/README.md +125 -2
- package/bin/flux.js +29 -0
- package/dependencies.json +15 -0
- package/package.json +60 -6
- package/payload/DrumBuddy-Setup.exe +4 -0
- package/scripts/install.js +60 -0
- package/scripts/version.js +12 -0
- package/source/AssetBootstrap.cs +49 -0
- package/source/Audio Decoders.txt +230 -0
- package/source/Audio.cs +453 -0
- package/source/AudioDeviceSettings.cs +126 -0
- package/source/AudioEndpointWatcher.cs +25 -0
- package/source/AudioInputs.cs +50 -0
- package/source/AudioOutputs.cs +307 -0
- package/source/AudioWorkspace.cs +72 -0
- package/source/BrandArtwork.cs +71 -0
- package/source/Build Instructions.txt +65 -0
- package/source/Build.ps1 +135 -0
- package/source/ControlMode.cs +687 -0
- package/source/ControlTests.cs +88 -0
- package/source/ConversionFormats.cs +59 -0
- package/source/ConversionTests.cs +66 -0
- package/source/DockablePane.cs +95 -0
- package/source/DriverSetup.cs +28 -0
- package/source/EditorCompactLayout.cs +36 -0
- package/source/EditorPlaybackTests.cs +87 -0
- package/source/EditorSelectionTests.cs +73 -0
- package/source/EditorToolInteractions.cs +227 -0
- package/source/EditorTools.cs +78 -0
- package/source/EditorToolsTests.cs +93 -0
- package/source/EditorView.cs +798 -0
- package/source/Fetch-Dependencies.ps1 +15 -0
- package/source/FloatRecording.cs +99 -0
- package/source/FloatRecordingTests.cs +170 -0
- package/source/Flux-Logo.png +0 -0
- package/source/Flux-Mark.svg +49 -0
- package/source/FluxTempo Logo Prompt.txt +3 -0
- package/source/FluxTempo-Mark.svg +49 -0
- package/source/HomeView.cs +32 -0
- package/source/InterfaceSettings.cs +225 -0
- package/source/InterfaceTests.cs +172 -0
- package/source/LICENSE.txt +23 -0
- package/source/Logo Prompt.txt +16 -0
- package/source/Main.cs +787 -0
- package/source/Main.xaml +289 -0
- package/source/MediaPlayerView.cs +274 -0
- package/source/MidiControl.cs +113 -0
- package/source/MidiPerformance.cs +125 -0
- package/source/Mixer.cs +280 -0
- package/source/NativeMemory.cs +165 -0
- package/source/PanelFeedback.cs +75 -0
- package/source/PlaybackTests.cs +141 -0
- package/source/Quick Start.txt +96 -0
- package/source/SelfTest.cs +78 -0
- package/source/SettingsWindow.cs +209 -0
- package/source/Setup.cs +196 -0
- package/source/Setup.xaml +28 -0
- package/source/ShellStyles.xaml +99 -0
- package/source/TempoGrid.cs +95 -0
- package/source/TempoGridTests.cs +108 -0
- package/source/Third Party Notices.txt +33 -0
- package/source/Timeline.cs +268 -0
- package/source/TimelineGestures.cs +57 -0
- package/source/TimelineZoom.cs +21 -0
- package/source/Uninstall.cs +69 -0
- package/source/WaveformDetail.cs +104 -0
- package/source/WindowsAudioPlayer.cs +137 -0
- package/source/WorkspaceTests.cs +202 -0
- package/source/ZoomTests.cs +95 -0
- package/source/app.manifest +7 -0
- package/source/brand-archive/Drum-Buddy-Black-Hair.png +0 -0
- package/source/dependency-lock.json +62 -0
- package/source/fluxtempo-mark.png +0 -0
- package/source/logo.png +0 -0
- package/source/r16-panel.png +0 -0
- package/source/references/R16 Format Research.txt +16 -0
- package/test/package.test.js +55 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Globalization;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Threading.Tasks;
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using System.Windows;
|
|
9
|
+
using System.Windows.Controls;
|
|
10
|
+
using System.Windows.Controls.Primitives;
|
|
11
|
+
using System.Windows.Input;
|
|
12
|
+
using System.Windows.Media;
|
|
13
|
+
using System.Windows.Shapes;
|
|
14
|
+
using System.Windows.Threading;
|
|
15
|
+
using Path = System.IO.Path;
|
|
16
|
+
|
|
17
|
+
namespace DrumBuddy
|
|
18
|
+
{
|
|
19
|
+
public sealed partial class EditorView : Grid, IDisposable
|
|
20
|
+
{
|
|
21
|
+
private const double RowHeight = 56, RulerHeight = 26;
|
|
22
|
+
private readonly Window owner;
|
|
23
|
+
private readonly string cache;
|
|
24
|
+
private readonly Action<string> status;
|
|
25
|
+
private readonly Action beforePlay;
|
|
26
|
+
private readonly Dictionary<string, Clip> assets = new Dictionary<string, Clip>(StringComparer.OrdinalIgnoreCase);
|
|
27
|
+
private readonly Stack<EditorSession> undo = new Stack<EditorSession>(), redo = new Stack<EditorSession>();
|
|
28
|
+
private EditorSession session = new EditorSession();
|
|
29
|
+
private TimelineClipboard clipboard;
|
|
30
|
+
private TimelineMix mix;
|
|
31
|
+
private readonly WorkspaceOutput output;
|
|
32
|
+
private readonly AudioOutputPicker outputPicker;
|
|
33
|
+
private readonly Canvas timeline;
|
|
34
|
+
private readonly StackPanel headers;
|
|
35
|
+
private readonly ScrollViewer scroll, headerScroll;
|
|
36
|
+
private readonly TextBlock title, positionText, selectionText;
|
|
37
|
+
private readonly TextBox selectionFrom, selectionTo;
|
|
38
|
+
private readonly ComboBox scope, exportFormat;
|
|
39
|
+
private readonly Slider zoomSlider;
|
|
40
|
+
private readonly TextBlock zoomSpan;
|
|
41
|
+
private readonly Button fitSelection;
|
|
42
|
+
private readonly WaveformDetail waveformDetail;
|
|
43
|
+
private bool updatingZoom;
|
|
44
|
+
private DateTime manualViewUntil;
|
|
45
|
+
private readonly CheckBox separate, selectionOnly, protect;
|
|
46
|
+
private readonly Button play, undoButton, redoButton, cancel;
|
|
47
|
+
private readonly Slider volume;
|
|
48
|
+
private readonly DispatcherTimer timer;
|
|
49
|
+
private readonly Line playLine;
|
|
50
|
+
private readonly TimelineGesture gesture = new TimelineGesture();
|
|
51
|
+
private readonly TextBox tempoBox;
|
|
52
|
+
private readonly ComboBox gridDivision, rulerMode;
|
|
53
|
+
private readonly CheckBox snapGrid, snapEdges, gridTriplet;
|
|
54
|
+
private readonly TextBlock meterText;
|
|
55
|
+
private readonly WrapPanel gridControls;
|
|
56
|
+
public Action GridSettingsRequested;
|
|
57
|
+
public TempoGridOptions GridOptions { get { return session.Grid.Copy(); } }
|
|
58
|
+
private readonly Grid editorBody;
|
|
59
|
+
private readonly DockablePane recordingPane;
|
|
60
|
+
private readonly Button recordButton;
|
|
61
|
+
private bool sliceCursor, clipSelection, selectionMoved;
|
|
62
|
+
private string pendingRegion;
|
|
63
|
+
private double selectionPointerX;
|
|
64
|
+
private readonly HashSet<string> selectedIds = new HashSet<string>();
|
|
65
|
+
private string[] previousIds;
|
|
66
|
+
private bool previousClipSelection;
|
|
67
|
+
private int previousTrack;
|
|
68
|
+
private long previousFrom, previousTo, previousPlayhead;
|
|
69
|
+
private sealed class RegionVisual { public TimelineRegion Region; public Rectangle Body, Glow; }
|
|
70
|
+
private readonly List<RegionVisual> regionVisuals = new List<RegionVisual>();
|
|
71
|
+
private readonly Button compactOptionsButton;
|
|
72
|
+
private FrameworkElement exportDetails;
|
|
73
|
+
private bool compactOptions;
|
|
74
|
+
private readonly Rectangle selectionOverlay;
|
|
75
|
+
private readonly Line selectionStartLine, selectionEndLine;
|
|
76
|
+
private readonly Polygon selectionStartHandle, selectionEndHandle;
|
|
77
|
+
private bool recording;
|
|
78
|
+
public Action RecordRequested;
|
|
79
|
+
public Action StopRequested;
|
|
80
|
+
public Action RecordingOptionsRequested;
|
|
81
|
+
public long PlayheadFrame { get { return playhead; } }
|
|
82
|
+
public int SelectedTrack { get { return track; } }
|
|
83
|
+
private readonly List<Button> fileButtons = new List<Button>();
|
|
84
|
+
private CancellationTokenSource operation;
|
|
85
|
+
private string sessionPath;
|
|
86
|
+
private int track;
|
|
87
|
+
private double pixelsPerSecond = 40;
|
|
88
|
+
private long playhead, from, to;
|
|
89
|
+
private bool updating, disposed, dirty, tempoEdited;
|
|
90
|
+
public bool IsBusy { get { return operation != null || recording || clipDrag != ClipDrag.None; } }
|
|
91
|
+
public bool Dirty { get { return dirty; } }
|
|
92
|
+
private bool AllTracks { get { return scope.SelectedIndex == 1; } }
|
|
93
|
+
|
|
94
|
+
public EditorView(Window owner, string cache, Action<string> status, Action beforePlay)
|
|
95
|
+
{
|
|
96
|
+
this.owner = owner; this.cache = cache; this.status = status; this.beforePlay = beforePlay;
|
|
97
|
+
session.Grid = InterfacePreferences.Current.Grid.Copy();
|
|
98
|
+
Background = WorkspaceUi.Color("#F3F5F5");
|
|
99
|
+
output = new WorkspaceOutput(Dispatcher);
|
|
100
|
+
editorBody = new Grid();
|
|
101
|
+
editorBody.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
|
102
|
+
editorBody.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
|
103
|
+
editorBody.RowDefinitions.Add(new RowDefinition());
|
|
104
|
+
editorBody.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
|
105
|
+
var top = new WrapPanel(); editorBody.Children.Add(top);
|
|
106
|
+
title = WorkspaceUi.Text("Editor · Untitled session", 24); title.FontWeight = FontWeights.SemiBold; title.Margin = new Thickness(0, 0, 20, 10); top.Children.Add(title);
|
|
107
|
+
cancel = WorkspaceUi.Button("Cancel", delegate { if (operation != null) operation.Cancel(); }); cancel.Visibility = Visibility.Collapsed; top.Children.Add(cancel);
|
|
108
|
+
|
|
109
|
+
var tools = new StackPanel(); SetRow(tools, 1); editorBody.Children.Add(tools);
|
|
110
|
+
var editing = new WrapPanel(); tools.Children.Add(editing);
|
|
111
|
+
recordButton = WorkspaceUi.Button("Record", delegate { if (RecordRequested != null) RecordRequested(); }); recordButton.Background = WorkspaceUi.Color("#B52C36"); recordButton.Foreground = System.Windows.Media.Brushes.White; editing.Children.Add(recordButton);
|
|
112
|
+
recordButton.SetResourceReference(Control.ForegroundProperty, "ThemeOnAccent");
|
|
113
|
+
editing.Children.Add(WorkspaceUi.Button("Record options", delegate { ShowRecordingOptions(!recordingPane.IsPaneVisible); }));
|
|
114
|
+
undoButton = WorkspaceUi.Button("Undo", delegate { Undo(false); }); editing.Children.Add(undoButton);
|
|
115
|
+
redoButton = WorkspaceUi.Button("Redo", delegate { Undo(true); }); editing.Children.Add(redoButton);
|
|
116
|
+
editing.Children.Add(WorkspaceUi.Button("Delete range", delegate { DeleteRange(); }));
|
|
117
|
+
scope = new ComboBox { ItemsSource = new[] { "Selected track", "All 16 tracks" }, SelectedIndex = 0, Width = 170, MinHeight = 36, FontSize = 14, Padding = new Thickness(6, 3, 6, 3), Margin = new Thickness(0, 0, 8, 8) }; editing.Children.Add(scope);
|
|
118
|
+
scope.SelectionChanged += delegate { if (!updating) { clipSelection = false; selectedIds.Clear(); } DrawCanvas(); };
|
|
119
|
+
separate = new CheckBox { Content = "Import files on separate tracks", IsChecked = true, FontSize = 14, Margin = new Thickness(4, 0, 0, 8), VerticalAlignment = VerticalAlignment.Center }; editing.Children.Add(separate);
|
|
120
|
+
compactOptionsButton = WorkspaceUi.Button("Audio / export options", delegate { compactOptions = !compactOptions; UpdateCompactLayout(); }); editing.Children.Add(compactOptionsButton);
|
|
121
|
+
|
|
122
|
+
gridControls = new WrapPanel { Margin = new Thickness(0, 0, 0, 3) }; tools.Children.Add(gridControls);
|
|
123
|
+
gridControls.Children.Add(WorkspaceUi.Text("BPM ", 12));
|
|
124
|
+
tempoBox = new TextBox { Width = 60, MinHeight = 28, Padding = new Thickness(5, 3, 5, 3), Margin = new Thickness(0, 0, 9, 5) }; gridControls.Children.Add(tempoBox);
|
|
125
|
+
tempoBox.TextChanged += delegate { if (!updating) tempoEdited = true; };
|
|
126
|
+
tempoBox.LostKeyboardFocus += delegate { if (!updating) CommitTempo(); };
|
|
127
|
+
tempoBox.KeyDown += delegate(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { CommitTempo(); e.Handled = true; } else if (e.Key == Key.Escape) { tempoBox.Text = session.Grid.Tempo.ToString("0.###", CultureInfo.InvariantCulture); tempoEdited = false; e.Handled = true; } };
|
|
128
|
+
meterText = WorkspaceUi.Text("4/4 ", 12); gridControls.Children.Add(meterText);
|
|
129
|
+
gridControls.Children.Add(WorkspaceUi.Text("Grid ", 12));
|
|
130
|
+
gridDivision = new ComboBox { ItemsSource = TempoGrid.Divisions.Select(v => "1/" + v).ToArray(), Width = 82, MinHeight = 28, Margin = new Thickness(0, 0, 9, 5) };
|
|
131
|
+
gridDivision.SelectionChanged += delegate { if (!updating && gridDivision.SelectedItem != null) { var value = session.Grid.Copy(); value.Division = int.Parse(((string)gridDivision.SelectedItem).Substring(2)); ApplyGridOptions(value); } }; gridControls.Children.Add(gridDivision);
|
|
132
|
+
gridTriplet = new CheckBox { Content = "Triplet", Margin = new Thickness(0, 0, 10, 5), VerticalAlignment = VerticalAlignment.Center };
|
|
133
|
+
gridTriplet.Click += delegate { if (!updating) { var value = session.Grid.Copy(); value.Triplet = gridTriplet.IsChecked == true; ApplyGridOptions(value); } }; gridControls.Children.Add(gridTriplet);
|
|
134
|
+
snapGrid = new CheckBox { Content = "Snap grid", Margin = new Thickness(0, 0, 10, 5), VerticalAlignment = VerticalAlignment.Center };
|
|
135
|
+
snapEdges = new CheckBox { Content = "Snap edges", Margin = new Thickness(0, 0, 10, 5), VerticalAlignment = VerticalAlignment.Center };
|
|
136
|
+
snapGrid.Click += delegate { SaveSnapChoices(); }; snapEdges.Click += delegate { SaveSnapChoices(); }; gridControls.Children.Add(snapGrid); gridControls.Children.Add(snapEdges);
|
|
137
|
+
rulerMode = new ComboBox { ItemsSource = new[] { "Bars / beats", "Seconds" }, Width = 116, MinHeight = 28, Margin = new Thickness(0, 0, 8, 5) };
|
|
138
|
+
rulerMode.SelectionChanged += delegate { if (!updating && rulerMode.SelectedIndex >= 0) { var value = session.Grid.Copy(); value.MusicalRuler = rulerMode.SelectedIndex == 0; ApplyGridOptions(value); } }; gridControls.Children.Add(rulerMode);
|
|
139
|
+
gridControls.Children.Add(WorkspaceUi.Button("Grid settings", delegate { if (GridSettingsRequested != null) GridSettingsRequested(); }));
|
|
140
|
+
|
|
141
|
+
var range = new WrapPanel { Margin = new Thickness(0, 0, 0, 5) }; tools.Children.Add(range);
|
|
142
|
+
range.Visibility = Visibility.Collapsed;
|
|
143
|
+
editing.Children.Add(WorkspaceUi.Button("Range", delegate { range.Visibility = range.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; }));
|
|
144
|
+
range.Children.Add(WorkspaceUi.Text("Selection, seconds ", 14));
|
|
145
|
+
selectionFrom = new TextBox { Text = "0.00000", Width = 100, FontSize = 15, Padding = new Thickness(5), Margin = new Thickness(0, 0, 8, 8) }; range.Children.Add(selectionFrom);
|
|
146
|
+
range.Children.Add(WorkspaceUi.Text("to ", 14));
|
|
147
|
+
selectionTo = new TextBox { Text = "0.00000", Width = 100, FontSize = 15, Padding = new Thickness(5), Margin = new Thickness(0, 0, 8, 8) }; range.Children.Add(selectionTo);
|
|
148
|
+
range.Children.Add(WorkspaceUi.Button("Set range", delegate { if (ReadSelection()) { clipSelection = false; selectedIds.Clear(); playhead = from; DrawCanvas(); } }));
|
|
149
|
+
selectionText = WorkspaceUi.Text("Drag the upper ruler to move the playhead. Drag across a track to select time; blue handles adjust the range. Home goes to the start.", 13); selectionText.Foreground = WorkspaceUi.Color("#526771"); selectionText.Margin = new Thickness(0, 0, 0, 8); tools.Children.Add(selectionText);
|
|
150
|
+
|
|
151
|
+
tools.Children.Remove(selectionText);
|
|
152
|
+
var viewControls = new DockPanel { LastChildFill = true, Margin = new Thickness(0, 0, 0, 3) }; tools.Children.Add(viewControls);
|
|
153
|
+
var zoomTools = new StackPanel { Orientation = Orientation.Horizontal, Name = "EditorZoomControls" }; DockPanel.SetDock(zoomTools, Dock.Right); viewControls.Children.Add(zoomTools);
|
|
154
|
+
zoomTools.Children.Add(WorkspaceUi.Text("Zoom ", 12));
|
|
155
|
+
var zoomOut = WorkspaceUi.Button("-", delegate { ZoomStep(.5); }); zoomOut.Name = "EditorZoomOut"; zoomOut.Width = 28; zoomOut.MinHeight = 26; zoomOut.Padding = new Thickness(4, 1, 4, 1); zoomOut.ToolTip = "Zoom out (Ctrl+-)"; zoomTools.Children.Add(zoomOut);
|
|
156
|
+
zoomSlider = new Slider { Minimum = Math.Log(TimelineZoom.Minimum, 2), Maximum = Math.Log(TimelineZoom.Maximum, 2), Value = Math.Log(pixelsPerSecond, 2), Width = 96, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 6), IsMoveToPointEnabled = true, ToolTip = "Timeline zoom" };
|
|
157
|
+
zoomSlider.ValueChanged += delegate { if (!updatingZoom) SetZoom(Math.Pow(2, zoomSlider.Value), ZoomAnchor()); }; zoomTools.Children.Add(zoomSlider);
|
|
158
|
+
var zoomIn = WorkspaceUi.Button("+", delegate { ZoomStep(2); }); zoomIn.Name = "EditorZoomIn"; zoomIn.Width = 28; zoomIn.MinHeight = 26; zoomIn.Padding = new Thickness(4, 1, 4, 1); zoomIn.ToolTip = "Zoom in (Ctrl+=)"; zoomTools.Children.Add(zoomIn);
|
|
159
|
+
zoomSpan = WorkspaceUi.Text("", 12); zoomSpan.Width = 72; zoomSpan.Margin = new Thickness(0, 0, 6, 6); zoomSpan.TextAlignment = TextAlignment.Right; zoomSpan.TextWrapping = TextWrapping.NoWrap; zoomTools.Children.Add(zoomSpan);
|
|
160
|
+
var fitProject = WorkspaceUi.Button("Fit", delegate { FitZoom(false); }); fitProject.Name = "EditorZoomFit"; fitProject.MinHeight = 26; fitProject.Padding = new Thickness(7, 1, 7, 1); fitProject.ToolTip = "Fit project (Ctrl+0)"; zoomTools.Children.Add(fitProject);
|
|
161
|
+
fitSelection = WorkspaceUi.Button("Selection", delegate { FitZoom(true); }); fitSelection.Name = "EditorZoomSelection"; fitSelection.MinHeight = 26; fitSelection.Padding = new Thickness(7, 1, 7, 1); fitSelection.ToolTip = "Fit selected time range (Ctrl+Shift+0)"; zoomTools.Children.Add(fitSelection);
|
|
162
|
+
selectionText.Margin = new Thickness(0, 0, 8, 6); selectionText.TextWrapping = TextWrapping.NoWrap; viewControls.Children.Add(selectionText);
|
|
163
|
+
|
|
164
|
+
var body = new Grid { Background = WorkspaceUi.Color("#FFFFFF") }; SetRow(body, 2); editorBody.Children.Add(body);
|
|
165
|
+
body.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) }); body.ColumnDefinitions.Add(new ColumnDefinition());
|
|
166
|
+
headers = new StackPanel();
|
|
167
|
+
headerScroll = new ScrollViewer { Content = headers, VerticalScrollBarVisibility = ScrollBarVisibility.Hidden, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled };
|
|
168
|
+
body.Children.Add(headerScroll);
|
|
169
|
+
timeline = new Canvas { Background = WorkspaceUi.Color("#F7FAF9"), ClipToBounds = true, Cursor = Cursors.Arrow, Focusable = true };
|
|
170
|
+
playLine = new Line { Stroke = WorkspaceUi.Color("#D0772C"), StrokeThickness = 2, IsHitTestVisible = false };
|
|
171
|
+
selectionOverlay = new Rectangle { Fill = WorkspaceUi.Color("#5596C8"), Opacity = .2, IsHitTestVisible = false };
|
|
172
|
+
selectionStartLine = new Line { Stroke = WorkspaceUi.Color("#277FA5"), StrokeThickness = 1.15, IsHitTestVisible = false };
|
|
173
|
+
selectionEndLine = new Line { Stroke = WorkspaceUi.Color("#386BA8"), StrokeThickness = 1.15, IsHitTestVisible = false };
|
|
174
|
+
selectionStartHandle = new Polygon { Fill = selectionStartLine.Stroke, IsHitTestVisible = false }; selectionEndHandle = new Polygon { Fill = selectionEndLine.Stroke, IsHitTestVisible = false };
|
|
175
|
+
scroll = new ScrollViewer { Content = timeline, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, CanContentScroll = false };
|
|
176
|
+
SetColumn(scroll, 1); body.Children.Add(scroll);
|
|
177
|
+
scroll.ScrollChanged += delegate(object sender, ScrollChangedEventArgs e) { headerScroll.ScrollToVerticalOffset(scroll.VerticalOffset); if (Math.Abs(e.HorizontalChange) > .01 || Math.Abs(e.ViewportWidthChange) > .01) DrawCanvas(); };
|
|
178
|
+
scroll.PreviewMouseWheel += delegate(object sender, MouseWheelEventArgs e) {
|
|
179
|
+
if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) {
|
|
180
|
+
if (gesture.Mode == TimelineDrag.None) SetZoom(pixelsPerSecond * Math.Pow(2, e.Delta / 480.0), e.GetPosition(scroll).X);
|
|
181
|
+
e.Handled = true;
|
|
182
|
+
}
|
|
183
|
+
else if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) { manualViewUntil = DateTime.UtcNow.AddSeconds(2); scroll.ScrollToHorizontalOffset(scroll.HorizontalOffset - e.Delta); e.Handled = true; }
|
|
184
|
+
};
|
|
185
|
+
headerScroll.PreviewMouseWheel += delegate(object sender, MouseWheelEventArgs e) { scroll.ScrollToVerticalOffset(scroll.VerticalOffset - e.Delta); e.Handled = true; };
|
|
186
|
+
timeline.MouseLeftButtonDown += BeginSelection;
|
|
187
|
+
timeline.MouseMove += delegate(object sender, MouseEventArgs e) {
|
|
188
|
+
var point = e.GetPosition(timeline);
|
|
189
|
+
if (clipDrag != ClipDrag.None) { PreviewClipDrag(point); return; }
|
|
190
|
+
if (gesture.Mode == TimelineDrag.None) { UpdateClipCursor(point); return; }
|
|
191
|
+
MovePointer(point.X);
|
|
192
|
+
};
|
|
193
|
+
timeline.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e) {
|
|
194
|
+
if (clipDrag != ClipDrag.None) { PreviewClipDrag(e.GetPosition(timeline)); FinishClipDrag(); timeline.ReleaseMouseCapture(); e.Handled = true; return; }
|
|
195
|
+
if (gesture.Mode == TimelineDrag.None) return;
|
|
196
|
+
EndPointer(e.GetPosition(timeline).X); timeline.ReleaseMouseCapture(); e.Handled = true;
|
|
197
|
+
};
|
|
198
|
+
timeline.LostMouseCapture += delegate { if (clipDrag != ClipDrag.None) CancelClipDrag(); if (gesture.Mode != TimelineDrag.None) { gesture.Finish(); ApplyGesture(); RefreshClipBounds(); DrawCanvas(); } };
|
|
199
|
+
|
|
200
|
+
InitializeClipTools(editing);
|
|
201
|
+
var bottom = new StackPanel { Margin = new Thickness(0, 12, 0, 0) }; SetRow(bottom, 3); editorBody.Children.Add(bottom);
|
|
202
|
+
var transport = new WrapPanel(); bottom.Children.Add(transport);
|
|
203
|
+
play = WorkspaceUi.Button("Play", delegate { PlayPause(); }); play.MinWidth = 90; transport.Children.Add(play);
|
|
204
|
+
transport.Children.Add(WorkspaceUi.Button("Stop", delegate { if (recording && StopRequested != null) StopRequested(); else { Stop(); UpdateTransport(); } }));
|
|
205
|
+
transport.Children.Add(WorkspaceUi.Button("To start", delegate { Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); }));
|
|
206
|
+
positionText = WorkspaceUi.Text("00:00.00 / 00:00.00", 16); positionText.FontFamily = new FontFamily("Consolas"); positionText.Margin = new Thickness(0, 0, 14, 8); transport.Children.Add(positionText);
|
|
207
|
+
transport.Children.Add(WorkspaceUi.Text("Listen volume ", 14));
|
|
208
|
+
volume = new Slider { Minimum = 0, Maximum = 100, Value = 75, Width = 120, Margin = new Thickness(0, 0, 16, 8), VerticalAlignment = VerticalAlignment.Center };
|
|
209
|
+
volume.ValueChanged += delegate { output.Volume((float)(volume.Value / 100)); }; transport.Children.Add(volume);
|
|
210
|
+
outputPicker = new AudioOutputPicker(delegate { Stop(); }, status); bottom.Children.Add(outputPicker);
|
|
211
|
+
var exports = new WrapPanel(); bottom.Children.Add(exports);
|
|
212
|
+
exportDetails = exports;
|
|
213
|
+
var rateLabel = WorkspaceUi.Text("44.1 kHz mix ", 13); rateLabel.Margin = new Thickness(0, 0, 0, 8); exports.Children.Add(rateLabel);
|
|
214
|
+
exportFormat = new ComboBox { ItemsSource = new[] { "24-bit PCM WAV", "16-bit PCM WAV", "32-bit float WAV" }, SelectedIndex = 0, MinHeight = 36, Width = 175, FontSize = 14, Padding = new Thickness(6), Margin = new Thickness(0, 0, 12, 0) }; exports.Children.Add(exportFormat);
|
|
215
|
+
protect = new CheckBox { Content = "Prevent clipping (PCM)", IsChecked = true, FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 15, 0) }; exports.Children.Add(protect);
|
|
216
|
+
selectionOnly = new CheckBox { Content = "Selection only", FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 15, 0) }; exports.Children.Add(selectionOnly);
|
|
217
|
+
AddFileButton(exports, "Export mix…", async delegate { await ExportMix(); });
|
|
218
|
+
ArrangeCompactChrome(top, editing, range, viewControls, zoomTools, bottom, transport, exports);
|
|
219
|
+
timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(80) };
|
|
220
|
+
timer.Tick += delegate { if (output.Playing || output.Paused) { playhead = (long)Math.Round(output.Position * Audio.Rate); UpdateTransport(); UpdatePlayLine(); if (output.Playing && DateTime.UtcNow >= manualViewUntil) EnsurePlayheadVisible(); } }; timer.Start();
|
|
221
|
+
waveformDetail = new WaveformDetail(Dispatcher, delegate { if (!disposed) DrawCanvas(); });
|
|
222
|
+
UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
223
|
+
recordingPane = new DockablePane(editorBody, null, "Record options", false, false, 260) { MinimumMainHeight = 300 }; Children.Add(recordingPane);
|
|
224
|
+
editorBody.SizeChanged += delegate { UpdateCompactLayout(); };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private void UpdateCompactLayout()
|
|
228
|
+
{
|
|
229
|
+
double width = editorBody.ActualWidth > 0 ? editorBody.ActualWidth : 1280;
|
|
230
|
+
bool compact = width < 1000 || (editorBody.ActualHeight > 0 && editorBody.ActualHeight < 480);
|
|
231
|
+
compactOptionsButton.Visibility = Visibility.Visible;
|
|
232
|
+
outputPicker.Visibility = separate.Visibility = Visibility.Visible;
|
|
233
|
+
exportDetails.Visibility = compactOptions ? Visibility.Visible : Visibility.Collapsed;
|
|
234
|
+
gridTriplet.Visibility = rulerMode.Visibility = meterText.Visibility = compact ? Visibility.Collapsed : Visibility.Visible;
|
|
235
|
+
title.FontSize = 14;
|
|
236
|
+
zoomSpan.Visibility = width < 700 ? Visibility.Collapsed : Visibility.Visible;
|
|
237
|
+
zoomSlider.Width = width < 700 ? 64 : 80;
|
|
238
|
+
selectionText.TextWrapping = TextWrapping.NoWrap;
|
|
239
|
+
selectionText.TextTrimming = TextTrimming.CharacterEllipsis;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private void AddFileButton(Panel panel, string text, Action action)
|
|
243
|
+
{ var button = WorkspaceUi.Button(text, action); panel.Children.Add(button); fileButtons.Add(button); }
|
|
244
|
+
public bool ApplyGridOptions(TempoGridOptions value)
|
|
245
|
+
{
|
|
246
|
+
if (value.Signature() == session.Grid.Signature()) return true;
|
|
247
|
+
if (IsBusy) return false;
|
|
248
|
+
value.Validate(); var copy = value.Copy();
|
|
249
|
+
Change(delegate { session.Grid = copy; return true; }, "Grid: " + copy.Tempo.ToString("0.###", CultureInfo.InvariantCulture) + " BPM, " + copy.BeatsPerBar + "/" + copy.BeatUnit + ", 1/" + copy.Division + (copy.Triplet ? " triplets." : "."));
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
public void RefreshGridPreferences() { DrawCanvas(); }
|
|
253
|
+
private bool CommitTempo()
|
|
254
|
+
{
|
|
255
|
+
if (updating || !tempoEdited) return true;
|
|
256
|
+
double value;
|
|
257
|
+
try {
|
|
258
|
+
if (!TempoGrid.TryTempo(tempoBox.Text, out value)) throw new IOException("Enter a tempo between 20 and 400 BPM.");
|
|
259
|
+
var options = session.Grid.Copy(); options.Tempo = value; options.Validate();
|
|
260
|
+
bool applied = ApplyGridOptions(options); if (applied) tempoEdited = false; return applied;
|
|
261
|
+
}
|
|
262
|
+
catch (Exception ex) { tempoBox.Text = session.Grid.Tempo.ToString("0.###", CultureInfo.InvariantCulture); tempoEdited = false; status(ex.Message); return false; }
|
|
263
|
+
}
|
|
264
|
+
private void SaveSnapChoices()
|
|
265
|
+
{
|
|
266
|
+
if (updating) return;
|
|
267
|
+
var value = InterfacePreferences.Current.Copy(); value.SnapToGrid = snapGrid.IsChecked == true; value.SnapToEdges = snapEdges.IsChecked == true;
|
|
268
|
+
try { InterfacePreferences.Save(value); } catch (Exception ex) { status(ex.Message); }
|
|
269
|
+
RefreshGridToolbar(); DrawCanvas();
|
|
270
|
+
}
|
|
271
|
+
private void RefreshGridToolbar()
|
|
272
|
+
{
|
|
273
|
+
bool previous = updating; updating = true;
|
|
274
|
+
try {
|
|
275
|
+
if (!tempoBox.IsKeyboardFocusWithin) { tempoBox.Text = session.Grid.Tempo.ToString("0.###", CultureInfo.InvariantCulture); tempoEdited = false; }
|
|
276
|
+
meterText.Text = session.Grid.BeatsPerBar + "/" + session.Grid.BeatUnit + " ";
|
|
277
|
+
gridDivision.SelectedItem = "1/" + session.Grid.Division; gridTriplet.IsChecked = session.Grid.Triplet;
|
|
278
|
+
rulerMode.SelectedIndex = session.Grid.MusicalRuler ? 0 : 1;
|
|
279
|
+
snapGrid.IsChecked = InterfacePreferences.Current.SnapToGrid; snapEdges.IsChecked = InterfacePreferences.Current.SnapToEdges;
|
|
280
|
+
gridControls.IsEnabled = !IsBusy;
|
|
281
|
+
}
|
|
282
|
+
finally { updating = previous; }
|
|
283
|
+
}
|
|
284
|
+
private long FrameAt(double x) { return Math.Max(0, Math.Min(EditorSession.MaxFrame, (long)Math.Round(x / pixelsPerSecond * Audio.Rate))); }
|
|
285
|
+
private long PointerFrame(double x)
|
|
286
|
+
{ return TimelineGesture.FrameAt(x, pixelsPerSecond, session.Regions.SelectMany(r => new[] { r.Start, r.End }), (Keyboard.Modifiers & ModifierKeys.Alt) != 0, session.Grid, InterfacePreferences.Current.SnapToGrid, InterfacePreferences.Current.SnapToEdges); }
|
|
287
|
+
private void ApplyGesture() { playhead = gesture.Playhead; from = gesture.From; to = gesture.To; UpdateSelectionText(); UpdateSelectionOverlay(); RefreshRegionHighlights(); UpdatePlayLine(); UpdateTransport(); }
|
|
288
|
+
private double X(long frame) { return frame / (double)Audio.Rate * pixelsPerSecond; }
|
|
289
|
+
private double ViewportWidth { get { return scroll != null && scroll.ViewportWidth > 0 ? scroll.ViewportWidth : 900; } }
|
|
290
|
+
private double ZoomAnchor()
|
|
291
|
+
{
|
|
292
|
+
double x = X(playhead) - scroll.HorizontalOffset;
|
|
293
|
+
return x >= 0 && x <= ViewportWidth ? x : ViewportWidth / 2;
|
|
294
|
+
}
|
|
295
|
+
private void ZoomStep(double factor) { SetZoom(pixelsPerSecond * factor, ZoomAnchor()); }
|
|
296
|
+
private void SetZoom(double value, double pointer)
|
|
297
|
+
{
|
|
298
|
+
if (scroll == null || gesture.Mode != TimelineDrag.None || clipDrag != ClipDrag.None || disposed) return;
|
|
299
|
+
double scale = TimelineZoom.Clamp(value), at = Math.Max(0, Math.Min(ViewportWidth, pointer));
|
|
300
|
+
double offset = TimelineZoom.Offset(pixelsPerSecond, scale, scroll.HorizontalOffset, at);
|
|
301
|
+
pixelsPerSecond = scale; manualViewUntil = DateTime.UtcNow.AddSeconds(2);
|
|
302
|
+
DrawCanvas(); scroll.UpdateLayout(); scroll.ScrollToHorizontalOffset(offset); scroll.UpdateLayout(); DrawCanvas();
|
|
303
|
+
}
|
|
304
|
+
private void FitZoom(bool selected)
|
|
305
|
+
{
|
|
306
|
+
if (gesture.Mode != TimelineDrag.None) return;
|
|
307
|
+
if (selected && to <= from) return;
|
|
308
|
+
long start = selected ? from : 0, end = selected ? to : session.End > 0 ? session.End : Audio.Rate * 15L;
|
|
309
|
+
SetZoom(TimelineZoom.Fit(end - start, ViewportWidth), 0);
|
|
310
|
+
scroll.ScrollToHorizontalOffset(Math.Max(0, X(start) - 16)); scroll.UpdateLayout(); DrawCanvas();
|
|
311
|
+
}
|
|
312
|
+
private void RefreshZoomControls()
|
|
313
|
+
{
|
|
314
|
+
updatingZoom = true; zoomSlider.Value = Math.Log(pixelsPerSecond, 2); updatingZoom = false;
|
|
315
|
+
double span = ViewportWidth / pixelsPerSecond;
|
|
316
|
+
zoomSpan.Text = span >= 3600 ? (span / 3600).ToString("0.#", CultureInfo.InvariantCulture) + " h" : span >= 60 ? (span / 60).ToString("0.#", CultureInfo.InvariantCulture) + " min" : span < 1 ? (span * 1000).ToString("0.##", CultureInfo.InvariantCulture) + " ms" : span.ToString("0.##", CultureInfo.InvariantCulture) + " s";
|
|
317
|
+
fitSelection.IsEnabled = to > from;
|
|
318
|
+
}
|
|
319
|
+
private void UpdateSelectionText()
|
|
320
|
+
{
|
|
321
|
+
selectionFrom.Text = (from / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture);
|
|
322
|
+
selectionTo.Text = (to / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture);
|
|
323
|
+
selectionText.Text = "Track " + (track + 1) + " | Selection " + (from / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture) + " – " + (to / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture) + " s";
|
|
324
|
+
if (fitSelection != null) fitSelection.IsEnabled = to > from;
|
|
325
|
+
if (clipSelection) selectionText.Text = selectedIds.Count == 1 ? "1 clip selected" : selectedIds.Count + " clips selected";
|
|
326
|
+
selectionText.Visibility = to > from || clipSelection ? Visibility.Visible : Visibility.Collapsed;
|
|
327
|
+
}
|
|
328
|
+
private bool ReadSelection()
|
|
329
|
+
{
|
|
330
|
+
double a, b;
|
|
331
|
+
if (!double.TryParse(selectionFrom.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out a) || !double.TryParse(selectionTo.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out b) ||
|
|
332
|
+
double.IsNaN(a) || double.IsNaN(b) || double.IsInfinity(a) || double.IsInfinity(b) || a < 0 || b < a || b > 86400) {
|
|
333
|
+
status("Enter a valid selection start and end in seconds."); return false;
|
|
334
|
+
}
|
|
335
|
+
from = (long)Math.Round(a * Audio.Rate); to = (long)Math.Round(b * Audio.Rate); DrawCanvas(); return true;
|
|
336
|
+
}
|
|
337
|
+
private void BeginSelection(object sender, MouseButtonEventArgs e)
|
|
338
|
+
{
|
|
339
|
+
if (IsBusy) return; Stop(); timeline.Focus();
|
|
340
|
+
BeginPointer(e.GetPosition(timeline), (Keyboard.Modifiers & ModifierKeys.Shift) != 0, e.ClickCount);
|
|
341
|
+
if (gesture.Mode != TimelineDrag.None || clipDrag != ClipDrag.None) timeline.CaptureMouse(); e.Handled = true;
|
|
342
|
+
}
|
|
343
|
+
private void BeginPointer(Point point, bool additive, int clicks)
|
|
344
|
+
{
|
|
345
|
+
previousIds = selectedIds.ToArray(); previousClipSelection = clipSelection; previousTrack = track;
|
|
346
|
+
previousFrom = from; previousTo = to; previousPlayhead = playhead;
|
|
347
|
+
long frame = PointerFrame(point.X); pendingRegion = null; selectionMoved = false;
|
|
348
|
+
if (point.Y < RulerHeight) {
|
|
349
|
+
var mode = TimelineGesture.RulerHit(point.X, point.Y, X(from), X(to), to > from);
|
|
350
|
+
if (mode != TimelineDrag.Playhead) { clipSelection = false; selectedIds.Clear(); }
|
|
351
|
+
gesture.Begin(mode, frame, playhead, from, to); ApplyGesture(); return;
|
|
352
|
+
}
|
|
353
|
+
track = Math.Max(0, Math.Min(15, (int)((point.Y - RulerHeight) / RowHeight)));
|
|
354
|
+
var region = RegionAt(point);
|
|
355
|
+
if (sliceCursor) { playhead = frame; Change(() => TimelineEdits.Split(session, track, false, frame), "Split at the cursor."); return; }
|
|
356
|
+
if (region != null && !additive && clicks == 1) { var tool = ClipToolAt(region, point); if (tool != ClipDrag.None) { BeginClipDrag(tool, region, point); return; } }
|
|
357
|
+
if (additive && region != null) { playhead = frame; SelectRegion(region, true); BuildHeaders(); DrawCanvas(); return; }
|
|
358
|
+
if (clicks == 2 && region != null) { playhead = region.Start; SelectRegion(region, false); BuildHeaders(); DrawCanvas(); return; }
|
|
359
|
+
selectionPointerX = point.X; pendingRegion = region == null ? null : region.Id;
|
|
360
|
+
selectedIds.Clear(); clipSelection = region != null; if (region != null) selectedIds.Add(region.Id);
|
|
361
|
+
gesture.Begin(TimelineDrag.Selection, frame, playhead, from, to); gesture.Playhead = frame;
|
|
362
|
+
ApplyGesture(); BuildHeaders(); DrawCanvas();
|
|
363
|
+
}
|
|
364
|
+
private void MovePointer(double x)
|
|
365
|
+
{
|
|
366
|
+
if (gesture.Mode == TimelineDrag.Selection && Math.Abs(x - selectionPointerX) >= 3) {
|
|
367
|
+
if (!selectionMoved) { selectionMoved = true; clipSelection = false; selectedIds.Clear(); DrawCanvas(); }
|
|
368
|
+
}
|
|
369
|
+
gesture.Move(PointerFrame(x)); ApplyGesture();
|
|
370
|
+
}
|
|
371
|
+
private void EndPointer(double x)
|
|
372
|
+
{
|
|
373
|
+
var mode = gesture.Mode; MovePointer(x); gesture.End(PointerFrame(x)); ApplyGesture();
|
|
374
|
+
if (mode == TimelineDrag.Selection && !selectionMoved && pendingRegion != null) {
|
|
375
|
+
var region = session.Regions.FirstOrDefault(r => r.Id == pendingRegion); if (region != null) SelectRegion(region, false);
|
|
376
|
+
}
|
|
377
|
+
else if (mode == TimelineDrag.Selection && to > from) playhead = from;
|
|
378
|
+
pendingRegion = null; UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
379
|
+
}
|
|
380
|
+
private void SelectRegion(TimelineRegion region, bool additive)
|
|
381
|
+
{
|
|
382
|
+
if (!additive) selectedIds.Clear();
|
|
383
|
+
if (!selectedIds.Add(region.Id)) selectedIds.Remove(region.Id);
|
|
384
|
+
clipSelection = selectedIds.Count > 0;
|
|
385
|
+
var selected = session.Regions.Where(r => selectedIds.Contains(r.Id)).ToArray();
|
|
386
|
+
from = selected.Length == 0 ? playhead : selected.Min(r => r.Start);
|
|
387
|
+
to = selected.Length == 0 ? playhead : selected.Max(r => r.End); UpdateSelectionText();
|
|
388
|
+
}
|
|
389
|
+
private bool RegionSelected(TimelineRegion region)
|
|
390
|
+
{ return clipSelection ? selectedIds.Contains(region.Id) : to > from && region.Start < to && region.End > from && (AllTracks || region.Track == track); }
|
|
391
|
+
private void RefreshRegionHighlights()
|
|
392
|
+
{
|
|
393
|
+
foreach (var visual in regionVisuals) {
|
|
394
|
+
bool selected = RegionSelected(visual.Region), muted = visual.Region.Muted || session.Tracks[visual.Region.Track].Mute;
|
|
395
|
+
visual.Body.Fill = WorkspaceUi.Color(selected ? "#DAD5FA" : muted ? "#CBD8D4" : "#B9DDD2");
|
|
396
|
+
visual.Body.Stroke = WorkspaceUi.Color(selected ? "#7359C7" : "#5E9D89");
|
|
397
|
+
visual.Body.StrokeThickness = selected ? 1.7 : 1; visual.Body.Tag = selected ? "SelectedRegion" : null;
|
|
398
|
+
visual.Glow.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
private void RefreshClipBounds()
|
|
402
|
+
{
|
|
403
|
+
if (!clipSelection) return;
|
|
404
|
+
var picked = session.Regions.Where(r => selectedIds.Contains(r.Id)).ToArray();
|
|
405
|
+
if (picked.Length == 0) { selectedIds.Clear(); clipSelection = false; from = to = playhead; }
|
|
406
|
+
else { from = picked.Min(r => r.Start); to = picked.Max(r => r.End); if (!picked.Any(r => r.Track == track)) track = picked[0].Track; }
|
|
407
|
+
UpdateSelectionText();
|
|
408
|
+
}
|
|
409
|
+
private void BuildHeaders()
|
|
410
|
+
{
|
|
411
|
+
updating = true; headers.Children.Clear();
|
|
412
|
+
headers.Children.Add(new Border { Height = RulerHeight, Background = WorkspaceUi.Color("#E4ECEA"), Child = WorkspaceUi.Text(" TRACKS / LEVEL", 12) });
|
|
413
|
+
for (int i = 0; i < 16; i++) {
|
|
414
|
+
int index = i; var state = session.Tracks[i];
|
|
415
|
+
var panel = new StackPanel { Margin = new Thickness(7, 3, 7, 3) };
|
|
416
|
+
var name = WorkspaceUi.Text((i + 1).ToString("00") + " " + state.Name, 14); name.TextWrapping = TextWrapping.NoWrap; name.TextTrimming = TextTrimming.CharacterEllipsis; name.FontWeight = FontWeights.SemiBold; panel.Children.Add(name);
|
|
417
|
+
var strip = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; panel.Children.Add(strip);
|
|
418
|
+
var mute = new ToggleButton { Content = "M", IsChecked = state.Mute, Width = 28, Height = 26, FontSize = 12, Margin = new Thickness(0, 0, 4, 0), ToolTip = "Mute track " + (i + 1) };
|
|
419
|
+
mute.Click += delegate { if (!updating) Change(delegate { session.Tracks[index].Mute = mute.IsChecked == true; return true; }, "Track mute changed."); }; strip.Children.Add(mute);
|
|
420
|
+
var solo = new ToggleButton { Content = "S", IsChecked = state.Solo, Width = 28, Height = 26, FontSize = 12, Margin = new Thickness(0, 0, 6, 0), ToolTip = "Solo track " + (i + 1) };
|
|
421
|
+
solo.Click += delegate { if (!updating) Change(delegate { session.Tracks[index].Solo = solo.IsChecked == true; return true; }, "Track solo changed."); }; strip.Children.Add(solo);
|
|
422
|
+
var levels = new[] { -60.0, -24, -18, -12, -6, 0, 3, 6, 12, state.GainDb }.Distinct().OrderBy(v => v).ToArray();
|
|
423
|
+
var level = new ComboBox { ItemsSource = levels.Select(v => v.ToString("0.#", CultureInfo.InvariantCulture) + " dB").ToArray(), SelectedIndex = Array.IndexOf(levels, state.GainDb), Width = 102, MinHeight = 26, Height = 26, FontSize = 12, Padding = new Thickness(3, 0, 3, 0), ToolTip = "Track playback / export level" };
|
|
424
|
+
level.SelectionChanged += delegate { if (!updating && level.SelectedIndex >= 0) { double value = levels[level.SelectedIndex]; Change(delegate { session.Tracks[index].GainDb = value; return true; }, "Track level changed."); } }; strip.Children.Add(level);
|
|
425
|
+
var border = new Border { Height = RowHeight, BorderThickness = new Thickness(0, 0, 1, 1), BorderBrush = WorkspaceUi.Color("#D6E1DF"), Background = WorkspaceUi.Color(i == track ? "#DCEFEB" : "#EDF3F1"), Child = panel };
|
|
426
|
+
border.PreviewMouseLeftButtonDown += delegate { if (!IsBusy && track != index) { track = index; UpdateSelectionText(); } };
|
|
427
|
+
border.MouseLeftButtonUp += delegate { BuildHeaders(); DrawCanvas(); };
|
|
428
|
+
headers.Children.Add(border);
|
|
429
|
+
}
|
|
430
|
+
updating = false;
|
|
431
|
+
}
|
|
432
|
+
private void DrawCanvas()
|
|
433
|
+
{
|
|
434
|
+
if (timeline == null || disposed) return;
|
|
435
|
+
timeline.Children.Clear(); regionVisuals.Clear(); timeline.Height = RulerHeight + RowHeight * 16;
|
|
436
|
+
timeline.Width = Math.Max(Math.Max(900, scroll.ViewportWidth), X(Math.Max(session.End, Math.Max(to, playhead)) + Audio.Rate * 15L));
|
|
437
|
+
double left = scroll.HorizontalOffset, right = left + Math.Max(900, scroll.ViewportWidth);
|
|
438
|
+
waveformDetail.BeginView(left.ToString("R", CultureInfo.InvariantCulture) + "|" + right.ToString("R", CultureInfo.InvariantCulture) + "|" + pixelsPerSecond.ToString("R", CultureInfo.InvariantCulture));
|
|
439
|
+
for (int i = 0; i < 16; i++) {
|
|
440
|
+
var row = new Rectangle { Width = Math.Max(900, scroll.ViewportWidth), Height = RowHeight, Fill = WorkspaceUi.Color(i == track ? "#F0F8F5" : i % 2 == 0 ? "#FCFDFC" : "#F4F8F6"), Stroke = WorkspaceUi.Color("#E0E8E5"), StrokeThickness = .5, IsHitTestVisible = false };
|
|
441
|
+
Canvas.SetLeft(row, left); Canvas.SetTop(row, RulerHeight + i * RowHeight); timeline.Children.Add(row);
|
|
442
|
+
}
|
|
443
|
+
foreach (var region in session.Regions.Where(r => X(r.End) >= left && X(r.Start) <= right)) DrawRegion(region, left, right);
|
|
444
|
+
RefreshGridToolbar();
|
|
445
|
+
RefreshZoomControls();
|
|
446
|
+
bool visibleLabel = false;
|
|
447
|
+
if (session.Grid.MusicalRuler) foreach (var tick in TempoGrid.VisibleTicks(session.Grid, left, right - left, pixelsPerSecond)) {
|
|
448
|
+
double x = X(tick.Frame);
|
|
449
|
+
timeline.Children.Add(new Line { X1 = x, X2 = x, Y1 = tick.Kind == 0 ? RulerHeight : 0, Y2 = timeline.Height,
|
|
450
|
+
Stroke = WorkspaceUi.Color(tick.Kind == 2 ? "#386BA8" : tick.Kind == 1 ? "#D6E1DF" : "#E0E8E5"), Opacity = tick.Kind == 2 ? .48 : tick.Kind == 1 ? .7 : .45, StrokeThickness = tick.Kind == 2 ? 1 : .5, IsHitTestVisible = false });
|
|
451
|
+
if (tick.Label != null) { if (x >= left && x < left + ViewportWidth) visibleLabel = true; var label = WorkspaceUi.Text(tick.Label, 11); label.IsHitTestVisible = false; Canvas.SetLeft(label, x + 4); Canvas.SetTop(label, 3); timeline.Children.Add(label); }
|
|
452
|
+
}
|
|
453
|
+
if (!session.Grid.MusicalRuler || !visibleLabel) DrawTimeRuler(left, right);
|
|
454
|
+
timeline.Children.Add(selectionOverlay); timeline.Children.Add(selectionStartLine); timeline.Children.Add(selectionEndLine); timeline.Children.Add(selectionStartHandle); timeline.Children.Add(selectionEndHandle); UpdateSelectionOverlay();
|
|
455
|
+
playLine.Y1 = 0; playLine.Y2 = timeline.Height; timeline.Children.Add(playLine); UpdatePlayLine(); UpdateTransport();
|
|
456
|
+
undoButton.IsEnabled = !IsBusy && undo.Count > 0; redoButton.IsEnabled = !IsBusy && redo.Count > 0;
|
|
457
|
+
title.Text = session.Name + (dirty ? " *" : "");
|
|
458
|
+
}
|
|
459
|
+
private void UpdateSelectionOverlay()
|
|
460
|
+
{
|
|
461
|
+
if (selectionOverlay == null) return;
|
|
462
|
+
var visibility = to > from ? Visibility.Visible : Visibility.Collapsed;
|
|
463
|
+
foreach (var item in new UIElement[] { selectionOverlay, selectionStartLine, selectionEndLine, selectionStartHandle, selectionEndHandle }) item.Visibility = visibility;
|
|
464
|
+
if (to <= from) return;
|
|
465
|
+
double left = scroll.HorizontalOffset, right = left + Math.Max(900, scroll.ViewportWidth), a = Math.Max(left, X(from)), b = Math.Min(right, X(to));
|
|
466
|
+
if (clipSelection) selectionOverlay.Visibility = Visibility.Collapsed;
|
|
467
|
+
selectionOverlay.Width = Math.Max(0, b - a); selectionOverlay.Height = AllTracks ? RowHeight * 16 : RowHeight;
|
|
468
|
+
Canvas.SetLeft(selectionOverlay, a); Canvas.SetTop(selectionOverlay, RulerHeight + (AllTracks ? 0 : track * RowHeight));
|
|
469
|
+
UpdateRangeStick(selectionStartLine, selectionStartHandle, X(from), true); UpdateRangeStick(selectionEndLine, selectionEndHandle, X(to), false);
|
|
470
|
+
}
|
|
471
|
+
private void DrawTimeRuler(double left, double right)
|
|
472
|
+
{
|
|
473
|
+
double spacing = TimelineZoom.SecondStep(pixelsPerSecond);
|
|
474
|
+
for (double second = Math.Floor(left / pixelsPerSecond / spacing) * spacing; second * pixelsPerSecond < right; second += spacing) {
|
|
475
|
+
double x = second * pixelsPerSecond;
|
|
476
|
+
timeline.Children.Add(new Line { X1 = x, X2 = x, Y1 = 0, Y2 = timeline.Height, Stroke = WorkspaceUi.Color("#DAE5E0"), StrokeThickness = .6, Opacity = .65, IsHitTestVisible = false });
|
|
477
|
+
string time = spacing >= 1 ? WorkspaceUi.Clock(second) : second.ToString("F" + Math.Max(1, (int)Math.Ceiling(-Math.Log10(spacing))), CultureInfo.InvariantCulture) + " s";
|
|
478
|
+
var label = WorkspaceUi.Text(time, 11); label.IsHitTestVisible = false; Canvas.SetLeft(label, x + 4); Canvas.SetTop(label, 3); timeline.Children.Add(label);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
private void UpdateRangeStick(Line line, Polygon handle, double x, bool start)
|
|
482
|
+
{
|
|
483
|
+
line.X1 = line.X2 = x; line.Y1 = 14; line.Y2 = timeline.Height;
|
|
484
|
+
handle.Points.Clear(); handle.Points.Add(new Point(x, 14)); handle.Points.Add(new Point(x + (start ? 9 : -9), 14)); handle.Points.Add(new Point(x, 25));
|
|
485
|
+
}
|
|
486
|
+
private void DrawRegion(TimelineRegion region, double left, double right)
|
|
487
|
+
{
|
|
488
|
+
double x = Math.Max(left, X(region.Start)), end = Math.Min(right, X(region.End)), y = RulerHeight + region.Track * RowHeight + 3;
|
|
489
|
+
double width = Math.Max(2, end - x); bool muted = region.Muted || session.Tracks[region.Track].Mute, selected = RegionSelected(region);
|
|
490
|
+
var rectangle = new Rectangle { Width = width, Height = RowHeight - 6, RadiusX = 5, RadiusY = 5, Fill = WorkspaceUi.Color(selected ? "#DAD5FA" : muted ? "#CBD8D4" : "#B9DDD2"), Stroke = WorkspaceUi.Color(selected ? "#7359C7" : "#5E9D89"), StrokeThickness = selected ? 1.7 : 1, Tag = selected ? "SelectedRegion" : null, IsHitTestVisible = false };
|
|
491
|
+
var glow = new Rectangle { Width = width + 2, Height = RowHeight - 4, RadiusX = 6, RadiusY = 6, Stroke = WorkspaceUi.Color("#7359C7"), StrokeThickness = 4, Opacity = .28, IsHitTestVisible = false, Visibility = selected ? Visibility.Visible : Visibility.Collapsed }; Canvas.SetLeft(glow, x - 1); Canvas.SetTop(glow, y - 1); timeline.Children.Add(glow);
|
|
492
|
+
Canvas.SetLeft(rectangle, x); Canvas.SetTop(rectangle, y); timeline.Children.Add(rectangle);
|
|
493
|
+
regionVisuals.Add(new RegionVisual { Region = region, Body = rectangle, Glow = glow });
|
|
494
|
+
Clip clip;
|
|
495
|
+
if (assets.TryGetValue(region.Source, out clip) && clip.Waveform != null && width > 2) {
|
|
496
|
+
long sourceStart = region.SourceStart + (long)Math.Floor((x - X(region.Start)) / pixelsPerSecond * Audio.Rate);
|
|
497
|
+
long sourceEnd = Math.Min(region.SourceStart + region.Length, region.SourceStart + (long)Math.Ceiling((end - X(region.Start)) / pixelsPerSecond * Audio.Rate));
|
|
498
|
+
var detail = pixelsPerSecond * clip.Duration / clip.Waveform.Length > 1 ? waveformDetail.Get(clip.Cache, sourceStart, sourceEnd, Math.Max(2, Math.Min(2000, (int)Math.Ceiling(width)))) : null;
|
|
499
|
+
int points = detail == null ? Math.Max(2, Math.Min(400, (int)(width / 2))) : detail.Low.Length;
|
|
500
|
+
var wave = new Polygon { Fill = WorkspaceUi.Color("#438B77"), Stroke = detail == null ? null : WorkspaceUi.Color("#438B77"), StrokeThickness = .8, Opacity = .7, IsHitTestVisible = false, Tag = detail == null ? "WaveformOverview" : "WaveformDetail" };
|
|
501
|
+
double sourceFrames = Math.Max(1, clip.Duration * Audio.Rate), scale = Math.Max(1, clip.Peak);
|
|
502
|
+
for (int side = 0; side < 2; side++) for (int p = 0; p < points; p++) {
|
|
503
|
+
int j = side == 0 ? p : points - p - 1; double px = x + j * width / (points - 1);
|
|
504
|
+
double sourceFrame = region.SourceStart + (px - X(region.Start)) / pixelsPerSecond * Audio.Rate;
|
|
505
|
+
int bin = Math.Max(0, Math.Min(clip.Waveform.Length - 1, (int)(sourceFrame / sourceFrames * clip.Waveform.Length)));
|
|
506
|
+
double value = detail == null ? clip.Waveform[bin] * (side == 0 ? 1 : -1) : side == 0 ? detail.High[j] : detail.Low[j];
|
|
507
|
+
value *= EditorTools.Envelope(region, (long)Math.Round(sourceFrame - region.SourceStart));
|
|
508
|
+
wave.Points.Add(new Point(px, y + RowHeight * .65 - value / scale * RowHeight * .22));
|
|
509
|
+
}
|
|
510
|
+
timeline.Children.Add(wave);
|
|
511
|
+
}
|
|
512
|
+
if (width > 45) {
|
|
513
|
+
var label = WorkspaceUi.Text(region.Name + (region.Muted ? " · muted" : ""), 12); label.MaxWidth = Math.Max(1, width - 10); label.TextWrapping = TextWrapping.NoWrap; label.TextTrimming = TextTrimming.CharacterEllipsis; label.IsHitTestVisible = false;
|
|
514
|
+
Canvas.SetLeft(label, x + 5); Canvas.SetTop(label, y + 3); timeline.Children.Add(label);
|
|
515
|
+
}
|
|
516
|
+
DrawClipFades(region, left, right, selected);
|
|
517
|
+
}
|
|
518
|
+
private void UpdatePlayLine() { playLine.X1 = playLine.X2 = X(playhead); }
|
|
519
|
+
private void UpdateTransport()
|
|
520
|
+
{ positionText.Text = WorkspaceUi.Clock(playhead / (double)Audio.Rate) + " / " + WorkspaceUi.Clock(session.End / (double)Audio.Rate); }
|
|
521
|
+
private void EnsurePlayheadVisible()
|
|
522
|
+
{ double x = X(playhead); if (x < scroll.HorizontalOffset || x > scroll.HorizontalOffset + scroll.ViewportWidth - 15) scroll.ScrollToHorizontalOffset(Math.Max(0, x - 30)); }
|
|
523
|
+
|
|
524
|
+
private void Change(Func<bool> action, string message)
|
|
525
|
+
{
|
|
526
|
+
if (IsBusy) return; Stop(); var before = session.Copy();
|
|
527
|
+
var oldIds = selectedIds.ToArray(); bool oldMode = clipSelection; long oldFrom = from, oldTo = to; int oldTrack = track;
|
|
528
|
+
Action restore = delegate { session = before; selectedIds.Clear(); foreach (string id in oldIds) selectedIds.Add(id); clipSelection = oldMode; from = oldFrom; to = oldTo; track = oldTrack; UpdateSelectionText(); BuildHeaders(); DrawCanvas(); };
|
|
529
|
+
try {
|
|
530
|
+
if (!action()) { restore(); status("No audio section was changed. Select a clip or a time range first."); return; }
|
|
531
|
+
session.Validate(); undo.Push(before); redo.Clear(); dirty = true; RefreshClipBounds(); BuildHeaders(); DrawCanvas(); status(message);
|
|
532
|
+
}
|
|
533
|
+
catch (Exception ex) { restore(); status(ex.Message); }
|
|
534
|
+
}
|
|
535
|
+
private void Undo(bool forward)
|
|
536
|
+
{
|
|
537
|
+
if (IsBusy) return; var source = forward ? redo : undo; var target = forward ? undo : redo;
|
|
538
|
+
if (source.Count == 0) return; Stop(); target.Push(session.Copy()); session = source.Pop(); dirty = true; RefreshClipBounds(); BuildHeaders(); DrawCanvas();
|
|
539
|
+
}
|
|
540
|
+
private void Split() { Change(() => TimelineEdits.Split(session, track, AllTracks, playhead, clipSelection ? selectedIds : null), "Split at the playhead."); }
|
|
541
|
+
private void Copy(bool cut)
|
|
542
|
+
{
|
|
543
|
+
RefreshClipBounds();
|
|
544
|
+
if (IsBusy || !ReadSelection() || to <= from) { status("Select a clip or time range to copy or cut."); return; }
|
|
545
|
+
bool all = clipSelection ? session.Regions.Where(r => selectedIds.Contains(r.Id)).Select(r => r.Track).Distinct().Count() > 1 : AllTracks;
|
|
546
|
+
clipboard = TimelineEdits.Copy(session, track, all, from, to, clipSelection ? selectedIds : null);
|
|
547
|
+
if (clipboard.Regions.Count == 0) { status("There is no audio in this selection."); return; }
|
|
548
|
+
if (cut) Change(() => TimelineEdits.Delete(session, track, AllTracks, from, to, clipSelection ? selectedIds : null), "Selection cut. Paste inserts it at the playhead.");
|
|
549
|
+
else status("Copied " + clipboard.Regions.Count + " audio section(s). Click the ruler for the paste position.");
|
|
550
|
+
}
|
|
551
|
+
private void DeleteRange()
|
|
552
|
+
{ if (ReadSelection() && to > from) Change(() => TimelineEdits.Delete(session, track, AllTracks, from, to, clipSelection ? selectedIds : null), "Selected audio removed; the timeline gap is kept."); }
|
|
553
|
+
private void Paste()
|
|
554
|
+
{
|
|
555
|
+
Change(delegate {
|
|
556
|
+
var before = new HashSet<string>(session.Regions.Select(r => r.Id));
|
|
557
|
+
if (!TimelineEdits.Paste(session, clipboard, track, playhead)) return false;
|
|
558
|
+
selectedIds.Clear(); foreach (var region in session.Regions.Where(r => !before.Contains(r.Id))) selectedIds.Add(region.Id);
|
|
559
|
+
clipSelection = true; return true;
|
|
560
|
+
}, "Audio pasted at the playhead.");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private async Task ChooseAudio()
|
|
564
|
+
{
|
|
565
|
+
if (IsBusy) return;
|
|
566
|
+
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Add audio to the timeline", Filter = WorkspaceUi.AudioFilter, Multiselect = true };
|
|
567
|
+
if (dialog.ShowDialog(owner) == true) await AddFiles(dialog.FileNames);
|
|
568
|
+
}
|
|
569
|
+
public async Task AddFiles(IEnumerable<string> paths)
|
|
570
|
+
{
|
|
571
|
+
if (IsBusy || disposed) return; var files = paths.Where(File.Exists).Select(Path.GetFullPath).ToArray(); if (files.Length == 0) return;
|
|
572
|
+
bool separateTracks = separate.IsChecked == true;
|
|
573
|
+
if (separateTracks && track + files.Length > 16) { status("Choose at most " + (16 - track) + " files for the remaining tracks, or turn off separate tracks."); return; }
|
|
574
|
+
Stop(); BeginWork();
|
|
575
|
+
var decoded = new List<Clip>();
|
|
576
|
+
try {
|
|
577
|
+
foreach (string path in files) {
|
|
578
|
+
Clip clip; status("Reading " + Path.GetFileName(path) + " for the timeline…");
|
|
579
|
+
if (!assets.TryGetValue(path, out clip)) { clip = await Task.Run(() => Audio.Load(path, cache, operation.Token, null)); assets[path] = clip; }
|
|
580
|
+
decoded.Add(clip);
|
|
581
|
+
}
|
|
582
|
+
operation.Token.ThrowIfCancellationRequested(); var before = session.Copy(); long at = playhead;
|
|
583
|
+
for (int i = 0; i < decoded.Count; i++) {
|
|
584
|
+
var clip = decoded[i]; int index = separateTracks ? track + i : track; long frames = (long)Math.Round(clip.Duration * Audio.Rate);
|
|
585
|
+
session.Regions.Add(new TimelineRegion { Track = index, Start = at, Length = frames, Source = clip.Source, Name = Path.GetFileNameWithoutExtension(clip.Name) });
|
|
586
|
+
if (session.Tracks[index].Name == "Track " + (index + 1)) session.Tracks[index].Name = Path.GetFileNameWithoutExtension(clip.Name);
|
|
587
|
+
if (!separateTracks) at += frames;
|
|
588
|
+
}
|
|
589
|
+
try { session.Validate(); } catch { session = before; throw; }
|
|
590
|
+
undo.Push(before); redo.Clear(); dirty = true; status("Audio added. Drag a time range, then cut, copy, split or paste.");
|
|
591
|
+
}
|
|
592
|
+
catch (OperationCanceledException) { status("Import cancelled. Your existing timeline is kept."); }
|
|
593
|
+
catch (Exception ex) { status("Could not add audio: " + ex.Message); }
|
|
594
|
+
finally { EndWork(); BuildHeaders(); DrawCanvas(); timeline.Focus(); }
|
|
595
|
+
}
|
|
596
|
+
private void BeginWork()
|
|
597
|
+
{ operation = new CancellationTokenSource(); foreach (var button in fileButtons) button.IsEnabled = false; play.IsEnabled = false; cancel.Visibility = Visibility.Visible; RefreshGridToolbar(); }
|
|
598
|
+
private void EndWork()
|
|
599
|
+
{ operation.Dispose(); operation = null; foreach (var button in fileButtons) button.IsEnabled = true; play.IsEnabled = true; cancel.Visibility = Visibility.Collapsed; RefreshGridToolbar(); }
|
|
600
|
+
private void NewSession()
|
|
601
|
+
{
|
|
602
|
+
if (IsBusy || !ConfirmClose()) return; Stop(); session = new EditorSession { Grid = InterfacePreferences.Current.Grid.Copy() }; sessionPath = null; assets.Clear(); undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
603
|
+
}
|
|
604
|
+
public bool SaveSession(bool saveAs)
|
|
605
|
+
{
|
|
606
|
+
if (IsBusy) return false;
|
|
607
|
+
string path = sessionPath;
|
|
608
|
+
if (saveAs || string.IsNullOrEmpty(path)) {
|
|
609
|
+
var dialog = new Microsoft.Win32.SaveFileDialog { Title = "Save the editor session", Filter = "Flux editor session|*.dbedit", DefaultExt = ".dbedit", FileName = session.Name == "Untitled session" ? "Practice.dbedit" : session.Name + ".dbedit" };
|
|
610
|
+
if (dialog.ShowDialog(owner) != true) return false; path = dialog.FileName;
|
|
611
|
+
}
|
|
612
|
+
try { session.Name = Path.GetFileNameWithoutExtension(path); session.Save(path); sessionPath = path; dirty = false; DrawCanvas(); status("Session saved. Keep the original source audio in its current location."); return true; }
|
|
613
|
+
catch (Exception ex) { status("Could not save the session: " + ex.Message); return false; }
|
|
614
|
+
}
|
|
615
|
+
private async Task OpenSession()
|
|
616
|
+
{
|
|
617
|
+
if (IsBusy || !ConfirmClose()) return;
|
|
618
|
+
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Open an editor session", Filter = "Flux editor session|*.dbedit" };
|
|
619
|
+
if (dialog.ShowDialog(owner) != true) return; Stop(); BeginWork();
|
|
620
|
+
try {
|
|
621
|
+
var loaded = EditorSession.Load(dialog.FileName); var decoded = new Dictionary<string, Clip>(StringComparer.OrdinalIgnoreCase);
|
|
622
|
+
foreach (var r in loaded.Regions) if (!Path.IsPathRooted(r.Source)) r.Source = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(dialog.FileName), r.Source));
|
|
623
|
+
foreach (string path in loaded.Regions.Select(r => r.Source).Distinct(StringComparer.OrdinalIgnoreCase)) {
|
|
624
|
+
status("Opening " + Path.GetFileName(path) + "…");
|
|
625
|
+
if (!File.Exists(path)) throw new IOException("Source audio is missing: " + path);
|
|
626
|
+
Clip clip; if (!assets.TryGetValue(path, out clip)) clip = await Task.Run(() => Audio.Load(path, cache, operation.Token, null));
|
|
627
|
+
decoded.Add(path, clip);
|
|
628
|
+
}
|
|
629
|
+
using (var verify = new TimelineMix(loaded, decoded)) { }
|
|
630
|
+
operation.Token.ThrowIfCancellationRequested(); session = loaded; assets.Clear(); foreach (var pair in decoded) assets.Add(pair.Key, pair.Value);
|
|
631
|
+
sessionPath = dialog.FileName; undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); status("Editor session opened.");
|
|
632
|
+
}
|
|
633
|
+
catch (OperationCanceledException) { status("Opening cancelled. Your current timeline is kept."); }
|
|
634
|
+
catch (Exception ex) { status("Could not open this session: " + ex.Message); }
|
|
635
|
+
finally { EndWork(); BuildHeaders(); DrawCanvas(); }
|
|
636
|
+
}
|
|
637
|
+
private async Task ExportMix()
|
|
638
|
+
{
|
|
639
|
+
if (IsBusy || session.End <= 0) { status("Add audio to the timeline before exporting."); return; }
|
|
640
|
+
bool selected = selectionOnly.IsChecked == true;
|
|
641
|
+
if (selected && (!ReadSelection() || to <= from)) { status("Select the time range to export."); return; }
|
|
642
|
+
var dialog = new Microsoft.Win32.SaveFileDialog { Title = "Export the timeline mix", Filter = "WAV audio|*.wav", DefaultExt = ".wav", FileName = session.Name + " mix.wav" };
|
|
643
|
+
if (dialog.ShowDialog(owner) != true) return;
|
|
644
|
+
if (assets.Keys.Any(p => string.Equals(Path.GetFullPath(p), Path.GetFullPath(dialog.FileName), StringComparison.OrdinalIgnoreCase))) { status("Choose a new file name to preserve the original audio."); return; }
|
|
645
|
+
Stop(); int bits = new[] { 24, 16, 32 }[exportFormat.SelectedIndex]; bool safe = protect.IsChecked == true;
|
|
646
|
+
var state = session.Copy(); var decoded = new Dictionary<string, Clip>(assets, StringComparer.OrdinalIgnoreCase); long start = selected ? from : 0, end = selected ? to : session.End;
|
|
647
|
+
BeginWork();
|
|
648
|
+
try {
|
|
649
|
+
await Task.Run(() => TimelineRender.Export(state, decoded, dialog.FileName, bits, safe, start, end, operation.Token, delegate(double p) { Dispatcher.BeginInvoke((Action)delegate { status("Exporting mix: " + (p * 100).ToString("0") + "%"); }); }));
|
|
650
|
+
status("Saved " + Path.GetFileName(dialog.FileName) + " as " + bits + (bits == 32 ? "-bit float" : "-bit PCM") + " WAV. Original audio is kept.");
|
|
651
|
+
}
|
|
652
|
+
catch (OperationCanceledException) { status("Export cancelled. Original files and previous exports are kept."); }
|
|
653
|
+
catch (Exception ex) { status("Could not export the mix: " + ex.Message); }
|
|
654
|
+
finally { EndWork(); DrawCanvas(); }
|
|
655
|
+
}
|
|
656
|
+
private void PlayPause()
|
|
657
|
+
{
|
|
658
|
+
if (IsBusy || session.End <= 0) return;
|
|
659
|
+
if (output.Playing) { output.Pause(); play.Content = "Resume"; return; }
|
|
660
|
+
if (output.Paused) { output.Resume(); play.Content = "Pause"; return; }
|
|
661
|
+
try {
|
|
662
|
+
beforePlay(); if (playhead >= session.End) playhead = 0;
|
|
663
|
+
mix = new TimelineMix(session, assets); mix.Seek(playhead);
|
|
664
|
+
output.Open(mix, playhead / (double)Audio.Rate, (float)(volume.Value / 100), delegate(Exception error) { if (error == null) playhead = session.End; Stop(); DrawCanvas(); if (error != null) status("Editor playback stopped: " + error.Message); }, outputPicker.Selected);
|
|
665
|
+
play.Content = "Pause";
|
|
666
|
+
}
|
|
667
|
+
catch (Exception ex) { Stop(); status("Could not play the timeline: " + ex.Message); }
|
|
668
|
+
}
|
|
669
|
+
public void Stop()
|
|
670
|
+
{
|
|
671
|
+
if (output.Playing || output.Paused) playhead = (long)Math.Round(output.Position * Audio.Rate);
|
|
672
|
+
output.Stop(); if (mix != null) { mix.Dispose(); mix = null; } play.Content = "Play";
|
|
673
|
+
}
|
|
674
|
+
public void AttachRecordingPanel(FrameworkElement panel)
|
|
675
|
+
{
|
|
676
|
+
panel.Margin = new Thickness(10); recordingPane.SetContent(panel);
|
|
677
|
+
panel.SizeChanged += delegate {
|
|
678
|
+
var stack = panel as Panel; if (stack == null) return;
|
|
679
|
+
foreach (var flow in stack.Children.OfType<WrapPanel>())
|
|
680
|
+
foreach (var group in flow.Children.OfType<FrameworkElement>()) group.Width = Math.Max(150, Math.Min(266, panel.ActualWidth - 10));
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
public void ShowRecordingOptions(bool visible)
|
|
684
|
+
{ if (visible && RecordingOptionsRequested != null) RecordingOptionsRequested(); recordingPane.ShowPane(visible); UpdateCompactLayout(); }
|
|
685
|
+
internal DockablePane RecordingPaneForTest { get { return recordingPane; } }
|
|
686
|
+
public void SetRecordingState(bool active)
|
|
687
|
+
{
|
|
688
|
+
recording = active; recordButton.Content = active ? "Stop recording" : "Record";
|
|
689
|
+
RefreshGridToolbar();
|
|
690
|
+
foreach (var button in fileButtons) button.IsEnabled = !active && operation == null;
|
|
691
|
+
if (active) { Stop(); ShowRecordingOptions(true); }
|
|
692
|
+
}
|
|
693
|
+
public async Task AddRecordedFiles(IEnumerable<string> paths, long at, int firstTrack)
|
|
694
|
+
{
|
|
695
|
+
if (IsBusy) throw new IOException("Finish the editor operation before adding the take.");
|
|
696
|
+
var files = paths.Where(File.Exists).ToArray();
|
|
697
|
+
if (firstTrack < 0 || firstTrack + files.Length > 16) throw new IOException("Choose enough empty track slots before recording.");
|
|
698
|
+
BeginWork();
|
|
699
|
+
try {
|
|
700
|
+
var decoded = new List<Clip>();
|
|
701
|
+
foreach (var path in files) { var clip = await Task.Run(() => Audio.Load(path, cache, operation.Token, null)); decoded.Add(clip); assets[path] = clip; }
|
|
702
|
+
var before = session.Copy();
|
|
703
|
+
for (int i = 0; i < decoded.Count; i++) {
|
|
704
|
+
var clip = decoded[i]; int index = firstTrack + i;
|
|
705
|
+
session.Regions.Add(new TimelineRegion { Source = clip.Source, Name = "Take " + Path.GetFileNameWithoutExtension(clip.Name), Track = index, Start = at, Length = (long)Math.Round(clip.Duration * Audio.Rate) });
|
|
706
|
+
if (session.Tracks[index].Name == "Track " + (index + 1)) session.Tracks[index].Name = Path.GetFileNameWithoutExtension(clip.Name);
|
|
707
|
+
}
|
|
708
|
+
try { session.Validate(); } catch { session = before; throw; }
|
|
709
|
+
undo.Push(before); redo.Clear(); dirty = true; track = firstTrack; playhead = at;
|
|
710
|
+
}
|
|
711
|
+
finally { EndWork(); BuildHeaders(); DrawCanvas(); }
|
|
712
|
+
}
|
|
713
|
+
public async Task<bool> ExecuteAction(string action)
|
|
714
|
+
{
|
|
715
|
+
if (action == "selectTool" && clipDrag != ClipDrag.None) CancelClipDrag();
|
|
716
|
+
if (IsBusy && action != "stop" && !action.StartsWith("zoom", StringComparison.Ordinal)) return false;
|
|
717
|
+
if (ExecuteClipAction(action)) return true;
|
|
718
|
+
switch (action) {
|
|
719
|
+
case "play": PlayPause(); break;
|
|
720
|
+
case "stop": Stop(); UpdateTransport(); break;
|
|
721
|
+
case "split": Split(); break;
|
|
722
|
+
case "selectTool": SetPointerTool("select"); break;
|
|
723
|
+
case "scissors": sliceCursor = !sliceCursor; timeline.Cursor = sliceCursor ? Cursors.Cross : Cursors.Arrow; break;
|
|
724
|
+
case "copy": Copy(false); break;
|
|
725
|
+
case "cut": Copy(true); break;
|
|
726
|
+
case "paste": Paste(); break;
|
|
727
|
+
case "delete": case "deleteAlternate": DeleteRange(); break;
|
|
728
|
+
case "undo": Undo(false); break;
|
|
729
|
+
case "redo": Undo(true); break;
|
|
730
|
+
case "save": if (CommitTempo()) SaveSession(false); break;
|
|
731
|
+
case "saveAs": if (CommitTempo()) SaveSession(true); break;
|
|
732
|
+
case "open": await OpenSession(); break;
|
|
733
|
+
case "new": NewSession(); break;
|
|
734
|
+
case "import": await ChooseAudio(); break;
|
|
735
|
+
case "export": await ExportMix(); break;
|
|
736
|
+
case "start": Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
737
|
+
case "end": Stop(); playhead = session.End; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
738
|
+
case "zoomIn": case "zoomInPlus": case "zoomInKeypad": ZoomStep(2); break;
|
|
739
|
+
case "zoomOut": case "zoomOutKeypad": ZoomStep(.5); break;
|
|
740
|
+
case "zoomFit": FitZoom(false); break;
|
|
741
|
+
case "zoomSelection": FitZoom(true); break;
|
|
742
|
+
default: return false;
|
|
743
|
+
}
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
public bool HandleKey(KeyEventArgs e)
|
|
747
|
+
{
|
|
748
|
+
if (e.Key == Key.Escape && clipDrag != ClipDrag.None) { CancelClipDrag(); timeline.ReleaseMouseCapture(); return true; }
|
|
749
|
+
if (Keyboard.FocusedElement is TextBox || Keyboard.FocusedElement is ComboBox) return false;
|
|
750
|
+
if (e.Key == Key.Escape && gesture.Mode != TimelineDrag.None) { gesture.Cancel(); selectedIds.Clear(); if (previousIds != null) foreach (string id in previousIds) selectedIds.Add(id); clipSelection = previousClipSelection; track = previousTrack; ApplyGesture(); BuildHeaders(); DrawCanvas(); timeline.ReleaseMouseCapture(); return true; }
|
|
751
|
+
string action = InterfacePreferences.Resolve(e.Key == Key.System ? e.SystemKey : e.Key, Keyboard.Modifiers);
|
|
752
|
+
if (action == null || action == "record") return false;
|
|
753
|
+
var pending = ExecuteAction(action);
|
|
754
|
+
pending.ContinueWith(task => { if (task.IsFaulted) Dispatcher.BeginInvoke((Action)delegate { status("Editor action failed: " + task.Exception.GetBaseException().Message); }); });
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
public bool ConfirmClose()
|
|
758
|
+
{
|
|
759
|
+
if (IsBusy) { status("Finish the editor task or click Cancel before closing."); return false; }
|
|
760
|
+
if (!dirty) return true;
|
|
761
|
+
var choice = MessageBox.Show(owner, "Save the editor session before continuing?", "Unsaved timeline", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
|
762
|
+
return choice == MessageBoxResult.No || (choice == MessageBoxResult.Yes && SaveSession(false));
|
|
763
|
+
}
|
|
764
|
+
internal void SetForTest(EditorSession state, IDictionary<string, Clip> decoded)
|
|
765
|
+
{ session = state.Copy(); assets.Clear(); foreach (var p in decoded) assets.Add(p.Key, p.Value); undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); BuildHeaders(); DrawCanvas(); }
|
|
766
|
+
internal EditorSession SessionForTest { get { return session.Copy(); } }
|
|
767
|
+
internal void ListenVolumeForTest(double value) { volume.Value = value; }
|
|
768
|
+
internal bool PlayingForTest { get { return output.Playing; } }
|
|
769
|
+
internal void ShowSelectionForTest(long start, long end) { clipSelection = false; selectedIds.Clear(); from = start; to = end; UpdateSelectionText(); DrawCanvas(); }
|
|
770
|
+
internal void PlayheadForTest(long value) { playhead = value; DrawCanvas(); }
|
|
771
|
+
internal void ZoomForTest(double value) { SetZoom(value, 0); }
|
|
772
|
+
internal double ZoomPixelsForTest { get { return pixelsPerSecond; } }
|
|
773
|
+
internal double OffsetForTest { get { return scroll.HorizontalOffset; } }
|
|
774
|
+
internal double WidthForTest { get { return ViewportWidth; } }
|
|
775
|
+
internal int DetailsForTest { get { return timeline.Children.OfType<Polygon>().Count(p => (string)p.Tag == "WaveformDetail"); } }
|
|
776
|
+
internal void ZoomAtForTest(double value, double pointer) { SetZoom(value, pointer); }
|
|
777
|
+
internal void OffsetForTestSet(double value) { scroll.ScrollToHorizontalOffset(value); scroll.UpdateLayout(); DrawCanvas(); }
|
|
778
|
+
internal void PointerDownForTest(long frame, int targetTrack, bool additive) { BeginPointer(new Point(X(frame), RulerHeight + targetTrack * RowHeight + 20), additive, 1); }
|
|
779
|
+
internal void PointerUpForTest(long frame) { if (gesture.Mode != TimelineDrag.None) EndPointer(X(frame)); }
|
|
780
|
+
internal void PointerMoveForTest(long frame) { MovePointer(X(frame)); }
|
|
781
|
+
internal int SelectedClipsForTest { get { return clipSelection ? selectedIds.Count : 0; } }
|
|
782
|
+
internal int HighlightedForTest { get { return regionVisuals.Count(v => (string)v.Body.Tag == "SelectedRegion"); } }
|
|
783
|
+
internal bool SelectCursorForTest { get { return timeline.Cursor == Cursors.Arrow; } }
|
|
784
|
+
internal double TimelineHeightForTest { get { return scroll.ViewportHeight; } }
|
|
785
|
+
internal bool CheckUndoRedoForTest()
|
|
786
|
+
{
|
|
787
|
+
var original = session.Copy();
|
|
788
|
+
from = Audio.Rate / 4; to = Audio.Rate / 2; track = 0; UpdateSelectionText();
|
|
789
|
+
Copy(true); var afterCut = session.Copy();
|
|
790
|
+
Undo(false); bool restored = session.Regions.Count == original.Regions.Count && session.Regions[0].Length == original.Regions[0].Length;
|
|
791
|
+
Undo(true); bool repeated = session.Regions.Count == afterCut.Regions.Count && session.Regions.All(r => r.Track != 0 || r.End <= from || r.Start >= to);
|
|
792
|
+
return restored && repeated && dirty;
|
|
793
|
+
}
|
|
794
|
+
public void Dispose()
|
|
795
|
+
{ if (disposed) return; disposed = true; waveformDetail.Dispose(); if (operation != null) operation.Cancel(); timer.Stop(); Stop(); output.Dispose(); }
|
|
796
|
+
public void CancelWork() { if (operation != null) operation.Cancel(); }
|
|
797
|
+
}
|
|
798
|
+
}
|