Skip to content

Commit ae645f5

Browse files
committed
Merge branch 'master' of https://github.com/RustAudio/cpal
2 parents 1d8014b + 3c592d9 commit ae645f5

File tree

7 files changed

+109
-160
lines changed

7 files changed

+109
-160
lines changed

.github/workflows/cpal.yml

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060

6161
cargo-publish:
6262
if: github.event_name == 'release'
63-
needs: [test-native, test-cross, test-wasm, test-android, test-ios]
63+
needs: [test-native, test-cross, test-wasm, test-android, test-ios, test-windows-versions]
6464
runs-on: ubuntu-latest
6565
steps:
6666
- uses: actions/checkout@v5
@@ -196,15 +196,9 @@ jobs:
196196
with:
197197
tool: cross
198198

199-
- name: Check without features
200-
run: cross check --target ${{ matrix.target }} --workspace --no-default-features --verbose
201-
202199
- name: Test without features
203200
run: cross test --target ${{ matrix.target }} --workspace --no-default-features --verbose
204201

205-
- name: Check all features
206-
run: cross check --target ${{ matrix.target }} --workspace --all-features --verbose
207-
208202
- name: Test all features
209203
run: cross test --target ${{ matrix.target }} --workspace --all-features --verbose
210204

@@ -318,3 +312,51 @@ jobs:
318312

319313
- name: Build iOS example
320314
run: cd examples/ios-feedback && xcodebuild -scheme cpal-ios-example -configuration Debug -derivedDataPath build -sdk iphonesimulator
315+
316+
# Windows crate compatibility testing
317+
test-windows-versions:
318+
runs-on: windows-latest
319+
strategy:
320+
fail-fast: false
321+
matrix:
322+
include:
323+
- windows-version: "0.59.0"
324+
- windows-version: "0.60.0"
325+
- windows-version: "0.61.3"
326+
# Skip 0.62.x since we already test current version in test-native
327+
name: test-windows-v${{ matrix.windows-version }}
328+
steps:
329+
- uses: actions/checkout@v5
330+
331+
- name: Install dependencies
332+
run: |
333+
choco install llvm
334+
335+
- name: Install Rust toolchain
336+
uses: dtolnay/rust-toolchain@stable
337+
338+
- name: Rust Cache
339+
uses: Swatinem/rust-cache@v2
340+
with:
341+
key: windows-v${{ matrix.windows-version }}
342+
343+
- name: Lock windows crate to specific version
344+
shell: bash
345+
run: |
346+
# Generate Cargo.lock first if it doesn't exist
347+
cargo generate-lockfile
348+
349+
# Use cargo update --precise to lock the windows crate to a specific version
350+
cargo update --precise ${{ matrix.windows-version }} windows
351+
352+
# Verify the version was locked correctly
353+
echo "Locked windows crate version:"
354+
cargo tree | grep "windows v" || echo "Windows crate not found in dependency tree"
355+
356+
# Also check Cargo.lock
357+
echo "Cargo.lock entry:"
358+
grep -A 5 "name = \"windows\"" Cargo.lock | head -10
359+
360+
- name: Check WASAPI with windows v${{ matrix.windows-version }}
361+
run: |
362+
cargo check --verbose

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- CI: Fix cargo publish to trigger on GitHub releases instead of every master commit.
1515
- CI: Replace cargo install commands with cached tool installation for faster builds.
1616
- CI: Update actions to latest versions (checkout@v5, rust-cache@v2).
17+
- CI: Verify compatibility with windows crates since v0.58.
1718
- CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values.
1819
- CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration.
1920
- CoreAudio: Add `i8`, `i32` and `I24` sample format support (24-bit samples stored in 4 bytes).
@@ -24,7 +25,7 @@
2425
- iOS: Fix example by properly activating audio session.
2526
- WASAPI: Expose `IMMDevice` from WASAPI host Device.
2627
- WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes).
27-
- WASAPI: Update `windows` to 0.62.
28+
- WASAPI: Update `windows` to >= 0.58, <= 0.62.
2829
- Wasm: Removed optional `wee-alloc` feature for security reasons.
2930

3031
# Version 0.16.0 (2025-06-07)

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ hound = "3.5"
3838
ringbuf = "0.4.8"
3939
clap = { version = "4.5", features = ["derive"] }
4040

41+
# Support a range of versions in order to avoid duplication of this crate. Make sure to test all
42+
# versions when bumping to a new release, and only increase the minimum when absolutely necessary.
43+
# When updating this, also update the "windows-version" matrix in the CI workflow.
4144
[target.'cfg(target_os = "windows")'.dependencies]
42-
windows = { version = "0.62", features = [
45+
windows = { version = ">=0.58, <=0.62", features = [
4346
"Win32_Media_Audio",
4447
"Win32_Foundation",
4548
"Win32_Devices_Properties",

src/host/alsa/mod.rs

Lines changed: 28 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use crate::{
2222
SupportedStreamConfigRange, SupportedStreamConfigsError, I24, U24,
2323
};
2424

25-
// ALSA Latency Model and Period Configuration
26-
// ===========================================
25+
// ALSA Buffer Size Behavior
26+
// =========================
2727
//
2828
// ## ALSA Latency Model
2929
//
@@ -35,34 +35,20 @@ use crate::{
3535
// period worth of data has been consumed by hardware, ALSA triggers a callback to refill
3636
// that period in the software buffer.
3737
//
38-
// **Effective Latency**: With N periods total, (N-1) periods contain "latency" (data waiting
39-
// to be played), while 1 period is always being transferred to/from hardware. Therefore:
40-
// `effective_latency = (total_periods - 1) × period_size`
38+
// ## BufferSize::Fixed Behavior
4139
//
42-
// **User Expectation**: When user requests buffer size X, they expect ~X frames of latency,
43-
// not ~X frames of total buffering. Our goal is: `period_size × (periods - 1) ≈ user_buffer`
40+
// When `BufferSize::Fixed(x)` is specified, cpal attempts to configure the period size
41+
// to approximately `x` frames to achieve the requested callback size. However, the
42+
// actual callback size may differ from the request:
4443
//
45-
// ## Period Configuration Strategy
44+
// - ALSA may round the period size to hardware-supported values
45+
// - Different devices have different period size constraints
46+
// - The callback size is not guaranteed to exactly match the request
47+
// - If the requested size cannot be accommodated, ALSA will choose the nearest
48+
// supported configuration
4649
//
47-
// **Goal**: Achieve user-requested latency with precision and device compatibility.
48-
//
49-
// **Step 1 - Query Device Limits**: Check the device's maximum period size to determine
50-
// which approaches are viable.
51-
//
52-
// **Step 2 - Prefer Double Buffering**: When user_buffer ≤ max_period_size, configure
53-
// 2 periods of user_buffer size each. This is the simplest configuration with direct
54-
// period size control.
55-
//
56-
// **Step 3 - Multi-Period When Required**: When user_buffer > max_period_size, calculate
57-
// the minimum periods needed and distribute the latency across them. This maintains
58-
// precision while respecting hardware constraints.
59-
//
60-
// **Step 4 - Fallback for Compatibility**: If precise approaches fail device validation,
61-
// use buffer-size-only configuration. Accept latency deviation to ensure functional audio.
62-
//
63-
// **Validation**: Accept exact matches for even period sizes, and ±1 for odd period sizes
64-
// (due to hardware alignment constraints). Reject results that deviate significantly
65-
// from the target latency.
50+
// This mirrors the behavior documented in the cpal API where `BufferSize::Fixed(x)`
51+
// requests but does not guarantee a specific callback size.
6652

6753
pub type SupportedInputConfigs = VecIntoIter<SupportedStreamConfigRange>;
6854
pub type SupportedOutputConfigs = VecIntoIter<SupportedStreamConfigRange>;
@@ -945,7 +931,7 @@ fn process_output(
945931
error_callback(err.into());
946932
continue;
947933
}
948-
Ok(result) if result as usize != stream.period_frames => {
934+
Ok(result) if result != stream.period_frames => {
949935
let description = format!(
950936
"unexpected number of frames written: expected {}, \
951937
result {result} (this should never happen)",
@@ -1198,109 +1184,6 @@ fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) {
11981184
}
11991185
}
12001186

1201-
// Try period configuration with specified period size and count
1202-
fn try_period_configuration(
1203-
pcm_handle: &alsa::pcm::PCM,
1204-
hw_params: &alsa::pcm::HwParams,
1205-
target_period_size: u32,
1206-
target_periods: u32,
1207-
) -> Option<usize> {
1208-
hw_params
1209-
.set_period_size_near(target_period_size as _, alsa::ValueOr::Nearest)
1210-
.ok()?;
1211-
hw_params
1212-
.set_periods(target_periods, alsa::ValueOr::Nearest)
1213-
.ok()?;
1214-
pcm_handle.hw_params(hw_params).ok()?;
1215-
1216-
let device_period_size = hw_params.get_period_size().ok()? as u32;
1217-
let device_periods = hw_params.get_periods().ok()? as u32;
1218-
1219-
// Period count must be exactly what we requested
1220-
if device_periods != target_periods {
1221-
return None;
1222-
}
1223-
1224-
// Period size validation: exact for even, ±1 for odd
1225-
let period_size_ok = if target_period_size % 2 == 0 {
1226-
device_period_size == target_period_size
1227-
} else {
1228-
let acceptable_range = (target_period_size.saturating_sub(1))..=(target_period_size + 1);
1229-
acceptable_range.contains(&device_period_size)
1230-
};
1231-
1232-
if period_size_ok {
1233-
Some(device_period_size as usize)
1234-
} else {
1235-
None // Device constraint issue
1236-
}
1237-
}
1238-
1239-
// Configure periods based on device capabilities
1240-
fn configure_periods(
1241-
pcm_handle: &alsa::pcm::PCM,
1242-
hw_params: &alsa::pcm::HwParams,
1243-
user_buffer_frames: u32,
1244-
config: &StreamConfig,
1245-
) -> Result<(), BackendSpecificError> {
1246-
// Query device maximum period size to determine approach
1247-
let max_period_size = hw_params
1248-
.get_period_size_max()
1249-
.map_err(|_| BackendSpecificError {
1250-
description: "Could not query device period size limits".to_string(),
1251-
})? as u32;
1252-
1253-
// Approach 1: Double buffering if user buffer fits within device limits
1254-
if user_buffer_frames <= max_period_size {
1255-
if let Some(_) = try_period_configuration(&pcm_handle, &hw_params, user_buffer_frames, 2) {
1256-
return Ok(());
1257-
}
1258-
}
1259-
1260-
// Approach 2: Multi-period with calculated period count and size
1261-
if user_buffer_frames > max_period_size {
1262-
// Calculate minimum periods needed: ceil(user_buffer / max_period_size) + 1
1263-
let min_periods = (user_buffer_frames + max_period_size - 1) / max_period_size + 1;
1264-
let target_period_size = user_buffer_frames / std::cmp::max(min_periods - 1, 1);
1265-
1266-
if let Some(_) =
1267-
try_period_configuration(&pcm_handle, &hw_params, target_period_size, min_periods)
1268-
{
1269-
return Ok(());
1270-
}
1271-
}
1272-
1273-
// Approach 3: Fallback - let ALSA choose everything based on buffer size
1274-
fallback_buffer_size(&pcm_handle, &hw_params, user_buffer_frames, config)
1275-
}
1276-
1277-
// Fallback: Use ALSA's buffer size approach when period-based approaches fail
1278-
fn fallback_buffer_size(
1279-
pcm_handle: &alsa::pcm::PCM,
1280-
base_hw_params: &alsa::pcm::HwParams,
1281-
user_buffer_frames: u32,
1282-
config: &StreamConfig,
1283-
) -> Result<(), BackendSpecificError> {
1284-
// Create fresh hw_params to avoid inheriting period constraints from previous attempts
1285-
let hw_params = alsa::pcm::HwParams::any(pcm_handle)?;
1286-
hw_params.set_access(base_hw_params.get_access()?)?;
1287-
hw_params.set_format(base_hw_params.get_format()?)?;
1288-
hw_params.set_rate(base_hw_params.get_rate()?, alsa::ValueOr::Nearest)?;
1289-
hw_params.set_channels(base_hw_params.get_channels()?)?;
1290-
1291-
// Only set buffer size - let ALSA choose optimal period size and count
1292-
hw_params
1293-
.set_buffer_size_near(user_buffer_frames as _)
1294-
.map_err(|_| BackendSpecificError {
1295-
description: format!(
1296-
"Buffer size '{:?}' is not supported by this backend",
1297-
config.buffer_size
1298-
),
1299-
})?;
1300-
1301-
pcm_handle.hw_params(&hw_params).map_err(Into::into)
1302-
}
1303-
13041187
fn set_hw_params_from_format(
13051188
pcm_handle: &alsa::pcm::PCM,
13061189
config: &StreamConfig,
@@ -1364,14 +1247,22 @@ fn set_hw_params_from_format(
13641247
hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?;
13651248
hw_params.set_channels(config.channels as u32)?;
13661249

1367-
// Smart period configuration: adapt to device capabilities for consistent latency
1368-
if let BufferSize::Fixed(user_buffer_frames) = config.buffer_size {
1369-
configure_periods(&pcm_handle, &hw_params, user_buffer_frames, config)?;
1370-
} else {
1371-
// Default buffer size - let device choose everything
1372-
pcm_handle.hw_params(&hw_params)?;
1250+
// Configure period size based on buffer size request
1251+
// When BufferSize::Fixed(x) is specified, we request a period size of x frames
1252+
// to achieve approximately x-sized callbacks. ALSA may adjust this to the nearest
1253+
// supported value based on hardware constraints.
1254+
if let BufferSize::Fixed(buffer_frames) = config.buffer_size {
1255+
hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?;
13731256
}
13741257

1258+
// We shouldn't fail if the driver isn't happy here.
1259+
// `default` pcm sometimes fails here, but there's no reason to as we
1260+
// provide a direction and 2 is strictly the minimum number of periods.
1261+
let _ = hw_params.set_periods(2, alsa::ValueOr::Greater);
1262+
1263+
// Apply hardware parameters
1264+
pcm_handle.hw_params(&hw_params)?;
1265+
13751266
Ok(hw_params.can_pause())
13761267
}
13771268

src/host/wasapi/device.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ impl Device {
337337
/// Ensures that `future_audio_client` contains a `Some` and returns a locked mutex to it.
338338
fn ensure_future_audio_client(
339339
&self,
340-
) -> Result<MutexGuard<Option<IAudioClientWrapper>>, windows::core::Error> {
340+
) -> Result<MutexGuard<'_, Option<IAudioClientWrapper>>, windows::core::Error> {
341341
let mut lock = self.future_audio_client.lock().unwrap();
342342
if lock.is_some() {
343343
return Ok(lock);

src/host/wasapi/stream.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,6 @@ fn boost_current_thread_priority(buffer_size: BufferSize, sample_rate: crate::Sa
345345

346346
#[cfg(not(feature = "audio_thread_priority"))]
347347
fn boost_current_thread_priority(_: BufferSize, _: crate::SampleRate) {
348-
use windows::Win32::Foundation::HANDLE;
349-
350348
unsafe {
351349
let thread_handle = Threading::GetCurrentThread();
352350

src/lib.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -220,22 +220,36 @@ where
220220
/// one frame contains two samples (left and right channels).
221221
pub type FrameCount = u32;
222222

223-
/// The buffer size controls the latency between your application and the audio hardware.
223+
/// The buffer size requests the callback size for audio streams.
224224
///
225-
/// This controls the size of the software buffer that cpal uses to transfer audio
226-
/// data between your application and the hardware device. This is distinct from
227-
/// end-to-end audio latency, which includes additional processing delays.
225+
/// This controls the approximate size of the audio buffer passed to your callback.
226+
/// The actual callback size depends on the host/platform implementation and hardware
227+
/// constraints, and may differ from or vary around the requested size.
228228
///
229-
/// [`Default`] uses the host's default buffer size, which may be surprisingly
230-
/// large, leading to higher buffering latency. If low latency is desired,
231-
/// [`Fixed(FrameCount)`] should be used in accordance with the [`SupportedBufferSize`]
232-
/// range produced by the [`SupportedStreamConfig`] API.
229+
/// ## Callback Size Expectations
230+
///
231+
/// When you specify [`BufferSize::Fixed(x)`], you are **requesting** that callbacks
232+
/// receive approximately `x` frames of audio data. However, **no guarantees can be
233+
/// made** about the actual callback size:
234+
///
235+
/// - The host may round to hardware-supported values
236+
/// - Different devices have different constraints
237+
/// - The callback size may vary between calls (especially on mobile platforms)
238+
/// - The actual size might be larger or smaller than requested
239+
///
240+
/// ## Latency Considerations
241+
///
242+
/// [`BufferSize::Default`] uses the host's default buffer size, which may be
243+
/// surprisingly large, leading to higher latency. If low latency is desired,
244+
/// [`BufferSize::Fixed`] should be used with a small value in accordance with
245+
/// the [`SupportedBufferSize`] range from [`SupportedStreamConfig`].
233246
///
234247
/// Smaller buffer sizes reduce latency but may increase CPU usage and risk audio
235-
/// dropouts if the callback cannot keep up with the requested buffer size.
248+
/// dropouts if the callback cannot process audio quickly enough.
236249
///
237-
/// [`Default`]: BufferSize::Default
238-
/// [`Fixed(FrameCount)`]: BufferSize::Fixed
250+
/// [`BufferSize::Default`]: BufferSize::Default
251+
/// [`BufferSize::Fixed`]: BufferSize::Fixed
252+
/// [`BufferSize::Fixed(x)`]: BufferSize::Fixed
239253
/// [`SupportedBufferSize`]: SupportedStreamConfig::buffer_size
240254
/// [`SupportedStreamConfig`]: SupportedStreamConfig
241255
#[derive(Clone, Copy, Debug, Eq, PartialEq)]

0 commit comments

Comments
 (0)