@pro-gram/flux 1.5.0 → 1.6.1
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/NOTICE.txt +4 -3
- package/README.md +42 -8
- package/dependencies.json +2 -1
- package/package.json +3 -3
- package/payload/DrumBuddy-Setup.exe +0 -0
- package/scripts/install.js +4 -2
- package/source/AssetBootstrap.cs +1 -1
- package/source/Audio Decoders.txt +230 -0
- package/source/AudioDeviceSettings.cs +1 -1
- package/source/AudioEndpointWatcher.cs +25 -0
- package/source/AudioOutputs.cs +40 -60
- package/source/AudioWorkspace.cs +2 -1
- package/source/BrandArtwork.cs +2 -2
- package/source/Build Instructions.txt +9 -3
- package/source/Build.ps1 +21 -9
- 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 +247 -75
- package/source/Flux-Logo.png +0 -0
- package/source/InterfaceSettings.cs +10 -4
- package/source/InterfaceTests.cs +6 -0
- package/source/Main.cs +19 -13
- package/source/Main.xaml +25 -28
- package/source/PlaybackTests.cs +13 -6
- package/source/Quick Start.txt +28 -6
- package/source/SettingsWindow.cs +4 -3
- package/source/Setup.cs +9 -5
- package/source/Third Party Notices.txt +4 -3
- package/source/Timeline.cs +24 -17
- package/source/TimelineZoom.cs +21 -0
- package/source/WaveformDetail.cs +104 -0
- package/source/WindowsAudioPlayer.cs +137 -0
- package/source/WorkspaceTests.cs +7 -3
- package/source/ZoomTests.cs +95 -0
package/source/EditorView.cs
CHANGED
|
@@ -16,7 +16,7 @@ using Path = System.IO.Path;
|
|
|
16
16
|
|
|
17
17
|
namespace DrumBuddy
|
|
18
18
|
{
|
|
19
|
-
public sealed class EditorView : Grid, IDisposable
|
|
19
|
+
public sealed partial class EditorView : Grid, IDisposable
|
|
20
20
|
{
|
|
21
21
|
private const double RowHeight = 56, RulerHeight = 26;
|
|
22
22
|
private readonly Window owner;
|
|
@@ -35,7 +35,13 @@ namespace DrumBuddy
|
|
|
35
35
|
private readonly ScrollViewer scroll, headerScroll;
|
|
36
36
|
private readonly TextBlock title, positionText, selectionText;
|
|
37
37
|
private readonly TextBox selectionFrom, selectionTo;
|
|
38
|
-
private readonly ComboBox
|
|
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;
|
|
39
45
|
private readonly CheckBox separate, selectionOnly, protect;
|
|
40
46
|
private readonly Button play, undoButton, redoButton, cancel;
|
|
41
47
|
private readonly Slider volume;
|
|
@@ -47,13 +53,21 @@ namespace DrumBuddy
|
|
|
47
53
|
private readonly CheckBox snapGrid, snapEdges, gridTriplet;
|
|
48
54
|
private readonly TextBlock meterText;
|
|
49
55
|
private readonly WrapPanel gridControls;
|
|
50
|
-
private static readonly double[] ZoomLevels = { 5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560 };
|
|
51
56
|
public Action GridSettingsRequested;
|
|
52
57
|
public TempoGridOptions GridOptions { get { return session.Grid.Copy(); } }
|
|
53
58
|
private readonly Grid editorBody;
|
|
54
59
|
private readonly DockablePane recordingPane;
|
|
55
60
|
private readonly Button recordButton;
|
|
56
|
-
private
|
|
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>();
|
|
57
71
|
private readonly Button compactOptionsButton;
|
|
58
72
|
private FrameworkElement exportDetails;
|
|
59
73
|
private bool compactOptions;
|
|
@@ -73,7 +87,7 @@ namespace DrumBuddy
|
|
|
73
87
|
private double pixelsPerSecond = 40;
|
|
74
88
|
private long playhead, from, to;
|
|
75
89
|
private bool updating, disposed, dirty, tempoEdited;
|
|
76
|
-
public bool IsBusy { get { return operation != null || recording; } }
|
|
90
|
+
public bool IsBusy { get { return operation != null || recording || clipDrag != ClipDrag.None; } }
|
|
77
91
|
public bool Dirty { get { return dirty; } }
|
|
78
92
|
private bool AllTracks { get { return scope.SelectedIndex == 1; } }
|
|
79
93
|
|
|
@@ -99,11 +113,9 @@ namespace DrumBuddy
|
|
|
99
113
|
editing.Children.Add(WorkspaceUi.Button("Record options", delegate { ShowRecordingOptions(!recordingPane.IsPaneVisible); }));
|
|
100
114
|
undoButton = WorkspaceUi.Button("Undo", delegate { Undo(false); }); editing.Children.Add(undoButton);
|
|
101
115
|
redoButton = WorkspaceUi.Button("Redo", delegate { Undo(true); }); editing.Children.Add(redoButton);
|
|
102
|
-
editing.Children.Add(WorkspaceUi.Button("✂ Split", delegate { Split(); }));
|
|
103
|
-
scissorsTool = new ToggleButton { Content = "Scissors tool", MinHeight = 30, Padding = new Thickness(7, 3, 7, 3), FontSize = 12, Margin = new Thickness(0, 0, 6, 6), ToolTip = "Click a clip to split it. S splits at the playhead; K toggles this tool by default." }; editing.Children.Add(scissorsTool);
|
|
104
116
|
editing.Children.Add(WorkspaceUi.Button("Delete range", delegate { DeleteRange(); }));
|
|
105
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);
|
|
106
|
-
scope.SelectionChanged += delegate { DrawCanvas(); };
|
|
118
|
+
scope.SelectionChanged += delegate { if (!updating) { clipSelection = false; selectedIds.Clear(); } DrawCanvas(); };
|
|
107
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);
|
|
108
120
|
compactOptionsButton = WorkspaceUi.Button("Audio / export options", delegate { compactOptions = !compactOptions; UpdateCompactLayout(); }); editing.Children.Add(compactOptionsButton);
|
|
109
121
|
|
|
@@ -128,23 +140,33 @@ namespace DrumBuddy
|
|
|
128
140
|
|
|
129
141
|
var range = new WrapPanel { Margin = new Thickness(0, 0, 0, 5) }; tools.Children.Add(range);
|
|
130
142
|
range.Visibility = Visibility.Collapsed;
|
|
131
|
-
editing.Children.Add(WorkspaceUi.Button("Range
|
|
143
|
+
editing.Children.Add(WorkspaceUi.Button("Range", delegate { range.Visibility = range.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; }));
|
|
132
144
|
range.Children.Add(WorkspaceUi.Text("Selection, seconds ", 14));
|
|
133
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);
|
|
134
146
|
range.Children.Add(WorkspaceUi.Text("to ", 14));
|
|
135
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);
|
|
136
|
-
range.Children.Add(WorkspaceUi.Button("Set range", delegate { if (ReadSelection()) { playhead = from; DrawCanvas(); } }));
|
|
137
|
-
range.Children.Add(WorkspaceUi.Text("Zoom ", 14));
|
|
138
|
-
zoom = new ComboBox { ItemsSource = ZoomLevels.Select(v => v + " px/s").ToArray(), SelectedIndex = 3, Width = 115, MinHeight = 36, FontSize = 14, Padding = new Thickness(4), Margin = new Thickness(0, 0, 8, 8) };
|
|
139
|
-
zoom.SelectionChanged += delegate { if (!updating && zoom.SelectedIndex >= 0) { pixelsPerSecond = ZoomLevels[zoom.SelectedIndex]; DrawCanvas(); EnsurePlayheadVisible(); } }; range.Children.Add(zoom);
|
|
148
|
+
range.Children.Add(WorkspaceUi.Button("Set range", delegate { if (ReadSelection()) { clipSelection = false; selectedIds.Clear(); playhead = from; DrawCanvas(); } }));
|
|
140
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);
|
|
141
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
|
+
|
|
142
164
|
var body = new Grid { Background = WorkspaceUi.Color("#FFFFFF") }; SetRow(body, 2); editorBody.Children.Add(body);
|
|
143
165
|
body.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) }); body.ColumnDefinitions.Add(new ColumnDefinition());
|
|
144
166
|
headers = new StackPanel();
|
|
145
167
|
headerScroll = new ScrollViewer { Content = headers, VerticalScrollBarVisibility = ScrollBarVisibility.Hidden, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled };
|
|
146
168
|
body.Children.Add(headerScroll);
|
|
147
|
-
timeline = new Canvas { Background = WorkspaceUi.Color("#F7FAF9"), ClipToBounds = true, Cursor = Cursors.
|
|
169
|
+
timeline = new Canvas { Background = WorkspaceUi.Color("#F7FAF9"), ClipToBounds = true, Cursor = Cursors.Arrow, Focusable = true };
|
|
148
170
|
playLine = new Line { Stroke = WorkspaceUi.Color("#D0772C"), StrokeThickness = 2, IsHitTestVisible = false };
|
|
149
171
|
selectionOverlay = new Rectangle { Fill = WorkspaceUi.Color("#5596C8"), Opacity = .2, IsHitTestVisible = false };
|
|
150
172
|
selectionStartLine = new Line { Stroke = WorkspaceUi.Color("#277FA5"), StrokeThickness = 1.15, IsHitTestVisible = false };
|
|
@@ -153,18 +175,29 @@ namespace DrumBuddy
|
|
|
153
175
|
scroll = new ScrollViewer { Content = timeline, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, CanContentScroll = false };
|
|
154
176
|
SetColumn(scroll, 1); body.Children.Add(scroll);
|
|
155
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
|
+
};
|
|
156
185
|
headerScroll.PreviewMouseWheel += delegate(object sender, MouseWheelEventArgs e) { scroll.ScrollToVerticalOffset(scroll.VerticalOffset - e.Delta); e.Handled = true; };
|
|
157
186
|
timeline.MouseLeftButtonDown += BeginSelection;
|
|
158
187
|
timeline.MouseMove += delegate(object sender, MouseEventArgs e) {
|
|
159
|
-
|
|
160
|
-
|
|
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);
|
|
161
192
|
};
|
|
162
193
|
timeline.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e) {
|
|
194
|
+
if (clipDrag != ClipDrag.None) { PreviewClipDrag(e.GetPosition(timeline)); FinishClipDrag(); timeline.ReleaseMouseCapture(); e.Handled = true; return; }
|
|
163
195
|
if (gesture.Mode == TimelineDrag.None) return;
|
|
164
|
-
|
|
196
|
+
EndPointer(e.GetPosition(timeline).X); timeline.ReleaseMouseCapture(); e.Handled = true;
|
|
165
197
|
};
|
|
166
|
-
timeline.LostMouseCapture += delegate { if (gesture.Mode != TimelineDrag.None) { gesture.Finish(); ApplyGesture(); } };
|
|
198
|
+
timeline.LostMouseCapture += delegate { if (clipDrag != ClipDrag.None) CancelClipDrag(); if (gesture.Mode != TimelineDrag.None) { gesture.Finish(); ApplyGesture(); RefreshClipBounds(); DrawCanvas(); } };
|
|
167
199
|
|
|
200
|
+
InitializeClipTools(editing);
|
|
168
201
|
var bottom = new StackPanel { Margin = new Thickness(0, 12, 0, 0) }; SetRow(bottom, 3); editorBody.Children.Add(bottom);
|
|
169
202
|
var transport = new WrapPanel(); bottom.Children.Add(transport);
|
|
170
203
|
play = WorkspaceUi.Button("Play", delegate { PlayPause(); }); play.MinWidth = 90; transport.Children.Add(play);
|
|
@@ -182,9 +215,11 @@ namespace DrumBuddy
|
|
|
182
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);
|
|
183
216
|
selectionOnly = new CheckBox { Content = "Selection only", FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 15, 0) }; exports.Children.Add(selectionOnly);
|
|
184
217
|
AddFileButton(exports, "Export mix…", async delegate { await ExportMix(); });
|
|
218
|
+
ArrangeCompactChrome(top, editing, range, viewControls, zoomTools, bottom, transport, exports);
|
|
185
219
|
timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(80) };
|
|
186
|
-
timer.Tick += delegate { if (output.Playing || output.Paused) { playhead = (long)Math.Round(output.Position * Audio.Rate); UpdateTransport(); UpdatePlayLine(); if (output.Playing) EnsurePlayheadVisible(); } }; timer.Start();
|
|
187
|
-
|
|
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();
|
|
188
223
|
recordingPane = new DockablePane(editorBody, null, "Record options", false, false, 260) { MinimumMainHeight = 300 }; Children.Add(recordingPane);
|
|
189
224
|
editorBody.SizeChanged += delegate { UpdateCompactLayout(); };
|
|
190
225
|
}
|
|
@@ -193,11 +228,14 @@ namespace DrumBuddy
|
|
|
193
228
|
{
|
|
194
229
|
double width = editorBody.ActualWidth > 0 ? editorBody.ActualWidth : 1280;
|
|
195
230
|
bool compact = width < 1000 || (editorBody.ActualHeight > 0 && editorBody.ActualHeight < 480);
|
|
196
|
-
compactOptionsButton.Visibility =
|
|
197
|
-
outputPicker.Visibility =
|
|
231
|
+
compactOptionsButton.Visibility = Visibility.Visible;
|
|
232
|
+
outputPicker.Visibility = separate.Visibility = Visibility.Visible;
|
|
233
|
+
exportDetails.Visibility = compactOptions ? Visibility.Visible : Visibility.Collapsed;
|
|
198
234
|
gridTriplet.Visibility = rulerMode.Visibility = meterText.Visibility = compact ? Visibility.Collapsed : Visibility.Visible;
|
|
199
|
-
title.FontSize =
|
|
200
|
-
|
|
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;
|
|
201
239
|
selectionText.TextTrimming = TextTrimming.CharacterEllipsis;
|
|
202
240
|
}
|
|
203
241
|
|
|
@@ -246,13 +284,46 @@ namespace DrumBuddy
|
|
|
246
284
|
private long FrameAt(double x) { return Math.Max(0, Math.Min(EditorSession.MaxFrame, (long)Math.Round(x / pixelsPerSecond * Audio.Rate))); }
|
|
247
285
|
private long PointerFrame(double x)
|
|
248
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); }
|
|
249
|
-
private void ApplyGesture() { playhead = gesture.Playhead; from = gesture.From; to = gesture.To; UpdateSelectionText(); UpdateSelectionOverlay(); UpdatePlayLine(); UpdateTransport(); }
|
|
287
|
+
private void ApplyGesture() { playhead = gesture.Playhead; from = gesture.From; to = gesture.To; UpdateSelectionText(); UpdateSelectionOverlay(); RefreshRegionHighlights(); UpdatePlayLine(); UpdateTransport(); }
|
|
250
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
|
+
}
|
|
251
319
|
private void UpdateSelectionText()
|
|
252
320
|
{
|
|
253
321
|
selectionFrom.Text = (from / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture);
|
|
254
322
|
selectionTo.Text = (to / (double)Audio.Rate).ToString("0.00000", CultureInfo.InvariantCulture);
|
|
255
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;
|
|
256
327
|
}
|
|
257
328
|
private bool ReadSelection()
|
|
258
329
|
{
|
|
@@ -265,24 +336,75 @@ namespace DrumBuddy
|
|
|
265
336
|
}
|
|
266
337
|
private void BeginSelection(object sender, MouseButtonEventArgs e)
|
|
267
338
|
{
|
|
268
|
-
if (IsBusy) return; Stop(); timeline.Focus();
|
|
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;
|
|
269
348
|
if (point.Y < RulerHeight) {
|
|
270
349
|
var mode = TimelineGesture.RulerHit(point.X, point.Y, X(from), X(to), to > from);
|
|
271
|
-
|
|
350
|
+
if (mode != TimelineDrag.Playhead) { clipSelection = false; selectedIds.Clear(); }
|
|
351
|
+
gesture.Begin(mode, frame, playhead, from, to); ApplyGesture(); return;
|
|
272
352
|
}
|
|
273
353
|
track = Math.Max(0, Math.Min(15, (int)((point.Y - RulerHeight) / RowHeight)));
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(); }
|
|
278
368
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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);
|
|
283
376
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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();
|
|
286
408
|
}
|
|
287
409
|
private void BuildHeaders()
|
|
288
410
|
{
|
|
@@ -309,34 +431,30 @@ namespace DrumBuddy
|
|
|
309
431
|
}
|
|
310
432
|
private void DrawCanvas()
|
|
311
433
|
{
|
|
312
|
-
if (timeline == null) return;
|
|
313
|
-
timeline.Children.Clear(); timeline.Height = RulerHeight + RowHeight * 16;
|
|
434
|
+
if (timeline == null || disposed) return;
|
|
435
|
+
timeline.Children.Clear(); regionVisuals.Clear(); timeline.Height = RulerHeight + RowHeight * 16;
|
|
314
436
|
timeline.Width = Math.Max(Math.Max(900, scroll.ViewportWidth), X(Math.Max(session.End, Math.Max(to, playhead)) + Audio.Rate * 15L));
|
|
315
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));
|
|
316
439
|
for (int i = 0; i < 16; i++) {
|
|
317
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 };
|
|
318
441
|
Canvas.SetLeft(row, left); Canvas.SetTop(row, RulerHeight + i * RowHeight); timeline.Children.Add(row);
|
|
319
442
|
}
|
|
320
443
|
foreach (var region in session.Regions.Where(r => X(r.End) >= left && X(r.Start) <= right)) DrawRegion(region, left, right);
|
|
321
444
|
RefreshGridToolbar();
|
|
445
|
+
RefreshZoomControls();
|
|
446
|
+
bool visibleLabel = false;
|
|
322
447
|
if (session.Grid.MusicalRuler) foreach (var tick in TempoGrid.VisibleTicks(session.Grid, left, right - left, pixelsPerSecond)) {
|
|
323
448
|
double x = X(tick.Frame);
|
|
324
449
|
timeline.Children.Add(new Line { X1 = x, X2 = x, Y1 = tick.Kind == 0 ? RulerHeight : 0, Y2 = timeline.Height,
|
|
325
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 });
|
|
326
|
-
if (tick.Label != null) { var label = WorkspaceUi.Text(tick.Label, 11); label.IsHitTestVisible = false; Canvas.SetLeft(label, x + 4); Canvas.SetTop(label, 3); timeline.Children.Add(label); }
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
double spacing = pixelsPerSecond >= 80 ? 1 : pixelsPerSecond >= 20 ? 5 : 10;
|
|
330
|
-
for (double second = Math.Floor(left / pixelsPerSecond / spacing) * spacing; second * pixelsPerSecond < right; second += spacing) {
|
|
331
|
-
double x = second * pixelsPerSecond;
|
|
332
|
-
var line = new Line { X1 = x, X2 = x, Y1 = 0, Y2 = timeline.Height, Stroke = WorkspaceUi.Color("#DAE5E0"), StrokeThickness = 1, IsHitTestVisible = false }; timeline.Children.Add(line);
|
|
333
|
-
var label = WorkspaceUi.Text(WorkspaceUi.Clock(second), 11); label.IsHitTestVisible = false; Canvas.SetLeft(label, x + 4); Canvas.SetTop(label, 3); timeline.Children.Add(label);
|
|
334
|
-
}
|
|
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); }
|
|
335
452
|
}
|
|
453
|
+
if (!session.Grid.MusicalRuler || !visibleLabel) DrawTimeRuler(left, right);
|
|
336
454
|
timeline.Children.Add(selectionOverlay); timeline.Children.Add(selectionStartLine); timeline.Children.Add(selectionEndLine); timeline.Children.Add(selectionStartHandle); timeline.Children.Add(selectionEndHandle); UpdateSelectionOverlay();
|
|
337
455
|
playLine.Y1 = 0; playLine.Y2 = timeline.Height; timeline.Children.Add(playLine); UpdatePlayLine(); UpdateTransport();
|
|
338
456
|
undoButton.IsEnabled = !IsBusy && undo.Count > 0; redoButton.IsEnabled = !IsBusy && redo.Count > 0;
|
|
339
|
-
title.Text =
|
|
457
|
+
title.Text = session.Name + (dirty ? " *" : "");
|
|
340
458
|
}
|
|
341
459
|
private void UpdateSelectionOverlay()
|
|
342
460
|
{
|
|
@@ -345,10 +463,21 @@ namespace DrumBuddy
|
|
|
345
463
|
foreach (var item in new UIElement[] { selectionOverlay, selectionStartLine, selectionEndLine, selectionStartHandle, selectionEndHandle }) item.Visibility = visibility;
|
|
346
464
|
if (to <= from) return;
|
|
347
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;
|
|
348
467
|
selectionOverlay.Width = Math.Max(0, b - a); selectionOverlay.Height = AllTracks ? RowHeight * 16 : RowHeight;
|
|
349
468
|
Canvas.SetLeft(selectionOverlay, a); Canvas.SetTop(selectionOverlay, RulerHeight + (AllTracks ? 0 : track * RowHeight));
|
|
350
469
|
UpdateRangeStick(selectionStartLine, selectionStartHandle, X(from), true); UpdateRangeStick(selectionEndLine, selectionEndHandle, X(to), false);
|
|
351
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
|
+
}
|
|
352
481
|
private void UpdateRangeStick(Line line, Polygon handle, double x, bool start)
|
|
353
482
|
{
|
|
354
483
|
line.X1 = line.X2 = x; line.Y1 = 14; line.Y2 = timeline.Height;
|
|
@@ -357,26 +486,34 @@ namespace DrumBuddy
|
|
|
357
486
|
private void DrawRegion(TimelineRegion region, double left, double right)
|
|
358
487
|
{
|
|
359
488
|
double x = Math.Max(left, X(region.Start)), end = Math.Min(right, X(region.End)), y = RulerHeight + region.Track * RowHeight + 3;
|
|
360
|
-
double width = Math.Max(2, end - x); bool muted = session.Tracks[region.Track].Mute;
|
|
361
|
-
var rectangle = new Rectangle { Width = width, Height = RowHeight - 6, RadiusX = 5, RadiusY = 5, Fill = WorkspaceUi.Color(muted ? "#CBD8D4" : "#B9DDD2"), Stroke = WorkspaceUi.Color("#5E9D89"), StrokeThickness = 1, IsHitTestVisible = false };
|
|
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);
|
|
362
492
|
Canvas.SetLeft(rectangle, x); Canvas.SetTop(rectangle, y); timeline.Children.Add(rectangle);
|
|
493
|
+
regionVisuals.Add(new RegionVisual { Region = region, Body = rectangle, Glow = glow });
|
|
363
494
|
Clip clip;
|
|
364
495
|
if (assets.TryGetValue(region.Source, out clip) && clip.Waveform != null && width > 2) {
|
|
365
|
-
|
|
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" };
|
|
366
501
|
double sourceFrames = Math.Max(1, clip.Duration * Audio.Rate), scale = Math.Max(1, clip.Peak);
|
|
367
502
|
for (int side = 0; side < 2; side++) for (int p = 0; p < points; p++) {
|
|
368
503
|
int j = side == 0 ? p : points - p - 1; double px = x + j * width / (points - 1);
|
|
369
504
|
double sourceFrame = region.SourceStart + (px - X(region.Start)) / pixelsPerSecond * Audio.Rate;
|
|
370
505
|
int bin = Math.Max(0, Math.Min(clip.Waveform.Length - 1, (int)(sourceFrame / sourceFrames * clip.Waveform.Length)));
|
|
371
|
-
double
|
|
372
|
-
|
|
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));
|
|
373
509
|
}
|
|
374
510
|
timeline.Children.Add(wave);
|
|
375
511
|
}
|
|
376
512
|
if (width > 45) {
|
|
377
|
-
var label = WorkspaceUi.Text(region.Name, 12); label.MaxWidth = Math.Max(1, width - 10); label.TextWrapping = TextWrapping.NoWrap; label.TextTrimming = TextTrimming.CharacterEllipsis; label.IsHitTestVisible = false;
|
|
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;
|
|
378
514
|
Canvas.SetLeft(label, x + 5); Canvas.SetTop(label, y + 3); timeline.Children.Add(label);
|
|
379
515
|
}
|
|
516
|
+
DrawClipFades(region, left, right, selected);
|
|
380
517
|
}
|
|
381
518
|
private void UpdatePlayLine() { playLine.X1 = playLine.X2 = X(playhead); }
|
|
382
519
|
private void UpdateTransport()
|
|
@@ -387,29 +524,41 @@ namespace DrumBuddy
|
|
|
387
524
|
private void Change(Func<bool> action, string message)
|
|
388
525
|
{
|
|
389
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(); };
|
|
390
529
|
try {
|
|
391
|
-
if (!action()) { status("No audio section was changed. Select a clip or a time range first."); return; }
|
|
392
|
-
session.Validate(); undo.Push(before); redo.Clear(); dirty = true; BuildHeaders(); DrawCanvas(); status(message);
|
|
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);
|
|
393
532
|
}
|
|
394
|
-
catch (Exception ex) {
|
|
533
|
+
catch (Exception ex) { restore(); status(ex.Message); }
|
|
395
534
|
}
|
|
396
535
|
private void Undo(bool forward)
|
|
397
536
|
{
|
|
398
537
|
if (IsBusy) return; var source = forward ? redo : undo; var target = forward ? undo : redo;
|
|
399
|
-
if (source.Count == 0) return; Stop(); target.Push(session.Copy()); session = source.Pop(); dirty = true; BuildHeaders(); DrawCanvas();
|
|
538
|
+
if (source.Count == 0) return; Stop(); target.Push(session.Copy()); session = source.Pop(); dirty = true; RefreshClipBounds(); BuildHeaders(); DrawCanvas();
|
|
400
539
|
}
|
|
401
|
-
private void Split() { Change(() => TimelineEdits.Split(session, track, AllTracks, playhead), "Split at the playhead."); }
|
|
540
|
+
private void Split() { Change(() => TimelineEdits.Split(session, track, AllTracks, playhead, clipSelection ? selectedIds : null), "Split at the playhead."); }
|
|
402
541
|
private void Copy(bool cut)
|
|
403
542
|
{
|
|
404
|
-
|
|
405
|
-
|
|
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);
|
|
406
547
|
if (clipboard.Regions.Count == 0) { status("There is no audio in this selection."); return; }
|
|
407
|
-
if (cut) Change(() => TimelineEdits.Delete(session, track, AllTracks, from, to), "Selection cut. Paste inserts it at the playhead.");
|
|
548
|
+
if (cut) Change(() => TimelineEdits.Delete(session, track, AllTracks, from, to, clipSelection ? selectedIds : null), "Selection cut. Paste inserts it at the playhead.");
|
|
408
549
|
else status("Copied " + clipboard.Regions.Count + " audio section(s). Click the ruler for the paste position.");
|
|
409
550
|
}
|
|
410
551
|
private void DeleteRange()
|
|
411
|
-
{ if (ReadSelection() && to > from) Change(() => TimelineEdits.Delete(session, track, AllTracks, from, to), "Selected audio removed; the timeline gap is kept."); }
|
|
412
|
-
private void Paste()
|
|
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
|
+
}
|
|
413
562
|
|
|
414
563
|
private async Task ChooseAudio()
|
|
415
564
|
{
|
|
@@ -442,7 +591,7 @@ namespace DrumBuddy
|
|
|
442
591
|
}
|
|
443
592
|
catch (OperationCanceledException) { status("Import cancelled. Your existing timeline is kept."); }
|
|
444
593
|
catch (Exception ex) { status("Could not add audio: " + ex.Message); }
|
|
445
|
-
finally { EndWork(); BuildHeaders(); DrawCanvas(); }
|
|
594
|
+
finally { EndWork(); BuildHeaders(); DrawCanvas(); timeline.Focus(); }
|
|
446
595
|
}
|
|
447
596
|
private void BeginWork()
|
|
448
597
|
{ operation = new CancellationTokenSource(); foreach (var button in fileButtons) button.IsEnabled = false; play.IsEnabled = false; cancel.Visibility = Visibility.Visible; RefreshGridToolbar(); }
|
|
@@ -450,7 +599,7 @@ namespace DrumBuddy
|
|
|
450
599
|
{ operation.Dispose(); operation = null; foreach (var button in fileButtons) button.IsEnabled = true; play.IsEnabled = true; cancel.Visibility = Visibility.Collapsed; RefreshGridToolbar(); }
|
|
451
600
|
private void NewSession()
|
|
452
601
|
{
|
|
453
|
-
if (IsBusy || !ConfirmClose()) return; Stop(); session = new EditorSession { Grid = InterfacePreferences.Current.Grid.Copy() }; sessionPath = null; assets.Clear(); undo.Clear(); redo.Clear(); dirty = false; from = to = playhead = 0; UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
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();
|
|
454
603
|
}
|
|
455
604
|
public bool SaveSession(bool saveAs)
|
|
456
605
|
{
|
|
@@ -479,7 +628,7 @@ namespace DrumBuddy
|
|
|
479
628
|
}
|
|
480
629
|
using (var verify = new TimelineMix(loaded, decoded)) { }
|
|
481
630
|
operation.Token.ThrowIfCancellationRequested(); session = loaded; assets.Clear(); foreach (var pair in decoded) assets.Add(pair.Key, pair.Value);
|
|
482
|
-
sessionPath = dialog.FileName; undo.Clear(); redo.Clear(); dirty = false; from = to = playhead = 0; UpdateSelectionText(); status("Editor session opened.");
|
|
631
|
+
sessionPath = dialog.FileName; undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); status("Editor session opened.");
|
|
483
632
|
}
|
|
484
633
|
catch (OperationCanceledException) { status("Opening cancelled. Your current timeline is kept."); }
|
|
485
634
|
catch (Exception ex) { status("Could not open this session: " + ex.Message); }
|
|
@@ -563,12 +712,15 @@ namespace DrumBuddy
|
|
|
563
712
|
}
|
|
564
713
|
public async Task<bool> ExecuteAction(string action)
|
|
565
714
|
{
|
|
566
|
-
if (
|
|
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;
|
|
567
718
|
switch (action) {
|
|
568
719
|
case "play": PlayPause(); break;
|
|
569
720
|
case "stop": Stop(); UpdateTransport(); break;
|
|
570
721
|
case "split": Split(); break;
|
|
571
|
-
case "
|
|
722
|
+
case "selectTool": SetPointerTool("select"); break;
|
|
723
|
+
case "scissors": sliceCursor = !sliceCursor; timeline.Cursor = sliceCursor ? Cursors.Cross : Cursors.Arrow; break;
|
|
572
724
|
case "copy": Copy(false); break;
|
|
573
725
|
case "cut": Copy(true); break;
|
|
574
726
|
case "paste": Paste(); break;
|
|
@@ -583,14 +735,19 @@ namespace DrumBuddy
|
|
|
583
735
|
case "export": await ExportMix(); break;
|
|
584
736
|
case "start": Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
585
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;
|
|
586
742
|
default: return false;
|
|
587
743
|
}
|
|
588
744
|
return true;
|
|
589
745
|
}
|
|
590
746
|
public bool HandleKey(KeyEventArgs e)
|
|
591
747
|
{
|
|
748
|
+
if (e.Key == Key.Escape && clipDrag != ClipDrag.None) { CancelClipDrag(); timeline.ReleaseMouseCapture(); return true; }
|
|
592
749
|
if (Keyboard.FocusedElement is TextBox || Keyboard.FocusedElement is ComboBox) return false;
|
|
593
|
-
if (e.Key == Key.Escape && gesture.Mode != TimelineDrag.None) { gesture.Cancel(); ApplyGesture(); timeline.ReleaseMouseCapture(); return true; }
|
|
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; }
|
|
594
751
|
string action = InterfacePreferences.Resolve(e.Key == Key.System ? e.SystemKey : e.Key, Keyboard.Modifiers);
|
|
595
752
|
if (action == null || action == "record") return false;
|
|
596
753
|
var pending = ExecuteAction(action);
|
|
@@ -605,11 +762,26 @@ namespace DrumBuddy
|
|
|
605
762
|
return choice == MessageBoxResult.No || (choice == MessageBoxResult.Yes && SaveSession(false));
|
|
606
763
|
}
|
|
607
764
|
internal void SetForTest(EditorSession state, IDictionary<string, Clip> decoded)
|
|
608
|
-
{ session = state.Copy(); assets.Clear(); foreach (var p in decoded) assets.Add(p.Key, p.Value); undo.Clear(); redo.Clear(); dirty = false; from = to = playhead = 0; UpdateSelectionText(); BuildHeaders(); DrawCanvas(); }
|
|
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(); }
|
|
609
766
|
internal EditorSession SessionForTest { get { return session.Copy(); } }
|
|
610
|
-
internal void
|
|
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(); }
|
|
611
770
|
internal void PlayheadForTest(long value) { playhead = value; DrawCanvas(); }
|
|
612
|
-
internal void ZoomForTest(double value) {
|
|
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; } }
|
|
613
785
|
internal bool CheckUndoRedoForTest()
|
|
614
786
|
{
|
|
615
787
|
var original = session.Copy();
|
|
@@ -620,7 +792,7 @@ namespace DrumBuddy
|
|
|
620
792
|
return restored && repeated && dirty;
|
|
621
793
|
}
|
|
622
794
|
public void Dispose()
|
|
623
|
-
{ if (disposed) return; disposed = true; if (operation != null) operation.Cancel(); timer.Stop(); Stop(); output.Dispose(); }
|
|
795
|
+
{ if (disposed) return; disposed = true; waveformDetail.Dispose(); if (operation != null) operation.Cancel(); timer.Stop(); Stop(); output.Dispose(); }
|
|
624
796
|
public void CancelWork() { if (operation != null) operation.Cancel(); }
|
|
625
797
|
}
|
|
626
798
|
}
|
|
Binary file
|