Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `DeviceTrait::build_duplex_stream` and `build_duplex_stream_raw` for synchronized input/output.
- `duplex` module with `DuplexStreamConfig` and `DuplexCallbackInfo` types.
- **CoreAudio**: Duplex stream support with hardware-synchronized input/output.
- Example `duplex_feedback` demonstrating duplex stream usage.
- `DeviceBusy` error variant for retriable device access errors (EBUSY, EAGAIN).
- **ALSA**: `Debug` implementations for `Host`, `Device`, `Stream`, and internal types.
- **ALSA**: Example demonstrating ALSA error suppression during enumeration.
- **WASAPI**: Enable as-necessary resampling in the WASAPI server process.

### Changed

- **POTENTIALLY BREAKING**: `DeviceTrait` now includes `build_duplex_stream()` and `build_duplex_stream_raw()` methods. The default implementation returns `StreamConfigNotSupported`, so external implementations are compatible without changes.
- Bump overall MSRV to 1.78.
- **ALSA**: Update `alsa` dependency to 0.11.
- **ALSA**: Bump MSRV to 1.82.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ name = "record_wav"
[[example]]
name = "synth_tones"

[[example]]
name = "duplex_feedback"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This library currently supports the following:
- Enumerate known supported input and output stream formats for a device.
- Get the current default input and output stream formats for a device.
- Build and run input and output PCM streams on a chosen device with a given stream format.
- Build and run duplex (simultaneous input/output) streams with hardware clock synchronization.

Currently, supported hosts include:

Expand Down Expand Up @@ -209,6 +210,7 @@ CPAL comes with several examples demonstrating various features:
- `beep` - Generate a simple sine wave tone
- `enumerate` - List all available audio devices and their capabilities
- `feedback` - Pass input audio directly to output (microphone loopback)
- `duplex_feedback` - Hardware-synchronized duplex stream loopback
- `record_wav` - Record audio from the default input device to a WAV file
- `synth_tones` - Generate multiple tones simultaneously

Expand Down
107 changes: 107 additions & 0 deletions examples/duplex_feedback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! Feeds back the input stream directly into the output stream using a duplex stream.
//!
//! Unlike the `feedback.rs` example which uses separate input/output streams with a ring buffer,
//! duplex streams provide hardware-synchronized input/output without additional buffering.
//!
//! Note: Currently only supported on macOS (CoreAudio). Windows (WASAPI) and Linux (ALSA)
//! implementations are planned.

#[cfg(target_os = "macos")]
mod imp {
use clap::Parser;
use cpal::duplex::DuplexStreamConfig;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{BufferSize, ChannelCount, FrameCount, Sample, SampleRate};

#[derive(Parser, Debug)]
#[command(version, about = "CPAL duplex feedback example", long_about = None)]
struct Opt {
/// The audio device to use (must support duplex operation)
#[arg(short, long, value_name = "DEVICE")]
device: Option<String>,

/// Number of input channels
#[arg(long, value_name = "CHANNELS", default_value_t = 2)]
input_channels: ChannelCount,

/// Number of output channels
#[arg(long, value_name = "CHANNELS", default_value_t = 2)]
output_channels: ChannelCount,

/// Sample rate in Hz
#[arg(short, long, value_name = "RATE", default_value_t = 48000)]
sample_rate: SampleRate,

/// Buffer size in frames (omit for device default)
#[arg(short, long, value_name = "FRAMES")]
buffer_size: Option<FrameCount>,
}

pub fn run() -> anyhow::Result<()> {
let opt = Opt::parse();
let host = cpal::default_host();

// Find the device by device ID or use default
let device = match opt.device {
Some(device_id_str) => {
let device_id = device_id_str.parse().expect("failed to parse device id");
host.device_by_id(&device_id)
.expect(&format!("failed to find device with id: {}", device_id_str))
}
None => host
.default_output_device()
.expect("no default output device"),
};

println!("Using device: \"{}\"", device.description()?.name());

// Create duplex stream configuration.
let config = DuplexStreamConfig {
input_channels: opt.input_channels,
output_channels: opt.output_channels,
sample_rate: opt.sample_rate,
buffer_size: opt
.buffer_size
.map(|s| BufferSize::Fixed(s))
.unwrap_or(BufferSize::Default),
};

println!("Building duplex stream with config: {config:?}");

let stream = device.build_duplex_stream::<f32, _, _>(
&config,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it is copy able. So we do not need & I think

move |input, output, _info| {
output.fill(Sample::EQUILIBRIUM);
let copy_len = input.len().min(output.len());
output[..copy_len].copy_from_slice(&input[..copy_len]);
},
|err| eprintln!("Stream error: {err}"),
None,
)?;

println!("Successfully built duplex stream.");
println!(
"Input: {} channels, Output: {} channels, Sample rate: {} Hz, Buffer size: {:?} frames",
opt.input_channels, opt.output_channels, opt.sample_rate, opt.buffer_size
);

println!("Starting duplex stream...");
stream.play()?;

println!("Playing for 10 seconds... (speak into your microphone)");
std::thread::sleep(std::time::Duration::from_secs(10));

println!("Done!");
Ok(())
}
}

fn main() {
#[cfg(target_os = "macos")]
imp::run().unwrap();

#[cfg(not(target_os = "macos"))]
{
eprintln!("Duplex streams are not supported on this platform.");
}
}
68 changes: 68 additions & 0 deletions src/duplex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Duplex audio stream support with synchronized input/output.
//!
//! This module provides types for building duplex (simultaneous input/output) audio streams
//! with hardware clock synchronization.
//!
//! See `examples/duplex_feedback.rs` for a working example.

use crate::{ChannelCount, InputStreamTimestamp, OutputStreamTimestamp, SampleRate};

/// Information passed to duplex callbacks.
///
/// This contains timing information for the current audio buffer, combining
/// both input and output timing. A duplex stream has a single callback invocation
/// that provides synchronized input and output data.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DuplexCallbackInfo {
input_timestamp: InputStreamTimestamp,
output_timestamp: OutputStreamTimestamp,
}

impl DuplexCallbackInfo {
/// Create a new DuplexCallbackInfo.
///
/// Note: Both timestamps will share the same `callback` instant since there is
/// only one callback invocation for a duplex stream.
pub fn new(
input_timestamp: InputStreamTimestamp,
output_timestamp: OutputStreamTimestamp,
) -> Self {
Self {
input_timestamp,
output_timestamp,
}
}

/// The timestamp for the input portion of the duplex stream.
///
/// Contains the callback instant and when the input data was captured.
pub fn input_timestamp(&self) -> InputStreamTimestamp {
self.input_timestamp
}

/// The timestamp for the output portion of the duplex stream.
///
/// Contains the callback instant and when the output data will be played.
pub fn output_timestamp(&self) -> OutputStreamTimestamp {
self.output_timestamp
}
}

/// Configuration for a duplex audio stream.
///
/// Unlike separate input/output streams, duplex streams require matching
/// configuration for both directions since they share a single device context.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DuplexStreamConfig {
/// Number of input channels.
pub input_channels: ChannelCount,

/// Number of output channels.
pub output_channels: ChannelCount,

/// Sample rate in Hz.
pub sample_rate: SampleRate,

/// Requested buffer size in frames.
pub buffer_size: crate::BufferSize,
}
Loading