@pro-gram/flux 1.6.0 → 1.6.2

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/NOTICE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Flux 1.6.0 - third-party components
1
+ Flux 1.6.1 - third-party components
2
2
 
3
3
  FFmpeg 9.0.2, release essentials build by Gyan Doshi
4
4
  Used as a separate command-line decoding process.
package/README.md CHANGED
@@ -7,10 +7,21 @@ Flux is a compact Windows audio studio with a 16-track editor/recorder, a Media
7
7
  Close the running audio application, then:
8
8
 
9
9
  ```powershell
10
- npm.cmd install -g @pro-gram/flux@latest
11
- flux.cmd
10
+ npm install -g @pro-gram/flux@latest
11
+ flux --setup
12
+ flux
12
13
  ```
13
14
 
15
+ **If npm skips setup or the Desktop shortcut is missing, run `flux --setup`.** This completes the desktop installation and creates the Flux Desktop and Start menu shortcuts. When npm allows the setup script, these steps happen automatically. An `allowScripts` notice can also appear after a successful installation. The first `flux` launch completes setup if it is needed.
16
+
17
+ To allow Flux's setup script for one installation:
18
+
19
+ ```powershell
20
+ npm install -g @pro-gram/flux@latest --allow-scripts=@pro-gram/flux
21
+ ```
22
+
23
+ If PowerShell blocks the `npm.ps1` or `flux.ps1` launcher, use `npm.cmd` and `flux.cmd` for the same commands.
24
+
14
25
  Windows 10/11 x64 is supported. macOS is planned. Basic tools work locally without an account.
15
26
 
16
27
  ### Moving from Drum Buddy / FluxTempo
@@ -51,7 +62,9 @@ The visible **- / + buttons and slider** zoom the timeline from a full-project o
51
62
 
52
63
  **Ctrl+Plus / Ctrl+Minus** zoom (including Ctrl+= and numeric-keypad keys), **Ctrl+0** fits the project and **Ctrl+Shift+0** fits the selection. These shortcuts are editable in Settings > Keybinds. At close zoom, waveform detail is read from decoded audio in the background, preserving positive/negative peaks and float headroom. The ruler displays fractional seconds when a close-up contains no labeled beat. Zoom changes the view without changing audio, the playhead or undo history.
53
64
 
54
- Record options contains microphone/input channels, sample rate and PCM/float format. Recording saves original takes and inserts them at the starting playhead on the selected track; separate input channels use consecutive tracks. Use hardware direct monitoring. Software overdub latency alignment is not included.
65
+ Record options contains microphone/input channels, sample rate and PCM/float format. **Record** or **R** reserves the selected track (consecutive tracks for separate input files) and shows a one-bar countdown at the project tempo and time signature. The countdown displays the destination tracks and selected input. Press Record again, Stop, or Escape to cancel it before capture starts.
66
+
67
+ After the final beat, capture starts through the selected input. A red playhead, growing recording highlights and elapsed timer follow the audio actually received. **Stop & save** preserves the original take and inserts it at the registered starting point; the playhead stays at the end of the take. Bar lines display **1, 2, 3**; detailed beat labels remain available when zoomed in. Use hardware direct monitoring. Software overdub latency alignment is not included.
55
68
 
56
69
  Recording controls, converter output settings and Media Player gain have draggable dividers, Collapse/Show and Side/Bottom positioning.
57
70
 
package/bin/flux.js CHANGED
@@ -10,7 +10,7 @@ const { needsUpdate } = require('../scripts/version.js');
10
10
  const arg = process.argv[2];
11
11
  if (arg === '--version' || arg === '-v') { console.log(pkg.version); return; }
12
12
  if (arg === '--help' || arg === '-h') {
13
- console.log('Flux ' + pkg.version + ' for Windows\n\nflux Open Flux\nflux --setup Install or repair Flux\nflux --audio-diagnostics Save an audio device report\nflux --version Show the npm package version\n\nAudio: H5studio native 32-bit float recording; 16/24-bit PCM recording and export.\nWindows/Realtek/USB microphones use 16-bit PCM input.\nWorkspaces: PCM/float Media Player and 16-track timeline Editor with recording.\nTempo grid: BPM, time signatures, 1/1 to 1/256 subdivisions, triplets and independent grid/edge snapping.\nCtrl+C copies; Backspace deletes selection; Ctrl+V pastes at the playhead.\nDark/light themes, Converter and editable keybind settings.\nUpdate: close Flux, then npm.cmd install -g @pro-gram/flux@latest\nRun flux.cmd afterward to finish setup if npm skipped the install script.'); return;
13
+ console.log('Flux ' + pkg.version + ' for Windows\n\nflux Open Flux\nflux --setup Install or repair Flux\nflux --audio-diagnostics Save an audio device report\nflux --version Show the npm package version\n\nAudio: H5studio native 32-bit float recording; 16/24-bit PCM recording and export.\nWindows/Realtek/USB microphones use 16-bit PCM input.\nWorkspaces: PCM/float Media Player and 16-track timeline Editor with recording.\nRecording: one-bar visual count-in, reserved input tracks and a moving recording playhead.\nTempo grid: BPM, time signatures, 1/1 to 1/256 subdivisions, triplets and independent grid/edge snapping.\nCtrl+C copies; Backspace deletes selection; Ctrl+V pastes at the playhead.\nDark/light themes, Converter and editable keybind settings.\nUpdate: close Flux, then npm.cmd install -g @pro-gram/flux@latest\nRun flux.cmd afterward to finish setup if npm skipped the install script.'); return;
14
14
  }
15
15
  if (process.platform !== 'win32' || process.arch !== 'x64') throw new Error('Flux needs Windows x64.');
16
16
  const root = installRoot();
package/dependencies.json CHANGED
@@ -1,15 +1,15 @@
1
- {
2
- "ffmpeg": {
3
- "url": "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-9.0.2-essentials_build.zip",
4
- "file": "ffmpeg-9.0.2.zip",
5
- "sha256": "60f467265b1e312373dbcd92200c2618a74850f98d3d078e94296bb3fa2047ba"
6
- },
7
- "panel": {
8
- "url": "https://zoomcorp.com/media/original_images/R16_list_0.png",
9
- "file": "r16-panel.png",
10
- "sha256": "315a71e8942a445c903b2d1aedb703b64aab30f054e46199a2bac447d3d03cb2",
11
- "alternateSha256": "5564fa622f101dc27ea402c399c5b9b1e1e039b8771daf40b185dab9772adf38"
12
- },
13
- "installerSha256": "0a57d2459fdb2c5c0c2917df12ca907b47c5a966006d2680e9d160d16bdfed9c",
14
- "bundledAssets": true
15
- }
1
+ {
2
+ "ffmpeg": {
3
+ "url": "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-9.0.2-essentials_build.zip",
4
+ "file": "ffmpeg-9.0.2.zip",
5
+ "sha256": "60f467265b1e312373dbcd92200c2618a74850f98d3d078e94296bb3fa2047ba"
6
+ },
7
+ "panel": {
8
+ "url": "https://zoomcorp.com/media/original_images/R16_list_0.png",
9
+ "file": "r16-panel.png",
10
+ "sha256": "315a71e8942a445c903b2d1aedb703b64aab30f054e46199a2bac447d3d03cb2",
11
+ "alternateSha256": "5564fa622f101dc27ea402c399c5b9b1e1e039b8771daf40b185dab9772adf38"
12
+ },
13
+ "installerSha256": "1d54666d43c987d0200d70634cec75da7e7cc269aaee224603c5cc7139a2cb81",
14
+ "bundledAssets": true
15
+ }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "@pro-gram/flux",
3
- "version": "1.6.0",
4
- "description": "Flux: a 16-track Windows audio studio with native shared playback, float/PCM recording, clip editing tools, tempo grids and bundled audio codecs.",
5
- "license": "MIT",
6
- "author": "pro-gram",
7
- "os": [
8
- "win32"
9
- ],
10
- "cpu": [
11
- "x64"
12
- ],
13
- "engines": {
14
- "node": ">=18"
15
- },
16
- "bin": {
17
- "flux": "bin/flux.js"
18
- },
19
- "scripts": {
20
- "postinstall": "node scripts/install.js",
21
- "test": "node --test test/*.test.js"
22
- },
23
- "files": [
24
- "bin/flux.js",
25
- "scripts/",
26
- "test/",
27
- "payload/DrumBuddy-Setup.exe",
28
- "dependencies.json",
29
- "source/",
30
- "LICENSE",
31
- "NOTICE.txt"
32
- ],
33
- "keywords": [
34
- "audio-editor",
35
- "media-player",
36
- "multitrack",
37
- "zoom-h5studio",
38
- "32-bit-float",
39
- "pcm",
40
- "zoom-r16",
41
- "drums",
42
- "audio",
43
- "midi",
44
- "wav",
45
- "windows",
46
- "recording",
47
- "fluxtempo",
48
- "realtek",
49
- "audio-workstation",
50
- "flux",
51
- "tempo-grid",
52
- "beat-grid",
53
- "snap-to-grid"
54
- ],
55
- "publishConfig": {
56
- "access": "public",
57
- "registry": "https://registry.npmjs.org/"
58
- },
59
- "homepage": "https://fluxtempo.ca"
60
- }
1
+ {
2
+ "name": "@pro-gram/flux",
3
+ "version": "1.6.2",
4
+ "description": "Flux: a 16-track Windows audio studio with native shared playback, float/PCM recording, clip editing tools, tempo grids and bundled audio codecs.",
5
+ "license": "MIT",
6
+ "author": "pro-gram",
7
+ "os": [
8
+ "win32"
9
+ ],
10
+ "cpu": [
11
+ "x64"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "bin": {
17
+ "flux": "bin/flux.js"
18
+ },
19
+ "scripts": {
20
+ "postinstall": "echo If npm skips setup, run flux --setup && node scripts/install.js",
21
+ "test": "node --test test/*.test.js"
22
+ },
23
+ "files": [
24
+ "bin/flux.js",
25
+ "scripts/",
26
+ "test/",
27
+ "payload/DrumBuddy-Setup.exe",
28
+ "dependencies.json",
29
+ "source/",
30
+ "LICENSE",
31
+ "NOTICE.txt"
32
+ ],
33
+ "keywords": [
34
+ "audio-editor",
35
+ "media-player",
36
+ "multitrack",
37
+ "zoom-h5studio",
38
+ "32-bit-float",
39
+ "pcm",
40
+ "zoom-r16",
41
+ "drums",
42
+ "audio",
43
+ "midi",
44
+ "wav",
45
+ "windows",
46
+ "recording",
47
+ "fluxtempo",
48
+ "realtek",
49
+ "audio-workstation",
50
+ "flux",
51
+ "tempo-grid",
52
+ "beat-grid",
53
+ "snap-to-grid"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "homepage": "https://fluxtempo.ca"
60
+ }
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/payload/DrumBuddy-Setup.exe
3
- size: 42842112 bytes
4
- sha256: 0a57d2459fdb2c5c0c2917df12ca907b47c5a966006d2680e9d160d16bdfed9c
3
+ size: 41726976 bytes
4
+ sha256: 1d54666d43c987d0200d70634cec75da7e7cc269aaee224603c5cc7139a2cb81
@@ -21,7 +21,7 @@ namespace DrumBuddySetup
21
21
  if(File.Exists(target) && expected.Contains(Hash(target),StringComparer.OrdinalIgnoreCase))return target;
22
22
  string temporary=target+"."+Guid.NewGuid().ToString("N")+".partial";
23
23
  try {
24
- using(var client=new WebClient()) { client.Headers[HttpRequestHeader.UserAgent]="DrumBuddy/1.6.0";client.DownloadFile(url,temporary); }
24
+ using(var client=new WebClient()) { client.Headers[HttpRequestHeader.UserAgent]="DrumBuddy/1.6.2";client.DownloadFile(url,temporary); }
25
25
  if(!expected.Contains(Hash(temporary),StringComparer.OrdinalIgnoreCase))throw new IOException("A downloaded component failed its integrity check. Please retry setup or obtain a newer Drum Buddy release.");
26
26
  File.Copy(temporary,target,true);return target;
27
27
  }finally { if(File.Exists(temporary))File.Delete(temporary); }
@@ -1,4 +1,4 @@
1
- Flux 1.6.0 bundled FFmpeg audio decoders
1
+ Flux 1.6.2 bundled FFmpeg audio decoders
2
2
  These decode imported files without Windows Media Player codecs.
3
3
 
4
4
  A..... = Audio
package/source/Audio.cs CHANGED
@@ -282,6 +282,7 @@ namespace DrumBuddy
282
282
  private Task writing;
283
283
  private readonly object gate = new object();
284
284
  private bool accepting;
285
+ private Action beginCapture;
285
286
  private int captureChannels, selectedInput, savedChannels;
286
287
  private bool separate, requireFloatInput;
287
288
  private readonly List<IRecordingWriter> writers = new List<IRecordingWriter>();
@@ -300,18 +301,27 @@ namespace DrumBuddy
300
301
 
301
302
  public void Start(InputChoice input, int mode, int bits, string folder, int rate)
302
303
  {
303
- if (input.Driver != null) { Start(input.Driver, mode, bits, folder, rate); return; }
304
+ Prepare(input, mode, bits, folder, rate); BeginCapture();
305
+ }
306
+
307
+ // Prepare drivers on the UI thread before the count-in, without capturing audio
308
+ // or creating a take. ASIO COM objects must stay on this thread.
309
+ public void Prepare(InputChoice input, int mode, int bits, string folder, int rate)
310
+ {
311
+ if (input == null) throw new ArgumentNullException("input");
312
+ if (device != null || windowsInput != null || queue != null || beginCapture != null) throw new InvalidOperationException("This recorder is already prepared.");
313
+ if (input.Driver != null) { PrepareAsio(input.Driver, mode, bits, folder, rate); return; }
304
314
  try {
305
315
  windowsChannels = AudioInputs.Channels(input);
306
316
  var layout = CaptureLayout.Create(windowsChannels, mode);
317
+ captureChannels = layout.Channels;
307
318
  windowsInput = new WaveInEvent { DeviceNumber = AudioInputs.Device(input), WaveFormat = new WaveFormat(rate, 16, windowsChannels), BufferMilliseconds = 50, NumberOfBuffers = 3 };
308
- StartBuffered(layout, bits, folder, rate, false);
309
319
  SourceFormat = "Windows 16-bit PCM"; SourceIsFloat = false;
310
320
  windowsInput.DataAvailable += delegate(object sender, WaveInEventArgs e) { ProcessWindowsPcm(e.Buffer, e.BytesRecorded, windowsChannels); };
311
321
  windowsInput.RecordingStopped += delegate(object sender, StoppedEventArgs e) {
312
322
  lock (gate) { if (accepting && Error == null) Error = e.Exception ?? new IOException("The microphone stopped. Check its connection and Windows microphone permissions."); }
313
323
  };
314
- windowsInput.StartRecording();
324
+ beginCapture = delegate { StartBuffered(layout, bits, folder, rate, false); windowsInput.StartRecording(); };
315
325
  }
316
326
  catch { Stop(); throw; }
317
327
  }
@@ -341,19 +351,30 @@ namespace DrumBuddy
341
351
  }
342
352
 
343
353
  public void Start(string driver, int mode, int bits, string folder, int rate)
354
+ { Start(new InputChoice { Driver = driver, Name = driver }, mode, bits, folder, rate); }
355
+
356
+ public void BeginCapture()
357
+ {
358
+ var start = beginCapture;
359
+ if (start == null) throw new InvalidOperationException("Prepare a recording input before starting capture.");
360
+ beginCapture = null;
361
+ try { start(); } catch { Stop(); throw; }
362
+ }
363
+
364
+ private void PrepareAsio(string driver, int mode, int bits, string folder, int rate)
344
365
  {
345
366
  try {
346
367
  device = new AsioOut(driver);
347
368
  var layout = CaptureLayout.Create(device.DriverInputChannelCount, mode);
369
+ captureChannels = layout.Channels;
348
370
  if (!device.IsSampleRateSupported(rate)) throw new IOException("This device does not support " + rate + " Hz in its current mode. Choose a supported rate, or set the recorder to Audio I/F Only.");
349
371
  device.InputChannelOffset = layout.FirstInput;
350
372
  device.InitRecordAndPlayback(null, layout.Channels, rate);
351
- StartBuffered(layout, bits, folder, rate, bits == 32 && CaptureLayout.IsH5studio(driver));
352
373
  device.AudioAvailable += Captured;
353
374
  device.PlaybackStopped += delegate(object sender, StoppedEventArgs e) {
354
375
  lock (gate) { if (accepting && Error == null) Error = e.Exception ?? new IOException("The recording device stopped sending audio. Check its USB connection."); }
355
376
  };
356
- device.Play();
377
+ beginCapture = delegate { StartBuffered(layout, bits, folder, rate, bits == 32 && CaptureLayout.IsH5studio(driver)); device.Play(); };
357
378
  }
358
379
  catch { Stop(); throw; }
359
380
  }
@@ -427,6 +448,7 @@ namespace DrumBuddy
427
448
  // Call on the UI thread: ASIO COM objects have thread affinity.
428
449
  public void Stop()
429
450
  {
451
+ beginCapture = null;
430
452
  lock (gate) accepting = false;
431
453
  if (windowsInput != null) {
432
454
  try { windowsInput.StopRecording(); } catch (Exception ex) { if (Error == null) Error = ex; }
@@ -18,6 +18,7 @@ Verification:
18
18
  DrumBuddy.exe --tools-self-test "C:\path\to\new-tool-test-folder"
19
19
  DrumBuddy.exe --workspace-self-test "C:\path\to\new-editor-test-folder"
20
20
  DrumBuddy.exe --float-self-test "C:\path\to\new-float-test-folder"
21
+ DrumBuddy.exe --recording-self-test "C:\path\to\new-recording-test-folder"
21
22
  DrumBuddy.exe --output-self-test "C:\path\to\new-output-test-folder"
22
23
  DrumBuddy.exe --editor-playback-self-test "C:\path\to\new-playback-test-folder"
23
24
  DrumBuddy.exe --interface-self-test "C:\path\to\new-interface-test-folder"
@@ -40,6 +41,9 @@ exposed. The output suite queries microphone formats without opening capture.
40
41
  The editor-playback suite imports float/PCM files and exercises the actual
41
42
  editor transport with muted listening, plus native pause/resume/drain checks.
42
43
  Windows microphone buffer tests cover routing, saved samples and queue draining.
44
+ The recording suite covers count-in timing/cancellation, frozen input and track
45
+ routing, captured-frame playhead progress, recording visuals, take insertion
46
+ and preserved originals using generated input buffers.
43
47
  The interface suite checks keybind capture, saved/unsaved settings, theme
44
48
  switching, integrated recording insertion and dark/light/compact layouts.
45
49
  The general-conversion suite checks original-rate float/PCM WAV, FLAC,
package/source/Build.ps1 CHANGED
@@ -100,6 +100,7 @@ $appArgs += Join-Path $PSScriptRoot 'EditorSelectionTests.cs'
100
100
  $appArgs += @('EditorTools.cs','EditorToolInteractions.cs') | ForEach-Object { Join-Path $PSScriptRoot $_ }
101
101
  $appArgs += Join-Path $PSScriptRoot 'EditorToolsTests.cs'
102
102
  $appArgs += Join-Path $PSScriptRoot 'EditorCompactLayout.cs'
103
+ $appArgs += @('RecordingSession.cs','EditorRecording.cs','RecordingTests.cs') | ForEach-Object { Join-Path $PSScriptRoot $_ }
103
104
  & $compiler @appArgs
104
105
  if ($LASTEXITCODE -ne 0) { throw 'App compilation failed.' }
105
106
  $iconVersion = [Reflection.AssemblyName]::GetAssemblyName((Join-Path $package 'DrumBuddy.exe')).Version.ToString(3)
@@ -119,7 +120,11 @@ if (Test-Path -LiteralPath $payload) { Remove-Item -LiteralPath $payload }
119
120
  [IO.Compression.ZipFile]::CreateFromDirectory($package,$payload,[IO.Compression.CompressionLevel]::Optimal,$false)
120
121
  $manifest = Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
121
122
  $relative = $_.FullName.Substring($package.Length + 1).Replace('\','/')
122
- (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash + ' ' + $relative
123
+ $payloadStream = [IO.File]::OpenRead($_.FullName)
124
+ $payloadHasher = [Security.Cryptography.SHA256]::Create()
125
+ try { $payloadHash = [BitConverter]::ToString($payloadHasher.ComputeHash($payloadStream)).Replace('-','') }
126
+ finally { $payloadStream.Dispose(); $payloadHasher.Dispose() }
127
+ $payloadHash + ' ' + $relative
123
128
  }
124
129
  $manifestPath = Join-Path $BuildFolder 'payload.sha256'
125
130
  [IO.File]::WriteAllLines($manifestPath,[string[]]$manifest,(New-Object Text.UTF8Encoding($false)))
@@ -0,0 +1,106 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Windows;
5
+ using System.Windows.Controls;
6
+ using System.Windows.Media;
7
+ using System.Windows.Shapes;
8
+
9
+ namespace DrumBuddy
10
+ {
11
+ public sealed partial class EditorView
12
+ {
13
+ private Border recordingBanner;
14
+ private TextBlock countdownText, recordingHeadline, recordingRouting;
15
+ private RecordingPlan recordingPlan;
16
+ private bool captureRunning;
17
+ private readonly List<Rectangle> recordingOverlays = new List<Rectangle>();
18
+
19
+ private void InitializeRecordingFeedback(Panel tools)
20
+ {
21
+ var body = new DockPanel();
22
+ countdownText = WorkspaceUi.Text("", 30); countdownText.FontWeight = FontWeights.Bold;
23
+ countdownText.Foreground = WorkspaceUi.Color("#B52C36"); countdownText.MinWidth = 72;
24
+ countdownText.Margin = new Thickness(0, 0, 12, 0); countdownText.VerticalAlignment = VerticalAlignment.Center;
25
+ DockPanel.SetDock(countdownText, Dock.Left); body.Children.Add(countdownText);
26
+ var details = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; body.Children.Add(details);
27
+ recordingHeadline = WorkspaceUi.Text("", 15); recordingHeadline.FontWeight = FontWeights.SemiBold; details.Children.Add(recordingHeadline);
28
+ recordingRouting = WorkspaceUi.Text("", 12); recordingRouting.Margin = new Thickness(0, 3, 0, 0); details.Children.Add(recordingRouting);
29
+ recordingBanner = new Border { Child = body, BorderBrush = WorkspaceUi.Color("#B52C36"), BorderThickness = new Thickness(2),
30
+ Background = WorkspaceUi.Color("#F3F5F5"), Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 0, 0, 6), Visibility = Visibility.Collapsed };
31
+ tools.Children.Add(recordingBanner);
32
+ }
33
+ public RecordingPlan PlanRecording(InputChoice input, int mode, int bits, int rate, int availableInputs)
34
+ {
35
+ if (!CommitTempo()) throw new System.IO.IOException("Enter a valid project tempo before recording.");
36
+ return new RecordingPlan(input, mode, bits, rate, availableInputs, track, playhead, session.Grid);
37
+ }
38
+ public void BeginRecordingCountdown(RecordingPlan plan)
39
+ {
40
+ recordingPlan = plan; captureRunning = false; playhead = plan.AnchorFrame;
41
+ SetRecordingState(true); recordButton.Content = "Cancel count-in";
42
+ recordingBanner.Visibility = Visibility.Visible; countdownText.Text = "...";
43
+ recordingHeadline.Text = "Preparing recording input"; recordingRouting.Text = plan.Routing;
44
+ BuildHeaders(); DrawCanvas(); EnsurePlayheadVisible();
45
+ double top = RulerHeight + plan.FirstTrack * RowHeight, bottom = top + plan.TrackCount * RowHeight;
46
+ if (scroll.ViewportHeight > 0) {
47
+ if (top < scroll.VerticalOffset) scroll.ScrollToVerticalOffset(Math.Max(0, top - RowHeight));
48
+ else if (bottom > scroll.VerticalOffset + scroll.ViewportHeight) scroll.ScrollToVerticalOffset(Math.Max(0, bottom - scroll.ViewportHeight));
49
+ }
50
+ }
51
+ public void ShowCountdownBeat(int remaining)
52
+ {
53
+ countdownText.Text = remaining.ToString();
54
+ recordingHeadline.Text = "Recording in " + remaining + (remaining == 1 ? " beat" : " beats");
55
+ }
56
+ public void BeginRecordingCapture()
57
+ {
58
+ captureRunning = true; recordButton.Content = "Stop recording"; countdownText.Text = "REC";
59
+ recordingHeadline.Text = "Recording | 00:00.00"; BuildHeaders(); DrawCanvas();
60
+ }
61
+ public void UpdateRecordingProgress(long capturedFrames, int sampleRate)
62
+ {
63
+ if (!recording || recordingPlan == null) return;
64
+ playhead = recordingPlan.TimelineFrame(capturedFrames, sampleRate);
65
+ timeline.Width = Math.Max(timeline.Width, X(Math.Min(EditorSession.MaxFrame, playhead + Audio.Rate * 15L)));
66
+ recordingHeadline.Text = "Recording | " + WorkspaceUi.Clock(capturedFrames / (double)sampleRate);
67
+ UpdateRecordingOverlays(); UpdatePlayLine(); UpdateTransport();
68
+ if (DateTime.UtcNow >= manualViewUntil) EnsurePlayheadVisible();
69
+ }
70
+ public void ShowRecordingSaving()
71
+ { recordButton.Content = "Saving..."; recordButton.IsEnabled = false; countdownText.Text = "..."; recordingHeadline.Text = "Saving recording"; }
72
+ public void ShowRecordingError(string message)
73
+ {
74
+ recordingBanner.Visibility = Visibility.Visible; countdownText.Text = "!";
75
+ recordingHeadline.Text = "Could not record"; recordingRouting.Text = message;
76
+ }
77
+ private bool RecordingTrack(int index)
78
+ { return recording && recordingPlan != null && index >= recordingPlan.FirstTrack && index < recordingPlan.FirstTrack + recordingPlan.TrackCount; }
79
+ private void DrawRecordingOverlays()
80
+ {
81
+ recordingOverlays.Clear(); if (recordingPlan == null || !recording) return;
82
+ for (int i = 0; i < recordingPlan.TrackCount; i++) {
83
+ var rectangle = new Rectangle { Fill = WorkspaceUi.Color("#B52C36"), Opacity = .24, Stroke = WorkspaceUi.Color("#B52C36"),
84
+ StrokeThickness = 2, Height = RowHeight - 6, IsHitTestVisible = false, Tag = "RecordingPreview" };
85
+ Canvas.SetTop(rectangle, RulerHeight + (recordingPlan.FirstTrack + i) * RowHeight + 3);
86
+ timeline.Children.Add(rectangle); recordingOverlays.Add(rectangle);
87
+ }
88
+ UpdateRecordingOverlays();
89
+ }
90
+ private void UpdateRecordingOverlays()
91
+ {
92
+ if (recordingPlan == null) return;
93
+ double left = Math.Max(scroll.HorizontalOffset, X(recordingPlan.AnchorFrame));
94
+ double right = Math.Min(scroll.HorizontalOffset + Math.Max(900, scroll.ViewportWidth), X(playhead));
95
+ foreach (var rectangle in recordingOverlays) {
96
+ Canvas.SetLeft(rectangle, left); rectangle.Width = Math.Max(2, right - left);
97
+ rectangle.Visibility = right >= left ? Visibility.Visible : Visibility.Collapsed;
98
+ }
99
+ }
100
+ internal string CountdownForTest { get { return countdownText.Text; } }
101
+ internal string RecordingHeadlineForTest { get { return recordingHeadline.Text; } }
102
+ internal string RoutingForTest { get { return recordingRouting.Text; } }
103
+ internal int[] ArmedTracksForTest { get { return Enumerable.Range(0, 16).Where(RecordingTrack).ToArray(); } }
104
+ internal int RecordingOverlaysForTest { get { return recordingOverlays.Count; } }
105
+ }
106
+ }
@@ -119,6 +119,7 @@ namespace DrumBuddy
119
119
  separate = new CheckBox { Content = "Import files on separate tracks", IsChecked = true, FontSize = 14, Margin = new Thickness(4, 0, 0, 8), VerticalAlignment = VerticalAlignment.Center }; editing.Children.Add(separate);
120
120
  compactOptionsButton = WorkspaceUi.Button("Audio / export options", delegate { compactOptions = !compactOptions; UpdateCompactLayout(); }); editing.Children.Add(compactOptionsButton);
121
121
 
122
+ InitializeRecordingFeedback(tools);
122
123
  gridControls = new WrapPanel { Margin = new Thickness(0, 0, 0, 3) }; tools.Children.Add(gridControls);
123
124
  gridControls.Children.Add(WorkspaceUi.Text("BPM ", 12));
124
125
  tempoBox = new TextBox { Width = 60, MinHeight = 28, Padding = new Thickness(5, 3, 5, 3), Margin = new Thickness(0, 0, 9, 5) }; gridControls.Children.Add(tempoBox);
@@ -202,7 +203,7 @@ namespace DrumBuddy
202
203
  var transport = new WrapPanel(); bottom.Children.Add(transport);
203
204
  play = WorkspaceUi.Button("Play", delegate { PlayPause(); }); play.MinWidth = 90; transport.Children.Add(play);
204
205
  transport.Children.Add(WorkspaceUi.Button("Stop", delegate { if (recording && StopRequested != null) StopRequested(); else { Stop(); UpdateTransport(); } }));
205
- transport.Children.Add(WorkspaceUi.Button("To start", delegate { Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); }));
206
+ transport.Children.Add(WorkspaceUi.Button("To start", delegate { if (IsBusy) return; Stop(); playhead = 0; DrawCanvas(); EnsurePlayheadVisible(); }));
206
207
  positionText = WorkspaceUi.Text("00:00.00 / 00:00.00", 16); positionText.FontFamily = new FontFamily("Consolas"); positionText.Margin = new Thickness(0, 0, 14, 8); transport.Children.Add(positionText);
207
208
  transport.Children.Add(WorkspaceUi.Text("Listen volume ", 14));
208
209
  volume = new Slider { Minimum = 0, Maximum = 100, Value = 75, Width = 120, Margin = new Thickness(0, 0, 16, 8), VerticalAlignment = VerticalAlignment.Center };
@@ -413,7 +414,8 @@ namespace DrumBuddy
413
414
  for (int i = 0; i < 16; i++) {
414
415
  int index = i; var state = session.Tracks[i];
415
416
  var panel = new StackPanel { Margin = new Thickness(7, 3, 7, 3) };
416
- var name = WorkspaceUi.Text((i + 1).ToString("00") + " " + state.Name, 14); name.TextWrapping = TextWrapping.NoWrap; name.TextTrimming = TextTrimming.CharacterEllipsis; name.FontWeight = FontWeights.SemiBold; panel.Children.Add(name);
417
+ var name = WorkspaceUi.Text((i + 1).ToString("00") + (RecordingTrack(i) ? (captureRunning ? " REC " : " ARM ") : " ") + state.Name, 14); name.TextWrapping = TextWrapping.NoWrap; name.TextTrimming = TextTrimming.CharacterEllipsis; name.FontWeight = FontWeights.SemiBold; panel.Children.Add(name);
418
+ if (RecordingTrack(i)) name.Foreground = WorkspaceUi.Color("#B52C36");
417
419
  var strip = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; panel.Children.Add(strip);
418
420
  var mute = new ToggleButton { Content = "M", IsChecked = state.Mute, Width = 28, Height = 26, FontSize = 12, Margin = new Thickness(0, 0, 4, 0), ToolTip = "Mute track " + (i + 1) };
419
421
  mute.Click += delegate { if (!updating) Change(delegate { session.Tracks[index].Mute = mute.IsChecked == true; return true; }, "Track mute changed."); }; strip.Children.Add(mute);
@@ -422,7 +424,7 @@ namespace DrumBuddy
422
424
  var levels = new[] { -60.0, -24, -18, -12, -6, 0, 3, 6, 12, state.GainDb }.Distinct().OrderBy(v => v).ToArray();
423
425
  var level = new ComboBox { ItemsSource = levels.Select(v => v.ToString("0.#", CultureInfo.InvariantCulture) + " dB").ToArray(), SelectedIndex = Array.IndexOf(levels, state.GainDb), Width = 102, MinHeight = 26, Height = 26, FontSize = 12, Padding = new Thickness(3, 0, 3, 0), ToolTip = "Track playback / export level" };
424
426
  level.SelectionChanged += delegate { if (!updating && level.SelectedIndex >= 0) { double value = levels[level.SelectedIndex]; Change(delegate { session.Tracks[index].GainDb = value; return true; }, "Track level changed."); } }; strip.Children.Add(level);
425
- var border = new Border { Height = RowHeight, BorderThickness = new Thickness(0, 0, 1, 1), BorderBrush = WorkspaceUi.Color("#D6E1DF"), Background = WorkspaceUi.Color(i == track ? "#DCEFEB" : "#EDF3F1"), Child = panel };
427
+ var border = new Border { Height = RowHeight, BorderThickness = new Thickness(RecordingTrack(i) ? 3 : 0, 0, 1, 1), BorderBrush = WorkspaceUi.Color(RecordingTrack(i) ? "#B52C36" : "#D6E1DF"), Background = WorkspaceUi.Color(i == track ? "#DCEFEB" : "#EDF3F1"), Child = panel };
426
428
  border.PreviewMouseLeftButtonDown += delegate { if (!IsBusy && track != index) { track = index; UpdateSelectionText(); } };
427
429
  border.MouseLeftButtonUp += delegate { BuildHeaders(); DrawCanvas(); };
428
430
  headers.Children.Add(border);
@@ -451,6 +453,7 @@ namespace DrumBuddy
451
453
  if (tick.Label != null) { if (x >= left && x < left + ViewportWidth) visibleLabel = true; var label = WorkspaceUi.Text(tick.Label, 11); label.IsHitTestVisible = false; Canvas.SetLeft(label, x + 4); Canvas.SetTop(label, 3); timeline.Children.Add(label); }
452
454
  }
453
455
  if (!session.Grid.MusicalRuler || !visibleLabel) DrawTimeRuler(left, right);
456
+ DrawRecordingOverlays();
454
457
  timeline.Children.Add(selectionOverlay); timeline.Children.Add(selectionStartLine); timeline.Children.Add(selectionEndLine); timeline.Children.Add(selectionStartHandle); timeline.Children.Add(selectionEndHandle); UpdateSelectionOverlay();
455
458
  playLine.Y1 = 0; playLine.Y2 = timeline.Height; timeline.Children.Add(playLine); UpdatePlayLine(); UpdateTransport();
456
459
  undoButton.IsEnabled = !IsBusy && undo.Count > 0; redoButton.IsEnabled = !IsBusy && redo.Count > 0;
@@ -517,7 +520,7 @@ namespace DrumBuddy
517
520
  }
518
521
  private void UpdatePlayLine() { playLine.X1 = playLine.X2 = X(playhead); }
519
522
  private void UpdateTransport()
520
- { positionText.Text = WorkspaceUi.Clock(playhead / (double)Audio.Rate) + " / " + WorkspaceUi.Clock(session.End / (double)Audio.Rate); }
523
+ { positionText.Text = WorkspaceUi.Clock(playhead / (double)Audio.Rate) + " / " + WorkspaceUi.Clock(Math.Max(session.End, recording ? playhead : 0) / (double)Audio.Rate); }
521
524
  private void EnsurePlayheadVisible()
522
525
  { double x = X(playhead); if (x < scroll.HorizontalOffset || x > scroll.HorizontalOffset + scroll.ViewportWidth - 15) scroll.ScrollToHorizontalOffset(Math.Max(0, x - 30)); }
523
526
 
@@ -685,12 +688,14 @@ namespace DrumBuddy
685
688
  internal DockablePane RecordingPaneForTest { get { return recordingPane; } }
686
689
  public void SetRecordingState(bool active)
687
690
  {
688
- recording = active; recordButton.Content = active ? "Stop recording" : "Record";
691
+ recording = active; recordButton.Content = active ? "Stop recording" : "Record"; recordButton.IsEnabled = true; play.IsEnabled = !active;
692
+ playLine.Stroke = WorkspaceUi.Color(active ? "#B52C36" : "#D0772C");
689
693
  RefreshGridToolbar();
690
694
  foreach (var button in fileButtons) button.IsEnabled = !active && operation == null;
691
695
  if (active) { Stop(); ShowRecordingOptions(true); }
696
+ else { recordingPlan = null; captureRunning = false; recordingBanner.Visibility = Visibility.Collapsed; BuildHeaders(); DrawCanvas(); }
692
697
  }
693
- public async Task AddRecordedFiles(IEnumerable<string> paths, long at, int firstTrack)
698
+ public async Task AddRecordedFiles(IEnumerable<string> paths, long at, int firstTrack, bool keepPlayhead = false)
694
699
  {
695
700
  if (IsBusy) throw new IOException("Finish the editor operation before adding the take.");
696
701
  var files = paths.Where(File.Exists).ToArray();
@@ -706,7 +711,8 @@ namespace DrumBuddy
706
711
  if (session.Tracks[index].Name == "Track " + (index + 1)) session.Tracks[index].Name = Path.GetFileNameWithoutExtension(clip.Name);
707
712
  }
708
713
  try { session.Validate(); } catch { session = before; throw; }
709
- undo.Push(before); redo.Clear(); dirty = true; track = firstTrack; playhead = at;
714
+ undo.Push(before); redo.Clear(); dirty = true; track = firstTrack;
715
+ playhead = keepPlayhead ? Math.Min(EditorSession.MaxFrame, at + decoded.Max(c => (long)Math.Round(c.Duration * Audio.Rate))) : at;
710
716
  }
711
717
  finally { EndWork(); BuildHeaders(); DrawCanvas(); }
712
718
  }
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.0.0")]
23
+ [assembly: AssemblyVersion("1.6.2.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"); };
@@ -450,6 +459,7 @@ namespace DrumBuddy
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;
@@ -472,7 +482,7 @@ namespace DrumBuddy
472
482
  case "keybinds": OpenSettings("keybinds"); return;
473
483
  case "control": C<Button>("ControlTab").RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); return;
474
484
  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;
485
+ case "record": if (recorder != null) await StopRecording(true); else await StartRecording(); return;
476
486
  case "play": if (recorder != null) { await StopRecording(true); return; } break;
477
487
  case "stop": if (recorder != null) { await StopRecording(true); return; } break;
478
488
  }
@@ -610,70 +620,112 @@ namespace DrumBuddy
610
620
  }
611
621
  finally { configuringCapture = false; }
612
622
  }
613
- private async void StartRecording()
623
+ private Task StartRecording()
614
624
  {
615
- if (busy || recorder != null) return;
625
+ if (busy || recorder != null || stopping) return Task.FromResult(0);
626
+ recordingStartup = StartRecordingCore(); return recordingStartup;
627
+ }
628
+ private void LockRecordingControls(bool locked)
629
+ {
630
+ foreach (string name in new[] { "RecordTab", "StartRecordButton", "InputDevice", "InputMode", "Record16", "Record24", "RecordFloat", "RecordRate", "RefreshDevicesButton", "ChangeRecordFolderButton" }) C<Control>(name).IsEnabled = !locked;
631
+ if (!locked) C<Button>("StartRecordButton").IsEnabled = C<ComboBox>("InputDevice").SelectedItem != null;
632
+ C<Button>("StopRecordButton").IsEnabled = locked;
633
+ }
634
+ private async Task StartRecordingCore()
635
+ {
636
+ if (workspace != 3 || editorView == null) OpenWorkspace(true);
637
+ if (editorView == null || editorView.IsBusy) { Status("Finish the editor task before recording."); return; }
616
638
  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; }
639
+ if (input == null) { editorView.ShowRecordingOptions(true); editorView.ShowRecordingError("Choose a recording input under Audio devices or Record options, then click Refresh."); return; }
618
640
  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;
641
+ if (!C<Button>("StartRecordButton").IsEnabled) { editorView.ShowRecordingOptions(true); editorView.ShowRecordingError(C<TextBlock>("CaptureFormatText").Text); return; }
642
+ var mode = C<ComboBox>("InputMode").SelectedItem as ModeChoice;
643
+ var rate = C<ComboBox>("RecordRate").SelectedItem as RateChoice;
644
+ if (mode == null || rate == null) { editorView.ShowRecordingError("Choose input channels and a sample rate in Record options."); return; }
623
645
  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;
646
+ StopWorkspaces(); RecordingPlan plan;
647
+ try { plan = editorView.PlanRecording(input, mode.Mode, bits, rate.Rate, configuredInputChannels); }
648
+ catch (Exception ex) { editorView.ShowRecordingError(ex.Message); return; }
649
+ recordingTrack = plan.FirstTrack; recordingAnchor = plan.AnchorFrame;
650
+ string folder = System.IO.Path.Combine(prefs.RecordFolder, "Take_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture) + "_" + Guid.NewGuid().ToString("N").Substring(0, 6));
651
+ var capture = new Recorder(); recorder = capture; captureActive = false;
652
+ var countdown = new CancellationTokenSource(); countIn = countdown;
653
+ LockRecordingControls(true); C<Button>("StopRecordButton").Content = "Cancel count-in";
654
+ C<TextBlock>("RecordTime").Text = "00:00.00";
655
+ foreach (var meter in meters) meter.Value = 0;
656
+ C<TextBlock>("RecordingState").Text = "Preparing input"; C<TextBlock>("RecordingState").Foreground = Brush("#B52C36");
657
+ editorView.BeginRecordingCountdown(plan);
658
+ Status("Preparing " + plan.Routing);
659
+ Exception startError = null; bool cancelled = false;
635
660
  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.");
661
+ // Let the armed tracks and connecting feedback render before a driver call.
662
+ await window.Dispatcher.InvokeAsync((Action)delegate { }, DispatcherPriority.Background);
663
+ countdown.Token.ThrowIfCancellationRequested();
664
+ capture.Prepare(plan.Input, plan.Mode, plan.Bits, folder, plan.SampleRate);
665
+ if (capture.ChannelCount != plan.InputChannels) throw new IOException("The device's input layout changed. Refresh Record options, check the destination tracks, and try again.");
666
+ await RecordingCountdown.Run(plan.Beats, plan.BeatSeconds, delegate(int remaining) {
667
+ editorView.ShowCountdownBeat(remaining);
668
+ C<TextBlock>("RecordingState").Text = "Recording in " + remaining;
669
+ C<TextBlock>("RecordTime").Text = remaining.ToString(CultureInfo.InvariantCulture);
670
+ Status("Count-in: " + remaining + " | " + plan.Routing + " | Stop or Escape cancels.");
671
+ }, countdown.Token);
672
+ countdown.Token.ThrowIfCancellationRequested();
673
+ capture.BeginCapture(); captureStarted = DateTime.UtcNow; captureActive = true;
674
+ editorView.BeginRecordingCapture();
675
+ C<Button>("StopRecordButton").Content = "Stop & save";
676
+ C<TextBlock>("RecordingState").Text = "Recording"; C<TextBlock>("RecordTime").Text = "00:00.00";
677
+ Status("Recording | " + plan.Routing + " | Click Stop & save when you finish.");
643
678
  }
679
+ catch (OperationCanceledException) { cancelled = true; }
644
680
  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();
681
+ finally { if (ReferenceEquals(countIn, countdown)) countIn = null; countdown.Dispose(); }
682
+ if (startError == null && !cancelled) return;
683
+ captureActive = false; capture.Stop(); await capture.Finish();
684
+ recorder = null; editorView.SetRecordingState(false); LockRecordingControls(false);
685
+ C<Button>("StopRecordButton").Content = "Stop & save";
686
+ C<TextBlock>("RecordTime").Text = "00:00.00";
687
+ C<TextBlock>("RecordingState").Text = cancelled ? "Count-in cancelled" : "Could not start recording";
688
+ if (cancelled) Status("Count-in cancelled. No recording was started.");
689
+ else {
690
+ editorView.ShowRecordingError(startError.Message);
691
+ Status("Could not start recording: " + startError.Message + " Check the selected input and close other audio apps.");
651
692
  }
652
693
  }
653
694
  private async Task StopRecording(bool addFiles)
654
695
  {
696
+ if (countIn != null) {
697
+ var startup = recordingStartup; countIn.Cancel(); if (startup != null) await startup; return;
698
+ }
655
699
  if (recorder == null || stopping) return;
656
- stopping = true; C<Button>("StopRecordButton").IsEnabled = false;
700
+ stopping = true; captureActive = false; C<Button>("StopRecordButton").IsEnabled = false;
657
701
  Recorder finished = recorder;
658
- Status("Saving your recording…");
659
- finished.Stop(); await finished.Finish(); recorder = null;
702
+ Status("Saving your recording...");
703
+ finished.Stop();
704
+ long frames = Interlocked.Read(ref finished.Frames);
705
+ if (editorView != null) { editorView.UpdateRecordingProgress(frames, finished.SampleRate); editorView.ShowRecordingSaving(); }
706
+ await finished.Finish(); recorder = null;
660
707
  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";
708
+ LockRecordingControls(false); C<Button>("StopRecordButton").Content = "Stop & save";
709
+ C<TextBlock>("RecordingState").Text = finished.Error != null ? "Recording stopped - check status" : frames > 0 ? "Take saved" : "No audio captured";
663
710
  C<TextBlock>("RecordingState").Foreground = Brush(finished.Error == null ? "#006D68" : "#B52C36");
664
711
  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; }
712
+ if (addFiles && frames > 0 && finished.Paths.Count > 0 && editorView != null) {
713
+ try { await editorView.AddRecordedFiles(finished.Paths, recordingAnchor, recordingTrack, true); }
714
+ catch (Exception ex) { editorView.ShowRecordingError("Take saved to its folder, but could not be added to the timeline: " + ex.Message); return; }
668
715
  }
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.");
716
+ if (finished.Error != null) {
717
+ editorView.ShowRecordingError(finished.Error.Message);
718
+ Status("Recording stopped: " + finished.Error.Message + " Captured audio was kept.");
719
+ }
720
+ 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
721
  }
671
722
  private async Task Tick()
672
723
  {
673
724
  if (closeWhenIdle && !busy && !WorkspacesBusy && recorder == null) { closeWhenIdle = false; window.Close(); return; }
674
- if (recorder == null || stopping) return;
725
+ if (recorder == null || stopping || !captureActive) return;
675
726
  long frames = Interlocked.Read(ref recorder.Frames);
676
727
  C<TextBlock>("RecordTime").Text = WorkspaceUi.Clock(frames / (double)recorder.SampleRate);
728
+ if (editorView != null) editorView.UpdateRecordingProgress(frames, recorder.SampleRate);
677
729
  if (frames == 0 && (DateTime.UtcNow - captureStarted).TotalSeconds > 8 && recorder.Error == null)
678
730
  recorder.Error = new IOException("No audio arrived from the selected input. Check its connection, microphone permissions and selected channels.");
679
731
  if (recorder.SourceFormat != null)
@@ -711,7 +763,7 @@ namespace DrumBuddy
711
763
  "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
764
  "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
765
  "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" +
766
+ "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" +
715
767
  "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
768
  "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
769
  "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" +
@@ -721,7 +773,7 @@ namespace DrumBuddy
721
773
  "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
774
  "EDITOR\nA 16-track audio timeline. Drag the upper ruler and release to place the playhead. The left edge snaps to zero; Home/To start goes to zero and End goes to the timeline end. Hold Alt to bypass snapping. Drag across a track to select time; two thinner blue sticks mark the selection start/end. Drag their lower-ruler handles to adjust the boundaries. Cut/copy/paste, split, undo/redo, Space to play/pause and Ctrl+S to save. Cuts leave a gap. Choose Selected track or All 16 tracks for edits. Save .dbedit sessions and export PCM or float WAV mixes. Original source files are preserved.\n\n" +
723
775
  "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.0 · FFmpeg + NAudio",
776
+ "Basic tools run locally without an account. This release is for Windows; macOS is planned.\n\nFlux 1.6.2 · FFmpeg + NAudio",
725
777
  "Flux — quick help",MessageBoxButton.OK,MessageBoxImage.Information);
726
778
  }
727
779
  }
@@ -735,6 +787,7 @@ namespace DrumBuddy
735
787
  {
736
788
  if (args.Length > 0 && args[0] == "--self-test") return SelfTest.Run(args.Skip(1).ToArray());
737
789
  if (args.Length > 0 && args[0] == "--float-self-test") return FloatRecordingTests.Run(args.Skip(1).ToArray());
790
+ if (args.Length > 0 && args[0] == "--recording-self-test") return RecordingTests.Run(args.Skip(1).ToArray());
738
791
  if (args.Length > 0 && args[0] == "--grid-self-test") return TempoGridTests.Run(args.Skip(1).ToArray());
739
792
  if (args.Length > 0 && args[0] == "--zoom-self-test") return ZoomTests.Run(args.Skip(1).ToArray());
740
793
  if (args.Length > 0 && args[0] == "--selection-self-test") return EditorSelectionTests.Run(args.Skip(1).ToArray());
package/source/Main.xaml CHANGED
@@ -261,7 +261,7 @@
261
261
  <TextBlock x:Name="RecordTime" Grid.Column="1" Text="00:00" FontSize="22" FontFamily="Consolas" />
262
262
  </Grid>
263
263
  <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" />
264
+ <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
265
  <Grid>
266
266
  <Grid.ColumnDefinitions>
267
267
  <ColumnDefinition />
@@ -1,7 +1,10 @@
1
- FLUX 1.6.0
1
+ FLUX 1.6.2
2
2
 
3
3
  INSTALL / UPDATE
4
4
  Close the app. Run npm.cmd install -g @pro-gram/flux@latest
5
+ If npm skips its setup script, run flux --setup to finish the desktop install
6
+ and create the Flux Desktop and Start menu shortcuts. With PowerShell scripts
7
+ disabled, use flux.cmd --setup instead. First launch also completes setup.
5
8
  Launch flux.cmd, or the Flux Desktop shortcut.
6
9
  Migrating: npm.cmd uninstall -g @pro-gram/drumbuddy before installing Flux.
7
10
  Windows 10/11 x64 only in this release.
@@ -29,8 +32,14 @@ Fit shows the full project; Selection fills the view with the selected range.
29
32
  Ctrl+= / Ctrl+- zoom; Ctrl+0 fits the project; Ctrl+Shift+0 fits the selection.
30
33
  Zoom can reveal individual samples. Close-up waveforms load in the background.
31
34
  Record options opens the sidebar. Select input, channels, rate and format.
32
- The take is saved at the playhead on the selected track; separate inputs use
33
- 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.
34
43
  Use direct headphone monitoring. Software overdub synchronization is not included.
35
44
 
36
45
  AUDIO DEVICES
@@ -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,141 @@
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
+ editor.SetRecordingState(false);
99
+ Check(!editor.IsBusy && editor.ArmedTracksForTest.Length == 0 && editor.RecordingOverlaysForTest == 0,
100
+ "Release reserved tracks and recording visuals after cancellation");
101
+ editor.BeginRecordingCountdown(plan); editor.BeginRecordingCapture();
102
+ var capture = new Recorder(); capture.StartBuffered(CaptureLayout.Create(2, -1), 32, Path.Combine(root, "take"), 48000, false);
103
+ byte[] pcm = new byte[48000 * 2 * 2];
104
+ for (int frame = 0; frame < 48000; frame++) {
105
+ Buffer.BlockCopy(BitConverter.GetBytes((short)8192), 0, pcm, frame * 4, 2);
106
+ Buffer.BlockCopy(BitConverter.GetBytes((short)-4096), 0, pcm, frame * 4 + 2, 2);
107
+ }
108
+ editor.UpdateRecordingProgress(0, 48000);
109
+ Check(editor.PlayheadFrame == plan.AnchorFrame, "Keep the playhead still until input frames actually arrive");
110
+ for (int second = 1; second <= 3; second++) {
111
+ capture.ProcessWindowsPcm(pcm, pcm.Length, 2); editor.UpdateRecordingProgress(Interlocked.Read(ref capture.Frames), capture.SampleRate);
112
+ Check(editor.PlayheadFrame == plan.AnchorFrame + second * Audio.Rate, "Advance the recording playhead by captured audio at second " + second);
113
+ }
114
+ Check(editor.RecordingOverlaysForTest == 2 && editor.RecordingHeadlineForTest.Contains("00:03.00"),
115
+ "Show a growing recording on both destination tracks and the elapsed take time");
116
+ Picture(editor, Path.Combine(root, "recording-dark.png"), 1280, 760);
117
+ Picture(editor, Path.Combine(root, "recording-compact-dark.png"), 850, 560);
118
+ ThemePalette.Apply(false); Picture(editor, Path.Combine(root, "recording-light.png"), 1280, 760); ThemePalette.Apply(true);
119
+ capture.Stop(); await capture.Finish(); editor.ShowRecordingSaving();
120
+ var originals = capture.Paths.Select(File.ReadAllBytes).ToArray();
121
+ editor.SetRecordingState(false); await editor.AddRecordedFiles(capture.Paths, plan.AnchorFrame, plan.FirstTrack, true);
122
+ var session = editor.SessionForTest;
123
+ 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 }),
124
+ "Save and insert both recorded inputs at the registered start and tracks");
125
+ Check(editor.PlayheadFrame == plan.AnchorFrame + Audio.Rate * 3L && editor.RecordingOverlaysForTest == 0 && !editor.IsBusy,
126
+ "Leave the playhead at the end of the saved take and clear live recording state");
127
+ Check(capture.Paths.Select((file, index) => File.ReadAllBytes(file).SequenceEqual(originals[index])).All(value => value),
128
+ "Preserve original recording files while importing the take");
129
+ await editor.ExecuteAction("undo");
130
+ Check(editor.SessionForTest.Regions.Count == 0 && capture.Paths.All(File.Exists), "Undo the recording insertion without deleting captured audio");
131
+ }
132
+ owner.Content = null; owner.Close();
133
+ });
134
+ log.Add("Generated audio buffers and offscreen UI. A physical microphone/ASIO device still requires an acceptance recording.");
135
+ File.WriteAllLines(Path.Combine(root, "recording-test-results.txt"), log, Encoding.UTF8); return 0;
136
+ }
137
+ catch (Exception ex) { log.Add("FAIL " + ex); File.WriteAllLines(Path.Combine(root, "recording-test-results.txt"), log, Encoding.UTF8); return 1; }
138
+ finally { InterfacePreferences.Current = old; ThemePalette.Apply(old.Theme == "Dark"); }
139
+ }
140
+ }
141
+ }
package/source/Setup.cs CHANGED
@@ -14,7 +14,7 @@ using System.Windows.Markup;
14
14
  using Microsoft.Win32;
15
15
 
16
16
  [assembly: AssemblyTitle("Flux Setup")]
17
- [assembly: AssemblyVersion("1.6.0.0")]
17
+ [assembly: AssemblyVersion("1.6.2.0")]
18
18
 
19
19
  namespace DrumBuddySetup
20
20
  {
@@ -101,7 +101,7 @@ namespace DrumBuddySetup
101
101
  Directory.CreateDirectory(Path.GetDirectoryName(destination));
102
102
  File.Copy(source,destination,true);
103
103
  }
104
- File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.0\n" + InstallPath,Encoding.UTF8);
104
+ File.WriteAllText(Path.Combine(InstallPath,".drumbuddy-installed"),"Flux 1.6.2\n" + InstallPath,Encoding.UTF8);
105
105
  string[] files = Directory.GetFiles(stage,"*",SearchOption.AllDirectories).Select(f => f.Substring(stage.Length + 1)).Concat(new[] { ".drumbuddy-installed","installed-files.txt" }).ToArray();
106
106
  File.WriteAllLines(Path.Combine(InstallPath,"installed-files.txt"),files,Encoding.UTF8);
107
107
  string exe = Path.Combine(InstallPath,"DrumBuddy.exe");
@@ -112,7 +112,7 @@ namespace DrumBuddySetup
112
112
  RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),"Programs","Drum Buddy.lnk"),exe);
113
113
  RemoveLegacyShortcut(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),"Drum Buddy.lnk"),exe);
114
114
  using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(RegistryKey)) {
115
- key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.0"); key.SetValue("Publisher","Flux");
115
+ key.SetValue("DisplayName","Flux"); key.SetValue("DisplayVersion","1.6.2"); key.SetValue("Publisher","Flux");
116
116
  key.SetValue("InstallLocation",InstallPath); key.SetValue("DisplayIcon",BrandIcon);
117
117
  key.SetValue("UninstallString","\"" + Path.Combine(InstallPath,"Uninstall.exe") + "\"");
118
118
  key.SetValue("NoModify",1); key.SetValue("NoRepair",1);
@@ -137,17 +137,18 @@ namespace DrumBuddySetup
137
137
  foreach (var entry in zip.Entries) {
138
138
  if (string.IsNullOrEmpty(entry.Name)) continue;
139
139
  string relative = entry.FullName.Replace('/',Path.DirectorySeparatorChar);
140
+ string manifestName = entry.FullName.Replace('\\','/');
140
141
  string target = Path.GetFullPath(Path.Combine(root,relative));
141
142
  if (!target.StartsWith(root,StringComparison.OrdinalIgnoreCase) || relative.Contains(":")) throw new IOException("Invalid installer file path.");
142
143
  string expected;
143
- if (!hashes.TryGetValue(entry.FullName,out expected)) throw new IOException("An installer file is not in the integrity manifest.");
144
+ if (!hashes.TryGetValue(manifestName,out expected)) throw new IOException("An installer file is not in the integrity manifest.");
144
145
  Directory.CreateDirectory(Path.GetDirectoryName(target));
145
146
  using (var input = entry.Open()) using (var output = new FileStream(target,FileMode.Create,FileAccess.Write)) input.CopyTo(output);
146
147
  using (var sha = SHA256.Create()) using (var file = File.OpenRead(target)) {
147
148
  string actual = BitConverter.ToString(sha.ComputeHash(file)).Replace("-","");
148
149
  if (!string.Equals(actual,expected,StringComparison.OrdinalIgnoreCase)) throw new IOException("An installer file failed its integrity check. Please obtain a fresh copy.");
149
150
  }
150
- hashes.Remove(entry.FullName);
151
+ hashes.Remove(manifestName);
151
152
  if (progress != null) progress((double)++count / zip.Entries.Count);
152
153
  }
153
154
  }
@@ -86,7 +86,7 @@ namespace DrumBuddy
86
86
  int count = 0;
87
87
  for (long index = first; index <= last && count++ < 5000; index += stride) {
88
88
  double frame = index * seconds * Audio.Rate; if (frame > EditorSession.MaxFrame) break;
89
- string label = kind == 2 && index % labelStride == 0 ? (index + 1).ToString(CultureInfo.InvariantCulture) + ".1" : null;
89
+ string label = kind == 2 && index % labelStride == 0 ? (index + 1).ToString(CultureInfo.InvariantCulture) : null;
90
90
  if (kind == 1 && spacing >= 58 && index % options.BeatsPerBar != 0) label = (index / options.BeatsPerBar + 1) + "." + (index % options.BeatsPerBar + 1);
91
91
  yield return new TempoGridTick { Frame = FrameAt(frame), Kind = kind, Label = label };
92
92
  }
@@ -49,6 +49,8 @@ namespace DrumBuddy
49
49
  long distant = (long)Math.Round(100000 * 240.0 / 123 / 128 * Audio.Rate);
50
50
  Check(Math.Abs(TempoGrid.Snap(distant, grid) - distant) <= 1, "Avoid accumulated frame rounding drift far into a fine grid");
51
51
  grid = new TempoGridOptions { Division = 32 };
52
+ var bars = TempoGrid.VisibleTicks(grid, 0, 400, 40).Where(t => t.Kind == 2 && t.Label != null).ToArray();
53
+ Check(bars.Length >= 2 && bars[0].Label == "1" && bars[1].Label == "2", "Show whole bar numbers on the musical ruler");
52
54
  long first = TimelineGesture.FrameAt(2.5, 40, new long[] { 0 }, false, grid, true, true);
53
55
  Check(first == 2756, "Do not swallow the first 1/32 grid line with the start-of-track magnet");
54
56
  Check(TimelineGesture.FrameAt(21.4, 40, new long[0], false, new TempoGridOptions(), true, false) == 22050, "Snap a playhead gesture to the nearest sixteenth-note frame");
@@ -1,4 +1,4 @@
1
- Flux 1.6.0 - third-party components
1
+ Flux 1.6.2 - third-party components
2
2
 
3
3
  FFmpeg 9.0.2, release essentials build by Gyan Doshi
4
4
  Used as a separate command-line decoding process.
@@ -1,16 +0,0 @@
1
- The independent C# R16 project container reader/writer uses file-format facts
2
- researched with zoomrlib 1.1.0 by Remy Taymans and Glenn Boysko:
3
- https://gitlab.com/remytms/zoomrlib
4
- https://pypi.org/project/zoomrlib/1.1.0/
5
-
6
- Its unchanged binary fixture projects were used for round-trip verification.
7
- The upstream Python implementation is not bundled or linked into Drum Buddy.
8
- Download the upstream release's tests/data to reproduce the native fixture tests:
9
- DrumBuddy.exe --control-self-test TEST_OUTPUT PATH_TO_TEST_FIXTURES
10
-
11
- Mackie control protocol reference (message assignments only):
12
- https://github.com/justinfrankel/reaper-sdk/blob/main/reaper-plugins/reaper_csurf/csurf_mcu.cpp
13
-
14
- Official R16 manuals:
15
- https://zoomcorp.com/media/documents/E_R16_.pdf
16
- https://zoomcorp.com/media/documents/E_R16_AIF_.pdf