Skip to main content

max / audiofiles

Upgrade cpal to 0.18 cpal 0.18 collapses DefaultStreamConfigError/BuildStreamError/PlayStreamError into a single cpal::Error, and build_output_stream now takes StreamConfig by value. AudioError keeps its three distinct variants so the diagnostics are unchanged.
Author: Max Johnson <me@maxj.phd> · 2026-07-22 03:40 UTC
Commit: ecf0ae478643d64c70e84ca025fc8bbc83031bdf
Parent: d90082d
3 files changed, +60 insertions, -56 deletions
M Cargo.lock +8 -10
@@ -993,14 +993,15 @@ dependencies = [
993 993
994 994 [[package]]
995 995 name = "cpal"
996 - version = "0.17.3"
996 + version = "0.18.1"
997 997 source = "registry+https://github.com/rust-lang/crates.io-index"
998 - checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
998 + checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028"
999 999 dependencies = [
1000 1000 "alsa",
1001 + "block2 0.6.2",
1001 1002 "coreaudio-rs",
1002 1003 "dasp_sample",
1003 - "jni 0.21.1",
1004 + "jni 0.22.4",
1004 1005 "js-sys",
1005 1006 "libc",
1006 1007 "mach2",
@@ -1015,10 +1016,9 @@ dependencies = [
1015 1016 "objc2-core-audio-types",
1016 1017 "objc2-core-foundation",
1017 1018 "objc2-foundation 0.3.2",
1018 - "wasm-bindgen",
1019 - "wasm-bindgen-futures",
1020 1019 "web-sys",
1021 1020 "windows",
1021 + "windows-core",
1022 1022 ]
1023 1023
1024 1024 [[package]]
@@ -2851,12 +2851,9 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
2851 2851
2852 2852 [[package]]
2853 2853 name = "mach2"
2854 - version = "0.5.0"
2854 + version = "0.6.0"
2855 2855 source = "registry+https://github.com/rust-lang/crates.io-index"
2856 - checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
2857 - dependencies = [
2858 - "libc",
2859 - ]
2856 + checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
2860 2857
2861 2858 [[package]]
2862 2859 name = "makeover"
@@ -3266,6 +3263,7 @@ version = "0.3.2"
3266 3263 source = "registry+https://github.com/rust-lang/crates.io-index"
3267 3264 checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
3268 3265 dependencies = [
3266 + "bitflags 2.13.1",
3269 3267 "objc2 0.6.4",
3270 3268 "objc2-foundation 0.3.2",
3271 3269 ]
M Cargo.toml +1 -1
@@ -15,7 +15,7 @@ audiofiles-rhai = { path = "crates/audiofiles-rhai" }
15 15 egui = { version = "0.34", default-features = false, features = ["default_fonts"] }
16 16 egui_extras = { version = "0.34", default-features = false }
17 17 eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }
18 - cpal = "0.17"
18 + cpal = "0.18"
19 19 rusqlite = { version = "0.39", features = ["bundled", "functions"] }
20 20 thiserror = "2.0.18"
21 21 sha2 = "0.11.0"
@@ -16,14 +16,16 @@ use tracing::instrument;
16 16 pub enum AudioError {
17 17 #[error("no output audio device found")]
18 18 NoDevice,
19 + // cpal 0.18 collapsed its per-operation error types into one `cpal::Error`,
20 + // so the call sites tag each failure rather than relying on `#[from]`.
19 21 #[error("default output config: {0}")]
20 - DefaultConfig(#[from] cpal::DefaultStreamConfigError),
22 + DefaultConfig(#[source] cpal::Error),
21 23 #[error("unsupported sample format: {0:?}")]
22 24 UnsupportedFormat(cpal::SampleFormat),
23 25 #[error("build stream: {0}")]
24 - BuildStream(#[from] cpal::BuildStreamError),
26 + BuildStream(#[source] cpal::Error),
25 27 #[error("stream play: {0}")]
26 - Play(#[from] cpal::PlayStreamError),
28 + Play(#[source] cpal::Error),
27 29 }
28 30
29 31 /// Build and start a cpal output stream that reads from the shared preview state.
@@ -39,7 +41,9 @@ pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, Str
39 41 .description()
40 42 .map(|d| d.name().to_string())
41 43 .unwrap_or_else(|_| "default".to_string());
42 - let config = device.default_output_config()?;
44 + let config = device
45 + .default_output_config()
46 + .map_err(AudioError::DefaultConfig)?;
43 47
44 48 let channels = config.channels() as usize;
45 49 let device_sample_rate = config.sample_rate();
@@ -69,7 +73,7 @@ pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, Str
69 73 fmt => Err(AudioError::UnsupportedFormat(fmt)),
70 74 }?;
71 75
72 - stream.play()?;
76 + stream.play().map_err(AudioError::Play)?;
73 77 Ok((stream, device_sample_rate, device_name))
74 78 }
75 79
@@ -96,46 +100,48 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
96 100 // recovers instead of going permanently silent.
97 101 let err_shared = Arc::clone(&shared);
98 102
99 - let stream = device.build_output_stream(
100 - config,
101 - move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
102 - let num_samples = data.len();
103 -
104 - // Backstop for an unexpectedly large block (pre-sized above to
105 - // avoid allocating here on the steady-state path).
106 - if mix_buf.len() < num_samples {
107 - mix_buf.resize(num_samples, 0.0);
108 - }
109 - let buf = &mut mix_buf[..num_samples];
110 -
111 - // Zero the mix buffer
112 - for s in buf.iter_mut() {
113 - *s = 0.0;
114 - }
115 -
116 - // Fill preview audio
117 - fill_preview(&shared.preview, buf, channels, device_sample_rate);
118 -
119 - // Fill instrument audio (additive)
120 - if let Some(mut inst) = shared.instrument.try_lock() {
121 - render_voices(&mut inst, buf, channels, device_sample_rate);
122 - }
123 -
124 - // Convert f32 mix → output format with clamp
125 - for (out, &mix) in data.iter_mut().zip(buf.iter()) {
126 - *out = T::from_sample(mix.clamp(-1.0, 1.0));
127 - }
128 - },
129 - move |err| {
130 - tracing::error!("audio stream error: {err}");
131 - // Signal the app loop to rebuild the stream. Lock-free store only
132 - // — this runs in cpal's error context, never take a lock here.
133 - err_shared
134 - .device_lost
135 - .store(true, std::sync::atomic::Ordering::Relaxed);
136 - },
137 - None,
138 - )?;
103 + let stream = device
104 + .build_output_stream(
105 + *config,
106 + move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
107 + let num_samples = data.len();
108 +
109 + // Backstop for an unexpectedly large block (pre-sized above to
110 + // avoid allocating here on the steady-state path).
111 + if mix_buf.len() < num_samples {
112 + mix_buf.resize(num_samples, 0.0);
113 + }
114 + let buf = &mut mix_buf[..num_samples];
115 +
116 + // Zero the mix buffer
117 + for s in buf.iter_mut() {
118 + *s = 0.0;
119 + }
120 +
121 + // Fill preview audio
122 + fill_preview(&shared.preview, buf, channels, device_sample_rate);
123 +
124 + // Fill instrument audio (additive)
125 + if let Some(mut inst) = shared.instrument.try_lock() {
126 + render_voices(&mut inst, buf, channels, device_sample_rate);
127 + }
128 +
129 + // Convert f32 mix → output format with clamp
130 + for (out, &mix) in data.iter_mut().zip(buf.iter()) {
131 + *out = T::from_sample(mix.clamp(-1.0, 1.0));
132 + }
133 + },
134 + move |err| {
135 + tracing::error!("audio stream error: {err}");
136 + // Signal the app loop to rebuild the stream. Lock-free store only
137 + // — this runs in cpal's error context, never take a lock here.
138 + err_shared
139 + .device_lost
140 + .store(true, std::sync::atomic::Ordering::Relaxed);
141 + },
142 + None,
143 + )
144 + .map_err(AudioError::BuildStream)?;
139 145 Ok(stream)
140 146 }
141 147