@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/README.md +18 -3
- package/bin/flux.js +1 -1
- package/dependencies.json +15 -15
- package/package.json +61 -60
- package/payload/DrumBuddy-Setup.exe +2 -2
- package/source/AssetBootstrap.cs +1 -1
- package/source/Audio Decoders.txt +1 -1
- package/source/Audio.cs +27 -5
- package/source/Build Instructions.txt +9 -1
- package/source/Build.ps1 +7 -1
- package/source/EditorCompactLayout.cs +1 -1
- package/source/EditorMixer.cs +267 -0
- package/source/EditorMixerTests.cs +159 -0
- package/source/EditorPlaybackTests.cs +22 -0
- package/source/EditorRecording.cs +106 -0
- package/source/EditorView.cs +49 -25
- package/source/InterfaceSettings.cs +11 -4
- package/source/Main.cs +110 -53
- package/source/Main.xaml +2 -1
- package/source/MediaPlayerView.cs +1 -1
- package/source/Quick Start.txt +25 -4
- package/source/RecordingSession.cs +59 -0
- package/source/RecordingTests.cs +143 -0
- package/source/Setup.cs +6 -5
- package/source/ShellStyles.xaml +11 -5
- package/source/TempoGrid.cs +15 -1
- package/source/TempoGridTests.cs +40 -0
- package/source/Third Party Notices.txt +1 -1
- package/source/Timeline.cs +39 -13
- package/source/TimelineMixer.cs +44 -0
- package/source/references/R16 Format Research.txt +0 -16
package/source/Main.cs
CHANGED
|
@@ -20,7 +20,7 @@ using NAudio.Wave.SampleProviders;
|
|
|
20
20
|
|
|
21
21
|
[assembly: AssemblyTitle("Flux")]
|
|
22
22
|
[assembly: AssemblyDescription("PCM/float media player, 16-track audio editor, H5studio recording and R16 control")]
|
|
23
|
-
[assembly: AssemblyVersion("1.6.
|
|
23
|
+
[assembly: AssemblyVersion("1.6.4.0")]
|
|
24
24
|
|
|
25
25
|
namespace DrumBuddy
|
|
26
26
|
{
|
|
@@ -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
|
|
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) {
|
|
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) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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 (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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();
|
|
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
|
-
|
|
662
|
-
C<TextBlock>("RecordingState").Text = finished.Error
|
|
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 (
|
|
666
|
-
try { await editorView.AddRecordedFiles(finished.Paths, recordingAnchor, recordingTrack); }
|
|
667
|
-
catch (Exception ex) {
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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="
|
|
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
|
}
|
package/source/Quick Start.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FLUX 1.6.
|
|
1
|
+
FLUX 1.6.4
|
|
2
2
|
|
|
3
3
|
INSTALL / UPDATE
|
|
4
4
|
Close the app. Run npm.cmd install -g @pro-gram/flux@latest
|
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
+
}
|