Skip to content

Same-device duplex, named I/O paths, and record-offset compensation - #5

Merged
mattpetters merged 1 commit into
mainfrom
cursor/duplex-io-routing-60f9
Oct 29, 2026
Merged

mattpetters merged 1 commit into
mainfrom
cursor/duplex-io-routing-60f9

Conversation

@mattpetters

Copy link
Copy Markdown
Owner

Summary

Record and playback can share one audio device, at one sample rate and one buffer. Named input and output paths carry hardware channel lists, tracks assign those paths (or busses), and a finished take is shifted so it lines up with what was heard.

cpal 0.18 has no single full-duplex callback. When Setup leaves the input unset, recording opens the output device's input side at the output rate and the shared buffer size. Driver timestamps supply input and output latency. If the driver reports neither and a fixed buffer is open, compensation is two buffers (one each way), plus the user offset from setup.record_offset.

Upstream work reused

Surveyed open PRs, issues, and docs/gaps.md on storytold/soundcraft before writing this. Nothing was opened against that repo.

Source What we took
grovebit, #84 (merged) Record-to-disk was already in this tree: lock-free capture ring, a writer thread, WAV headers rewritten every second. Docs still described it as open; they now say playback streaming is the remaining half.
Testsubject1337, #575 The I/O Setup model: named input, output, and bus paths, explicit hardware channels, active flag, rename updates track routes. Not the channel-grid window or automatic sub-path generator (that PR is ~4.4k lines and stacks on other unmerged work).
mrrobbiec, #808 Monitor-ring backlog trim: keep one input callback plus the current read plus 32 frames, and only drop once the queue exceeds that allowance by itself or by 128 frames.
Jafz2001, #206 Low Latency Monitoring bypasses inserts (and here, plugin-delay alignment) on tracks that are hearing their input. Not the temporary smaller-buffer device reopen, which fights the chosen buffer and the ASIO open deadlock in storytold#769.

Looked at and did not merge: #490 (lock-free monitor ring; the existing ring already converts rates and only try_locks), #1034 (mix block follows the device buffer, up to 8192), #491 (JACK/PipeWire), #43 (DiskWavReader, not wired into the mix), #1077 (headless setup.playback_engine still only stores the session values).

What landed

  • Same device, rate, and buffer for input and output when the input is unset. An explicit input device is left alone.
  • setup.record_offset (samples, compensate). Placing a take moves it earlier by the measured latency. A shift past sample 0 trims the head of the file. Loop passes stay on the loop's timeline.
  • InputPath / OutputPath channel lists. setup.io actions: create_input, rename_input, delete_input, set_channels. New sessions start with In 1-2, In 1, and In 2. Older sessions still parse "In 3".
  • Tracks route with the existing track.input / track.output commands. A hardware output path sums onto those device channels and is not folded into a narrower device.
  • Options ▸ Low Latency Monitoring skips inserts and delay-compensation alignment on monitored or record-armed tracks that are hearing the input.
  • The monitor ring drops a stale backlog instead of playing it as extra latency.
  • I/O Setup lists inputs, channel numbers (1-based), and the record offset.

Remaining gaps

Tests

cargo test --lib on Rust 1.95, after cargo fmt and cargo clippy -D warnings for model, mix, playback, engine, and ui-egui (all targets):

  • soundcraft-model: 48 passed
  • soundcraft-mix: 51 passed
  • soundcraft-engine: 317 passed
  • soundcraft-playback: 58 passed

New coverage: channel maps and old sessions without input paths, hardware outputs on device channels (and not folded into stereo), a named input monitored on its channel, low-latency monitoring against a lookahead limiter, record-offset placement and the trim at sample 0, monitor backlog trim vs jitter, and one duplex buffer advancing input and output together.

Open in Web Open in Cursor 

…amed I/O paths.

Recording opens the output device when no separate input is chosen, at that device's rate and buffer. A take is shifted earlier by the driver's input and output latency, or by two buffers when the driver reports none. Named input and output paths carry hardware channel lists, and Low Latency Monitoring bypasses inserts and plugin-delay alignment on tracks that are hearing their input.

Co-authored-by: matt petters <mattpetters@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mattpetters
mattpetters merged commit 824b884 into main Oct 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants