@pro-gram/flux 1.6.2 → 1.6.4
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/README.md +15 -2
- package/dependencies.json +1 -1
- package/package.json +4 -3
- package/payload/DrumBuddy-Setup.exe +2 -2
- package/source/AssetBootstrap.cs +1 -1
- package/source/Audio Decoders.txt +1 -1
- package/source/Build Instructions.txt +5 -1
- package/source/Build.ps1 +1 -0
- package/source/EditorCompactLayout.cs +1 -1
- package/source/EditorMixer.cs +267 -0
- package/source/EditorMixerTests.cs +159 -0
- package/source/EditorPlaybackTests.cs +22 -0
- package/source/EditorView.cs +36 -18
- package/source/InterfaceSettings.cs +11 -4
- package/source/Main.cs +12 -8
- package/source/Main.xaml +1 -0
- package/source/MediaPlayerView.cs +1 -1
- package/source/Quick Start.txt +17 -2
- package/source/RecordingTests.cs +2 -0
- package/source/Setup.cs +3 -3
- package/source/ShellStyles.xaml +11 -5
- package/source/TempoGrid.cs +14 -0
- package/source/TempoGridTests.cs +38 -0
- package/source/Third Party Notices.txt +1 -1
- package/source/Timeline.cs +39 -13
- package/source/TimelineMixer.cs +44 -0
package/source/EditorView.cs
CHANGED
|
@@ -110,6 +110,7 @@ namespace DrumBuddy
|
|
|
110
110
|
var editing = new WrapPanel(); tools.Children.Add(editing);
|
|
111
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
112
|
recordButton.SetResourceReference(Control.ForegroundProperty, "ThemeOnAccent");
|
|
113
|
+
editing.Children.Add(WorkspaceUi.Button("Mixer", delegate { ToggleMixer(); }));
|
|
113
114
|
editing.Children.Add(WorkspaceUi.Button("Record options", delegate { ShowRecordingOptions(!recordingPane.IsPaneVisible); }));
|
|
114
115
|
undoButton = WorkspaceUi.Button("Undo", delegate { Undo(false); }); editing.Children.Add(undoButton);
|
|
115
116
|
redoButton = WorkspaceUi.Button("Redo", delegate { Undo(true); }); editing.Children.Add(redoButton);
|
|
@@ -205,8 +206,9 @@ namespace DrumBuddy
|
|
|
205
206
|
transport.Children.Add(WorkspaceUi.Button("Stop", delegate { if (recording && StopRequested != null) StopRequested(); else { Stop(); UpdateTransport(); } }));
|
|
206
207
|
transport.Children.Add(WorkspaceUi.Button("To start", delegate { if (IsBusy) return; Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); }));
|
|
207
208
|
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);
|
|
208
|
-
transport.Children.Add(WorkspaceUi.Text("
|
|
209
|
+
transport.Children.Add(WorkspaceUi.Text("Monitor volume ", 14));
|
|
209
210
|
volume = new Slider { Minimum = 0, Maximum = 100, Value = 75, Width = 120, Margin = new Thickness(0, 0, 16, 8), VerticalAlignment = VerticalAlignment.Center };
|
|
211
|
+
volume.ToolTip = "Listening volume only. Mixer track and master faders also affect exported audio.";
|
|
210
212
|
volume.ValueChanged += delegate { output.Volume((float)(volume.Value / 100)); }; transport.Children.Add(volume);
|
|
211
213
|
outputPicker = new AudioOutputPicker(delegate { Stop(); }, status); bottom.Children.Add(outputPicker);
|
|
212
214
|
var exports = new WrapPanel(); bottom.Children.Add(exports);
|
|
@@ -218,10 +220,11 @@ namespace DrumBuddy
|
|
|
218
220
|
AddFileButton(exports, "Export mix…", async delegate { await ExportMix(); });
|
|
219
221
|
ArrangeCompactChrome(top, editing, range, viewControls, zoomTools, bottom, transport, exports);
|
|
220
222
|
timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(80) };
|
|
221
|
-
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();
|
|
223
|
+
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(); } TickMixerMeters(); }; timer.Start();
|
|
222
224
|
waveformDetail = new WaveformDetail(Dispatcher, delegate { if (!disposed) DrawCanvas(); });
|
|
223
225
|
UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
224
|
-
|
|
226
|
+
mixerPane = new DockablePane(editorBody, CreateMixer(), "Mixer", true, false, 360) { MinimumMainHeight = 220 };
|
|
227
|
+
recordingPane = new DockablePane(mixerPane, null, "Record options", false, false, 260) { MinimumMainHeight = 300 }; Children.Add(recordingPane);
|
|
225
228
|
editorBody.SizeChanged += delegate { UpdateCompactLayout(); };
|
|
226
229
|
}
|
|
227
230
|
|
|
@@ -418,18 +421,19 @@ namespace DrumBuddy
|
|
|
418
421
|
if (RecordingTrack(i)) name.Foreground = WorkspaceUi.Color("#B52C36");
|
|
419
422
|
var strip = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; panel.Children.Add(strip);
|
|
420
423
|
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) };
|
|
421
|
-
mute.Click += delegate { if (!updating)
|
|
424
|
+
mute.Click += delegate { if (!updating) SetMixerMute(index, mute.IsChecked == true); }; strip.Children.Add(mute); headerMutes[i] = mute;
|
|
422
425
|
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) };
|
|
423
|
-
solo.Click += delegate { if (!updating)
|
|
424
|
-
var levels = new[] { -60.0, -24, -18, -12, -6, 0, 3, 6, 12, state.GainDb }.Distinct().OrderBy(v => v).ToArray();
|
|
426
|
+
solo.Click += delegate { if (!updating) SetMixerSolo(index, solo.IsChecked == true); }; strip.Children.Add(solo); headerSolos[i] = solo;
|
|
427
|
+
var levels = new[] { -60.0, -24, -18, -12, -6, 0, 3, 6, 12, 18, 24, state.GainDb }.Distinct().OrderBy(v => v).ToArray();
|
|
425
428
|
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" };
|
|
426
|
-
level.SelectionChanged += delegate { if (!updating && level.
|
|
429
|
+
level.SelectionChanged += delegate { if (!updating && level.SelectedItem != null) { double value; if (TempoGrid.TryTempo(level.SelectedItem.ToString().Replace(" dB", ""), out value)) SetMixerGain(index, value); } }; strip.Children.Add(level); headerLevels[i] = level;
|
|
427
430
|
var border = new Border { Height = RowHeight, BorderThickness = new Thickness(RecordingTrack(i) ? 3 : 0, 0, 1, 1), BorderBrush = WorkspaceUi.Color(RecordingTrack(i) ? "#B52C36" : "#D6E1DF"), Background = WorkspaceUi.Color(i == track ? "#DCEFEB" : "#EDF3F1"), Child = panel };
|
|
428
431
|
border.PreviewMouseLeftButtonDown += delegate { if (!IsBusy && track != index) { track = index; UpdateSelectionText(); } };
|
|
429
432
|
border.MouseLeftButtonUp += delegate { BuildHeaders(); DrawCanvas(); };
|
|
430
433
|
headers.Children.Add(border);
|
|
431
434
|
}
|
|
432
435
|
updating = false;
|
|
436
|
+
RefreshMixerControls();
|
|
433
437
|
}
|
|
434
438
|
private void DrawCanvas()
|
|
435
439
|
{
|
|
@@ -458,6 +462,7 @@ namespace DrumBuddy
|
|
|
458
462
|
playLine.Y1 = 0; playLine.Y2 = timeline.Height; timeline.Children.Add(playLine); UpdatePlayLine(); UpdateTransport();
|
|
459
463
|
undoButton.IsEnabled = !IsBusy && undo.Count > 0; redoButton.IsEnabled = !IsBusy && redo.Count > 0;
|
|
460
464
|
title.Text = session.Name + (dirty ? " *" : "");
|
|
465
|
+
RefreshMixerControls();
|
|
461
466
|
}
|
|
462
467
|
private void UpdateSelectionOverlay()
|
|
463
468
|
{
|
|
@@ -526,7 +531,7 @@ namespace DrumBuddy
|
|
|
526
531
|
|
|
527
532
|
private void Change(Func<bool> action, string message)
|
|
528
533
|
{
|
|
529
|
-
if (IsBusy) return; Stop(); var before = session.Copy();
|
|
534
|
+
if (IsBusy) return; FinishMixerGesture(); Stop(); var before = session.Copy();
|
|
530
535
|
var oldIds = selectedIds.ToArray(); bool oldMode = clipSelection; long oldFrom = from, oldTo = to; int oldTrack = track;
|
|
531
536
|
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(); };
|
|
532
537
|
try {
|
|
@@ -538,7 +543,7 @@ namespace DrumBuddy
|
|
|
538
543
|
private void Undo(bool forward)
|
|
539
544
|
{
|
|
540
545
|
if (IsBusy) return; var source = forward ? redo : undo; var target = forward ? undo : redo;
|
|
541
|
-
if (source.Count == 0) return; Stop(); target.Push(session.Copy()); session = source.Pop(); dirty = true; RefreshClipBounds(); BuildHeaders(); DrawCanvas();
|
|
546
|
+
if (source.Count == 0) return; FinishMixerGesture(); Stop(); target.Push(session.Copy()); session = source.Pop(); dirty = true; RefreshClipBounds(); BuildHeaders(); DrawCanvas();
|
|
542
547
|
}
|
|
543
548
|
private void Split() { Change(() => TimelineEdits.Split(session, track, AllTracks, playhead, clipSelection ? selectedIds : null), "Split at the playhead."); }
|
|
544
549
|
private void Copy(bool cut)
|
|
@@ -597,16 +602,16 @@ namespace DrumBuddy
|
|
|
597
602
|
finally { EndWork(); BuildHeaders(); DrawCanvas(); timeline.Focus(); }
|
|
598
603
|
}
|
|
599
604
|
private void BeginWork()
|
|
600
|
-
{ operation = new CancellationTokenSource(); foreach (var button in fileButtons) button.IsEnabled = false; play.IsEnabled = false; cancel.Visibility = Visibility.Visible; RefreshGridToolbar(); }
|
|
605
|
+
{ operation = new CancellationTokenSource(); FinishMixerGesture(); foreach (var button in fileButtons) button.IsEnabled = false; play.IsEnabled = false; cancel.Visibility = Visibility.Visible; RefreshGridToolbar(); RefreshMixerControls(); }
|
|
601
606
|
private void EndWork()
|
|
602
|
-
{ operation.Dispose(); operation = null; foreach (var button in fileButtons) button.IsEnabled = true; play.IsEnabled = true; cancel.Visibility = Visibility.Collapsed; RefreshGridToolbar(); }
|
|
607
|
+
{ operation.Dispose(); operation = null; foreach (var button in fileButtons) button.IsEnabled = true; play.IsEnabled = true; cancel.Visibility = Visibility.Collapsed; RefreshGridToolbar(); RefreshMixerControls(); }
|
|
603
608
|
private void NewSession()
|
|
604
609
|
{
|
|
605
|
-
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();
|
|
610
|
+
if (IsBusy || !ConfirmClose()) return; FinishMixerGesture(); Stop(); session = new EditorSession { Grid = InterfacePreferences.Current.Grid.Copy() }; ResetMixerMeters(); sessionPath = null; assets.Clear(); undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); BuildHeaders(); DrawCanvas();
|
|
606
611
|
}
|
|
607
612
|
public bool SaveSession(bool saveAs)
|
|
608
613
|
{
|
|
609
|
-
if (IsBusy) return false;
|
|
614
|
+
if (IsBusy) return false; FinishMixerGesture();
|
|
610
615
|
string path = sessionPath;
|
|
611
616
|
if (saveAs || string.IsNullOrEmpty(path)) {
|
|
612
617
|
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" };
|
|
@@ -630,7 +635,7 @@ namespace DrumBuddy
|
|
|
630
635
|
decoded.Add(path, clip);
|
|
631
636
|
}
|
|
632
637
|
using (var verify = new TimelineMix(loaded, decoded)) { }
|
|
633
|
-
operation.Token.ThrowIfCancellationRequested(); session = loaded; assets.Clear(); foreach (var pair in decoded) assets.Add(pair.Key, pair.Value);
|
|
638
|
+
operation.Token.ThrowIfCancellationRequested(); session = loaded; ResetMixerMeters(); assets.Clear(); foreach (var pair in decoded) assets.Add(pair.Key, pair.Value);
|
|
634
639
|
sessionPath = dialog.FileName; undo.Clear(); redo.Clear(); dirty = false; selectedIds.Clear(); clipSelection = false; from = to = playhead = 0; UpdateSelectionText(); status("Editor session opened.");
|
|
635
640
|
}
|
|
636
641
|
catch (OperationCanceledException) { status("Opening cancelled. Your current timeline is kept."); }
|
|
@@ -674,6 +679,15 @@ namespace DrumBuddy
|
|
|
674
679
|
if (output.Playing || output.Paused) playhead = (long)Math.Round(output.Position * Audio.Rate);
|
|
675
680
|
output.Stop(); if (mix != null) { mix.Dispose(); mix = null; } play.Content = "Play";
|
|
676
681
|
}
|
|
682
|
+
private void JumpBar(int direction)
|
|
683
|
+
{
|
|
684
|
+
if (!CommitTempo()) return;
|
|
685
|
+
bool playing = output.Playing, paused = output.Paused;
|
|
686
|
+
Stop(); playhead = TempoGrid.AdjacentBar(playhead, session.Grid, direction);
|
|
687
|
+
DrawCanvas(); EnsurePlayheadVisible();
|
|
688
|
+
if (playing && playhead < session.End) PlayPause();
|
|
689
|
+
else if (paused) play.Content = "Resume";
|
|
690
|
+
}
|
|
677
691
|
public void AttachRecordingPanel(FrameworkElement panel)
|
|
678
692
|
{
|
|
679
693
|
panel.Margin = new Thickness(10); recordingPane.SetContent(panel);
|
|
@@ -690,9 +704,10 @@ namespace DrumBuddy
|
|
|
690
704
|
{
|
|
691
705
|
recording = active; recordButton.Content = active ? "Stop recording" : "Record"; recordButton.IsEnabled = true; play.IsEnabled = !active;
|
|
692
706
|
playLine.Stroke = WorkspaceUi.Color(active ? "#B52C36" : "#D0772C");
|
|
707
|
+
FinishMixerGesture(); RefreshMixerControls();
|
|
693
708
|
RefreshGridToolbar();
|
|
694
709
|
foreach (var button in fileButtons) button.IsEnabled = !active && operation == null;
|
|
695
|
-
if (active) { Stop(); ShowRecordingOptions(true); }
|
|
710
|
+
if (active) { Stop(); ResetMixerMeters(); UpdateRecordingInputMeters(new float[8]); ShowRecordingOptions(true); }
|
|
696
711
|
else { recordingPlan = null; captureRunning = false; recordingBanner.Visibility = Visibility.Collapsed; BuildHeaders(); DrawCanvas(); }
|
|
697
712
|
}
|
|
698
713
|
public async Task AddRecordedFiles(IEnumerable<string> paths, long at, int firstTrack, bool keepPlayhead = false)
|
|
@@ -719,10 +734,11 @@ namespace DrumBuddy
|
|
|
719
734
|
public async Task<bool> ExecuteAction(string action)
|
|
720
735
|
{
|
|
721
736
|
if (action == "selectTool" && clipDrag != ClipDrag.None) CancelClipDrag();
|
|
722
|
-
if (IsBusy && action != "stop" && !action.StartsWith("zoom", StringComparison.Ordinal)) return false;
|
|
737
|
+
if (IsBusy && action != "stop" && action != "mixer" && !action.StartsWith("zoom", StringComparison.Ordinal)) return false;
|
|
723
738
|
if (ExecuteClipAction(action)) return true;
|
|
724
739
|
switch (action) {
|
|
725
740
|
case "play": PlayPause(); break;
|
|
741
|
+
case "mixer": ToggleMixer(); break;
|
|
726
742
|
case "stop": Stop(); UpdateTransport(); break;
|
|
727
743
|
case "split": Split(); break;
|
|
728
744
|
case "selectTool": SetPointerTool("select"); break;
|
|
@@ -739,7 +755,9 @@ namespace DrumBuddy
|
|
|
739
755
|
case "new": NewSession(); break;
|
|
740
756
|
case "import": await ChooseAudio(); break;
|
|
741
757
|
case "export": await ExportMix(); break;
|
|
742
|
-
case "start": Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
758
|
+
case "start": case "startAlternate": Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
759
|
+
case "previousBar": JumpBar(-1); break;
|
|
760
|
+
case "nextBar": JumpBar(1); break;
|
|
743
761
|
case "end": Stop(); playhead = session.End; DrawCanvas(); EnsurePlayheadVisible(); break;
|
|
744
762
|
case "zoomIn": case "zoomInPlus": case "zoomInKeypad": ZoomStep(2); break;
|
|
745
763
|
case "zoomOut": case "zoomOutKeypad": ZoomStep(.5); break;
|
|
@@ -768,7 +786,7 @@ namespace DrumBuddy
|
|
|
768
786
|
return choice == MessageBoxResult.No || (choice == MessageBoxResult.Yes && SaveSession(false));
|
|
769
787
|
}
|
|
770
788
|
internal void SetForTest(EditorSession state, IDictionary<string, Clip> decoded)
|
|
771
|
-
{ 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(); }
|
|
789
|
+
{ session = state.Copy(); ResetMixerMeters(); 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(); }
|
|
772
790
|
internal EditorSession SessionForTest { get { return session.Copy(); } }
|
|
773
791
|
internal void ListenVolumeForTest(double value) { volume.Value = value; }
|
|
774
792
|
internal bool PlayingForTest { get { return output.Playing; } }
|
|
@@ -29,7 +29,7 @@ namespace DrumBuddy
|
|
|
29
29
|
if ((mods & ModifierKeys.Alt) != 0) text += "Alt+";
|
|
30
30
|
if ((mods & ModifierKeys.Shift) != 0) text += "Shift+";
|
|
31
31
|
if ((mods & ModifierKeys.Windows) != 0) text += "Win+";
|
|
32
|
-
return text + (Key == "Back" ? "Backspace" : Key == "OemComma" ? "," : Key == "OemPlus" ? "=" : Key == "OemMinus" ? "-" : Key.StartsWith("D") && Key.Length == 2 && char.IsDigit(Key[1]) ? Key.Substring(1) : Key);
|
|
32
|
+
return text + (Key == "Return" || Key == "Enter" ? "Enter" : Key == "Back" ? "Backspace" : Key == "OemComma" ? "," : Key == "OemPlus" ? "=" : Key == "OemMinus" ? "-" : Key.StartsWith("D") && Key.Length == 2 && char.IsDigit(Key[1]) ? Key.Substring(1) : Key);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -60,6 +60,8 @@ namespace DrumBuddy
|
|
|
60
60
|
{ "undo", "Undo" }, { "redo", "Redo" }, { "save", "Save session" }, { "saveAs", "Save session as" },
|
|
61
61
|
{ "open", "Open session" }, { "new", "New session" }, { "import", "Import audio" }, { "export", "Export mix" },
|
|
62
62
|
{ "start", "Go to start" }, { "end", "Go to end" }, { "home", "Open Home" }, { "editor", "Open Editor" },
|
|
63
|
+
{ "startAlternate", "Go to start (alternate)" }, { "previousBar", "Go to previous bar" }, { "nextBar", "Go to next bar" },
|
|
64
|
+
{ "mixer", "Show / hide Editor mixer" },
|
|
63
65
|
{ "converter", "Open Converter" }, { "player", "Open Media Player" }, { "settings", "Open Settings" }
|
|
64
66
|
, { "timelineSettings", "Open Timeline settings" }, { "zoomIn", "Editor zoom in" }, { "zoomInPlus", "Editor zoom in (+ key)" }, { "zoomInKeypad", "Editor zoom in (keypad)" }, { "zoomOut", "Editor zoom out" }, { "zoomOutKeypad", "Editor zoom out (keypad)" }, { "zoomFit", "Fit project in Editor" }, { "zoomSelection", "Fit selection in Editor" }
|
|
65
67
|
};
|
|
@@ -69,6 +71,8 @@ namespace DrumBuddy
|
|
|
69
71
|
string[] simple = { "play:Space", "stop:Space:Shift", "record:R", "split:S", "scissors:None", "copy:C:Control", "cut:X:Control", "paste:V:Control", "delete:Back", "deleteAlternate:Delete", "undo:Z:Control", "redo:Y:Control", "save:S:Control", "saveAs:S:Control,Shift", "open:O:Control", "new:N:Control", "import:I:Control", "export:E:Control", "start:Home", "end:End", "home:D1:Control", "editor:D2:Control", "converter:D3:Control", "player:D4:Control", "settings:OemComma:Control", "timelineSettings:None" };
|
|
70
72
|
simple = simple.Concat(new[] { "selectTool:V", "zoomIn:OemPlus:Control", "zoomInPlus:OemPlus:Control,Shift", "zoomInKeypad:Add:Control", "zoomOut:OemMinus:Control", "zoomOutKeypad:Subtract:Control", "zoomFit:D0:Control", "zoomSelection:D0:Control,Shift" }).ToArray();
|
|
71
73
|
simple = simple.Concat(new[] { "slipTool:Y", "fadeTool:F", "duplicate:D:Control", "repeatClips:L:Control", "muteClips:M", "crossfade:X", "clearFades:None" }).ToArray();
|
|
74
|
+
simple = simple.Concat(new[] { "startAlternate:Return", "previousBar:Left:Control", "nextBar:Right:Control" }).ToArray();
|
|
75
|
+
simple = simple.Concat(new[] { "mixer:None" }).ToArray();
|
|
72
76
|
foreach (string text in simple) {
|
|
73
77
|
string[] parts = text.Split(':');
|
|
74
78
|
settings.Keybinds.Add(new ShortcutBinding { Action = parts[0], Key = parts[1], Modifiers = parts.Length > 2 ? (int)(ModifierKeys)Enum.Parse(typeof(ModifierKeys), parts[2]) : 0 });
|
|
@@ -99,7 +103,7 @@ namespace DrumBuddy
|
|
|
99
103
|
Key key;
|
|
100
104
|
if (string.IsNullOrEmpty(binding.Key) || binding.Key == "None") continue;
|
|
101
105
|
if (!Enum.TryParse<Key>(binding.Key, out key) || IsModifier(key) || binding.Modifiers < 0 || binding.Modifiers > 15) throw new IOException("Invalid shortcut for " + Actions[binding.Action] + ".");
|
|
102
|
-
string signature =
|
|
106
|
+
string signature = (int)key + ":" + binding.Modifiers;
|
|
103
107
|
if (used.ContainsKey(signature)) throw new IOException(binding.Gesture + " is assigned to both " + Actions[used[signature]] + " and " + Actions[binding.Action] + ". Change or clear one binding.");
|
|
104
108
|
used[signature] = binding.Action;
|
|
105
109
|
}
|
|
@@ -115,7 +119,8 @@ namespace DrumBuddy
|
|
|
115
119
|
settings.KeymapRevision = 1;
|
|
116
120
|
}
|
|
117
121
|
foreach (var binding in Defaults().Keybinds) if (!settings.Keybinds.Any(b => b.Action == binding.Action)) {
|
|
118
|
-
|
|
122
|
+
Key defaultKey = (Key)Enum.Parse(typeof(Key), binding.Key);
|
|
123
|
+
if (settings.Keybinds.Any(b => SameKey(b.Key, defaultKey) && b.Modifiers == binding.Modifiers)) { binding.Key = "None"; binding.Modifiers = 0; }
|
|
119
124
|
settings.Keybinds.Add(binding);
|
|
120
125
|
}
|
|
121
126
|
}
|
|
@@ -137,9 +142,11 @@ namespace DrumBuddy
|
|
|
137
142
|
}
|
|
138
143
|
public static bool IsModifier(Key key)
|
|
139
144
|
{ return key == Key.LeftCtrl || key == Key.RightCtrl || key == Key.LeftAlt || key == Key.RightAlt || key == Key.LeftShift || key == Key.RightShift || key == Key.LWin || key == Key.RWin || key == Key.None; }
|
|
145
|
+
private static bool SameKey(string name, Key key)
|
|
146
|
+
{ Key parsed; return Enum.TryParse<Key>(name, out parsed) && parsed == key; }
|
|
140
147
|
public static string Resolve(Key key, ModifierKeys modifiers)
|
|
141
148
|
{
|
|
142
|
-
var binding = Current.Keybinds.FirstOrDefault(b => b.Key
|
|
149
|
+
var binding = Current.Keybinds.FirstOrDefault(b => SameKey(b.Key, key) && b.Modifiers == (int)modifiers);
|
|
143
150
|
return binding == null ? null : binding.Action;
|
|
144
151
|
}
|
|
145
152
|
public static string Gesture(string action)
|
package/source/Main.cs
CHANGED
|
@@ -20,7 +20,7 @@ using NAudio.Wave.SampleProviders;
|
|
|
20
20
|
|
|
21
21
|
[assembly: AssemblyTitle("Flux")]
|
|
22
22
|
[assembly: AssemblyDescription("PCM/float media player, 16-track audio editor, H5studio recording and R16 control")]
|
|
23
|
-
[assembly: AssemblyVersion("1.6.
|
|
23
|
+
[assembly: AssemblyVersion("1.6.4.0")]
|
|
24
24
|
|
|
25
25
|
namespace DrumBuddy
|
|
26
26
|
{
|
|
@@ -444,7 +444,7 @@ namespace DrumBuddy
|
|
|
444
444
|
}
|
|
445
445
|
private void BindMenus()
|
|
446
446
|
{
|
|
447
|
-
string[,] entries = { { "FileNew", "new" }, { "FileOpen", "open" }, { "FileSave", "save" }, { "FileSaveAs", "saveAs" }, { "FileImport", "import" }, { "FileExport", "export" }, { "ViewHome", "home" }, { "ViewEditor", "editor" }, { "ViewConverter", "converter" }, { "ViewPlayer", "player" }, { "ViewControl", "control" }, { "ViewRecording", "recordOptions" }, { "SettingsGeneral", "settings" }, { "SettingsTimeline", "timelineSettings" }, { "SettingsAudio", "audio" }, { "SettingsKeys", "keybinds" } };
|
|
447
|
+
string[,] entries = { { "FileNew", "new" }, { "FileOpen", "open" }, { "FileSave", "save" }, { "FileSaveAs", "saveAs" }, { "FileImport", "import" }, { "FileExport", "export" }, { "ViewHome", "home" }, { "ViewEditor", "editor" }, { "ViewConverter", "converter" }, { "ViewPlayer", "player" }, { "ViewControl", "control" }, { "ViewRecording", "recordOptions" }, { "ViewMixer", "mixer" }, { "SettingsGeneral", "settings" }, { "SettingsTimeline", "timelineSettings" }, { "SettingsAudio", "audio" }, { "SettingsKeys", "keybinds" } };
|
|
448
448
|
for (int i = 0; i < entries.GetLength(0); i++) { string action = entries[i, 1]; C<MenuItem>(entries[i, 0]).Click += async delegate { await ExecuteInterfaceAction(action); }; }
|
|
449
449
|
C<MenuItem>("FileExit").Click += delegate { window.Close(); };
|
|
450
450
|
C<MenuItem>("InstallAudioDrivers").Click += delegate { DriverSetup.ShowGuide(window); };
|
|
@@ -453,7 +453,7 @@ namespace DrumBuddy
|
|
|
453
453
|
}
|
|
454
454
|
private void RefreshMenuGestures()
|
|
455
455
|
{
|
|
456
|
-
string[,] entries = { { "FileNew", "new" }, { "FileOpen", "open" }, { "FileSave", "save" }, { "FileSaveAs", "saveAs" }, { "FileImport", "import" }, { "FileExport", "export" }, { "ViewHome", "home" }, { "ViewEditor", "editor" }, { "ViewConverter", "converter" }, { "ViewPlayer", "player" }, { "SettingsGeneral", "settings" } };
|
|
456
|
+
string[,] entries = { { "FileNew", "new" }, { "FileOpen", "open" }, { "FileSave", "save" }, { "FileSaveAs", "saveAs" }, { "FileImport", "import" }, { "FileExport", "export" }, { "ViewHome", "home" }, { "ViewEditor", "editor" }, { "ViewConverter", "converter" }, { "ViewPlayer", "player" }, { "ViewMixer", "mixer" }, { "SettingsGeneral", "settings" } };
|
|
457
457
|
for (int i = 0; i < entries.GetLength(0); i++) { string gesture = InterfacePreferences.Gesture(entries[i, 1]); C<MenuItem>(entries[i, 0]).InputGestureText = gesture == "Unassigned" ? "" : gesture; }
|
|
458
458
|
}
|
|
459
459
|
private async void HandleInterfaceKey(object sender, System.Windows.Input.KeyEventArgs e)
|
|
@@ -466,7 +466,7 @@ namespace DrumBuddy
|
|
|
466
466
|
bool text = System.Windows.Input.Keyboard.FocusedElement is TextBox || System.Windows.Input.Keyboard.FocusedElement is ComboBox;
|
|
467
467
|
if (text && !action.StartsWith("zoom", StringComparison.Ordinal) && !new[] { "save", "saveAs", "open", "new", "settings" }.Contains(action)) return;
|
|
468
468
|
if (e.IsRepeat && !action.StartsWith("zoomIn", StringComparison.Ordinal) && !action.StartsWith("zoomOut", StringComparison.Ordinal)) { e.Handled = true; return; }
|
|
469
|
-
if (workspace != 3 && !(workspace == 2 && new[] { "play", "stop", "start", "end" }.Contains(action)) && !new[] { "home", "editor", "converter", "player", "settings", "timelineSettings", "new", "open", "save", "saveAs", "import", "export" }.Contains(action)) return;
|
|
469
|
+
if (workspace != 3 && !(workspace == 2 && new[] { "play", "stop", "start", "startAlternate", "end" }.Contains(action)) && !new[] { "home", "editor", "converter", "player", "mixer", "settings", "timelineSettings", "new", "open", "save", "saveAs", "import", "export" }.Contains(action)) return;
|
|
470
470
|
e.Handled = true; await ExecuteInterfaceAction(action);
|
|
471
471
|
}
|
|
472
472
|
private async Task ExecuteInterfaceAction(string action)
|
|
@@ -481,6 +481,7 @@ namespace DrumBuddy
|
|
|
481
481
|
case "audio": OpenSettings("audio"); return;
|
|
482
482
|
case "keybinds": OpenSettings("keybinds"); return;
|
|
483
483
|
case "control": C<Button>("ControlTab").RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); return;
|
|
484
|
+
case "mixer": if (workspace != 3) OpenWorkspace(true); if (workspace == 3 && editorView != null) editorView.ToggleMixer(); return;
|
|
484
485
|
case "recordOptions": if (workspace != 3) OpenWorkspace(true); if (editorView != null) editorView.ShowRecordingOptions(true); return;
|
|
485
486
|
case "record": if (recorder != null) await StopRecording(true); else await StartRecording(); return;
|
|
486
487
|
case "play": if (recorder != null) { await StopRecording(true); return; } break;
|
|
@@ -730,15 +731,16 @@ namespace DrumBuddy
|
|
|
730
731
|
recorder.Error = new IOException("No audio arrived from the selected input. Check its connection, microphone permissions and selected channels.");
|
|
731
732
|
if (recorder.SourceFormat != null)
|
|
732
733
|
C<TextBlock>("CaptureFormatText").Text = "Input: " + (recorder.SourceIsFloat ? "native 32-bit float" : recorder.SourceFormat) + " | WAV: " + recorder.Bits + (recorder.Bits == 32 ? "-bit float" : "-bit PCM") + " | " + recorder.SampleRate + " Hz";
|
|
733
|
-
bool tooLoud = false;
|
|
734
|
+
bool tooLoud = false; var mixerInputs = new float[8];
|
|
734
735
|
for (int i = 0; i < 8; i++) {
|
|
735
|
-
float peak = Interlocked.Exchange(ref recorder.Peaks[i],0);
|
|
736
|
+
float peak = Interlocked.Exchange(ref recorder.Peaks[i],0); mixerInputs[i] = peak;
|
|
736
737
|
double db = peak > 0 ? 20 * Math.Log10(peak) : -60;
|
|
737
738
|
meters[i].Value = Math.Max(0,Math.Min(1,(db + 60) / 60));
|
|
738
739
|
meters[i].Foreground = Brush(peak >= .99 ? "#B52C36" : "#007A6B");
|
|
739
740
|
meterLabels[i].Text = peak > .001 ? db.ToString("0",CultureInfo.InvariantCulture) + " dB" : "—";
|
|
740
741
|
tooLoud |= peak >= .99;
|
|
741
742
|
}
|
|
743
|
+
if (editorView != null) editorView.UpdateRecordingInputMeters(mixerInputs);
|
|
742
744
|
C<TextBlock>("RecordHint").Text = recorder.Bits == 32 && recorder.SourceIsFloat
|
|
743
745
|
? (tooLoud ? "Peaks above 0 dBFS are kept in the float master. Enable Prevent PCM clipping when making a PCM copy." : "Recording the original float samples. Monitor through the recorder's headphones.")
|
|
744
746
|
: (tooLoud ? "Input is near full scale. Turn down the recorder's input level to prevent clipping." : "Set input levels on the recorder. Adjust a copy in Media Player.");
|
|
@@ -764,6 +766,7 @@ namespace DrumBuddy
|
|
|
764
766
|
"Set safe level brings the loudest peak to −1 dBFS. Prevent clipping caps excessive gain evenly; it preserves the dynamics of your drums.\n\n" +
|
|
765
767
|
"ZOOM R16\nExports are 44.1 kHz PCM WAV, mono or stereo, with names up to 8 uppercase characters. Use the bit depth of your R16 project. Copy WAVs to that project's AUDIO folder, then assign them to tracks on the R16.\n\n" +
|
|
766
768
|
"EDITOR + RECORD\nThe Editor opens by default. Record options contains input, channels, WAV format and sample rate. Record or R arms the selected track and shows one bar of countdown at the project tempo. Separate inputs reserve consecutive tracks. Stop, Record again or Escape cancels the countdown. Capture then starts through the selected input; the red playhead and elapsed timer follow captured audio. Stop & save preserves the take and adds it at the original starting point, leaving the playhead at its end. Use hardware direct monitoring while recording; software overdub synchronisation is not provided.\n\n" +
|
|
769
|
+
"MIXER\nOpen Mixer in the Editor or View menu. Track faders, pan, mute/solo and a master fader affect playback and exported mixes. File peak and Before fader meters help identify quiet sources; After fader shows the adjusted signal. Red peak indicators latch at 0 dBFS; click to clear. Monitor changes listening volume only. During recording, meters show the selected raw inputs; set capture gain on the input device.\n\n" +
|
|
767
770
|
"H5STUDIO FLOAT\nOn the recorder choose USB > Audio Interface > Connect. Audio I/F Only supports 44.1, 48 and 96 kHz. Stereo mix exposes a mixed stereo signal; Multi Track exposes separate inputs. Choose 32-bit float to keep the original float samples, including peaks above 0 dBFS. The app checks that the H5studio driver is actually sending float samples.\n\n" +
|
|
768
771
|
"PLAYBACK COMPATIBILITY\nSome players reject float or multichannel WAV files. Open Converter from View, enable Prevent PCM clipping and choose 16-bit or 24-bit WAV PCM. The original float master remains unchanged.\n\n" +
|
|
769
772
|
"FULL CONTROL\nAn R16 front-panel view with PC multitrack playback, mixer, EQ, recording and Mackie MIDI control. Open Menu reveals files, track settings, saved effects and devices. Escape returns home.\n\n" +
|
|
@@ -771,9 +774,9 @@ namespace DrumBuddy
|
|
|
771
774
|
"Use the R16 gain knobs to set recording levels. Listen through the R16's direct headphone monitoring.\n\n" +
|
|
772
775
|
"Install Drivers opens a short guide and links to Zoom's official driver download pages.\n\n" +
|
|
773
776
|
"MEDIA PLAYER\nOpen audio or video to listen. Gain / Save amplifies or reduces playback and saves adjusted WAV copies; Playback volume changes listening level only. Reset, Set safe level and Prevent clipping are available. Choose speakers/headphones under Audio output. Gain / Save can be resized, collapsed, or displayed on the side or bottom.\n\n" +
|
|
774
|
-
"EDITOR\nA 16-track audio timeline. Drag the upper ruler and release to place the playhead. The left edge snaps to zero; Home
|
|
777
|
+
"EDITOR\nA 16-track audio timeline. Drag the upper ruler and release to place the playhead. The left edge snaps to zero; Home, Enter and To start go to zero; End goes to the timeline end. Ctrl+Left/Right jumps to the nearest bar line strictly left/right using the project tempo and time signature. Hold Alt to bypass snapping. Drag across a track to select time; two thinner blue sticks mark the selection start/end. Drag their lower-ruler handles to adjust the boundaries. Cut/copy/paste, split, undo/redo, Space to play/pause and Ctrl+S to save. Cuts leave a gap. Choose Selected track or All 16 tracks for edits. Save .dbedit sessions and export PCM or float WAV mixes. Original source files are preserved.\n\n" +
|
|
775
778
|
"THEME AND SETTINGS\nUse the top-right switch for dark or light theme. Settings has categories for General, Audio and Keybinds. Click Edit on a keybind and press the new shortcut immediately. Save writes the changes; leaving with pending changes asks whether to save them. S splits at the playhead; V returns to the selection cursor.\n\n" +
|
|
776
|
-
"Basic tools run locally without an account. This release is for Windows; macOS is planned.\n\nFlux 1.6.
|
|
779
|
+
"Basic tools run locally without an account. This release is for Windows; macOS is planned.\n\nFlux 1.6.4 · FFmpeg + NAudio",
|
|
777
780
|
"Flux — quick help",MessageBoxButton.OK,MessageBoxImage.Information);
|
|
778
781
|
}
|
|
779
782
|
}
|
|
@@ -788,6 +791,7 @@ namespace DrumBuddy
|
|
|
788
791
|
if (args.Length > 0 && args[0] == "--self-test") return SelfTest.Run(args.Skip(1).ToArray());
|
|
789
792
|
if (args.Length > 0 && args[0] == "--float-self-test") return FloatRecordingTests.Run(args.Skip(1).ToArray());
|
|
790
793
|
if (args.Length > 0 && args[0] == "--recording-self-test") return RecordingTests.Run(args.Skip(1).ToArray());
|
|
794
|
+
if (args.Length > 0 && args[0] == "--mixer-self-test") return EditorMixerTests.Run(args.Skip(1).ToArray());
|
|
791
795
|
if (args.Length > 0 && args[0] == "--grid-self-test") return TempoGridTests.Run(args.Skip(1).ToArray());
|
|
792
796
|
if (args.Length > 0 && args[0] == "--zoom-self-test") return ZoomTests.Run(args.Skip(1).ToArray());
|
|
793
797
|
if (args.Length > 0 && args[0] == "--selection-self-test") return EditorSelectionTests.Run(args.Skip(1).ToArray());
|
package/source/Main.xaml
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<MenuItem x:Name="ViewHome" Header="Home" />
|
|
85
85
|
<MenuItem x:Name="ViewEditor" Header="Editor + Record" />
|
|
86
86
|
<MenuItem x:Name="ViewRecording" Header="Recording options" />
|
|
87
|
+
<MenuItem x:Name="ViewMixer" Header="Mixer" />
|
|
87
88
|
<MenuItem x:Name="ViewConverter" Header="Converter" />
|
|
88
89
|
<MenuItem x:Name="ViewPlayer" Header="Media Player" />
|
|
89
90
|
<MenuItem x:Name="ViewControl" Header="R16 Full Control" />
|
|
@@ -187,7 +187,7 @@ namespace DrumBuddy
|
|
|
187
187
|
case "import": await ChooseFiles(); break;
|
|
188
188
|
case "play": PlayPause(); break;
|
|
189
189
|
case "stop": Stop(); break;
|
|
190
|
-
case "start": Seek(0); break;
|
|
190
|
+
case "start": case "startAlternate": Seek(0); break;
|
|
191
191
|
case "end": if (Selected != null) Seek(Selected.Duration); break;
|
|
192
192
|
default: return false;
|
|
193
193
|
}
|
package/source/Quick Start.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FLUX 1.6.
|
|
1
|
+
FLUX 1.6.4
|
|
2
2
|
|
|
3
3
|
INSTALL / UPDATE
|
|
4
4
|
Close the app. Run npm.cmd install -g @pro-gram/flux@latest
|
|
@@ -42,6 +42,19 @@ leaving the playhead at its end. Source files are preserved.
|
|
|
42
42
|
Bar lines are labeled 1, 2, 3; beat labels remain available when zoomed in.
|
|
43
43
|
Use direct headphone monitoring. Software overdub synchronization is not included.
|
|
44
44
|
|
|
45
|
+
MIXER
|
|
46
|
+
Open Mixer from the Editor toolbar or View menu. Scroll through all 16 tracks;
|
|
47
|
+
the master stays visible. Drag the divider to resize, or choose Side/Bottom.
|
|
48
|
+
Track faders (-60 to +24 dB), pan, mute and solo apply during playback and export.
|
|
49
|
+
The master fader (-60 to +12 dB) changes the entire mix. Settings save in .dbedit
|
|
50
|
+
sessions and support Undo/Redo. Original audio files stay unchanged.
|
|
51
|
+
File peak shows the source level independently of gain. Before fader shows
|
|
52
|
+
source signal, including muted tracks; After fader shows the adjusted level.
|
|
53
|
+
Red peak readouts latch at 0 dBFS; click a readout or Clear peaks to reset.
|
|
54
|
+
Monitor is listening volume only and does not affect exports or meter readings.
|
|
55
|
+
During recording the strips show the registered raw inputs. Set capture gain
|
|
56
|
+
on your input device; the mixer does not change the recorded original.
|
|
57
|
+
|
|
45
58
|
AUDIO DEVICES
|
|
46
59
|
Audio devices at the top right opens Settings > Audio. Save device changes.
|
|
47
60
|
Windows default shows and follows the current Windows speakers/headphones.
|
|
@@ -81,7 +94,9 @@ Ctrl+D: duplicate. Ctrl+L: repeat selected clips/range. M: clip mute. X: crossfa
|
|
|
81
94
|
Right-click audio for the editing menu. Edits, fades and mute support Undo.
|
|
82
95
|
Ctrl+X/C/V: cut/copy/paste. Backspace/Delete: remove selection. Ctrl+Z/Y: undo/redo.
|
|
83
96
|
Ctrl+S: save session. Ctrl+Shift+S: save as. Ctrl+O/N: open/new.
|
|
84
|
-
Ctrl+I/E: import/export. Home
|
|
97
|
+
Ctrl+I/E: import/export. Home or Enter: timeline beginning. End: timeline end.
|
|
98
|
+
Ctrl+Left/Right: nearest bar line strictly left/right using the project tempo
|
|
99
|
+
and time signature. From a bar line, jump to the previous/next bar.
|
|
85
100
|
Ctrl+1/2/3/4: Home/Editor/Converter/Player. Ctrl+comma: Settings.
|
|
86
101
|
|
|
87
102
|
MEDIA PLAYER / R16
|
package/source/RecordingTests.cs
CHANGED
|
@@ -95,6 +95,8 @@ namespace DrumBuddy
|
|
|
95
95
|
Picture(editor, Path.Combine(root, "countdown-dark.png"), 1280, 760);
|
|
96
96
|
Check(!await editor.ExecuteAction("start") && editor.PlayheadFrame == plan.AnchorFrame,
|
|
97
97
|
"Protect the recording anchor from transport edits during count-in");
|
|
98
|
+
Check(!await editor.ExecuteAction("previousBar") && !await editor.ExecuteAction("nextBar") && !await editor.ExecuteAction("startAlternate") && editor.PlayheadFrame == plan.AnchorFrame,
|
|
99
|
+
"Ignore bar navigation and Enter during count-in so the registered recording anchor cannot move");
|
|
98
100
|
editor.SetRecordingState(false);
|
|
99
101
|
Check(!editor.IsBusy && editor.ArmedTracksForTest.Length == 0 && editor.RecordingOverlaysForTest == 0,
|
|
100
102
|
"Release reserved tracks and recording visuals after cancellation");
|
package/source/Setup.cs
CHANGED
|
@@ -14,7 +14,7 @@ using System.Windows.Markup;
|
|
|
14
14
|
using Microsoft.Win32;
|
|
15
15
|
|
|
16
16
|
[assembly: AssemblyTitle("Flux Setup")]
|
|
17
|
-
[assembly: AssemblyVersion("1.6.
|
|
17
|
+
[assembly: AssemblyVersion("1.6.4.0")]
|
|
18
18
|
|
|
19
19
|
namespace DrumBuddySetup
|
|
20
20
|
{
|
|
@@ -101,7 +101,7 @@ namespace DrumBuddySetup
|
|
|
101
101
|
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
|
102
102
|
File.Copy(source,destination,true);
|
|
103
103
|
}
|
|
104
|
-
File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.
|
|
104
|
+
File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.4\n" + InstallPath,Encoding.UTF8);
|
|
105
105
|
string[] files = Directory.GetFiles(stage,"*",SearchOption.AllDirectories).Select(f => f.Substring(stage.Length + 1)).Concat(new[] { ".drumbuddy-installed","installed-files.txt" }).ToArray();
|
|
106
106
|
File.WriteAllLines(Path.Combine(InstallPath,"installed-files.txt"),files,Encoding.UTF8);
|
|
107
107
|
string exe = Path.Combine(InstallPath,"DrumBuddy.exe");
|
|
@@ -112,7 +112,7 @@ namespace DrumBuddySetup
|
|
|
112
112
|
RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),"Programs","Drum Buddy.lnk"),exe);
|
|
113
113
|
RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),"Drum Buddy.lnk"),exe);
|
|
114
114
|
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(RegistryKey)) {
|
|
115
|
-
key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.
|
|
115
|
+
key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.4"); key.SetValue("Publisher","Flux");
|
|
116
116
|
key.SetValue("InstallLocation",InstallPath); key.SetValue("DisplayIcon",BrandIcon);
|
|
117
117
|
key.SetValue("UninstallString","\"" + Path.Combine(InstallPath,"Uninstall.exe") + "\"");
|
|
118
118
|
key.SetValue("NoModify",1); key.SetValue("NoRepair",1);
|
package/source/ShellStyles.xaml
CHANGED
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
</Style>
|
|
26
26
|
<Style TargetType="Slider">
|
|
27
27
|
<Setter Property="Height" Value="22"/>
|
|
28
|
-
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Slider"><Track x:Name="PART_Track" Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}" Value="{Binding Value,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" VerticalAlignment="Center">
|
|
29
|
-
<Track.DecreaseRepeatButton><RepeatButton Command="Slider.DecreaseLarge"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{
|
|
30
|
-
<Track.Thumb><Thumb Width="12" Height="12"><Thumb.Template><ControlTemplate TargetType="Thumb"><Ellipse Fill="{DynamicResource Theme182C37}"/></ControlTemplate></Thumb.Template></Thumb></Track.Thumb>
|
|
31
|
-
<Track.IncreaseRepeatButton><RepeatButton Command="Slider.IncreaseLarge"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{
|
|
32
|
-
</Track
|
|
28
|
+
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Slider"><Track x:Name="PART_Track" Orientation="{TemplateBinding Orientation}" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}" Value="{Binding Value,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" VerticalAlignment="Center">
|
|
29
|
+
<Track.DecreaseRepeatButton><RepeatButton x:Name="Decrease" Command="Slider.DecreaseLarge" Height="3" Background="{DynamicResource Theme006D68}"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{TemplateBinding Background}" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.DecreaseRepeatButton>
|
|
30
|
+
<Track.Thumb><Thumb x:Name="SliderThumb" Width="12" Height="12"><Thumb.Template><ControlTemplate TargetType="Thumb"><Ellipse Fill="{DynamicResource Theme182C37}"/></ControlTemplate></Thumb.Template></Thumb></Track.Thumb>
|
|
31
|
+
<Track.IncreaseRepeatButton><RepeatButton x:Name="Increase" Command="Slider.IncreaseLarge" Height="3" Background="{DynamicResource ThemeC3CED0}"><RepeatButton.Template><ControlTemplate TargetType="RepeatButton"><Border Background="{TemplateBinding Background}" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.IncreaseRepeatButton>
|
|
32
|
+
</Track><ControlTemplate.Triggers><Trigger Property="Orientation" Value="Vertical">
|
|
33
|
+
<Setter TargetName="PART_Track" Property="VerticalAlignment" Value="Stretch"/>
|
|
34
|
+
<Setter TargetName="Decrease" Property="Height" Value="Auto"/><Setter TargetName="Decrease" Property="Width" Value="3"/>
|
|
35
|
+
<Setter TargetName="Increase" Property="Height" Value="Auto"/><Setter TargetName="Increase" Property="Width" Value="3"/>
|
|
36
|
+
<Setter TargetName="SliderThumb" Property="Width" Value="22"/><Setter TargetName="SliderThumb" Property="Height" Value="12"/>
|
|
37
|
+
<Setter TargetName="SliderThumb" Property="Template"><Setter.Value><ControlTemplate TargetType="Thumb"><Border Background="{DynamicResource Theme182C37}" BorderBrush="{DynamicResource Theme006D68}" BorderThickness="1" CornerRadius="2"><Border Height="1" Margin="3,0" Background="{DynamicResource Theme526771}"/></Border></ControlTemplate></Setter.Value></Setter>
|
|
38
|
+
</Trigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter>
|
|
33
39
|
</Style>
|
|
34
40
|
<Style TargetType="TextBox">
|
|
35
41
|
<Setter Property="Background" Value="{DynamicResource ThemeFFFFFF}"/><Setter Property="Foreground" Value="{DynamicResource Theme182C37}"/>
|
package/source/TempoGrid.cs
CHANGED
|
@@ -34,6 +34,20 @@ namespace DrumBuddy
|
|
|
34
34
|
public static double StepSeconds(TempoGridOptions options) { return 240.0 / options.Tempo / options.Division * (options.Triplet ? 2.0 / 3 : 1); }
|
|
35
35
|
public static double BeatSeconds(TempoGridOptions options) { return 240.0 / options.Tempo / options.BeatUnit; }
|
|
36
36
|
public static double BarSeconds(TempoGridOptions options) { return BeatSeconds(options) * options.BeatsPerBar; }
|
|
37
|
+
public static long AdjacentBar(long frame, TempoGridOptions options, int direction)
|
|
38
|
+
{
|
|
39
|
+
options.Validate();
|
|
40
|
+
if (direction != -1 && direction != 1) throw new ArgumentOutOfRangeException("direction");
|
|
41
|
+
frame = Math.Max(0, Math.Min(EditorSession.MaxFrame, frame));
|
|
42
|
+
if (direction < 0 && frame == 0 || direction > 0 && frame == EditorSession.MaxFrame) return frame;
|
|
43
|
+
double framesPerBar = BarSeconds(options) * Audio.Rate;
|
|
44
|
+
long index = (long)Math.Floor(frame / framesPerBar);
|
|
45
|
+
if (direction > 0) {
|
|
46
|
+
do { index++; } while (FrameAt(index * framesPerBar) <= frame);
|
|
47
|
+
}
|
|
48
|
+
else while (index > 0 && FrameAt(index * framesPerBar) >= frame) index--;
|
|
49
|
+
return FrameAt(index * framesPerBar);
|
|
50
|
+
}
|
|
37
51
|
public static long Snap(long frame, TempoGridOptions options)
|
|
38
52
|
{
|
|
39
53
|
double seconds = Math.Max(0, frame) / (double)Audio.Rate, bar = BarSeconds(options), step = StepSeconds(options);
|
package/source/TempoGridTests.cs
CHANGED
|
@@ -35,6 +35,32 @@ namespace DrumBuddy
|
|
|
35
35
|
try {
|
|
36
36
|
InterfacePreferences.Current = InterfacePreferences.Defaults();
|
|
37
37
|
var grid = new TempoGridOptions(); grid.Validate();
|
|
38
|
+
Check(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control) == "previousBar" && InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control) == "nextBar", "Resolve Ctrl+Left and Ctrl+Right to bar navigation");
|
|
39
|
+
Check(InterfacePreferences.Resolve(Key.Enter, ModifierKeys.None) == "startAlternate" && InterfacePreferences.Resolve(Key.Return, ModifierKeys.None) == "startAlternate" && InterfacePreferences.Resolve(Key.Home, ModifierKeys.None) == "start" && InterfacePreferences.Gesture("startAlternate") == "Enter", "Resolve Enter and Return to the beginning while retaining Home and showing Enter in keybind settings");
|
|
40
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 3L, grid, -1) == Audio.Rate * 2L && TempoGrid.AdjacentBar(Audio.Rate * 3L, grid, 1) == Audio.Rate * 4L, "Jump from within a bar to the nearest bar strictly left or right");
|
|
41
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 2L, grid, -1) == 0 && TempoGrid.AdjacentBar(Audio.Rate * 2L, grid, 1) == Audio.Rate * 4L, "Jump a full bar when already exactly on a bar line");
|
|
42
|
+
Check(TempoGrid.AdjacentBar(Audio.Rate * 2L - 1, grid, 1) == Audio.Rate * 2L && TempoGrid.AdjacentBar(Audio.Rate * 2L + 1, grid, -1) == Audio.Rate * 2L, "Find the neighboring bar from one sample on either side of its boundary");
|
|
43
|
+
Check(TempoGrid.AdjacentBar(0, grid, -1) == 0 && TempoGrid.AdjacentBar(EditorSession.MaxFrame, grid, 1) == EditorSession.MaxFrame && TempoGrid.AdjacentBar(EditorSession.MaxFrame - 1, grid, 1) == EditorSession.MaxFrame, "Clamp bar navigation at the beginning and timeline limit");
|
|
44
|
+
var oddMeter = new TempoGridOptions { Tempo = 123, BeatsPerBar = 7, BeatUnit = 8, Division = 256, Triplet = true };
|
|
45
|
+
double barFrames = TempoGrid.BarSeconds(oddMeter) * Audio.Rate;
|
|
46
|
+
foreach (int barIndex in new[] { 1, 2, 13, 10000, 50000 }) {
|
|
47
|
+
long barFrame = (long)Math.Round(barIndex * barFrames, MidpointRounding.AwayFromZero);
|
|
48
|
+
Check(TempoGrid.AdjacentBar(barFrame, oddMeter, -1) == (long)Math.Round((barIndex - 1) * barFrames, MidpointRounding.AwayFromZero)
|
|
49
|
+
&& TempoGrid.AdjacentBar(barFrame, oddMeter, 1) == (long)Math.Round((barIndex + 1) * barFrames, MidpointRounding.AwayFromZero)
|
|
50
|
+
&& TempoGrid.AdjacentBar(barFrame + 1, oddMeter, -1) == barFrame && TempoGrid.AdjacentBar(barFrame - 1, oddMeter, 1) == barFrame,
|
|
51
|
+
"Avoid rounded-frame drift with fractional bar lengths in 7/8 at bar " + barIndex);
|
|
52
|
+
}
|
|
53
|
+
var oldKeys = InterfacePreferences.Defaults(); oldKeys.Keybinds.RemoveAll(b => new[] { "previousBar", "nextBar", "startAlternate" }.Contains(b.Action));
|
|
54
|
+
InterfacePreferences.Upgrade(oldKeys); InterfacePreferences.Validate(oldKeys);
|
|
55
|
+
Check(oldKeys.Keybinds.Any(b => b.Action == "previousBar" && b.Key == "Left" && b.Modifiers == (int)ModifierKeys.Control) && oldKeys.Keybinds.Any(b => b.Action == "startAlternate" && b.Key == "Return"), "Add new navigation shortcuts to existing saved keymaps");
|
|
56
|
+
var customNavigation = InterfacePreferences.Defaults(); customNavigation.Keybinds.RemoveAll(b => new[] { "previousBar", "nextBar", "startAlternate" }.Contains(b.Action));
|
|
57
|
+
var customLeft = customNavigation.Keybinds.First(b => b.Action == "split"); customLeft.Key = "Left"; customLeft.Modifiers = (int)ModifierKeys.Control;
|
|
58
|
+
customNavigation.Keybinds.First(b => b.Action == "play").Key = "Enter";
|
|
59
|
+
InterfacePreferences.Upgrade(customNavigation); InterfacePreferences.Validate(customNavigation);
|
|
60
|
+
Check(customNavigation.Keybinds.First(b => b.Action == "previousBar").Key == "None" && customNavigation.Keybinds.First(b => b.Action == "startAlternate").Key == "None" && customNavigation.Keybinds.First(b => b.Action == "play").Key == "Enter", "Preserve customized Ctrl+Left and Enter bindings when upgrading a keymap");
|
|
61
|
+
var duplicateEnter = InterfacePreferences.Defaults(); duplicateEnter.Keybinds.First(b => b.Action == "play").Key = "Enter"; bool aliasRejected = false;
|
|
62
|
+
try { InterfacePreferences.Validate(duplicateEnter); } catch (IOException) { aliasRejected = true; }
|
|
63
|
+
Check(aliasRejected, "Treat Enter and Return as the same key when checking shortcut conflicts");
|
|
38
64
|
Check(Math.Abs(TempoGrid.StepSeconds(grid) - .125) < 1e-12, "At 120 BPM, a sixteenth note lasts 0.125 seconds");
|
|
39
65
|
grid.Division = 32; Check(Math.Abs(TempoGrid.StepSeconds(grid) - .0625) < 1e-12, "At 120 BPM, a thirty-second note lasts 0.0625 seconds");
|
|
40
66
|
grid.Division = 256; Check(Math.Abs(TempoGrid.StepSeconds(grid) - .0078125) < 1e-12, "Allow fine divisions through 1/256");
|
|
@@ -99,6 +125,18 @@ namespace DrumBuddy
|
|
|
99
125
|
editor.ApplyGridOptions(new TempoGridOptions { Tempo = 150, Division = 32 }); RunUi(() => editor.ExecuteAction("undo"));
|
|
100
126
|
Check(editor.GridOptions.Tempo == 120 && editor.SessionForTest.Regions.Any(r => r.Start == 88200), "Undo a tempo-grid change without undoing the audio paste");
|
|
101
127
|
Check(original.SequenceEqual(File.ReadAllBytes(source)), "Keep the original float audio unchanged through grid editing and copy/delete/paste");
|
|
128
|
+
editor.PlayheadForTest(Audio.Rate * 3L);
|
|
129
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control)));
|
|
130
|
+
Check(editor.PlayheadFrame == Audio.Rate * 2L, "Move the actual editor playhead to the left bar line");
|
|
131
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Left, ModifierKeys.Control)));
|
|
132
|
+
Check(editor.PlayheadFrame == 0, "Repeated Ctrl+Left moves to the preceding bar from an exact boundary");
|
|
133
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control)));
|
|
134
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Right, ModifierKeys.Control)));
|
|
135
|
+
Check(editor.PlayheadFrame == Audio.Rate * 4L, "Repeated Ctrl+Right moves the editor forward by complete bars");
|
|
136
|
+
RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Enter, ModifierKeys.None)));
|
|
137
|
+
Check(editor.PlayheadFrame == 0, "Enter returns the actual editor playhead to the beginning");
|
|
138
|
+
editor.PlayheadForTest(Audio.Rate * 3L); RunUi(() => editor.ExecuteAction(InterfacePreferences.Resolve(Key.Home, ModifierKeys.None)));
|
|
139
|
+
Check(editor.PlayheadFrame == 0 && original.SequenceEqual(File.ReadAllBytes(source)), "Home still returns to the beginning and transport navigation preserves original audio");
|
|
102
140
|
}
|
|
103
141
|
owner.Content = null; owner.Close();
|
|
104
142
|
File.WriteAllLines(Path.Combine(root, "grid-test-results.txt"), log, Encoding.UTF8); return 0;
|