@pro-gram/flux 1.6.1 → 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/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.1.0")]
23
+ [assembly: AssemblyVersion("1.6.4.0")]
24
24
 
25
25
  namespace DrumBuddy
26
26
  {
@@ -79,6 +79,9 @@ namespace DrumBuddy
79
79
  private CancellationTokenSource cancel;
80
80
  private bool busy, closeWhenIdle, stopping;
81
81
  private Recorder recorder;
82
+ private CancellationTokenSource countIn;
83
+ private Task recordingStartup;
84
+ private bool captureActive;
82
85
  private readonly ProgressBar[] meters = new ProgressBar[8];
83
86
  private readonly TextBlock[] meterLabels = new TextBlock[8];
84
87
  private readonly DispatcherTimer timer;
@@ -161,7 +164,7 @@ namespace DrumBuddy
161
164
  C<RadioButton>("RecordFloat").Checked += delegate { ShowRecordingFormat(); };
162
165
  C<RadioButton>("Record16").Checked += delegate { ShowRecordingFormat(); };
163
166
  C<RadioButton>("Record24").Checked += delegate { ShowRecordingFormat(); };
164
- C<Button>("StartRecordButton").Click += delegate { StartRecording(); };
167
+ C<Button>("StartRecordButton").Click += async delegate { await StartRecording(); };
165
168
  C<Button>("StopRecordButton").Click += async delegate { await StopRecording(true); };
166
169
  C<Button>("HelpButton").Click += delegate { ShowHelp(); };
167
170
  C<Button>("InstallDriversButton").Click += delegate { DriverSetup.ShowGuide(window); };
@@ -184,6 +187,7 @@ namespace DrumBuddy
184
187
  e.Handled = true;
185
188
  };
186
189
  window.Closing += async delegate(object sender, System.ComponentModel.CancelEventArgs e) {
190
+ if (countIn != null) { e.Cancel = true; await StopRecording(false); window.Close(); return; }
187
191
  if (recorder != null) {
188
192
  e.Cancel = true;
189
193
  if (stopping) return;
@@ -244,6 +248,11 @@ namespace DrumBuddy
244
248
  public void Show() { window.Show(); }
245
249
  public Window NativeWindow { get { return window; } }
246
250
  internal void OpenEditorForTest() { OpenWorkspace(true); }
251
+ internal Task StartRecordingForTest() { return StartRecording(); }
252
+ internal Task StopRecordingForTest() { return StopRecording(true); }
253
+ internal Task RecordingTickForTest() { return Tick(); }
254
+ internal bool RecordingBusyForTest { get { return recorder != null || countIn != null; } }
255
+ internal EditorView RecordingEditorForTest { get { return editorView; } }
247
256
  internal void OpenHomeForTest() { OpenHome(); }
248
257
  internal void EditorFixtureForTest(EditorSession session, IDictionary<string, Clip> clips) { editorView.SetForTest(session, clips); editorView.ShowSelectionForTest(Audio.Rate, Audio.Rate * 4L); }
249
258
  internal void RecordingPanelForTest()
@@ -291,7 +300,7 @@ namespace DrumBuddy
291
300
  editorView = new EditorView(window, System.IO.Path.Combine(cache, "editor"), Status, delegate { if (mediaPlayerView != null) mediaPlayerView.Stop(); });
292
301
  C<Grid>("EditorHost").Children.Add(editorView);
293
302
  var panel = C<StackPanel>("RecordPage"); var parent = panel.Parent as Panel; if (parent != null) parent.Children.Remove(panel);
294
- panel.Visibility = Visibility.Visible; editorView.AttachRecordingPanel(panel); editorView.RecordRequested = async delegate { if (recorder == null) StartRecording(); else await StopRecording(true); };
303
+ panel.Visibility = Visibility.Visible; editorView.AttachRecordingPanel(panel); editorView.RecordRequested = async delegate { if (recorder == null) await StartRecording(); else await StopRecording(true); };
295
304
  editorView.StopRequested = async delegate { if (recorder != null) await StopRecording(true); };
296
305
  editorView.RecordingOptionsRequested = delegate { var input = C<ComboBox>("InputDevice").SelectedItem as InputChoice; if (input != null && configuredDriver != input.Key && recorder == null) ConfigureRecordingDevice(); };
297
306
  editorView.GridSettingsRequested = delegate { OpenSettings("timeline"); };
@@ -435,7 +444,7 @@ namespace DrumBuddy
435
444
  }
436
445
  private void BindMenus()
437
446
  {
438
- 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" } };
439
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); }; }
440
449
  C<MenuItem>("FileExit").Click += delegate { window.Close(); };
441
450
  C<MenuItem>("InstallAudioDrivers").Click += delegate { DriverSetup.ShowGuide(window); };
@@ -444,19 +453,20 @@ namespace DrumBuddy
444
453
  }
445
454
  private void RefreshMenuGestures()
446
455
  {
447
- 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" } };
448
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; }
449
458
  }
450
459
  private async void HandleInterfaceKey(object sender, System.Windows.Input.KeyEventArgs e)
451
460
  {
452
461
  var key = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key;
462
+ if (key == System.Windows.Input.Key.Escape && countIn != null) { e.Handled = true; await StopRecording(true); return; }
453
463
  if (key == System.Windows.Input.Key.Escape && workspace == 3 && editorView != null && editorView.HandleKey(e)) { e.Handled = true; return; }
454
464
  string action = InterfacePreferences.Resolve(key, System.Windows.Input.Keyboard.Modifiers);
455
465
  if (action == null) return;
456
466
  bool text = System.Windows.Input.Keyboard.FocusedElement is TextBox || System.Windows.Input.Keyboard.FocusedElement is ComboBox;
457
467
  if (text && !action.StartsWith("zoom", StringComparison.Ordinal) && !new[] { "save", "saveAs", "open", "new", "settings" }.Contains(action)) return;
458
468
  if (e.IsRepeat && !action.StartsWith("zoomIn", StringComparison.Ordinal) && !action.StartsWith("zoomOut", StringComparison.Ordinal)) { e.Handled = true; return; }
459
- 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;
460
470
  e.Handled = true; await ExecuteInterfaceAction(action);
461
471
  }
462
472
  private async Task ExecuteInterfaceAction(string action)
@@ -471,8 +481,9 @@ namespace DrumBuddy
471
481
  case "audio": OpenSettings("audio"); return;
472
482
  case "keybinds": OpenSettings("keybinds"); return;
473
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;
474
485
  case "recordOptions": if (workspace != 3) OpenWorkspace(true); if (editorView != null) editorView.ShowRecordingOptions(true); return;
475
- case "record": if (recorder != null) await StopRecording(true); else StartRecording(); return;
486
+ case "record": if (recorder != null) await StopRecording(true); else await StartRecording(); return;
476
487
  case "play": if (recorder != null) { await StopRecording(true); return; } break;
477
488
  case "stop": if (recorder != null) { await StopRecording(true); return; } break;
478
489
  }
@@ -610,83 +621,126 @@ namespace DrumBuddy
610
621
  }
611
622
  finally { configuringCapture = false; }
612
623
  }
613
- private async void StartRecording()
624
+ private Task StartRecording()
614
625
  {
615
- if (busy || recorder != null) return;
626
+ if (busy || recorder != null || stopping) return Task.FromResult(0);
627
+ recordingStartup = StartRecordingCore(); return recordingStartup;
628
+ }
629
+ private void LockRecordingControls(bool locked)
630
+ {
631
+ foreach (string name in new[] { "RecordTab", "StartRecordButton", "InputDevice", "InputMode", "Record16", "Record24", "RecordFloat", "RecordRate", "RefreshDevicesButton", "ChangeRecordFolderButton" }) C<Control>(name).IsEnabled = !locked;
632
+ if (!locked) C<Button>("StartRecordButton").IsEnabled = C<ComboBox>("InputDevice").SelectedItem != null;
633
+ C<Button>("StopRecordButton").IsEnabled = locked;
634
+ }
635
+ private async Task StartRecordingCore()
636
+ {
637
+ if (workspace != 3 || editorView == null) OpenWorkspace(true);
638
+ if (editorView == null || editorView.IsBusy) { Status("Finish the editor task before recording."); return; }
616
639
  var input = C<ComboBox>("InputDevice").SelectedItem as InputChoice;
617
- if (input == null) { Status("Choose a recording input under Audio devices or Record drums, then click Refresh."); return; }
640
+ if (input == null) { editorView.ShowRecordingOptions(true); editorView.ShowRecordingError("Choose a recording input under Audio devices or Record options, then click Refresh."); return; }
618
641
  if (configuredDriver != input.Key) ConfigureRecordingDevice();
619
- if (!C<Button>("StartRecordButton").IsEnabled) { Status("Choose a ready recording input in Record options."); return; }
620
- string driver = input.Name;
621
-
622
- int mode = ((ModeChoice)C<ComboBox>("InputMode").SelectedItem).Mode;
642
+ if (!C<Button>("StartRecordButton").IsEnabled) { editorView.ShowRecordingOptions(true); editorView.ShowRecordingError(C<TextBlock>("CaptureFormatText").Text); return; }
643
+ var mode = C<ComboBox>("InputMode").SelectedItem as ModeChoice;
644
+ var rate = C<ComboBox>("RecordRate").SelectedItem as RateChoice;
645
+ if (mode == null || rate == null) { editorView.ShowRecordingError("Choose input channels and a sample rate in Record options."); return; }
623
646
  int bits = C<RadioButton>("RecordFloat").IsChecked == true ? 32 : C<RadioButton>("Record16").IsChecked == true ? 16 : 24;
624
- var rateChoice = C<ComboBox>("RecordRate").SelectedItem as RateChoice;
625
- if (rateChoice == null) { Status("Choose a sample rate before recording."); return; }
626
- if (editorView == null) OpenWorkspace(true);
627
- if (editorView == null || editorView.IsBusy) { Status("Finish the editor task before recording."); return; }
628
- int files = mode == -1 ? configuredInputChannels : 1;
629
- StopWorkspaces();
630
- recordingTrack = editorView.SelectedTrack; recordingAnchor = editorView.PlayheadFrame;
631
- if (recordingTrack + files > 16) { Status("Select an earlier track so all recording channels fit within the 16-track timeline."); return; }
632
- string folder = System.IO.Path.Combine(prefs.RecordFolder,"Take_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff",CultureInfo.InvariantCulture));
633
- recorder = new Recorder();
634
- Exception startError = null;
647
+ StopWorkspaces(); RecordingPlan plan;
648
+ try { plan = editorView.PlanRecording(input, mode.Mode, bits, rate.Rate, configuredInputChannels); }
649
+ catch (Exception ex) { editorView.ShowRecordingError(ex.Message); return; }
650
+ recordingTrack = plan.FirstTrack; recordingAnchor = plan.AnchorFrame;
651
+ string folder = System.IO.Path.Combine(prefs.RecordFolder, "Take_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture) + "_" + Guid.NewGuid().ToString("N").Substring(0, 6));
652
+ var capture = new Recorder(); recorder = capture; captureActive = false;
653
+ var countdown = new CancellationTokenSource(); countIn = countdown;
654
+ LockRecordingControls(true); C<Button>("StopRecordButton").Content = "Cancel count-in";
655
+ C<TextBlock>("RecordTime").Text = "00:00.00";
656
+ foreach (var meter in meters) meter.Value = 0;
657
+ C<TextBlock>("RecordingState").Text = "Preparing input"; C<TextBlock>("RecordingState").Foreground = Brush("#B52C36");
658
+ editorView.BeginRecordingCountdown(plan);
659
+ Status("Preparing " + plan.Routing);
660
+ Exception startError = null; bool cancelled = false;
635
661
  try {
636
- Status("Connecting to " + driver + "…");
637
- recorder.Start(input,mode,bits,folder,rateChoice.Rate); captureStarted = DateTime.UtcNow;
638
- editorView.SetRecordingState(true);
639
- foreach (string name in new[] { "RecordTab", "StartRecordButton", "InputDevice", "InputMode", "Record16", "Record24", "RecordFloat", "RecordRate", "RefreshDevicesButton", "ChangeRecordFolderButton" }) C<Control>(name).IsEnabled = false;
640
- C<Button>("StopRecordButton").IsEnabled = true;
641
- C<TextBlock>("RecordingState").Text = "● Recording"; C<TextBlock>("RecordingState").Foreground = Brush("#B52C36");
642
- Status("Recording. Click Stop & save when you finish.");
662
+ // Let the armed tracks and connecting feedback render before a driver call.
663
+ await window.Dispatcher.InvokeAsync((Action)delegate { }, DispatcherPriority.Background);
664
+ countdown.Token.ThrowIfCancellationRequested();
665
+ capture.Prepare(plan.Input, plan.Mode, plan.Bits, folder, plan.SampleRate);
666
+ if (capture.ChannelCount != plan.InputChannels) throw new IOException("The device's input layout changed. Refresh Record options, check the destination tracks, and try again.");
667
+ await RecordingCountdown.Run(plan.Beats, plan.BeatSeconds, delegate(int remaining) {
668
+ editorView.ShowCountdownBeat(remaining);
669
+ C<TextBlock>("RecordingState").Text = "Recording in " + remaining;
670
+ C<TextBlock>("RecordTime").Text = remaining.ToString(CultureInfo.InvariantCulture);
671
+ Status("Count-in: " + remaining + " | " + plan.Routing + " | Stop or Escape cancels.");
672
+ }, countdown.Token);
673
+ countdown.Token.ThrowIfCancellationRequested();
674
+ capture.BeginCapture(); captureStarted = DateTime.UtcNow; captureActive = true;
675
+ editorView.BeginRecordingCapture();
676
+ C<Button>("StopRecordButton").Content = "Stop & save";
677
+ C<TextBlock>("RecordingState").Text = "Recording"; C<TextBlock>("RecordTime").Text = "00:00.00";
678
+ Status("Recording | " + plan.Routing + " | Click Stop & save when you finish.");
643
679
  }
680
+ catch (OperationCanceledException) { cancelled = true; }
644
681
  catch (Exception ex) { startError = ex; }
645
- if (startError != null) {
646
- if (editorView != null) editorView.SetRecordingState(false);
647
- Recorder failed = recorder; recorder = null; failed.Stop(); await failed.Finish();
648
- // Keep any partial audio from a failed start; never delete a take.
649
- Status("Could not start recording: " + startError.Message + " Check Audio Interface mode and close other audio apps.");
650
- RefreshDevices();
682
+ finally { if (ReferenceEquals(countIn, countdown)) countIn = null; countdown.Dispose(); }
683
+ if (startError == null && !cancelled) return;
684
+ captureActive = false; capture.Stop(); await capture.Finish();
685
+ recorder = null; editorView.SetRecordingState(false); LockRecordingControls(false);
686
+ C<Button>("StopRecordButton").Content = "Stop & save";
687
+ C<TextBlock>("RecordTime").Text = "00:00.00";
688
+ C<TextBlock>("RecordingState").Text = cancelled ? "Count-in cancelled" : "Could not start recording";
689
+ if (cancelled) Status("Count-in cancelled. No recording was started.");
690
+ else {
691
+ editorView.ShowRecordingError(startError.Message);
692
+ Status("Could not start recording: " + startError.Message + " Check the selected input and close other audio apps.");
651
693
  }
652
694
  }
653
695
  private async Task StopRecording(bool addFiles)
654
696
  {
697
+ if (countIn != null) {
698
+ var startup = recordingStartup; countIn.Cancel(); if (startup != null) await startup; return;
699
+ }
655
700
  if (recorder == null || stopping) return;
656
- stopping = true; C<Button>("StopRecordButton").IsEnabled = false;
701
+ stopping = true; captureActive = false; C<Button>("StopRecordButton").IsEnabled = false;
657
702
  Recorder finished = recorder;
658
- Status("Saving your recording…");
659
- finished.Stop(); await finished.Finish(); recorder = null;
703
+ Status("Saving your recording...");
704
+ finished.Stop();
705
+ long frames = Interlocked.Read(ref finished.Frames);
706
+ if (editorView != null) { editorView.UpdateRecordingProgress(frames, finished.SampleRate); editorView.ShowRecordingSaving(); }
707
+ await finished.Finish(); recorder = null;
660
708
  if (editorView != null) editorView.SetRecordingState(false);
661
- foreach (string name in new[] { "RecordTab", "StartRecordButton", "InputDevice", "InputMode", "Record16", "Record24", "RecordFloat", "RecordRate", "RefreshDevicesButton", "ChangeRecordFolderButton" }) C<Control>(name).IsEnabled = true;
662
- C<TextBlock>("RecordingState").Text = finished.Error == null ? "Take saved" : "Recording stopped - check status";
709
+ LockRecordingControls(false); C<Button>("StopRecordButton").Content = "Stop & save";
710
+ C<TextBlock>("RecordingState").Text = finished.Error != null ? "Recording stopped - check status" : frames > 0 ? "Take saved" : "No audio captured";
663
711
  C<TextBlock>("RecordingState").Foreground = Brush(finished.Error == null ? "#006D68" : "#B52C36");
664
712
  stopping = false;
665
- if (Interlocked.Read(ref finished.Frames) > 0 && finished.Paths.Count > 0 && editorView != null) {
666
- try { await editorView.AddRecordedFiles(finished.Paths, recordingAnchor, recordingTrack); }
667
- catch (Exception ex) { Status("Take saved to its folder, but could not be added to the timeline: " + ex.Message); return; }
713
+ if (addFiles && frames > 0 && finished.Paths.Count > 0 && editorView != null) {
714
+ try { await editorView.AddRecordedFiles(finished.Paths, recordingAnchor, recordingTrack, true); }
715
+ catch (Exception ex) { editorView.ShowRecordingError("Take saved to its folder, but could not be added to the timeline: " + ex.Message); return; }
668
716
  }
669
- Status(finished.Error == null ? "Take saved and added to the timeline. Original recording files are preserved." : "Recording stopped: " + finished.Error.Message + " Captured audio was kept.");
717
+ if (finished.Error != null) {
718
+ editorView.ShowRecordingError(finished.Error.Message);
719
+ Status("Recording stopped: " + finished.Error.Message + " Captured audio was kept.");
720
+ }
721
+ else Status(frames == 0 ? "No audio was captured. Check the selected input device and microphone permissions." : addFiles ? "Take saved and added to the timeline. Original recording files are preserved." : "Take saved. Original recording files are preserved.");
670
722
  }
671
723
  private async Task Tick()
672
724
  {
673
725
  if (closeWhenIdle && !busy && !WorkspacesBusy && recorder == null) { closeWhenIdle = false; window.Close(); return; }
674
- if (recorder == null || stopping) return;
726
+ if (recorder == null || stopping || !captureActive) return;
675
727
  long frames = Interlocked.Read(ref recorder.Frames);
676
728
  C<TextBlock>("RecordTime").Text = WorkspaceUi.Clock(frames / (double)recorder.SampleRate);
729
+ if (editorView != null) editorView.UpdateRecordingProgress(frames, recorder.SampleRate);
677
730
  if (frames == 0 && (DateTime.UtcNow - captureStarted).TotalSeconds > 8 && recorder.Error == null)
678
731
  recorder.Error = new IOException("No audio arrived from the selected input. Check its connection, microphone permissions and selected channels.");
679
732
  if (recorder.SourceFormat != null)
680
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";
681
- bool tooLoud = false;
734
+ bool tooLoud = false; var mixerInputs = new float[8];
682
735
  for (int i = 0; i < 8; i++) {
683
- float peak = Interlocked.Exchange(ref recorder.Peaks[i],0);
736
+ float peak = Interlocked.Exchange(ref recorder.Peaks[i],0); mixerInputs[i] = peak;
684
737
  double db = peak > 0 ? 20 * Math.Log10(peak) : -60;
685
738
  meters[i].Value = Math.Max(0,Math.Min(1,(db + 60) / 60));
686
739
  meters[i].Foreground = Brush(peak >= .99 ? "#B52C36" : "#007A6B");
687
740
  meterLabels[i].Text = peak > .001 ? db.ToString("0",CultureInfo.InvariantCulture) + " dB" : "—";
688
741
  tooLoud |= peak >= .99;
689
742
  }
743
+ if (editorView != null) editorView.UpdateRecordingInputMeters(mixerInputs);
690
744
  C<TextBlock>("RecordHint").Text = recorder.Bits == 32 && recorder.SourceIsFloat
691
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.")
692
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.");
@@ -711,7 +765,8 @@ namespace DrumBuddy
711
765
  "HOME AND CONVERTER\nOpen Converter from View or Home. Drop audio or video, choose an output type and click Convert files. WAV PCM, WAV float, AIFF, FLAC, MP3 and AAC copies are supported. Output settings can be resized, collapsed, or displayed on the side or bottom. The source is preserved.\n\n" +
712
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" +
713
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" +
714
- "EDITOR + RECORD\nThe Editor opens by default. Record options contains input, channels, WAV format and sample rate. Record or R starts a take at the playhead on the selected track. Stop & save preserves the take and adds it to the timeline. All inputs uses consecutive tracks. Use hardware direct monitoring while recording; software overdub synchronisation is not provided.\n\n" +
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" +
715
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" +
716
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" +
717
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" +
@@ -719,9 +774,9 @@ namespace DrumBuddy
719
774
  "Use the R16 gain knobs to set recording levels. Listen through the R16's direct headphone monitoring.\n\n" +
720
775
  "Install Drivers opens a short guide and links to Zoom's official driver download pages.\n\n" +
721
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" +
722
- "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" +
723
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" +
724
- "Basic tools run locally without an account. This release is for Windows; macOS is planned.\n\nFlux 1.6.1 · 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",
725
780
  "Flux — quick help",MessageBoxButton.OK,MessageBoxImage.Information);
726
781
  }
727
782
  }
@@ -735,6 +790,8 @@ namespace DrumBuddy
735
790
  {
736
791
  if (args.Length > 0 && args[0] == "--self-test") return SelfTest.Run(args.Skip(1).ToArray());
737
792
  if (args.Length > 0 && args[0] == "--float-self-test") return FloatRecordingTests.Run(args.Skip(1).ToArray());
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());
738
795
  if (args.Length > 0 && args[0] == "--grid-self-test") return TempoGridTests.Run(args.Skip(1).ToArray());
739
796
  if (args.Length > 0 && args[0] == "--zoom-self-test") return ZoomTests.Run(args.Skip(1).ToArray());
740
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" />
@@ -261,7 +262,7 @@
261
262
  <TextBlock x:Name="RecordTime" Grid.Column="1" Text="00:00" FontSize="22" FontFamily="Consolas" />
262
263
  </Grid>
263
264
  <UniformGrid x:Name="MeterPanel" Columns="4" Margin="0,7,0,8" />
264
- <TextBlock x:Name="RecordHint" Text="Recordings are added at the timeline playhead on the selected track." FontSize="11" Foreground="{DynamicResource Theme526771}" TextWrapping="Wrap" Margin="0,0,0,10" />
265
+ <TextBlock x:Name="RecordHint" Text="One bar count-in at the project tempo, then record at the playhead on the selected track. Stop or Escape cancels the count-in." FontSize="11" Foreground="{DynamicResource Theme526771}" TextWrapping="Wrap" Margin="0,0,0,10" />
265
266
  <Grid>
266
267
  <Grid.ColumnDefinitions>
267
268
  <ColumnDefinition />
@@ -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.1
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
@@ -32,10 +32,29 @@ Fit shows the full project; Selection fills the view with the selected range.
32
32
  Ctrl+= / Ctrl+- zoom; Ctrl+0 fits the project; Ctrl+Shift+0 fits the selection.
33
33
  Zoom can reveal individual samples. Close-up waveforms load in the background.
34
34
  Record options opens the sidebar. Select input, channels, rate and format.
35
- The take is saved at the playhead on the selected track; separate inputs use
36
- consecutive tracks. Stop saves and inserts it. Source files are preserved.
35
+ Record or R arms the selected track (consecutive tracks for separate inputs),
36
+ then shows a one-bar countdown at the project tempo and time signature.
37
+ Record again, Stop, or Escape cancels the countdown without starting capture.
38
+ Capture begins after the last beat using the selected input device.
39
+ The red playhead, growing recording highlights and elapsed timer follow the
40
+ captured audio. Stop saves and inserts the take at its original starting point,
41
+ leaving the playhead at its end. Source files are preserved.
42
+ Bar lines are labeled 1, 2, 3; beat labels remain available when zoomed in.
37
43
  Use direct headphone monitoring. Software overdub synchronization is not included.
38
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
+
39
58
  AUDIO DEVICES
40
59
  Audio devices at the top right opens Settings > Audio. Save device changes.
41
60
  Windows default shows and follows the current Windows speakers/headphones.
@@ -75,7 +94,9 @@ Ctrl+D: duplicate. Ctrl+L: repeat selected clips/range. M: clip mute. X: crossfa
75
94
  Right-click audio for the editing menu. Edits, fades and mute support Undo.
76
95
  Ctrl+X/C/V: cut/copy/paste. Backspace/Delete: remove selection. Ctrl+Z/Y: undo/redo.
77
96
  Ctrl+S: save session. Ctrl+Shift+S: save as. Ctrl+O/N: open/new.
78
- 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.
79
100
  Ctrl+1/2/3/4: Home/Editor/Converter/Player. Ctrl+comma: Settings.
80
101
 
81
102
  MEDIA PLAYER / R16
@@ -0,0 +1,59 @@
1
+ using System;
2
+ using System.Diagnostics;
3
+ using System.Globalization;
4
+ using System.IO;
5
+ using System.Threading;
6
+ using System.Threading.Tasks;
7
+
8
+ namespace DrumBuddy
9
+ {
10
+ // Snapshot the routing before the count-in; later selection changes cannot reroute a take.
11
+ public sealed class RecordingPlan
12
+ {
13
+ private readonly string inputName, inputDriver, inputEndpoint;
14
+ public readonly int Mode, Bits, SampleRate, FirstTrack, TrackCount, InputChannels, Beats;
15
+ public readonly long AnchorFrame;
16
+ public readonly double BeatSeconds;
17
+ public InputChoice Input { get { return new InputChoice { Name = inputName, Driver = inputDriver, EndpointId = inputEndpoint }; } }
18
+ public string Tracks { get { return TrackCount == 1 ? "Track " + (FirstTrack + 1) : "Tracks " + (FirstTrack + 1) + "-" + (FirstTrack + TrackCount); } }
19
+ public string Routing { get; private set; }
20
+
21
+ public RecordingPlan(InputChoice input, int mode, int bits, int rate, int availableInputs, int firstTrack, long anchor, TempoGridOptions grid)
22
+ {
23
+ if (input == null) throw new IOException("Choose a recording input in Record options.");
24
+ grid.Validate(); var layout = CaptureLayout.Create(availableInputs, mode);
25
+ if (bits != 16 && bits != 24 && bits != 32) throw new IOException("Choose a WAV recording format.");
26
+ if (rate <= 0) throw new IOException("Choose a recording sample rate.");
27
+ int tracks = layout.Separate ? layout.Channels : 1;
28
+ if (firstTrack < 0 || firstTrack + tracks > 16) throw new IOException("Select an earlier track so all recording channels fit within the 16-track timeline.");
29
+ if (anchor < 0 || anchor >= EditorSession.MaxFrame) throw new IOException("Move the playhead before the end of the timeline to record.");
30
+ inputName = input.Name; inputDriver = input.Driver; inputEndpoint = input.EndpointId;
31
+ Mode = mode; Bits = bits; SampleRate = rate; FirstTrack = firstTrack; TrackCount = tracks; InputChannels = layout.Channels; AnchorFrame = anchor;
32
+ Beats = grid.BeatsPerBar; BeatSeconds = TempoGrid.BeatSeconds(grid);
33
+ string channels = layout.Channels == 1 ? "Input " + (layout.FirstInput + 1) : "Inputs " + (layout.FirstInput + 1) + "-" + (layout.FirstInput + layout.Channels);
34
+ Routing = Tracks + " | " + channels + " | " + inputName + " | " + (rate / 1000.0).ToString("0.#", CultureInfo.InvariantCulture) + " kHz";
35
+ }
36
+ public long TimelineFrame(long capturedFrames, int sampleRate)
37
+ {
38
+ if (sampleRate <= 0) throw new ArgumentOutOfRangeException("sampleRate");
39
+ double elapsed = Math.Max(0, capturedFrames) * (double)Audio.Rate / sampleRate;
40
+ return AnchorFrame + (long)Math.Round(Math.Min(EditorSession.MaxFrame - AnchorFrame, elapsed));
41
+ }
42
+ }
43
+
44
+ public static class RecordingCountdown
45
+ {
46
+ public static async Task Run(int beats, double beatSeconds, Action<int> showBeat, CancellationToken token)
47
+ {
48
+ if (beats < 1 || beats > 32 || beatSeconds <= 0 || double.IsNaN(beatSeconds) || double.IsInfinity(beatSeconds))
49
+ throw new ArgumentOutOfRangeException("beats");
50
+ var clock = Stopwatch.StartNew();
51
+ for (int beat = 0; beat < beats; beat++) {
52
+ token.ThrowIfCancellationRequested(); showBeat(beats - beat);
53
+ double remaining = (beat + 1) * beatSeconds - clock.Elapsed.TotalSeconds;
54
+ if (remaining > 0) await Task.Delay(TimeSpan.FromSeconds(remaining), token);
55
+ }
56
+ token.ThrowIfCancellationRequested();
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,143 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Diagnostics;
4
+ using System.IO;
5
+ using System.Linq;
6
+ using System.Text;
7
+ using System.Threading;
8
+ using System.Threading.Tasks;
9
+ using System.Windows;
10
+ using System.Windows.Controls;
11
+ using System.Windows.Media;
12
+ using System.Windows.Media.Imaging;
13
+ using System.Windows.Threading;
14
+
15
+ namespace DrumBuddy
16
+ {
17
+ public static class RecordingTests
18
+ {
19
+ private static readonly List<string> log = new List<string>();
20
+ private static void Check(bool value, string message) { if (!value) throw new Exception(message); log.Add("PASS " + message); }
21
+ private static void RunUi(Func<Task> action)
22
+ {
23
+ var previous = SynchronizationContext.Current; var dispatcher = Dispatcher.CurrentDispatcher;
24
+ SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(dispatcher));
25
+ try {
26
+ var task = action(); var frame = new DispatcherFrame();
27
+ task.ContinueWith(delegate { dispatcher.BeginInvoke((Action)delegate { frame.Continue = false; }); });
28
+ if (!task.IsCompleted) Dispatcher.PushFrame(frame); task.GetAwaiter().GetResult();
29
+ }
30
+ finally { SynchronizationContext.SetSynchronizationContext(previous); }
31
+ }
32
+ private static void Picture(FrameworkElement view, string file, int width, int height)
33
+ {
34
+ view.Measure(new Size(width, height)); view.Arrange(new Rect(0, 0, width, height)); view.UpdateLayout();
35
+ var image = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); image.Render(view);
36
+ var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(image)); using (var stream = File.Create(file)) encoder.Save(stream);
37
+ }
38
+ public static int Run(string[] args)
39
+ {
40
+ string root = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(Path.GetTempPath(), "FluxRecording_" + Guid.NewGuid().ToString("N")));
41
+ Directory.CreateDirectory(root); log.Clear(); var old = InterfacePreferences.Current;
42
+ try {
43
+ InterfacePreferences.Current = InterfacePreferences.Defaults(); ThemePalette.Apply(true);
44
+ var input = new InputChoice { Name = "Selected USB microphone", EndpointId = "selected-endpoint" };
45
+ var grid = new TempoGridOptions();
46
+ var plan = new RecordingPlan(input, -1, 32, 48000, 2, 5, Audio.Rate * 2L, grid);
47
+ input.EndpointId = "other-endpoint"; grid.Tempo = 200; grid.BeatsPerBar = 3;
48
+ Check(plan.Input.EndpointId == "selected-endpoint" && plan.FirstTrack == 5 && plan.TrackCount == 2 && plan.InputChannels == 2 && plan.Beats == 4 && plan.BeatSeconds == .5,
49
+ "Freeze the selected endpoint, input layout, destination tracks, anchor and project tempo before counting in");
50
+ var stereo = new RecordingPlan(plan.Input, 0, 24, 96000, 2, 15, 0, grid);
51
+ Check(stereo.TrackCount == 1 && stereo.Beats == 3 && Math.Abs(stereo.BeatSeconds - .3) < 1e-12,
52
+ "Route a stereo file to one track and use one bar of the actual project meter");
53
+ bool rejected = false;
54
+ try { new RecordingPlan(plan.Input, -1, 24, 48000, 8, 10, 0, grid); } catch (IOException) { rejected = true; }
55
+ Check(rejected, "Reject an overflowing multitrack take before countdown or capture");
56
+ Check(plan.TimelineFrame(48000, 96000) == plan.AnchorFrame + Audio.Rate / 2 && plan.TimelineFrame(long.MaxValue, 48000) == EditorSession.MaxFrame,
57
+ "Convert captured frames to the editor rate without exceeding the timeline limit");
58
+ RunUi(async delegate {
59
+ var beats = new List<int>(); var watch = Stopwatch.StartNew(); bool began = false;
60
+ await RecordingCountdown.Run(4, .04, value => beats.Add(value), CancellationToken.None); began = true;
61
+ Check(began && beats.SequenceEqual(new[] { 4, 3, 2, 1 }) && watch.Elapsed.TotalSeconds >= .145,
62
+ "Display every count-in beat and wait the full bar before starting capture");
63
+ var cancelled = new CancellationTokenSource(); began = false; bool stopped = false;
64
+ try { await RecordingCountdown.Run(4, .02, value => { if (value == 1) cancelled.Cancel(); }, cancelled.Token); began = true; }
65
+ catch (OperationCanceledException) { stopped = true; }
66
+ Check(stopped && !began, "Cancelling on the last countdown beat prevents recording from starting"); cancelled.Dispose();
67
+ cancelled = new CancellationTokenSource(); cancelled.Cancel(); beats.Clear();
68
+ try { await RecordingCountdown.Run(4, .02, beats.Add, cancelled.Token); } catch (OperationCanceledException) { }
69
+ Check(beats.Count == 0, "A cancelled startup cannot display another beat or restart capture"); cancelled.Dispose();
70
+
71
+ // Prepare a real Windows endpoint, but cancel before it is ever opened for capture.
72
+ var main = new MainWindow(Path.Combine(root, "shell-preferences.xml"), true);
73
+ main.OpenEditorForTest(); main.RecordingPanelForTest();
74
+ if (((Button)main.NativeWindow.FindName("StartRecordButton")).IsEnabled) {
75
+ var startup = main.StartRecordingForTest();
76
+ await main.RecordingTickForTest();
77
+ Check(main.RecordingBusyForTest && main.RecordingEditorForTest.IsBusy,
78
+ "Lock the actual recording controller and editor before driver preparation or the first countdown beat");
79
+ await main.StartRecordingForTest();
80
+ await main.NativeWindow.Dispatcher.InvokeAsync((Action)delegate { }, DispatcherPriority.Background);
81
+ Check(main.RecordingEditorForTest.CountdownForTest == "4", "The actual Record action displays the first countdown beat");
82
+ Picture((FrameworkElement)main.NativeWindow.Content, Path.Combine(root, "countdown-shell-dark.png"), 1280, 800);
83
+ await main.StopRecordingForTest(); await startup;
84
+ Check(!main.RecordingBusyForTest && !main.RecordingEditorForTest.IsBusy && ((TextBlock)main.NativeWindow.FindName("RecordingState")).Text == "Count-in cancelled",
85
+ "Stop cancels the actual startup, releases the prepared device and restores the recording controls");
86
+ }
87
+ else log.Add("SKIP No ready Windows endpoint for the controller preparation/cancellation check; generated-buffer coverage still runs.");
88
+ main.NativeWindow.Close();
89
+
90
+ var owner = new Window(); ThemePalette.Install(owner);
91
+ using (var editor = new EditorView(owner, Path.Combine(root, "cache"), delegate(string message) { }, delegate { })) {
92
+ owner.Content = editor; editor.BeginRecordingCountdown(plan); editor.ShowCountdownBeat(4);
93
+ Check(editor.IsBusy && editor.CountdownForTest == "4" && editor.ArmedTracksForTest.SequenceEqual(new[] { 5, 6 }) && editor.RoutingForTest.Contains("Selected USB microphone"),
94
+ "Show the countdown and exact armed tracks with the selected device in the editor");
95
+ Picture(editor, Path.Combine(root, "countdown-dark.png"), 1280, 760);
96
+ Check(!await editor.ExecuteAction("start") && editor.PlayheadFrame == plan.AnchorFrame,
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");
100
+ editor.SetRecordingState(false);
101
+ Check(!editor.IsBusy && editor.ArmedTracksForTest.Length == 0 && editor.RecordingOverlaysForTest == 0,
102
+ "Release reserved tracks and recording visuals after cancellation");
103
+ editor.BeginRecordingCountdown(plan); editor.BeginRecordingCapture();
104
+ var capture = new Recorder(); capture.StartBuffered(CaptureLayout.Create(2, -1), 32, Path.Combine(root, "take"), 48000, false);
105
+ byte[] pcm = new byte[48000 * 2 * 2];
106
+ for (int frame = 0; frame < 48000; frame++) {
107
+ Buffer.BlockCopy(BitConverter.GetBytes((short)8192), 0, pcm, frame * 4, 2);
108
+ Buffer.BlockCopy(BitConverter.GetBytes((short)-4096), 0, pcm, frame * 4 + 2, 2);
109
+ }
110
+ editor.UpdateRecordingProgress(0, 48000);
111
+ Check(editor.PlayheadFrame == plan.AnchorFrame, "Keep the playhead still until input frames actually arrive");
112
+ for (int second = 1; second <= 3; second++) {
113
+ capture.ProcessWindowsPcm(pcm, pcm.Length, 2); editor.UpdateRecordingProgress(Interlocked.Read(ref capture.Frames), capture.SampleRate);
114
+ Check(editor.PlayheadFrame == plan.AnchorFrame + second * Audio.Rate, "Advance the recording playhead by captured audio at second " + second);
115
+ }
116
+ Check(editor.RecordingOverlaysForTest == 2 && editor.RecordingHeadlineForTest.Contains("00:03.00"),
117
+ "Show a growing recording on both destination tracks and the elapsed take time");
118
+ Picture(editor, Path.Combine(root, "recording-dark.png"), 1280, 760);
119
+ Picture(editor, Path.Combine(root, "recording-compact-dark.png"), 850, 560);
120
+ ThemePalette.Apply(false); Picture(editor, Path.Combine(root, "recording-light.png"), 1280, 760); ThemePalette.Apply(true);
121
+ capture.Stop(); await capture.Finish(); editor.ShowRecordingSaving();
122
+ var originals = capture.Paths.Select(File.ReadAllBytes).ToArray();
123
+ editor.SetRecordingState(false); await editor.AddRecordedFiles(capture.Paths, plan.AnchorFrame, plan.FirstTrack, true);
124
+ var session = editor.SessionForTest;
125
+ Check(capture.Error == null && session.Regions.Count == 2 && session.Regions.All(r => r.Start == plan.AnchorFrame && r.Length == Audio.Rate * 3L) && session.Regions.Select(r => r.Track).OrderBy(t => t).SequenceEqual(new[] { 5, 6 }),
126
+ "Save and insert both recorded inputs at the registered start and tracks");
127
+ Check(editor.PlayheadFrame == plan.AnchorFrame + Audio.Rate * 3L && editor.RecordingOverlaysForTest == 0 && !editor.IsBusy,
128
+ "Leave the playhead at the end of the saved take and clear live recording state");
129
+ Check(capture.Paths.Select((file, index) => File.ReadAllBytes(file).SequenceEqual(originals[index])).All(value => value),
130
+ "Preserve original recording files while importing the take");
131
+ await editor.ExecuteAction("undo");
132
+ Check(editor.SessionForTest.Regions.Count == 0 && capture.Paths.All(File.Exists), "Undo the recording insertion without deleting captured audio");
133
+ }
134
+ owner.Content = null; owner.Close();
135
+ });
136
+ log.Add("Generated audio buffers and offscreen UI. A physical microphone/ASIO device still requires an acceptance recording.");
137
+ File.WriteAllLines(Path.Combine(root, "recording-test-results.txt"), log, Encoding.UTF8); return 0;
138
+ }
139
+ catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "recording-test-results.txt"), log, Encoding.UTF8); return 1; }
140
+ finally { InterfacePreferences.Current = old; ThemePalette.Apply(old.Theme == "Dark"); }
141
+ }
142
+ }
143
+ }