| 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 |
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 |
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 |
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 |
|
|