Skip to content

Instantly share code, notes, and snippets.

@ahkohd
Last active November 14, 2025 07:00
Show Gist options
  • Select an option

  • Save ahkohd/35476b148532b41aeac068e6700b7020 to your computer and use it in GitHub Desktop.

Select an option

Save ahkohd/35476b148532b41aeac068e6700b7020 to your computer and use it in GitHub Desktop.
// ...
let host = cpal::default_host();
// Mic device (channel 0 / left)
let mic_device = host.default_input_device().expect("no input device");
let mic_config = mic_device.default_input_config().expect("no mic config");
println!("[dual-source] Mic: {:?}", mic_device.name());
// System audio loopback device (channel 1 / right)
let loopback_device = host.default_output_device().expect("no output device");
// Get config: use output config even if device doesn't support input (that's the trick for loopback)
let loopback_config = if loopback_device.supported_input_configs().is_ok()
&& loopback_device
.supported_input_configs()
.unwrap()
.next()
.is_some()
{
println!(
"[dual-source] System audio: {:?}",
loopback_device.name()
);
loopback_device
.default_input_config()
.expect("failed to get input config")
} else {
println!(
"[dual-source] System audio: {:?}",
loopback_device.name()
);
loopback_device
.default_output_config()
.expect("failed to get output config")
};
// ....
// Calculate decimation ratio for loopback resampling
let decimation_ratio = if loopback_sample_rate > mic_sample_rate {
loopback_sample_rate / mic_sample_rate
} else {
1
};
// Start mic stream - use native config
let mic_buf_clone = mic_buffer.clone();
let mic_stream = match mic_config.sample_format() {
SampleFormat::F32 => mic_device.build_input_stream(
&mic_config.clone().into(),
move |data: &[f32], _| {
let mut buf = mic_buf_clone.lock().unwrap();
for &sample in data {
buf.push((sample * 32767.0) as i16);
}
},
|err| eprintln!("[mic] error: {}", err),
None,
),
SampleFormat::I16 => mic_device.build_input_stream(
&mic_config.clone().into(),
move |data: &[i16], _| {
let mut buf = mic_buf_clone.lock().unwrap();
buf.extend_from_slice(data);
},
|err| eprintln!("[mic] error: {}", err),
None,
),
format => panic!("unsupported mic format: {:?}", format),
}
.expect("failed to build mic stream");
// ...
let loopback_stream_result = match loopback_config.sample_format() {
SampleFormat::F32 => {
let decim = decimation_ratio as usize;
loopback_device.build_input_stream(
&loopback_config.clone().into(),
move |data: &[f32], _| {
let mut buf = sys_buf_clone.lock().unwrap();
// Stereo to mono + decimation (sample rate conversion)
if loopback_channels == 2 {
for (i, chunk) in data.chunks_exact(2).enumerate() {
if i % decim == 0 {
let mixed = (chunk[0] + chunk[1]) / 2.0;
buf.push((mixed * 32767.0) as i16);
}
}
} else {
for (i, &sample) in data.iter().enumerate() {
if i % decim == 0 {
buf.push((sample * 32767.0) as i16);
}
}
}
},
|err| eprintln!("[loopback] error: {}", err),
None,
)
}
SampleFormat::I16 => {
let decim = decimation_ratio as usize;
loopback_device.build_input_stream(
&loopback_config.clone().into(),
move |data: &[i16], _| {
let mut buf = sys_buf_clone.lock().unwrap();
// Stereo to mono + decimation (sample rate conversion)
if loopback_channels == 2 {
for (i, chunk) in data.chunks_exact(2).enumerate() {
if i % decim == 0 {
let mixed = (chunk[0] as i32 + chunk[1] as i32) / 2;
buf.push(mixed as i16);
}
}
} else {
for (i, &sample) in data.iter().enumerate() {
if i % decim == 0 {
buf.push(sample);
}
}
}
},
|err| eprintln!("[loopback] error: {}", err),
None,
)
}
format => panic!("unsupported loopback format: {:?}", format),
};
// ...
[dependencies]
cpal = { git = "https://github.com/RustAudio/cpal.git", rev = "a8269d3c993f7d375d4655b53d3437429d4f6bd8" }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment