Skip to content

Commit 5e6df5c

Browse files
committed
fix: clamped cpal backend output stream to MAX_CHANNELS (32)
1 parent 1686a04 commit 5e6df5c

File tree

3 files changed

+52
-31
lines changed

3 files changed

+52
-31
lines changed

src/context/online.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
99
use crate::media_streams::{MediaStream, MediaStreamTrack};
1010
use crate::message::ControlMessage;
1111
use crate::node::{self, ChannelConfigOptions};
12+
use crate::render::graph::Graph;
1213
use crate::MediaElement;
1314
use crate::{AudioRenderCapacity, Event};
1415

@@ -172,8 +173,8 @@ impl AudioContext {
172173
event_recv,
173174
} = control_thread_init;
174175

175-
let graph = crate::render::graph::Graph::new();
176-
let message = crate::message::ControlMessage::Startup { graph };
176+
let graph = Graph::new();
177+
let message = ControlMessage::Startup { graph };
177178
ctrl_msg_send.send(message).unwrap();
178179

179180
let base = ConcreteBaseAudioContext::new(

src/io/cpal.rs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::context::AudioContextOptions;
1616
use crate::io::microphone::MicrophoneRender;
1717
use crate::media_devices::{MediaDeviceInfo, MediaDeviceInfoKind};
1818
use crate::render::RenderThread;
19-
use crate::AtomicF64;
19+
use crate::{AtomicF64, MAX_CHANNELS};
2020

2121
use crossbeam_channel::Receiver;
2222

@@ -142,38 +142,50 @@ impl AudioBackendManager for CpalBackend {
142142

143143
log::info!("Output device: {:?}", device.name());
144144

145-
let supported = device
145+
let default_device_config = device
146146
.default_output_config()
147147
.expect("error while querying config");
148148

149-
let mut preferred: StreamConfig = supported.clone().into();
149+
// we grab the largest number of channels provided by the soundcard
150+
// clamped to MAX_CHANNELS, this value cannot be changed by the user
151+
let number_of_channels = usize::from(default_device_config.channels()).min(MAX_CHANNELS);
152+
153+
// override default device configuration with the options provided by
154+
// the user when creating the `AudioContext`
155+
let mut preferred_config: StreamConfig = default_device_config.clone().into();
156+
// make sure the number of channels is clamped to MAX_CHANNELS
157+
preferred_config.channels = number_of_channels as u16;
150158

151159
// set specific sample rate if requested
152160
if let Some(sample_rate) = options.sample_rate {
153161
crate::assert_valid_sample_rate(sample_rate);
154-
preferred.sample_rate.0 = sample_rate as u32;
162+
preferred_config.sample_rate.0 = sample_rate as u32;
155163
}
156164

157165
// always try to set a decent buffer size
158166
let buffer_size = super::buffer_size_for_latency_category(
159167
options.latency_hint,
160-
preferred.sample_rate.0 as f32,
168+
preferred_config.sample_rate.0 as f32,
161169
) as u32;
162170

163-
let clamped_buffer_size: u32 = match supported.buffer_size() {
171+
let clamped_buffer_size: u32 = match default_device_config.buffer_size() {
164172
SupportedBufferSize::Unknown => buffer_size,
165173
SupportedBufferSize::Range { min, max } => buffer_size.clamp(*min, *max),
166174
};
167175

168-
preferred.buffer_size = cpal::BufferSize::Fixed(clamped_buffer_size);
176+
preferred_config.buffer_size = cpal::BufferSize::Fixed(clamped_buffer_size);
177+
178+
// report the picked sample rate to the render thread, i.e. if the requested
179+
// sample rate is not supported by the hardware, it will fallback to the
180+
// default device sample rate
181+
let mut sample_rate = preferred_config.sample_rate.0 as f32;
169182

183+
// shared atomic to report output latency to the control thread
170184
let output_latency = Arc::new(AtomicF64::new(0.));
171-
let mut number_of_channels = usize::from(preferred.channels);
172-
let mut sample_rate = preferred.sample_rate.0 as f32;
173185

174186
let renderer = RenderThread::new(
175187
sample_rate,
176-
preferred.channels as usize,
188+
preferred_config.channels as usize,
177189
ctrl_msg_recv.clone(),
178190
Arc::clone(&frames_played),
179191
Some(load_value_send.clone()),
@@ -182,12 +194,13 @@ impl AudioBackendManager for CpalBackend {
182194

183195
log::debug!(
184196
"Attempt output stream with preferred config: {:?}",
185-
&preferred
197+
&preferred_config
186198
);
199+
187200
let spawned = spawn_output_stream(
188201
&device,
189-
supported.sample_format(),
190-
&preferred,
202+
default_device_config.sample_format(),
203+
&preferred_config,
191204
renderer,
192205
Arc::clone(&output_latency),
193206
);
@@ -200,8 +213,10 @@ impl AudioBackendManager for CpalBackend {
200213
Err(e) => {
201214
log::warn!("Output stream build failed with preferred config: {}", e);
202215

203-
let supported_config: StreamConfig = supported.clone().into();
204-
number_of_channels = usize::from(supported_config.channels);
216+
let mut supported_config: StreamConfig = default_device_config.clone().into();
217+
// make sure number of channels is clamped to MAX_CHANNELS
218+
supported_config.channels = number_of_channels as u16;
219+
// fallback to device default sample rate
205220
sample_rate = supported_config.sample_rate.0 as f32;
206221

207222
log::debug!(
@@ -220,11 +235,12 @@ impl AudioBackendManager for CpalBackend {
220235

221236
let spawned = spawn_output_stream(
222237
&device,
223-
supported.sample_format(),
238+
default_device_config.sample_format(),
224239
&supported_config,
225240
renderer,
226241
Arc::clone(&output_latency),
227242
);
243+
228244
spawned.expect("OutputStream build failed with default config")
229245
}
230246
};

src/render/thread.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ use super::graph::Graph;
2323
pub(crate) struct RenderThread {
2424
graph: Option<Graph>,
2525
sample_rate: f32,
26+
/// number of channels of the backend stream, i.e. sound card number of
27+
/// channels clamped to MAX_CHANNELS
2628
number_of_channels: usize,
2729
frames_played: Arc<AtomicU64>,
2830
receiver: Option<Receiver<ControlMessage>>,
@@ -176,12 +178,12 @@ impl RenderThread {
176178
buffer
177179
}
178180

179-
pub fn render<S: FromSample<f32> + Clone>(&mut self, buffer: &mut [S]) {
181+
pub fn render<S: FromSample<f32> + Clone>(&mut self, output_buffer: &mut [S]) {
180182
// collect timing information
181183
let render_start = Instant::now();
182184

183185
// perform actual rendering
184-
self.render_inner(buffer);
186+
self.render_inner(output_buffer);
185187

186188
// calculate load value and ship to control thread
187189
if let Some(load_value_sender) = &self.load_value_sender {
@@ -198,13 +200,13 @@ impl RenderThread {
198200
}
199201
}
200202

201-
fn render_inner<S: FromSample<f32> + Clone>(&mut self, mut buffer: &mut [S]) {
203+
fn render_inner<S: FromSample<f32> + Clone>(&mut self, mut output_buffer: &mut [S]) {
202204
// There may be audio frames left over from the previous render call,
203205
// if the cpal buffer size did not align with our internal RENDER_QUANTUM_SIZE
204206
if let Some((offset, prev_rendered)) = self.buffer_offset.take() {
205207
let leftover_len = (RENDER_QUANTUM_SIZE - offset) * self.number_of_channels;
206208
// split the leftover frames slice, to fit in `buffer`
207-
let (first, next) = buffer.split_at_mut(leftover_len.min(buffer.len()));
209+
let (first, next) = output_buffer.split_at_mut(leftover_len.min(output_buffer.len()));
208210

209211
// copy rendered audio into output slice
210212
for i in 0..self.number_of_channels {
@@ -226,23 +228,23 @@ impl RenderThread {
226228
}
227229

228230
// if there's still space left in the buffer, continue rendering
229-
buffer = next;
231+
output_buffer = next;
230232
}
231233

232234
// handle addition/removal of nodes/edges
233235
self.handle_control_messages();
234236

235237
// if the thread is still booting, or shutting down, fill with silence
236238
if self.graph.is_none() {
237-
buffer.fill(S::from_sample_(0.));
239+
output_buffer.fill(S::from_sample_(0.));
238240
return;
239241
}
240242

241243
// The audio graph is rendered in chunks of RENDER_QUANTUM_SIZE frames. But some audio backends
242244
// may not be able to emit chunks of this size.
243245
let chunk_size = RENDER_QUANTUM_SIZE * self.number_of_channels;
244246

245-
for data in buffer.chunks_mut(chunk_size) {
247+
for data in output_buffer.chunks_mut(chunk_size) {
246248
// update time
247249
let current_frame = self
248250
.frames_played
@@ -258,17 +260,19 @@ impl RenderThread {
258260
};
259261

260262
// render audio graph, clone it in case we need to mutate/store the value later
261-
let mut rendered = self.graph.as_mut().unwrap().render(&scope).clone();
263+
let mut destination_buffer = self.graph.as_mut().unwrap().render(&scope).clone();
262264

263-
// online AudioContext allows channel count to be less than no of hardware channels
264-
if rendered.number_of_channels() != self.number_of_channels {
265-
rendered.mix(self.number_of_channels, ChannelInterpretation::Discrete);
265+
// online AudioContext allows channel count to be less than the number
266+
// of channels of the backend stream, i.e. number of channels of the
267+
// soundcard clamped to MAX_CHANNELS.
268+
if destination_buffer.number_of_channels() < self.number_of_channels {
269+
destination_buffer.mix(self.number_of_channels, ChannelInterpretation::Discrete);
266270
}
267271

268272
// copy rendered audio into output slice
269273
for i in 0..self.number_of_channels {
270274
let output = data.iter_mut().skip(i).step_by(self.number_of_channels);
271-
let channel = rendered.channel_data(i).iter();
275+
let channel = destination_buffer.channel_data(i).iter();
272276
for (sample, input) in output.zip(channel) {
273277
let value = S::from_sample_(*input);
274278
*sample = value;
@@ -279,7 +283,7 @@ impl RenderThread {
279283
// this is the last chunk, and it contained less than RENDER_QUANTUM_SIZE samples
280284
let channel_offset = data.len() / self.number_of_channels;
281285
debug_assert!(channel_offset < RENDER_QUANTUM_SIZE);
282-
self.buffer_offset = Some((channel_offset, rendered));
286+
self.buffer_offset = Some((channel_offset, destination_buffer));
283287
}
284288

285289
// handle addition/removal of nodes/edges

0 commit comments

Comments
 (0)