Bump to v1.5.0: cross-lane routing fix, recording double-tap fix, menu reorg
Bug fixes: * BothIndependent recording was double-tapped — when both WASAPI and ASIO outputs were ticked, recordings came out garbled and ~2x the expected duration. AudioRecorder now uses four per-lane rings (sent-wasapi, sent-asio, recv-wasapi, recv-asio) plus a writer- thread mix step that drains min(wasapi, asio) frames and sum-mixes with the soft-tanh limiter. Tap signatures gained a RenderRoute parameter; Mixed maps to the wasapi slot. * A peer announcing on the WASAPI lane was inaudible when the receiver only had an ASIO output ticked (and vice versa). The session opened, samples flowed into the SessionPlayout ring, but ReadForRoute(AsioLane) skipped any session whose Route was WasapiLane so nothing drained the ring. PlayoutEngine now tracks per-lane "is this lane backed by a device" via volatile bools settable through SetLaneActive(RenderRoute, bool), called from CompositeRenderBackend.SetOutputDevices whenever the device split changes. ReadForRoute admits orphan sessions when the other lane is inactive. Menu reorganisation: * New Options menu (Alt+O) holds Recording settings (Alt+S), Keyboard shortcuts (Alt+K, Ctrl+K), Startup behaviour (Alt+T), Preferences (Alt+P, Ctrl+P). Pre-v1.5 these were scattered across File menu, Record menu, and inside the Preferences dialog itself. * Record menu mnemonic moved from Alt+O to Alt+K, rendered as "Record (Alt+K)" so the chord is visible despite K not being a letter in "Record". Alt+R is taken by Receive audio. * File menu — new Recent profiles submenu (Alt+F, R) listing the five most-recently-opened profiles. Press 1..5 inside the submenu to jump to a slot. Missing files are skipped from the menu but kept in storage. * Rename current profile moves to Alt+M (was R), Minimise to tray moves to Alt+N (was M). * Lock to audio clock was Alt+K, now Alt+D. UX additions: * Ctrl+O = Open profile (matches the menu chord). * New global hotkey: Start / Stop recording. Pickable from Options -> Keyboard shortcuts. Unbound by default. Works system-wide. Wire format and audio pipeline unchanged from v1.4 — v1.4 and v1.5 peers interoperate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b96ba82378
commit
4915a9afe1
@@ -179,10 +179,12 @@ public sealed class AudioSender : IDisposable
|
||||
/// Optional callback invoked every time a SenderLane is about to encode a buffer of
|
||||
/// captured float audio. The span is 48 kHz interleaved stereo float, lives on the
|
||||
/// audio thread, and must be processed quickly or copied — the buffer is reused on
|
||||
/// the very next callback. The recorder uses this tap to capture "what we sent" with
|
||||
/// zero impact on the wire path (no allocation, no extra encoder pass). Null = no tap.
|
||||
/// the very next callback. The <see cref="RenderRoute"/> tag identifies which
|
||||
/// SenderLane invoked the callback (Mixed in classic modes; WasapiLane or AsioLane in
|
||||
/// BothIndependent) so the recorder can keep per-lane streams separate and mix them
|
||||
/// at drain time rather than appending sequentially. Null = no tap.
|
||||
/// </summary>
|
||||
public Action<ReadOnlyMemory<float>>? OnSentSamples { get; set; }
|
||||
public Action<ReadOnlyMemory<float>, RenderRoute>? OnSentSamples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper for <see cref="SenderLane"/> to invoke <see cref="OnSentSamples"/>
|
||||
@@ -190,11 +192,11 @@ public sealed class AudioSender : IDisposable
|
||||
/// any exception from the user callback — a misbehaving recorder must not crash the
|
||||
/// audio thread.
|
||||
/// </summary>
|
||||
internal void DispatchSentSamples(ReadOnlyMemory<float> samples)
|
||||
internal void DispatchSentSamples(ReadOnlyMemory<float> samples, RenderRoute lane)
|
||||
{
|
||||
var cb = OnSentSamples;
|
||||
if (cb is null) return;
|
||||
try { cb(samples); } catch { /* recorder failure isolated from audio path */ }
|
||||
try { cb(samples, lane); } catch { /* recorder failure isolated from audio path */ }
|
||||
}
|
||||
|
||||
public AudioSender()
|
||||
|
||||
@@ -158,8 +158,12 @@ internal sealed class SenderLane
|
||||
// Recording tap — the recorder gets the float audio about to be encoded. The lane
|
||||
// doesn't know whether the recorder is running; the dispatcher early-outs when no
|
||||
// callback is wired. Captured here (before encoding) so the recording is bit-clean
|
||||
// float, independent of which codec the wire is using.
|
||||
owner.DispatchSentSamples(stereoFloats);
|
||||
// float, independent of which codec the wire is using. The lane tag is forwarded so
|
||||
// BothIndependent mode (where both WASAPI and ASIO SenderLanes fire on every capture
|
||||
// callback) can be correctly handled by the recorder — each lane writes into its own
|
||||
// ring, and the recorder mixes them rather than appending them sequentially (which
|
||||
// would double the file's effective sample rate). 2026-05-15 fix.
|
||||
owner.DispatchSentSamples(stereoFloats, route);
|
||||
|
||||
// Discontinuity probe — what does the audio look like just before we encode it?
|
||||
// Compared to the receiver's per-stage probes, this tells us whether artefacts are
|
||||
|
||||
Reference in New Issue
Block a user