Conversation
|
Testing and diagnostic script. |
|
We hadn't settled on this behavior in #11302. @jbirchall-svg and @FoamyGuy what do you think? |
|
I agree with Scott from the linked issue:
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. |
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. USBSpeaker and USBMicrophone report that as host_mute, host_volume and host_gain, so an application can follow the host's level, for instance by assigning host_gain to an audiomixer.MixerVoice level. The board does not scale the samples itself unless apply_host_volume is set, which is per direction and off by default: a device that claims the control is expected to honour it, but doing so silently surprises an application that manages its own levels, so the default leaves the choice to the application and the switch gives the plain sound-card behaviour. All three report the effective setting for the first channel, master and per-channel controls in series. Windows drives the per-channel volume and leaves the master at 0 dB, so reporting the master alone read unchanged however the slider moved. 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. Addresses adafruit#11302: the host's setting is now available to the application, which decides what to do with it. The default behaviour of a speaker is unchanged, so the issue is not closed by this.
e421713 to
82f5a9d
Compare
|
Code and description updated |
|
Sorry for the delay. I really like the approach taken in the latest request @peterbay . For us, disconnecting the CircuitPython behaviour from the host PC is a feature, not a bug. We want to have full application and device control over the emitted volume regardless of what the PC OS wants to set it to be and the latest PR preserves that option. At the same time, default behaviour for most hobbyists is going to be that the PC volume slider "just works" with their code. The compromise is really nice and we appreciate the consideration. The only minor comment would be to default it to reflect what the host volume indicates rather than default it to ignore it as it does currently. In general, backwards compatibility would be paramount, but the usb_audio module is brand new and now is the chance to set it up as intuitively for new comers as possible. Nice work and thank you! |
|
I'll leave the decision on the default state up to the CircuitPython developers. Feel free to change it as needed. |
tannewt
left a comment
There was a problem hiding this comment.
Let's simplify this a bunch and have one .volume or .gain that is a synthio.BlockInput that has the host_gain value. That way it can be assigned straight to a MixerVoice's level. That way only a mixer is used for volume and the code doesn't need to be repeated here.
Code written by Claude Code, guided and corrected by @peterbay.
Addresses #11302.
The problem
usb_audiotells the host it has a volume and a mute control — the feature unit in both the microphone and the speaker descriptor declaresMUTEandVOLUMEas read/write, and the control requests for them are answered — but nothing is ever done with the values. They are stored inusb_audio_mute[]andusb_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.
What this does
The first version of this PR applied the host's gain to the samples. @tannewt and @FoamyGuy asked in #11302 and here for the host's setting to be exposed to CircuitPython rather than acted on automatically, so that is what it now does, with the automatic behaviour available as an opt-in.
The host's mute and volume are reported.
usb_speakerandusb_microphonegrowhost_mute,host_volume(dB) andhost_gain(linear,0.0–1.0, mute folded in).host_gainis the same number the applying path uses, just divided, so what is reported and what would be applied cannot disagree. It can be assigned straight to anaudiomixer.MixerVoicelevel.Nothing is applied unless asked.
apply_host_volumeis per direction andFalseby default. Setting it toTruegives the plain sound-card behaviour: the host's slider and mute work with no code of your own.All three report the effective setting for the first channel, master and per-channel controls in series. This matters: Windows drives the per-channel volume and leaves the master at 0 dB, so an earlier version of this patch that reported the master alone read
0.0 dBandFalsehowever far the slider moved. Measured, not assumed.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.
The volume range is now −60 dB to 0 dB, in 1 dB steps, instead of −90 dB to +90 dB. There is nothing above unity 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 volume from the host is clamped into the range it was given.
sample_ratenow has to be at least 1000. The microphone hands the host one millisecond of audio at a time, computed assample_rate / 1000, so anything below 1 kHz sized that buffer at zero. It was validated against a minimum of 1.Cost on a Seeed XIAO nRF52840 Sense build: +992 bytes of flash, +4 bytes of RAM. No new translatable strings.
Why #11302 is not closed by this
Out of the box the behaviour of a speaker is unchanged: the host's slider still does nothing until the application does something with
host_gain, or setsapply_host_volume = True. The issue is addressed in the sense that the information is now available and acting on it is one line, but a program that does neither behaves as before, so this does not close it.Testing
Seeed XIAO nRF52840 Sense with an Adafruit Audio BFF, Windows 11.
boot.pyenables a speaker at 16 kHz stereo;code.pyreports the peak amplitude arriving from the host together with the new properties. 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. The endpoint volume (the taskbar slider, not a per-app session volume, which would be applied before USB and prove nothing) is stepped through the Core Audio API.Default,
apply_host_volume = False— the samples are untouched and the properties track the host:host_volumehost_gainhost_muteapply_host_volume = True— the peak followshost_gain, and mute is digital silence:host_gain)The remaining difference is Q15 quantisation. The gap between the host's dB and
host_volumeis the 1 dB step the device declares inGET_RANGE.A headset (
microphone=True, speaker=True) was also checked on this board: it enumerates and Windows offers both a playback and a recording endpoint. The microphone side of the gain is the same code as the speaker side but is not in the tables — this board's PDM microphone was not wired up for it.