@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.
@@ -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("Listen volume ", 14));
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
- recordingPane = new DockablePane(editorBody, null, "Record options", false, false, 260) { MinimumMainHeight = 300 }; Children.Add(recordingPane);
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) Change(delegate { session.Tracks[index].Mute = mute.IsChecked == true; return true; }, "Track mute changed."); }; strip.Children.Add(mute);
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) Change(delegate { session.Tracks[index].Solo = solo.IsChecked == true; return true; }, "Track solo changed."); }; strip.Children.Add(solo);
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.SelectedIndex >= 0) { double value = levels[level.SelectedIndex]; Change(delegate { session.Tracks[index].GainDb = value; return true; }, "Track level changed."); } }; strip.Children.Add(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 = binding.Key + ":" + binding.Modifiers;
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
- if (settings.Keybinds.Any(b => b.Key == binding.Key && b.Modifiers == binding.Modifiers)) { binding.Key = "None"; binding.Modifiers = 0; }
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 == key.ToString() && b.Modifiers == (int)modifiers);
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.2.0")]
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/To start goes to zero and End goes to the timeline end. 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" +
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.2 · FFmpeg + NAudio",
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
  }
@@ -1,4 +1,4 @@
1
- FLUX 1.6.2
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/End: timeline beginning/end.
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
@@ -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.2.0")]
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.2\n" + InstallPath,Encoding.UTF8);
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.2"); key.SetValue("Publisher","Flux");
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);
@@ -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="{DynamicResource Theme006D68}" Height="3" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.DecreaseRepeatButton>
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="{DynamicResource ThemeC3CED0}" Height="3" CornerRadius="1"/></ControlTemplate></RepeatButton.Template></RepeatButton></Track.IncreaseRepeatButton>
32
- </Track></ControlTemplate></Setter.Value></Setter>
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}"/>
@@ -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);
@@ -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;
@@ -1,4 +1,4 @@
1
- Flux 1.6.2 - third-party components
1
+ Flux 1.6.4 - third-party components
2
2
 
3
3
  FFmpeg 9.0.2, release essentials build by Gyan Doshi
4
4
  Used as a separate command-line decoding process.