Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions src/openhuman/voice/audio_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,7 @@ fn record_on_thread(
.build_input_stream(
&stream_config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let floats: Vec<f32> =
data.iter().map(|&s| s as f32 / 32768.0).collect();
let floats = i16_to_f32(data);
let mono = to_mono(&floats, source_channels);
update_peak_rms(&rms_tracker, &mono);
let gated = gate.lock().process(&mono);
Expand All @@ -389,10 +388,7 @@ fn record_on_thread(
.build_input_stream(
&stream_config,
move |data: &[u16], _: &cpal::InputCallbackInfo| {
let floats: Vec<f32> = data
.iter()
.map(|&s| (s as f32 - 32768.0) / 32768.0)
.collect();
let floats = u16_to_f32(data);
let mono = to_mono(&floats, source_channels);
update_peak_rms(&rms_tracker, &mono);
let gated = gate.lock().process(&mono);
Expand Down Expand Up @@ -446,8 +442,7 @@ fn record_on_thread(
.build_input_stream(
&sc,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let floats: Vec<f32> =
data.iter().map(|&s| s as f32 / 32768.0).collect();
let floats = i16_to_f32(data);
let mono = to_mono(&floats, ch);
update_peak_rms(&rt, &mono);
let gated = gate.lock().process(&mono);
Expand All @@ -459,7 +454,23 @@ fn record_on_thread(
None,
)
.map_err(|e| format!("fallback i16 stream failed: {e}")),
_ => Err(format!("unsupported fallback format: {fmt:?}")),
SampleFormat::U16 => device
.build_input_stream(
&sc,
move |data: &[u16], _: &cpal::InputCallbackInfo| {
let floats = u16_to_f32(data);
let mono = to_mono(&floats, ch);
update_peak_rms(&rt, &mono);
let gated = gate.lock().process(&mono);
if !gated.is_empty() {
sw.lock().extend_from_slice(&gated);
}
},
|err| error!("{LOG_PREFIX} audio stream error: {err}"),
None,
)
.map_err(|e| format!("fallback u16 stream failed: {e}")),
other => Err(format!("unsupported fallback format: {other:?}")),
};
match fallback_stream {
Ok(s) => (s, sr, ch),
Expand Down Expand Up @@ -519,6 +530,22 @@ pub fn list_input_devices() -> Result<Vec<String>, String> {
Ok(names)
}

/// Convert interleaved signed `i16` PCM samples to normalised `f32` in
/// `[-1.0, 1.0)` (`s / 32768`). Extracted so the preferred and fallback stream
/// paths share one conversion instead of duplicating it inline.
pub(crate) fn i16_to_f32(data: &[i16]) -> Vec<f32> {
data.iter().map(|&s| s as f32 / 32768.0).collect()
}

/// Convert interleaved unsigned `u16` PCM samples (mid-scale 32768) to
/// normalised `f32` in `[-1.0, 1.0)` (`(s - 32768) / 32768`). Shared by the
/// preferred and fallback stream paths.
pub(crate) fn u16_to_f32(data: &[u16]) -> Vec<f32> {
data.iter()
.map(|&s| (s as f32 - 32768.0) / 32768.0)
.collect()
}

/// Convert interleaved multi-channel samples to mono by averaging channels.
pub(crate) fn to_mono(samples: &[f32], channels: usize) -> Vec<f32> {
if channels <= 1 {
Expand Down
19 changes: 19 additions & 0 deletions src/openhuman/voice/audio_capture_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
use super::*;
use cpal::{SampleFormat, SampleRate, SupportedBufferSize, SupportedStreamConfigRange};

#[test]
fn i16_to_f32_normalises_to_unit_range() {
assert_eq!(i16_to_f32(&[0]), vec![0.0]);
assert_eq!(i16_to_f32(&[16384]), vec![0.5]);
assert_eq!(i16_to_f32(&[-32768]), vec![-1.0]);
// Interleaved frames are converted element-wise, order preserved.
assert_eq!(i16_to_f32(&[0, 16384, -16384]), vec![0.0, 0.5, -0.5]);
}

#[test]
fn u16_to_f32_centers_on_midscale() {
// Unsigned PCM is offset-binary: 32768 is silence (0.0), the endpoints map
// to the extremes. This is the conversion the fallback path was missing.
assert_eq!(u16_to_f32(&[32768]), vec![0.0]);
assert_eq!(u16_to_f32(&[49152]), vec![0.5]);
assert_eq!(u16_to_f32(&[0]), vec![-1.0]);
assert_eq!(u16_to_f32(&[32768, 49152, 16384]), vec![0.0, 0.5, -0.5]);
}

#[test]
fn to_mono_passthrough_single_channel() {
let input = vec![0.1, 0.2, 0.3];
Expand Down
Loading