Skip to content

usb_audio: apply the host's mute and volume to the samples - #11386

Open
peterbay wants to merge 1 commit into
adafruit:mainfrom
peterbay:usb-audio-host-mute-and-volume
Open

peterbay wants to merge 1 commit into
adafruit:mainfrom
peterbay:usb-audio-host-mute-and-volume

Conversation

@peterbay

Copy link
Copy Markdown

Code written by Claude Code, guided and corrected by @peterbay.

Fixes #11302.

The problem

usb_audio tells the host it has a volume and a mute control — the feature unit in both the microphone and the speaker descriptor declares MUTE and VOLUME as read/write, and the control requests for them are answered — but nothing is ever done with the values. They are stored in usb_audio_mute[] and usb_audio_volume[] and read back to the host, and that is all. The samples pass through untouched.

Because the device claims those controls, Windows hands the whole job to it: it sends the stream at full scale and expects the device to attenuate. So the slider moves, the device dutifully reports back whatever it was told, and the audio does not change. Muting does nothing either.

Two smaller things fall out of the same area:

  • The volume range offered to the host was −90 dB to +90 dB. Above 0 dB there is nothing to give — the samples are already at full scale — so the top half of the host's slider could only ask for gain that does not exist.
  • A headset exposes a feature unit per direction, but the mute and volume arrays were single and shared, so the two directions could not hold different settings. That does not show while nothing is applied; it would as soon as anything is.

The changes

  • The host's mute and volume are applied to the samples. Each feature unit keeps its own mute and volume, and a Q15 gain is worked out from them — master and per-channel volume in series, mute folded in as zero — so the sample paths only multiply. usb_audio_speaker_task scales what the host sent before it reaches the sample, and usb_audio_microphone_task scales what the board is about to send. At the default of 0 dB the gain is exactly unity and the samples are passed through unchanged, not multiplied by 1.

  • The volume range is now −60 dB to 0 dB, in 1 dB steps. Nothing above unity, so the host's slider cannot ask the device to clip.

  • Mute and volume are per feature unit. The single-direction functions have one; the headset has one for its speaker and one for its microphone, and they are now independent.

  • A volume from the host is clamped into the range it was given. Hosts stay inside it, but what gets applied is kept inside it whatever arrives.

  • sample_rate now has to be at least 1000. The microphone hands the host one millisecond of audio at a time, computed as sample_rate / 1000, so anything below 1 kHz sized that buffer at zero. It was validated against a minimum of 1.

  • A docstring paragraph for the new behaviour, the per-mode endpoint and interface counts (the old text gave the microphone's numbers for all three modes), and one stale comment in tusb_config.h that still said the speaker descriptor had not landed yet.

Testing

Seeed XIAO nRF52840 Sense with an Adafruit Audio BFF, on two builds differing only by this patch. boot.py enables a speaker at 16 kHz mono; code.py bridges usb_speaker.read() into audiobusio.I2SOut and prints the peak amplitude it received each second.

The host plays a steady 440 Hz tone whose own amplitude is 12000, so the peak the board reports is the gain that was applied, in absolute numbers.

Windows volume before after
maximum 12001 12001
10 steps down 12001 8496
20 steps down 12001 4778
muted 12001 0
unmuted again 12001 4778

Before the patch the peak does not move at all — not for the slider, not for mute. After it, it tracks both, mute is digital silence, and at maximum it is the same 12001 as before, which is the tone arriving unscaled.

Audible on the BFF speaker throughout, and the volume slider audibly works after the patch.

A headset (microphone=True, speaker=True) was also checked on this board: it enumerates on both builds and Windows offers both a playback and a recording endpoint, with no change from this patch. The microphone side of the gain is the same code as the speaker side but is not in the table — this board's PDM microphone was not wired up for it.

No new translatable strings.

The feature unit declares MUTE and VOLUME as read/write and the control
requests for them are answered, but the values were only stored and read
back. Because the device claims those controls, the host sends the stream at
full scale and leaves the attenuation to it, so the slider moved and nothing
happened.

Each feature unit now keeps its own mute and volume and a Q15 gain worked out
from them, which the speaker task applies to what the host sent and the
microphone task to what the board is about to send. At the default of 0 dB the
gain is unity and the samples pass through unchanged.

The range offered to the host was -90 dB to +90 dB; there is nothing above
unity to give, so it is now -60 dB to 0 dB, and a volume from the host is
clamped into it.

Mute and volume were single shared arrays, so a headset's two feature units
could not hold different settings.

sample_rate was validated against a minimum of 1, but the microphone sizes its
buffer as sample_rate / 1000, so anything below 1 kHz gave it a zero-length
one.

Fixes adafruit#11302.
@peterbay

Copy link
Copy Markdown
Author

Testing and diagnostic script.
usb_audio_host_mute_and_volume.py

@tannewt

tannewt commented Sep 14, 2026

Copy link
Copy Markdown
Member

We hadn't settled on this behavior in #11302. @jbirchall-svg and @FoamyGuy what do you think?

@FoamyGuy

Copy link
Copy Markdown
Collaborator

I agree with Scott from the linked issue:

I'd be interested in adding a way to expose the desired volume as a state that could optionally be plugged into Mixer. I don't think it should be done automatically though.

I think this volume should not be applied automatically. It would be nice to have it exposed to the CircuitPython side so that it can be checked and responded to from there, but making it adjust automatically seems to me like it will increase chances for people to think it's not working when the volume is set on the host side and they don't realize it.

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.

usb_audio.USBSpeaker: Speaker audio does not adjust based on Windows volume slider/mute.

3 participants