Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Duplex API #553

Closed
wants to merge 9 commits into from
Closed

Proposal: Duplex API #553

wants to merge 9 commits into from

Conversation

ollpu
Copy link
Contributor

@ollpu ollpu commented Mar 16, 2021

We're working on an audio editor / DAW, and full duplex audio IO is a fairly important feature for such software that is currently missing from CPAL.

For reference, duplex (or more accurately, full duplex) means that both input and output are processed at the same time, on the same stream, implicitly synchronized. Hosts have different levels of support for this: some provide it natively (JACK, CoreAudio), while on others this API merely allows input and output to run on the same loop to simplify synchronization. Without it, a CPAL user needs to handle synchronization manually and buffer an indeterminate amount to avoid over/underruns, as is done in the feedback example. This proposal does not address combining input and output streams from separate devices since in general that opens up further responsibilities such as clock drift compensation.

The API I have laid out in this PR is based on the one previously discussed here, though some things in CPAL have since changed. It is mostly a mirror of the existing input / output APIs, forming a third type of stream called "duplex". In particular,

  • input/output_devices -> duplex_devices, default_input/output_device -> default_duplex_device
  • supported_input/output_configs -> supported_duplex_configs, default_input/output_config -> default_duplex_config
  • build_input/output_stream -> build_duplex_stream
    • The data callback for duplex has the signature FnMut(&Data, &mut Data, &DuplexCallbackInfo). In general the order of parameters should always be input, then output.

The largest difference is with how channels are configured. I determined it doesn't make sense to list out every possible combination of the amount of input and output channels as separate SupportedDuplexStreamConfigRange instances for the user to iterate over. Therefore I instead incorporate ranges of possible channel configurations (min, max, default) for both input and output in the SupportedDuplexStreamConfigRange structs. Hopefully this doesn't make the API too illogical. I wrote up a more in-depth rationale in this commit.

In addition, it should be straightforward to add a conversion layer to convert input/output SupportedStreamConfig into SupportedDuplexStreamConfig with zero channels of the other type. This would allow users of CPAL to only write one type of callback handler, but have their code still work even if duplex devices aren't available. Internally, build_duplex_stream would delegate to the respective raw methods so the host implementation doesn't have to be aware of it or try to open devices with zero channels.

Feedback and improvement ideas are welcome. If this is deemed the way to go, I and anyone interested can start implementing support for it in more hosts.

Host implementation status

  • JACK, part of this PR.

ollpu added 7 commits January 3, 2022 01:25
Stubbing hosts is up next.
This makes the duplex API somewhat asymmetric compared to standalone
input / output. However, something more robust than providing all
possible channel combinations is necessary for the duplex mode.

Rationale:

- Hosts might provide e.g. 32 different channel count options for both
  input and output, and even this could be limiting for some high-end
  devices or esoteric setups. 32*32 = 1024 is too much to provide as
  separate `SupportedDuplexStreamConfigRange` items.

- The duplex mode is meant for "pro audio" use-cases, where static
  channel semantics (mono, stereo, surround, etc.) are less prevalent
  compared to binding a bunch of channels, and leaving semantics to the
  user. This makes for a vague justification for the asymmetry.

- min/max is not quite sufficient, and neither is defaulting to stereo or mono.
  This is why I've added a `default` field for the channel ranges in
  duplex mode.

  On some hosts, like jack, there is no meaningful maximum channel
  count, since channels can be patched arbitrarily. There is however,
  often a physical device available where default counts can potentially
  be inferred from. Alternatively the default could be stereo.

  On CoreAudio, audio streams are always bound to concrete devices
  (either physical or statically configured). There it makes sense to
  default to the maximum channel count for the device in question.

  The host can opt to report no defaults, in which case stereo is
  preferred in `default_with_sample_rate` and
  `default_with_max_sample_rate`.

Before a more thorough redesign of the configuration API as a whole,
this seems like a reasonable middle-ground.
@nyanpasu64
Copy link
Contributor

Initially done, and it works (try the duplex example!).

Why is JACK support in a separate branch (https://github.com/rustydaw/cpal/tree/duplex-jack) not included in this PR? Right now, this PR doesn't compile when I enable the JACK feature.

ollpu and others added 2 commits January 4, 2022 18:11
Rebased with help from nyanpasu64.

---

It almost compiles.

Original (outdated) commit message:

The way the temporary interlacing buffers are handled currently is not
great. The root of the issue seems to be that JACK actually provides a
callback that allows performing allocations on the realtime thread when
buffer size changes, but this is not exposed as such in rust-jack.

See: RustAudio/rust-jack#137

The duplex implementation now mirrors input, and this panicks if buffer
size becomes greater than the original.

Co-authored-by: nyanpasu64 <[email protected]>
@ollpu
Copy link
Contributor Author

ollpu commented Jan 4, 2022

Why is JACK support in a separate branch (https://github.com/rustydaw/cpal/tree/duplex-jack) not included in this PR?

My thought was that it wouldn't make sense to collect all host implementations into this PR, so it's easier to review the design. Having one implementation as an example isn't a bad idea so I've incorporated it here now.

Apologies for leaving this PR hanging for so long – I was sort of hoping for some comments before continuing work on it. It should now be good to merge as-is, and other host implementations can be added in later.

Edit: Looks like the default heuristic comparator still needs an implementation.

@ollpu ollpu marked this pull request as ready for review January 4, 2022 16:36
@nyanpasu64
Copy link
Contributor

(copied from Discord) When running under pipewire-jack, cargo run --example duplex --features jack -- -j has the minimum possible latency (equal to the current pipewire quantum) as measured by jack_iodelay, so it seems to work:

Screenshot_2022-01-04_15-58-46

Latency is unaffected when I chain 3 cpal jack duplex clients:

Screenshot_2022-01-04_16-03-05

I assume latency should be deterministic, since the JACK server calls clients with input, and waits for it to return output synchronously.

Startup seems glitch-free most of the time?

Repository owner closed this by deleting the head repository Oct 12, 2022
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.

3 participants