Skip to content

fix(voice): handle U16 sample format in the fallback capture path - #4537

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
mysma-9403:fix/voice-u16-fallback-format
Jul 22, 2026
Merged

senamakel merged 2 commits into
tinyhumansai:mainfrom
mysma-9403:fix/voice-u16-fallback-format

Conversation

@mysma-9403

@mysma-9403 mysma-9403 commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

What

Voice capture builds the input stream for the preferred device config, and if that fails, retries with the device's default config. The preferred path handles three sample formats — F32, I16, U16 — but the fallback path handles only F32 and I16:

// fallback (src/openhuman/voice/audio_capture.rs)
let fallback_stream = match fmt {
    SampleFormat::F32 => …,
    SampleFormat::I16 => …,
    _ => Err(format!("unsupported fallback format: {fmt:?}")),  // ← U16 dies here
};

So when the preferred config fails and the device's default config is U16 (common on some Windows drivers and older hardware), voice recording fails outright with unsupported fallback format: U16 — even though U16 is fully supported on the preferred path. Nothing marks the omission as intentional; it's an asymmetry between two paths that are meant to mirror each other.

Change

  • Add the missing SampleFormat::U16 arm to the fallback match, mirroring the preferred U16 handler.
  • While here, extract the two per-format conversions that were duplicated across the preferred and fallback closures into small pure helpers i16_to_f32 / u16_to_f32, and call them from all four sites. This removes the copy-paste and — unlike the inline closures, which need a live audio device — makes the conversion math unit-testable.

Correctness

u16_to_f32 uses the same mid-scale formula the preferred path already used ((s - 32768) / 32768), and i16_to_f32 the same s / 32768. No behaviour change on the existing F32/I16 paths; the only new behaviour is that a U16 fallback device now records instead of erroring.

Tests

i16_to_f32 / u16_to_f32 unit tests in audio_capture_tests.rs pin the normalisation (mid-scale → 0.0, full-scale endpoints → ±1.0). The stream-building itself needs a real cpal device and stays uncovered, consistent with the rest of this module.

cargo test --lib voice::audio_capture green locally.

Summary by CodeRabbit

  • Refactor

    • Centralized microphone PCM-to-float conversion for signed and unsigned 16-bit audio, reducing duplicated per-sample handling while keeping normalization behavior consistent.
  • Tests

    • Added unit tests for both signed and unsigned 16-bit sample conversions, validating midpoint (silence), endpoints, and element-wise behavior.

Voice capture builds the input stream for the preferred device config, and on
failure retries with the device's default config. The preferred path handles
F32, I16 and U16, but the fallback match handled only F32 and I16 — U16 fell
through to `Err("unsupported fallback format: U16")`. So when the preferred
config failed and the device's default config was U16 (common on some Windows
drivers / older hardware), recording failed outright despite U16 being fully
supported on the preferred path. Nothing marked the omission as intentional;
it's an asymmetry between two paths meant to mirror each other.

Add the missing SampleFormat::U16 fallback arm mirroring the preferred handler.
While here, extract the i16/u16 -> f32 conversions (duplicated across the
preferred and fallback closures) into pure i16_to_f32 / u16_to_f32 helpers used
at all four sites — removing the copy-paste and, unlike the inline closures
that need a live audio device, making the conversion math unit-testable.

Tests: i16_to_f32 / u16_to_f32 pin the normalisation (mid-scale -> 0.0,
endpoints -> ±1.0, order preserved). Stream building still needs a real cpal
device and stays uncovered, consistent with the rest of the module.
@mysma-9403
mysma-9403 requested a review from a team July 5, 2026 05:00
@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e1e1e73-cea1-4f8a-a6e9-c62b1b142627

📥 Commits

Reviewing files that changed from the base of the PR and between 263194b and 88a937f.

📒 Files selected for processing (1)
  • src/openhuman/voice/audio_capture.rs

📝 Walkthrough

Walkthrough

This PR extracts duplicated PCM-to-f32 conversion logic into shared i16/u16 helper functions, uses them in preferred and fallback audio stream callbacks, and adds unit tests covering normalization and unsigned PCM silence handling.

Changes

PCM Conversion Refactor

Layer / File(s) Summary
Shared conversion helpers
src/openhuman/voice/audio_capture.rs
Adds i16_to_f32 and u16_to_f32 helpers that centralize signed and unsigned PCM normalization to [-1.0, 1.0).
Callback wiring
src/openhuman/voice/audio_capture.rs
Uses the shared helpers in preferred and fallback I16/U16 stream callbacks before mono conversion and silence processing.
Helper unit tests
src/openhuman/voice/audio_capture_tests.rs
Tests signed normalization, interleaved conversion, unsigned midpoint centering, and endpoint handling.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

Bits of sound, once tangled and duplicated,
Now hop through helpers, neatly consolidated 🐇
i16, u16, both find their way,
To floats between -1 and near 1 each day.
Tests confirm the silence at midscale rest.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding U16 support in the fallback voice capture path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 5, 2026
Comment thread src/openhuman/voice/audio_capture.rs Outdated
@@ -520,6 +531,22 @@ pub fn list_input_devices() -> Result<Vec<String>, String> {
}

/// Convert interleaved multi-channel samples to mono by averaging channels.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick (documentation): The new helpers were inserted between to_mono's doc comment and its fn, so this line — /// Convert interleaved multi-channel samples to mono by averaging channels. — is now attached to i16_to_f32. As a result i16_to_f32 carries a stray, wrong first doc line, and to_mono (now at ~line 550) has no doc comment at all.

Move that line back down to sit immediately above pub(crate) fn to_mono:

// remove this stray line from above i16_to_f32
/// 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> { ... }

// ...

/// Convert interleaved multi-channel samples to mono by averaging channels.
pub(crate) fn to_mono(samples: &[f32], channels: usize) -> Vec<f32> { ... }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 88a937fba. Moved the /// Convert interleaved multi-channel samples to mono … line back down to sit immediately above pub(crate) fn to_mono, so to_mono regains its doc comment and i16_to_f32 no longer carries the stray, wrong first line. Good catch — thanks.

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #4537 — fix(voice): handle U16 sample format in the fallback capture path

Walkthrough

This PR closes a real asymmetry in record_on_thread: the preferred stream-building path handled F32/I16/U16, but the default-config fallback path handled only F32/I16, so a device whose default config is U16 (some Windows drivers, older hardware) would fail voice capture with unsupported fallback format: U16 even though U16 is fully supported on the preferred path. The fix adds the missing U16 fallback arm and, opportunistically, extracts the duplicated inline i16→f32 / u16→f32 conversions into two pub(crate) helpers that are now called from all four call sites and unit-tested. The change is small, correct, behaviour-preserving on existing formats, and does exactly what the title claims — a clean, well-motivated fix.

Changes

File Summary
src/openhuman/voice/audio_capture.rs Adds SampleFormat::U16 arm to the fallback stream match; extracts i16_to_f32 / u16_to_f32 helpers and calls them from the preferred + fallback callbacks.
src/openhuman/voice/audio_capture_tests.rs Adds unit tests pinning the normalisation of both helpers (mid-scale → 0.0, endpoints → ±1.0, order preservation).

Actionable comments (1)

💡 Refactor / suggestion

1. src/openhuman/voice/audio_capture.rs:533 — orphaned to_mono doc comment

The two new helpers were inserted between to_mono's doc comment (/// Convert interleaved multi-channel samples to mono by averaging channels.) and the to_mono fn. As a result that line now documents i16_to_f32 (giving it a wrong, stray first doc line), and to_mono (~line 550) is left with no doc comment at all. Purely cosmetic — no runtime effect — but worth fixing while the code is fresh.

Suggested change:

// before — stray line above i16_to_f32
/// Convert interleaved multi-channel samples to mono by averaging channels.
/// Convert interleaved signed `i16` PCM samples to normalised `f32` in
/// `[-1.0, 1.0)` (`s / 32768`). ...
pub(crate) fn i16_to_f32(data: &[i16]) -> Vec<f32> { ... }
...
pub(crate) fn to_mono(samples: &[f32], channels: usize) -> Vec<f32> { ... }

// after — helper docs clean, to_mono keeps its own doc
/// Convert interleaved signed `i16` PCM samples to normalised `f32` in
/// `[-1.0, 1.0)` (`s / 32768`). ...
pub(crate) fn i16_to_f32(data: &[i16]) -> Vec<f32> { ... }
...
/// Convert interleaved multi-channel samples to mono by averaging channels.
pub(crate) fn to_mono(samples: &[f32], channels: usize) -> Vec<f32> { ... }

Nitpicks (0)

None.

Questions for the author (0)

None.

Verified / looks good

  • Correctness of the new U16 arm: mirrors the preferred U16 handler exactly — same u16_to_f32 conversion, to_mono, update_peak_rms, silence gate, and sw.lock().extend_from_slice. The trailing other => Err(...) catch-all is preserved, so genuinely unsupported formats still error cleanly.
  • Normalisation math: i16_to_f32 (s / 32768) and u16_to_f32 ((s − 32768) / 32768) both land in [-1.0, 1.0); the docstrings match the formulas. No behaviour change on the existing F32/I16 paths.
  • Tests: assert_eq! on floats is safe here — every expected value (0.0, ±0.5, ±1.0) and every divisor (2^n) is exactly representable, so no float-tolerance flakiness. Endpoints and interleaved-order cases are covered. The stream-building itself needs a live cpal device and stays uncovered, consistent with the rest of the module.
  • Repo conventions: helpers live in the domain file, are pub(crate), and are documented; no root-level .rs, no controller/registry/event-bus concerns touched. Change is well under the size guideline.

Note: agreeing with CodeRabbit's automated pass (no actionable comments) — the logic is sound; the only thing it missed is the misplaced doc comment above. Leaving this as COMMENT only; approval is the maintainer's call.

@Coding-Dev-Tools

Copy link
Copy Markdown

🤖 Council Merge Gate — APPROVE (merge blocked by branch protection)

The model council review gate returned APPROVE (session council-ef067cf7-04fa-472e-a9e1-e8ab6db48141), so the change is council-cleared.

However, gh pr merge was rejected by GitHub branch protection: the base branch policy prohibits the merge. This repo requires passing status checks and/or an approving review before merge, and those requirements are not currently satisfied (e.g. failing required CI checks).

Action needed (human): resolve the failing required checks / obtain the required review, then merge. A maintainer with admin rights may also merge explicitly. The PR is left OPEN.

Posted automatically by the council_gate_merge cron job.

@Coding-Dev-Tools

Copy link
Copy Markdown

🤖 Council Merge Gate — APPROVE (merge blocked by branch protection)

The model council review gate returned APPROVE (session council-34ee3b4c-ec98-4ead-b86a-80ab785860a1), so the change is council-cleared.

However, gh pr merge was rejected by GitHub branch protection: the base branch policy prohibits the merge. This repo requires passing status checks and/or an approving review before merge, and those requirements are not currently satisfied (e.g. failing required CI checks).

Action needed (human): resolve the failing required checks / obtain the required review, then merge. A maintainer with admin rights may also merge explicitly. The PR is left OPEN.

Posted automatically by the council_gate_merge cron job.

@Coding-Dev-Tools

Copy link
Copy Markdown

🤖 Council Merge Gate — APPROVE (merge blocked by branch protection)

The model council review gate returned APPROVE (session council-d2c410eb-fa8a-4d51-994d-96db3f4967c2), so the change is council-cleared.

However, gh pr merge was rejected by GitHub branch protection: the base branch policy prohibits the merge. This repo requires passing status checks and/or an approving review before merge, and those requirements are not currently satisfied (e.g. failing required CI checks).

Action needed (human): resolve the failing required checks / obtain the required review, then merge. A maintainer with admin rights may also merge explicitly. The PR is left OPEN.

Posted automatically by the council_gate_merge cron job.

The extracted i16_to_f32/u16_to_f32 helpers were inserted between to_mono's
doc comment and its fn, leaving to_mono undocumented and i16_to_f32 with a
stray wrong first doc line. Move the line back above to_mono.
@senamakel
senamakel merged commit f18414f into tinyhumansai:main Jul 22, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants