@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,227 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Linq;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Windows;
|
|
5
|
+
using System.Windows.Controls;
|
|
6
|
+
using System.Windows.Input;
|
|
7
|
+
using System.Windows.Media;
|
|
8
|
+
using System.Windows.Shapes;
|
|
9
|
+
using System.Windows.Threading;
|
|
10
|
+
|
|
11
|
+
namespace DrumBuddy
|
|
12
|
+
{
|
|
13
|
+
public sealed partial class EditorView
|
|
14
|
+
{
|
|
15
|
+
private ClipDrag clipDrag;
|
|
16
|
+
private EditorSession clipBefore;
|
|
17
|
+
private Dictionary<string, TimelineRegion> clipOriginal;
|
|
18
|
+
private string dragId, pointerTool = "select";
|
|
19
|
+
private Point dragOrigin;
|
|
20
|
+
private long dragOriginFrame;
|
|
21
|
+
private int dragOriginTrack;
|
|
22
|
+
private bool clipChanged, renderPending, changingTool;
|
|
23
|
+
private ComboBox toolPicker;
|
|
24
|
+
|
|
25
|
+
private void InitializeClipTools(Panel toolbar)
|
|
26
|
+
{
|
|
27
|
+
toolPicker = new ComboBox { ItemsSource = new[] { "Select (V)", "Slip (Y)", "Fade (F)" }, SelectedIndex = 0, Width = 112, MinHeight = 30, Margin = new Thickness(0, 0, 6, 6) };
|
|
28
|
+
toolPicker.SelectionChanged += delegate { if (!changingTool) SetPointerTool(toolPicker.SelectedIndex == 1 ? "slip" : toolPicker.SelectedIndex == 2 ? "fade" : "select"); }; toolbar.Children.Add(toolPicker);
|
|
29
|
+
var menu = new ContextMenu(); timeline.ContextMenu = menu;
|
|
30
|
+
string[,] commands = {
|
|
31
|
+
{ "Selection cursor", "selectTool" }, { "Slip source contents", "slipTool" }, { "Fade tool", "fadeTool" },
|
|
32
|
+
{ "Split at playhead", "split" }, { "Duplicate", "duplicate" }, { "Repeat…", "repeatClips" },
|
|
33
|
+
{ "Mute / unmute clips", "muteClips" }, { "Crossfade selected clips", "crossfade" }, { "Clear clip fades", "clearFades" },
|
|
34
|
+
{ "Cut", "cut" }, { "Copy", "copy" }, { "Paste", "paste" }, { "Delete", "delete" }
|
|
35
|
+
};
|
|
36
|
+
for (int i = 0; i < commands.GetLength(0); i++) {
|
|
37
|
+
string action = commands[i, 1]; var item = new MenuItem { Header = commands[i, 0], Tag = action };
|
|
38
|
+
item.Click += async delegate { await ExecuteAction(action); }; menu.Items.Add(item);
|
|
39
|
+
}
|
|
40
|
+
timeline.PreviewMouseRightButtonDown += delegate(object sender, MouseButtonEventArgs e) {
|
|
41
|
+
SelectContextTarget(e.GetPosition(timeline));
|
|
42
|
+
};
|
|
43
|
+
timeline.ContextMenuOpening += delegate(object sender, ContextMenuEventArgs e) {
|
|
44
|
+
if (IsBusy) { e.Handled = true; return; }
|
|
45
|
+
foreach (MenuItem item in menu.Items) { string key = InterfacePreferences.Gesture((string)item.Tag); item.InputGestureText = key == "Unassigned" ? "" : key; }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
private void SelectContextTarget(Point point)
|
|
49
|
+
{
|
|
50
|
+
if (IsBusy) return; var region = RegionAt(point); long frame = FrameAt(point.X);
|
|
51
|
+
bool inRange = region != null && !clipSelection && to > from && frame >= from && frame < to && (AllTracks || region.Track == track);
|
|
52
|
+
if (region != null && !inRange && (!clipSelection || !selectedIds.Contains(region.Id))) { track = region.Track; SelectRegion(region, false); BuildHeaders(); DrawCanvas(); }
|
|
53
|
+
timeline.Focus();
|
|
54
|
+
}
|
|
55
|
+
private void SetPointerTool(string tool)
|
|
56
|
+
{
|
|
57
|
+
pointerTool = tool; sliceCursor = false;
|
|
58
|
+
changingTool = true; toolPicker.SelectedIndex = tool == "slip" ? 1 : tool == "fade" ? 2 : 0; changingTool = false;
|
|
59
|
+
timeline.Cursor = tool == "select" ? Cursors.Arrow : Cursors.SizeWE; timeline.Focus(); UpdateSelectionText();
|
|
60
|
+
}
|
|
61
|
+
private TimelineRegion RegionAt(Point point)
|
|
62
|
+
{
|
|
63
|
+
if (point.Y < RulerHeight) return null;
|
|
64
|
+
int row = Math.Max(0, Math.Min(15, (int)((point.Y - RulerHeight) / RowHeight))); long frame = FrameAt(point.X);
|
|
65
|
+
var hit = session.Regions.LastOrDefault(r => r.Track == row && r.Start <= frame && r.End > frame);
|
|
66
|
+
if (hit == null || (pointerTool == "select" && clipSelection && point.Y - RulerHeight - row * RowHeight >= 13)) {
|
|
67
|
+
var edge = session.Regions.Where(r => r.Track == row && (hit == null || selectedIds.Contains(r.Id))).OrderBy(r => Math.Min(Math.Abs(point.X - X(r.Start)), Math.Abs(point.X - X(r.End)))).FirstOrDefault();
|
|
68
|
+
if (edge != null && Math.Min(Math.Abs(point.X - X(edge.Start)), Math.Abs(point.X - X(edge.End))) <= 6) return edge;
|
|
69
|
+
}
|
|
70
|
+
return hit;
|
|
71
|
+
}
|
|
72
|
+
private ClipDrag ClipToolAt(TimelineRegion region, Point point)
|
|
73
|
+
{
|
|
74
|
+
if (pointerTool == "slip") return ClipDrag.Slip;
|
|
75
|
+
if (pointerTool == "fade") return point.X < (X(region.Start) + X(region.End)) / 2 ? ClipDrag.FadeIn : ClipDrag.FadeOut;
|
|
76
|
+
double y = point.Y - RulerHeight - region.Track * RowHeight;
|
|
77
|
+
if (RegionSelected(region) && y < 13) {
|
|
78
|
+
if (Math.Abs(point.X - FadeX(region, true)) <= 7) return ClipDrag.FadeIn;
|
|
79
|
+
if (Math.Abs(point.X - FadeX(region, false)) <= 7) return ClipDrag.FadeOut;
|
|
80
|
+
}
|
|
81
|
+
if (y >= 13 && Math.Abs(point.X - X(region.Start)) <= 6) return ClipDrag.TrimLeft;
|
|
82
|
+
if (y >= 13 && Math.Abs(point.X - X(region.End)) <= 6) return ClipDrag.TrimRight;
|
|
83
|
+
return y < 19 ? ClipDrag.Move : ClipDrag.None;
|
|
84
|
+
}
|
|
85
|
+
private void UpdateClipCursor(Point point)
|
|
86
|
+
{
|
|
87
|
+
if (sliceCursor) { timeline.Cursor = Cursors.Cross; return; }
|
|
88
|
+
var region = RegionAt(point); var tool = region == null ? ClipDrag.None : ClipToolAt(region, point);
|
|
89
|
+
timeline.Cursor = tool == ClipDrag.Move ? Cursors.SizeAll : tool == ClipDrag.None ? Cursors.Arrow : Cursors.SizeWE;
|
|
90
|
+
}
|
|
91
|
+
private void BeginClipDrag(ClipDrag tool, TimelineRegion region, Point point)
|
|
92
|
+
{
|
|
93
|
+
if (tool != ClipDrag.Move || !clipSelection || !selectedIds.Contains(region.Id)) SelectRegion(region, false);
|
|
94
|
+
clipBefore = session.Copy(); clipOriginal = session.Regions.Where(r => selectedIds.Contains(r.Id)).ToDictionary(r => r.Id, r => r.Copy());
|
|
95
|
+
clipDrag = tool; dragId = region.Id; dragOrigin = point; dragOriginFrame = FrameAt(point.X); dragOriginTrack = region.Track; clipChanged = false;
|
|
96
|
+
playhead = PointerFrame(point.X); RefreshGridToolbar(); RefreshClipBounds(); BuildHeaders(); DrawCanvas();
|
|
97
|
+
}
|
|
98
|
+
private long SourceFrames(TimelineRegion region)
|
|
99
|
+
{
|
|
100
|
+
Clip clip; if (!assets.TryGetValue(region.Source, out clip)) throw new System.IO.IOException("The clip's source is missing. Re-import it before editing.");
|
|
101
|
+
return (long)Math.Round(clip.Duration * Audio.Rate);
|
|
102
|
+
}
|
|
103
|
+
private long ToolFrame(double x)
|
|
104
|
+
{ return TimelineGesture.FrameAt(x, pixelsPerSecond, session.Regions.Where(r => !selectedIds.Contains(r.Id)).SelectMany(r => new[] { r.Start, r.End }), (Keyboard.Modifiers & ModifierKeys.Alt) != 0, session.Grid, InterfacePreferences.Current.SnapToGrid, InterfacePreferences.Current.SnapToEdges); }
|
|
105
|
+
private void PreviewClipDrag(Point point)
|
|
106
|
+
{
|
|
107
|
+
if (clipDrag == ClipDrag.None) return;
|
|
108
|
+
try {
|
|
109
|
+
foreach (var region in session.Regions) {
|
|
110
|
+
TimelineRegion original; if (!clipOriginal.TryGetValue(region.Id, out original)) continue;
|
|
111
|
+
region.Start = original.Start; region.Track = original.Track; region.SourceStart = original.SourceStart; region.Length = original.Length;
|
|
112
|
+
region.FadeIn = original.FadeIn; region.FadeOut = original.FadeOut; region.FadeInOffset = original.FadeInOffset; region.FadeOutOffset = original.FadeOutOffset;
|
|
113
|
+
}
|
|
114
|
+
var target = session.Regions.First(r => r.Id == dragId);
|
|
115
|
+
if (clipDrag == ClipDrag.Move) {
|
|
116
|
+
long first = clipOriginal.Values.Min(r => r.Start);
|
|
117
|
+
double anchor = (dragOriginFrame - first) / (double)Audio.Rate * pixelsPerSecond;
|
|
118
|
+
long delta = ToolFrame(point.X - anchor) - first; int row = (int)Math.Floor((point.Y - RulerHeight) / RowHeight);
|
|
119
|
+
EditorTools.Move(session, selectedIds, delta, row - dragOriginTrack); track = target.Track;
|
|
120
|
+
}
|
|
121
|
+
else if (clipDrag == ClipDrag.TrimLeft || clipDrag == ClipDrag.TrimRight) EditorTools.Trim(target, clipDrag == ClipDrag.TrimLeft, ToolFrame(point.X), SourceFrames(target));
|
|
122
|
+
else if (clipDrag == ClipDrag.Slip) {
|
|
123
|
+
long delta = (long)Math.Round((dragOrigin.X - point.X) / pixelsPerSecond * Audio.Rate);
|
|
124
|
+
if (InterfacePreferences.Current.SnapToGrid && (Keyboard.Modifiers & ModifierKeys.Alt) == 0) { double step = TempoGrid.StepSeconds(session.Grid) * Audio.Rate; delta = (long)Math.Round(Math.Round(delta / step) * step); }
|
|
125
|
+
EditorTools.Slip(target, delta, SourceFrames(target));
|
|
126
|
+
}
|
|
127
|
+
else EditorTools.Fade(target, clipDrag == ClipDrag.FadeIn, clipDrag == ClipDrag.FadeIn ? ToolFrame(point.X) - target.Start : target.End - ToolFrame(point.X));
|
|
128
|
+
clipChanged = session.Regions.Where(r => clipOriginal.ContainsKey(r.Id)).Any(r => {
|
|
129
|
+
var old = clipOriginal[r.Id]; return r.Start != old.Start || r.Track != old.Track || r.SourceStart != old.SourceStart || r.Length != old.Length || r.FadeIn != old.FadeIn || r.FadeOut != old.FadeOut || r.FadeInOffset != old.FadeInOffset || r.FadeOutOffset != old.FadeOutOffset;
|
|
130
|
+
});
|
|
131
|
+
RefreshClipBounds(); RequestClipRender();
|
|
132
|
+
}
|
|
133
|
+
catch (Exception ex) { CancelClipDrag(); status("Could not edit this clip: " + ex.Message); }
|
|
134
|
+
}
|
|
135
|
+
private void RequestClipRender()
|
|
136
|
+
{
|
|
137
|
+
if (renderPending) return; renderPending = true;
|
|
138
|
+
Dispatcher.BeginInvoke(DispatcherPriority.Render, (Action)delegate { renderPending = false; if (!disposed) DrawCanvas(); });
|
|
139
|
+
}
|
|
140
|
+
private void FinishClipDrag()
|
|
141
|
+
{
|
|
142
|
+
if (clipDrag == ClipDrag.None) return; var action = clipDrag;
|
|
143
|
+
try { session.Validate(); if (clipChanged) { undo.Push(clipBefore); redo.Clear(); dirty = true; } }
|
|
144
|
+
catch (Exception ex) { session = clipBefore; clipChanged = false; status("Could not finish the clip edit: " + ex.Message); }
|
|
145
|
+
clipDrag = ClipDrag.None; clipBefore = null; clipOriginal = null; RefreshClipBounds(); RefreshGridToolbar(); BuildHeaders(); DrawCanvas();
|
|
146
|
+
if (clipChanged) status(action == ClipDrag.Move ? "Clips moved." : action == ClipDrag.Slip ? "Source contents slipped." : action == ClipDrag.TrimLeft || action == ClipDrag.TrimRight ? "Clip trimmed." : "Clip fade updated.");
|
|
147
|
+
}
|
|
148
|
+
private void CancelClipDrag()
|
|
149
|
+
{
|
|
150
|
+
if (clipDrag == ClipDrag.None) return; session = clipBefore; clipDrag = ClipDrag.None; clipBefore = null; clipOriginal = null;
|
|
151
|
+
selectedIds.Clear(); if (previousIds != null) foreach (string id in previousIds) selectedIds.Add(id); clipSelection = previousClipSelection; track = previousTrack;
|
|
152
|
+
from = previousFrom; to = previousTo; playhead = previousPlayhead;
|
|
153
|
+
RefreshClipBounds(); UpdateSelectionText(); RefreshGridToolbar(); BuildHeaders(); DrawCanvas();
|
|
154
|
+
}
|
|
155
|
+
private double FadeX(TimelineRegion region, bool incoming)
|
|
156
|
+
{
|
|
157
|
+
long frames = incoming ? Math.Max(0, region.FadeIn - region.FadeInOffset) : Math.Max(0, region.FadeOut - region.FadeOutOffset);
|
|
158
|
+
return X(incoming ? region.Start + Math.Min(region.Length, frames) : region.End - Math.Min(region.Length, frames));
|
|
159
|
+
}
|
|
160
|
+
private void DrawClipFades(TimelineRegion region, double left, double right, bool selected)
|
|
161
|
+
{
|
|
162
|
+
double y = RulerHeight + region.Track * RowHeight + 4;
|
|
163
|
+
if (region.FadeIn > 0 || region.FadeOut > 0) {
|
|
164
|
+
var line = new Polyline { Stroke = WorkspaceUi.Color("#7359C7"), StrokeThickness = 1.3, IsHitTestVisible = false };
|
|
165
|
+
var positions = new[] { Math.Max(left, X(region.Start)), Math.Min(right, X(region.End)), FadeX(region, true), FadeX(region, false) }.Where(x => x >= left && x <= right && x >= X(region.Start) && x <= X(region.End)).Distinct().OrderBy(x => x);
|
|
166
|
+
foreach (double x in positions) line.Points.Add(new Point(x, y + 4 + (1 - EditorTools.Envelope(region, Math.Min(region.Length - 1, Math.Max(0, FrameAt(x) - region.Start)))) * (RowHeight - 14)));
|
|
167
|
+
timeline.Children.Add(line);
|
|
168
|
+
}
|
|
169
|
+
if (selected) foreach (bool incoming in new[] { true, false }) {
|
|
170
|
+
double x = FadeX(region, incoming); if (x < left || x > right) continue;
|
|
171
|
+
var handle = new Polygon { Fill = WorkspaceUi.Color("#7359C7"), IsHitTestVisible = false };
|
|
172
|
+
handle.Points.Add(new Point(x, y)); handle.Points.Add(new Point(x + (incoming ? 7 : -7), y)); handle.Points.Add(new Point(x, y + 8)); timeline.Children.Add(handle);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
private HashSet<string> EditableClipIds()
|
|
176
|
+
{
|
|
177
|
+
if (clipSelection) return new HashSet<string>(selectedIds);
|
|
178
|
+
if (to <= from) return new HashSet<string>();
|
|
179
|
+
TimelineEdits.Split(session, track, AllTracks, from); TimelineEdits.Split(session, track, AllTracks, to);
|
|
180
|
+
return new HashSet<string>(session.Regions.Where(r => (AllTracks || r.Track == track) && r.Start >= from && r.End <= to).Select(r => r.Id));
|
|
181
|
+
}
|
|
182
|
+
private void SelectAdded(IEnumerable<string> ids)
|
|
183
|
+
{ selectedIds.Clear(); foreach (string id in ids) selectedIds.Add(id); clipSelection = selectedIds.Count > 0; RefreshClipBounds(); }
|
|
184
|
+
private void RepeatClips(int count)
|
|
185
|
+
{
|
|
186
|
+
Change(delegate {
|
|
187
|
+
if (clipSelection) { SelectAdded(EditorTools.Repeat(session, selectedIds, count)); return true; }
|
|
188
|
+
if (to <= from) return false; var copied = TimelineEdits.Copy(session, track, AllTracks, from, to); if (copied.Regions.Count == 0) return false;
|
|
189
|
+
var before = new HashSet<string>(session.Regions.Select(r => r.Id)); long start = from;
|
|
190
|
+
for (int n = 1; n <= count; n++) TimelineEdits.Paste(session, copied, track, start + copied.Length * n);
|
|
191
|
+
SelectAdded(session.Regions.Where(r => !before.Contains(r.Id)).Select(r => r.Id).ToArray()); return true;
|
|
192
|
+
}, count == 1 ? "Selection duplicated." : "Selection repeated.");
|
|
193
|
+
}
|
|
194
|
+
private bool ExecuteClipAction(string action)
|
|
195
|
+
{
|
|
196
|
+
switch (action) {
|
|
197
|
+
case "slipTool": SetPointerTool("slip"); return true;
|
|
198
|
+
case "fadeTool": SetPointerTool("fade"); return true;
|
|
199
|
+
case "duplicate": RepeatClips(1); return true;
|
|
200
|
+
case "repeatClips": var count = AskRepeatCount(); if (count.HasValue) RepeatClips(count.Value); return true;
|
|
201
|
+
case "muteClips": Change(delegate { var ids = EditableClipIds(); bool changed = EditorTools.ToggleMute(session, ids); if (changed) SelectAdded(ids); return changed; }, "Clip mute updated."); return true;
|
|
202
|
+
case "crossfade": Change(() => EditorTools.Crossfade(session, selectedIds), "Crossfade applied."); return true;
|
|
203
|
+
case "clearFades": Change(delegate { var ids = EditableClipIds(); bool changed = false; foreach (var region in session.Regions.Where(r => ids.Contains(r.Id))) { changed |= region.FadeIn > 0 || region.FadeOut > 0; region.FadeIn = region.FadeOut = region.FadeInOffset = region.FadeOutOffset = 0; } if (changed) SelectAdded(ids); return changed; }, "Clip fades cleared."); return true;
|
|
204
|
+
default: return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
private int? AskRepeatCount()
|
|
208
|
+
{
|
|
209
|
+
var dialog = new Window { Owner = owner, Title = "Repeat selected audio", Width = 330, Height = 180, ResizeMode = ResizeMode.NoResize, WindowStartupLocation = WindowStartupLocation.CenterOwner, ShowInTaskbar = false, Icon = BrandArtwork.Raster() }; ThemePalette.Install(dialog);
|
|
210
|
+
var body = new StackPanel { Margin = new Thickness(18) }; dialog.Content = body; body.Children.Add(WorkspaceUi.Text("Additional copies (1–64)", 14));
|
|
211
|
+
var input = new TextBox { Text = "3", Margin = new Thickness(0, 8, 0, 10), Padding = new Thickness(5) }; body.Children.Add(input);
|
|
212
|
+
var buttons = new StackPanel { Orientation = Orientation.Horizontal }; body.Children.Add(buttons); int result = 0;
|
|
213
|
+
var apply = WorkspaceUi.Button("Repeat", delegate { int value; if (int.TryParse(input.Text, out value) && value >= 1 && value <= 64) { result = value; dialog.DialogResult = true; } else input.SelectAll(); }); apply.IsDefault = true; buttons.Children.Add(apply);
|
|
214
|
+
var close = WorkspaceUi.Button("Cancel", delegate { dialog.DialogResult = false; }); close.IsCancel = true; buttons.Children.Add(close); dialog.Loaded += delegate { input.Focus(); input.SelectAll(); };
|
|
215
|
+
return dialog.ShowDialog() == true ? (int?)result : null;
|
|
216
|
+
}
|
|
217
|
+
internal void ToolDownForTest(string id, string tool, long frame)
|
|
218
|
+
{
|
|
219
|
+
var region = session.Regions.Single(r => r.Id == id); previousIds = selectedIds.ToArray(); previousClipSelection = clipSelection; previousTrack = track;
|
|
220
|
+
previousFrom = from; previousTo = to; previousPlayhead = playhead;
|
|
221
|
+
BeginClipDrag((ClipDrag)Enum.Parse(typeof(ClipDrag), tool), region, new Point(X(frame), RulerHeight + region.Track * RowHeight + 8));
|
|
222
|
+
}
|
|
223
|
+
internal void ToolMoveForTest(long frame, int row) { PreviewClipDrag(new Point(X(frame), RulerHeight + row * RowHeight + 8)); }
|
|
224
|
+
internal void ToolEndForTest(bool cancel) { if (cancel) CancelClipDrag(); else FinishClipDrag(); }
|
|
225
|
+
internal void ContextForTest(long frame, int row) { SelectContextTarget(new Point(X(frame), RulerHeight + row * RowHeight + 35)); }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
|
|
6
|
+
namespace DrumBuddy
|
|
7
|
+
{
|
|
8
|
+
internal enum ClipDrag { None, Move, TrimLeft, TrimRight, Slip, FadeIn, FadeOut }
|
|
9
|
+
public static class EditorTools
|
|
10
|
+
{
|
|
11
|
+
public static double Envelope(TimelineRegion region, long offset)
|
|
12
|
+
{
|
|
13
|
+
double value = 1;
|
|
14
|
+
if (region.FadeIn > 0) value = Math.Min(value, Math.Max(0, offset + region.FadeInOffset) / (double)region.FadeIn);
|
|
15
|
+
if (region.FadeOut > 0) value = Math.Min(value, Math.Max(0, region.Length - 1 - offset + region.FadeOutOffset) / (double)region.FadeOut);
|
|
16
|
+
return Math.Max(0, Math.Min(1, value));
|
|
17
|
+
}
|
|
18
|
+
public static void ClampFades(TimelineRegion region)
|
|
19
|
+
{ region.FadeIn = Math.Max(0, Math.Min(region.Length, region.FadeIn)); region.FadeOut = Math.Max(0, Math.Min(region.Length, region.FadeOut)); region.FadeInOffset = region.FadeOutOffset = 0; }
|
|
20
|
+
public static bool Move(EditorSession session, ISet<string> ids, long delta, int tracks)
|
|
21
|
+
{
|
|
22
|
+
var selected = session.Regions.Where(r => ids.Contains(r.Id)).ToArray(); if (selected.Length == 0) return false;
|
|
23
|
+
delta = Math.Max(-selected.Min(r => r.Start), Math.Min(EditorSession.MaxFrame - selected.Max(r => r.End), delta));
|
|
24
|
+
tracks = Math.Max(-selected.Min(r => r.Track), Math.Min(15 - selected.Max(r => r.Track), tracks));
|
|
25
|
+
if (delta == 0 && tracks == 0) return false;
|
|
26
|
+
foreach (var region in selected) { region.Start += delta; region.Track += tracks; } return true;
|
|
27
|
+
}
|
|
28
|
+
public static bool Trim(TimelineRegion region, bool left, long boundary, long sourceFrames)
|
|
29
|
+
{
|
|
30
|
+
if (sourceFrames < region.SourceStart + region.Length) throw new IOException("Re-import the source before extending this clip.");
|
|
31
|
+
long before = region.Length;
|
|
32
|
+
if (left) {
|
|
33
|
+
long start = Math.Max(Math.Max(0, region.Start - region.SourceStart), Math.Min(region.End - 1, boundary));
|
|
34
|
+
long delta = start - region.Start; region.Start = start; region.SourceStart += delta; region.Length -= delta;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
long end = Math.Max(region.Start + 1, Math.Min(Math.Min(EditorSession.MaxFrame, region.Start + sourceFrames - region.SourceStart), boundary)); region.Length = end - region.Start;
|
|
38
|
+
}
|
|
39
|
+
ClampFades(region); return before != region.Length;
|
|
40
|
+
}
|
|
41
|
+
public static bool Slip(TimelineRegion region, long delta, long sourceFrames)
|
|
42
|
+
{
|
|
43
|
+
long before = region.SourceStart; region.SourceStart = Math.Max(0, Math.Min(sourceFrames - region.Length, region.SourceStart + delta));
|
|
44
|
+
return before != region.SourceStart;
|
|
45
|
+
}
|
|
46
|
+
public static bool Fade(TimelineRegion region, bool fadeIn, long length)
|
|
47
|
+
{
|
|
48
|
+
length = Math.Max(0, Math.Min(region.Length, length)); long before = fadeIn ? region.FadeIn : region.FadeOut;
|
|
49
|
+
if (fadeIn) { region.FadeIn = length; region.FadeInOffset = 0; } else { region.FadeOut = length; region.FadeOutOffset = 0; } return before != length;
|
|
50
|
+
}
|
|
51
|
+
public static List<string> Repeat(EditorSession session, ISet<string> ids, int copies)
|
|
52
|
+
{
|
|
53
|
+
var source = session.Regions.Where(r => ids.Contains(r.Id)).ToArray();
|
|
54
|
+
if (source.Length == 0) throw new IOException("Select clips to duplicate or repeat.");
|
|
55
|
+
if (copies < 1 || copies > 64 || session.Regions.Count + copies * source.Length > 20000) throw new IOException("Choose 1 to 64 additional copies within the session's region limit.");
|
|
56
|
+
long length = source.Max(r => r.End) - source.Min(r => r.Start);
|
|
57
|
+
if (length > (EditorSession.MaxFrame - source.Max(r => r.End)) / copies) throw new IOException("The repeats would exceed the 24-hour timeline.");
|
|
58
|
+
var added = new List<string>();
|
|
59
|
+
for (int n = 1; n <= copies; n++) foreach (var item in source) {
|
|
60
|
+
var copy = item.Copy(); copy.Id = Guid.NewGuid().ToString("N"); copy.Start += length * n; session.Regions.Add(copy); added.Add(copy.Id);
|
|
61
|
+
}
|
|
62
|
+
return added;
|
|
63
|
+
}
|
|
64
|
+
public static bool ToggleMute(EditorSession session, ISet<string> ids)
|
|
65
|
+
{
|
|
66
|
+
var selected = session.Regions.Where(r => ids.Contains(r.Id)).ToArray(); if (selected.Length == 0) return false;
|
|
67
|
+
bool mute = selected.Any(r => !r.Muted); foreach (var region in selected) region.Muted = mute; return true;
|
|
68
|
+
}
|
|
69
|
+
public static bool Crossfade(EditorSession session, ISet<string> ids)
|
|
70
|
+
{
|
|
71
|
+
var selected = session.Regions.Where(r => ids.Contains(r.Id)).OrderBy(r => r.Start).ToArray();
|
|
72
|
+
if (selected.Length != 2 || selected[0].Track != selected[1].Track || selected[0].Start >= selected[1].Start || selected[0].End <= selected[1].Start || selected[0].End > selected[1].End)
|
|
73
|
+
throw new IOException("Select two overlapping clips on one track, with the second extending beyond the first, to crossfade.");
|
|
74
|
+
long overlap = selected[0].End - selected[1].Start;
|
|
75
|
+
selected[0].FadeOut = overlap; selected[0].FadeOutOffset = 0; selected[1].FadeIn = overlap; selected[1].FadeInOffset = 1; return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.IO;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Collections.Generic;
|
|
7
|
+
using System.Windows;
|
|
8
|
+
using System.Windows.Media;
|
|
9
|
+
using System.Windows.Media.Imaging;
|
|
10
|
+
|
|
11
|
+
namespace DrumBuddy
|
|
12
|
+
{
|
|
13
|
+
public static class EditorToolsTests
|
|
14
|
+
{
|
|
15
|
+
private static readonly List<string> log = new List<string>();
|
|
16
|
+
private static void Check(bool value, string text) { if (!value) throw new Exception(text); log.Add("PASS " + text); }
|
|
17
|
+
private static long Frame(double seconds) { return (long)Math.Round(seconds * Audio.Rate); }
|
|
18
|
+
private static float[] Render(EditorSession session, IDictionary<string, Clip> assets)
|
|
19
|
+
{
|
|
20
|
+
using (var mix = new TimelineMix(session, assets)) { var output = new float[checked((int)session.End * 2)]; int n = 0, read; while ((read = mix.Read(output, n, output.Length - n)) > 0) n += read; return output; }
|
|
21
|
+
}
|
|
22
|
+
private static void Picture(FrameworkElement view, string file)
|
|
23
|
+
{
|
|
24
|
+
view.Measure(new Size(1100, 650)); view.Arrange(new Rect(0, 0, 1100, 650)); view.UpdateLayout();
|
|
25
|
+
var bitmap = new RenderTargetBitmap(1100, 650, 96, 96, PixelFormats.Pbgra32); bitmap.Render(view);
|
|
26
|
+
var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); using (var output = File.Create(file)) encoder.Save(output);
|
|
27
|
+
}
|
|
28
|
+
public static int Run(string[] args)
|
|
29
|
+
{
|
|
30
|
+
string root = Path.GetFullPath(args[0]); Directory.CreateDirectory(root); log.Clear(); var preferences = InterfacePreferences.Current;
|
|
31
|
+
try {
|
|
32
|
+
InterfacePreferences.Current = InterfacePreferences.Defaults(); InterfacePreferences.Current.SnapToGrid = InterfacePreferences.Current.SnapToEdges = false;
|
|
33
|
+
string file = Path.Combine(root, "Original float.wav"); var samples = Enumerable.Repeat(1.5f, Audio.Rate * 4).ToArray();
|
|
34
|
+
using (var writer = new FloatWaveWriter(file, 1, Audio.Rate)) writer.Write(samples, samples.Length, 1);
|
|
35
|
+
byte[] original = File.ReadAllBytes(file);
|
|
36
|
+
var clip = new Clip { Source = file, Cache = file, Name = "Original float.wav", Channels = 1, Duration = 4, Peak = 1.5, Waveform = Enumerable.Repeat(1.5f, 900).ToArray(), Ready = true };
|
|
37
|
+
var assets = new Dictionary<string, Clip> { { file, clip } };
|
|
38
|
+
var fade = new EditorSession(); fade.Regions.Add(new TimelineRegion { Id = "fade", Source = file, Length = 100, FadeIn = 20, FadeOut = 20 });
|
|
39
|
+
var rendered = Render(fade, assets);
|
|
40
|
+
Check(rendered[0] == 0 && rendered[40] == 1.5f && rendered[198] == 0, "Apply clip fade-in/out to actual float samples while retaining headroom");
|
|
41
|
+
var split = fade.Copy(); TimelineEdits.Split(split, 0, false, 10); split.Validate(); Check(rendered.SequenceEqual(Render(split, assets)), "Splitting inside a fade preserves the exact audible envelope");
|
|
42
|
+
var copied = TimelineEdits.Copy(fade, 0, false, 5, 85); var pasted = new EditorSession(); TimelineEdits.Paste(pasted, copied, 0, 0);
|
|
43
|
+
Check(Render(pasted, assets).SequenceEqual(rendered.Skip(10).Take(160)), "Copy/paste part of a faded clip preserves its original sample levels");
|
|
44
|
+
var muted = fade.Copy(); EditorTools.ToggleMute(muted, new HashSet<string> { "fade" }); Check(Render(muted, assets).All(v => v == 0), "Clip mute silences playback and mix output without changing source audio");
|
|
45
|
+
var cross = new EditorSession(); cross.Regions.Add(new TimelineRegion { Id = "a", Source = file, Length = 100 }); cross.Regions.Add(new TimelineRegion { Id = "b", Source = file, Start = 80, Length = 100 });
|
|
46
|
+
EditorTools.Crossfade(cross, new HashSet<string> { "a", "b" });
|
|
47
|
+
Check(Render(cross, assets).Skip(160).Take(40).All(v => Math.Abs(v - 1.5f) < 1e-6), "Crossfade overlapping clips with complementary levels across the overlap");
|
|
48
|
+
string exported = Path.Combine(root, "Faded mix.wav"); TimelineRender.Export(fade, assets, exported, 32, false, 0, 0, CancellationToken.None, null);
|
|
49
|
+
using (var wave = new NAudio.Wave.WaveFileReader(exported)) { var bytes = new byte[checked((int)wave.Length)]; wave.Read(bytes, 0, bytes.Length); var actual = new float[bytes.Length / 4]; Buffer.BlockCopy(bytes, 0, actual, 0, bytes.Length); Check(actual.SequenceEqual(rendered), "Save the same clip fades heard in playback into a float mix export"); }
|
|
50
|
+
string project = Path.Combine(root, "Fades.dbedit"); split.Regions[0].Muted = true; split.Save(project); var loaded = EditorSession.Load(project);
|
|
51
|
+
Check(loaded.Regions[0].Muted && loaded.Regions[1].FadeInOffset == 10, "Persist clip mute, fades and split-envelope offsets in project files");
|
|
52
|
+
var state = new EditorSession { Name = "Clip tools" };
|
|
53
|
+
state.Regions.Add(new TimelineRegion { Id = "first", Name = "Move / trim / slip", Source = file, Start = Frame(1), SourceStart = Frame(1), Length = Frame(2) });
|
|
54
|
+
state.Regions.Add(new TimelineRegion { Id = "second", Name = "Group item", Source = file, Track = 1, Start = Frame(2), Length = Frame(1) });
|
|
55
|
+
var owner = new Window(); ThemePalette.Apply(true); ThemePalette.Install(owner);
|
|
56
|
+
using (var editor = new EditorView(owner, Path.Combine(root, "cache"), delegate { }, delegate { })) {
|
|
57
|
+
owner.Content = editor; editor.SetForTest(state, assets); editor.ZoomForTest(250); Picture(editor, Path.Combine(root, "clip-tools-start.png"));
|
|
58
|
+
editor.PointerDownForTest(Frame(1.25), 0, false); editor.PointerUpForTest(Frame(1.25)); editor.PointerDownForTest(Frame(2.25), 1, true);
|
|
59
|
+
editor.ToolDownForTest("first", "Move", Frame(1.5)); editor.ToolMoveForTest(Frame(3.5), 2); editor.ToolEndForTest(false);
|
|
60
|
+
var moved = editor.SessionForTest;
|
|
61
|
+
Check(moved.Regions.Single(r => r.Id == "first").Start == Frame(3) && moved.Regions.Single(r => r.Id == "second").Start == Frame(4) && moved.Regions.Single(r => r.Id == "first").Track == 2 && moved.Regions.Single(r => r.Id == "second").Track == 3, "Move a selected clip group in time and across tracks without losing relative placement");
|
|
62
|
+
editor.ExecuteAction("undo").GetAwaiter().GetResult(); Check(editor.SessionForTest.Regions.Single(r => r.Id == "first").Start == Frame(1), "Undo a complete clip drag as one edit");
|
|
63
|
+
editor.SetForTest(state, assets); editor.ToolDownForTest("first", "Move", Frame(1.5)); editor.ToolMoveForTest(Frame(2.5), 2); editor.ToolEndForTest(true);
|
|
64
|
+
Check(!editor.Dirty && editor.SessionForTest.Regions.Single(r => r.Id == "first").Track == 0 && editor.SessionForTest.Regions.Single(r => r.Id == "first").Start == Frame(1), "Cancel a clip drag without changing the session or dirty state");
|
|
65
|
+
editor.ToolDownForTest("first", "TrimLeft", Frame(1)); editor.ToolMoveForTest(Frame(1.5), 0); editor.ToolEndForTest(false);
|
|
66
|
+
var trimmed = editor.SessionForTest.Regions.Single(r => r.Id == "first"); Check(trimmed.Start == Frame(1.5) && trimmed.SourceStart == Frame(1.5) && trimmed.Length == Frame(1.5), "Trim the left edge while advancing the exact source offset");
|
|
67
|
+
editor.SetForTest(state, assets); editor.ToolDownForTest("first", "TrimRight", Frame(3)); editor.ToolMoveForTest(Frame(8), 0); editor.ToolEndForTest(false);
|
|
68
|
+
Check(editor.SessionForTest.Regions.Single(r => r.Id == "first").End == Frame(4), "Clamp edge extension to available source audio");
|
|
69
|
+
editor.SetForTest(state, assets); editor.ToolDownForTest("first", "Slip", Frame(1.5)); editor.ToolMoveForTest(Frame(1.25), 0); editor.ToolEndForTest(false);
|
|
70
|
+
var slipped = editor.SessionForTest.Regions.Single(r => r.Id == "first"); Check(slipped.SourceStart == Frame(1.25) && slipped.Start == Frame(1) && slipped.Length == Frame(2), "Slip source contents while retaining clip start and end");
|
|
71
|
+
editor.ToolDownForTest("first", "FadeIn", Frame(1)); editor.ToolMoveForTest(Frame(1.5), 0); editor.ToolEndForTest(false); editor.ToolDownForTest("first", "FadeOut", Frame(3)); editor.ToolMoveForTest(Frame(2.5), 0); editor.ToolEndForTest(false);
|
|
72
|
+
Check(editor.SessionForTest.Regions.Single(r => r.Id == "first").FadeIn == Frame(.5) && editor.SessionForTest.Regions.Single(r => r.Id == "first").FadeOut == Frame(.5), "Commit both fade handles as audible, undoable clip properties");
|
|
73
|
+
editor.ExecuteAction("zoomSelection").GetAwaiter().GetResult(); Picture(editor, Path.Combine(root, "clip-tools-fades.png"));
|
|
74
|
+
editor.SetForTest(state, assets); editor.PointerDownForTest(Frame(1.25), 0, false); editor.PointerUpForTest(Frame(1.25)); editor.ExecuteAction("duplicate").GetAwaiter().GetResult();
|
|
75
|
+
Check(editor.SessionForTest.Regions.Any(r => r.Id != "first" && r.Track == 0 && r.Start == Frame(3) && r.SourceStart == Frame(1)), "Duplicate selected audio directly after its original duration");
|
|
76
|
+
editor.SetForTest(state, assets); editor.ShowSelectionForTest(Frame(1.25), Frame(1.75)); editor.ContextForTest(Frame(1.5), 0);
|
|
77
|
+
Check(editor.SelectedClipsForTest == 0, "Keep a dragged time range active when opening its context menu");
|
|
78
|
+
editor.ExecuteAction("clearFades").GetAwaiter().GetResult();
|
|
79
|
+
Check(!editor.Dirty && editor.SessionForTest.Regions.Count == 2, "Roll back temporary splits when a range operation makes no audio change");
|
|
80
|
+
}
|
|
81
|
+
owner.Content = null; owner.Close();
|
|
82
|
+
var repeats = state.Copy(); EditorTools.Repeat(repeats, new HashSet<string> { "first", "second" }, 3);
|
|
83
|
+
Check(repeats.Regions.Count == 8 && repeats.Regions.Any(r => r.Track == 1 && r.Start == Frame(8)), "Repeat selected multitrack clips with their timing and source windows intact");
|
|
84
|
+
bool rejected = false; int before = repeats.Regions.Count; try { EditorTools.Repeat(repeats, new HashSet<string> { "first" }, 65); } catch (IOException) { rejected = true; }
|
|
85
|
+
Check(rejected && repeats.Regions.Count == before, "Reject excessive repeats before partially editing the project");
|
|
86
|
+
Check(original.SequenceEqual(File.ReadAllBytes(file)), "Keep original audio byte-for-byte unchanged across all new clip tools");
|
|
87
|
+
File.WriteAllLines(Path.Combine(root, "tools-test-results.txt"), log, Encoding.UTF8); return 0;
|
|
88
|
+
}
|
|
89
|
+
catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "tools-test-results.txt"), log, Encoding.UTF8); return 1; }
|
|
90
|
+
finally { InterfacePreferences.Current = preferences; ThemePalette.Apply(preferences.Theme == "Dark"); }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|