diff --git a/shared-bindings/usb_audio/USBMicrophone.c b/shared-bindings/usb_audio/USBMicrophone.c index c66c78942dd..cc161591b78 100644 --- a/shared-bindings/usb_audio/USBMicrophone.c +++ b/shared-bindings/usb_audio/USBMicrophone.c @@ -153,6 +153,75 @@ MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_paused_obj, usb_audio_usbm MP_PROPERTY_GETTER(usb_audio_usbmicrophone_paused_obj, (mp_obj_t)&usb_audio_usbmicrophone_get_paused_obj); +//| host_mute: bool +//| """True when the host has muted this microphone. (read-only)""" +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_host_mute(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(usb_audio_host_mute(USB_AUDIO_DIR_MICROPHONE)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_host_mute_obj, + usb_audio_usbmicrophone_obj_get_host_mute); + +MP_PROPERTY_GETTER(usb_audio_usbmicrophone_host_mute_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_host_mute_obj); + +//| host_volume: float +//| """The volume the host has set, in decibels from ``-60.0`` to ``0.0``. (read-only)""" +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_host_volume(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_float(usb_audio_host_volume(USB_AUDIO_DIR_MICROPHONE)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_host_volume_obj, + usb_audio_usbmicrophone_obj_get_host_volume); + +MP_PROPERTY_GETTER(usb_audio_usbmicrophone_host_volume_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_host_volume_obj); + +//| host_gain: float +//| """`host_volume` and `host_mute` as a linear factor from ``0.0`` to ``1.0``. (read-only)""" +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_host_gain(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_float(usb_audio_host_gain(USB_AUDIO_DIR_MICROPHONE)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_host_gain_obj, + usb_audio_usbmicrophone_obj_get_host_gain); + +MP_PROPERTY_GETTER(usb_audio_usbmicrophone_host_gain_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_host_gain_obj); + +//| apply_host_volume: bool +//| """Whether the board scales the samples it sends by `host_gain` itself. +//| `False` by default, leaving the level to your code; `True` makes the host's +//| recording level and mute work on their own.""" +//| +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_apply_host_volume(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(usb_audio_apply_host_volume(USB_AUDIO_DIR_MICROPHONE)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_apply_host_volume_obj, + usb_audio_usbmicrophone_obj_get_apply_host_volume); + +static mp_obj_t usb_audio_usbmicrophone_obj_set_apply_host_volume(mp_obj_t self_in, mp_obj_t value) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + usb_audio_set_apply_host_volume(USB_AUDIO_DIR_MICROPHONE, mp_obj_is_true(value)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(usb_audio_usbmicrophone_set_apply_host_volume_obj, + usb_audio_usbmicrophone_obj_set_apply_host_volume); + +MP_PROPERTY_GETSET(usb_audio_usbmicrophone_apply_host_volume_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_apply_host_volume_obj, + (mp_obj_t)&usb_audio_usbmicrophone_set_apply_host_volume_obj); + static const mp_rom_map_elem_t usb_audio_usbmicrophone_locals_dict_table[] = { // Methods { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_audio_usbmicrophone_deinit_obj) }, @@ -167,6 +236,10 @@ static const mp_rom_map_elem_t usb_audio_usbmicrophone_locals_dict_table[] = { // Properties { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&usb_audio_usbmicrophone_playing_obj) }, { MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&usb_audio_usbmicrophone_paused_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_mute), MP_ROM_PTR(&usb_audio_usbmicrophone_host_mute_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_volume), MP_ROM_PTR(&usb_audio_usbmicrophone_host_volume_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_gain), MP_ROM_PTR(&usb_audio_usbmicrophone_host_gain_obj) }, + { MP_ROM_QSTR(MP_QSTR_apply_host_volume), MP_ROM_PTR(&usb_audio_usbmicrophone_apply_host_volume_obj) }, }; static MP_DEFINE_CONST_DICT(usb_audio_usbmicrophone_locals_dict, usb_audio_usbmicrophone_locals_dict_table); diff --git a/shared-bindings/usb_audio/USBSpeaker.c b/shared-bindings/usb_audio/USBSpeaker.c index 78c5e6099b9..b76ca6b5c1b 100644 --- a/shared-bindings/usb_audio/USBSpeaker.c +++ b/shared-bindings/usb_audio/USBSpeaker.c @@ -128,6 +128,71 @@ MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_connected_obj, usb_audio_usbs MP_PROPERTY_GETTER(usb_audio_usbspeaker_connected_obj, (mp_obj_t)&usb_audio_usbspeaker_get_connected_obj); +//| host_mute: bool +//| """True when the host has muted this speaker. (read-only)""" +//| +static mp_obj_t usb_audio_usbspeaker_obj_get_host_mute(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(usb_audio_host_mute(USB_AUDIO_DIR_SPEAKER)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_host_mute_obj, usb_audio_usbspeaker_obj_get_host_mute); + +MP_PROPERTY_GETTER(usb_audio_usbspeaker_host_mute_obj, + (mp_obj_t)&usb_audio_usbspeaker_get_host_mute_obj); + +//| host_volume: float +//| """The volume the host has set, in decibels from ``-60.0`` to ``0.0``. (read-only)""" +//| +static mp_obj_t usb_audio_usbspeaker_obj_get_host_volume(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_float(usb_audio_host_volume(USB_AUDIO_DIR_SPEAKER)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_host_volume_obj, usb_audio_usbspeaker_obj_get_host_volume); + +MP_PROPERTY_GETTER(usb_audio_usbspeaker_host_volume_obj, + (mp_obj_t)&usb_audio_usbspeaker_get_host_volume_obj); + +//| host_gain: float +//| """`host_volume` and `host_mute` as a linear factor from ``0.0`` to ``1.0``. (read-only)""" +//| +static mp_obj_t usb_audio_usbspeaker_obj_get_host_gain(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_float(usb_audio_host_gain(USB_AUDIO_DIR_SPEAKER)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_host_gain_obj, usb_audio_usbspeaker_obj_get_host_gain); + +MP_PROPERTY_GETTER(usb_audio_usbspeaker_host_gain_obj, + (mp_obj_t)&usb_audio_usbspeaker_get_host_gain_obj); + +//| apply_host_volume: bool +//| """Whether the board scales the samples it receives by `host_gain` itself. +//| `False` by default, leaving the level to your code; `True` makes the host's +//| volume slider and mute work on their own.""" +//| +static mp_obj_t usb_audio_usbspeaker_obj_get_apply_host_volume(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(usb_audio_apply_host_volume(USB_AUDIO_DIR_SPEAKER)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_apply_host_volume_obj, + usb_audio_usbspeaker_obj_get_apply_host_volume); + +static mp_obj_t usb_audio_usbspeaker_obj_set_apply_host_volume(mp_obj_t self_in, mp_obj_t value) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + usb_audio_set_apply_host_volume(USB_AUDIO_DIR_SPEAKER, mp_obj_is_true(value)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(usb_audio_usbspeaker_set_apply_host_volume_obj, + usb_audio_usbspeaker_obj_set_apply_host_volume); + +MP_PROPERTY_GETSET(usb_audio_usbspeaker_apply_host_volume_obj, + (mp_obj_t)&usb_audio_usbspeaker_get_apply_host_volume_obj, + (mp_obj_t)&usb_audio_usbspeaker_set_apply_host_volume_obj); + //| sample_rate: int //| """The sample rate negotiated with the host in ``boot.py``. (read-only)""" //| @@ -148,6 +213,10 @@ static const mp_rom_map_elem_t usb_audio_usbspeaker_locals_dict_table[] = { // Properties { MP_ROM_QSTR(MP_QSTR_connected), MP_ROM_PTR(&usb_audio_usbspeaker_connected_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_mute), MP_ROM_PTR(&usb_audio_usbspeaker_host_mute_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_volume), MP_ROM_PTR(&usb_audio_usbspeaker_host_volume_obj) }, + { MP_ROM_QSTR(MP_QSTR_host_gain), MP_ROM_PTR(&usb_audio_usbspeaker_host_gain_obj) }, + { MP_ROM_QSTR(MP_QSTR_apply_host_volume), MP_ROM_PTR(&usb_audio_usbspeaker_apply_host_volume_obj) }, AUDIOSAMPLE_FIELDS, }; static MP_DEFINE_CONST_DICT(usb_audio_usbspeaker_locals_dict, usb_audio_usbspeaker_locals_dict_table); diff --git a/shared-bindings/usb_audio/__init__.c b/shared-bindings/usb_audio/__init__.c index 31d3ab549fd..48df91814bc 100644 --- a/shared-bindings/usb_audio/__init__.c +++ b/shared-bindings/usb_audio/__init__.c @@ -19,12 +19,21 @@ //| Audio Class (UAC2) microphone: the board is the audio *source* and streams //| samples to the host over a USB isochronous IN endpoint. //| -//| This mode requires 2 IN endpoints and 2 interfaces. +//| A microphone takes one isochronous IN endpoint and two interfaces, a speaker +//| one isochronous OUT endpoint and two interfaces, and a headset one of each +//| and three interfaces. //| Generally, microcontrollers have a limit on the number of endpoints. If you exceed the number //| of endpoints, CircuitPython will automatically enter Safe Mode. Even in this case, you may be //| able to enable USB audio by also disabling other USB functions, such as //| `usb_hid` or `usb_midi`. //| +//| The host's mute and volume are reported by ``host_mute``, ``host_volume`` and +//| ``host_gain`` on `usb_microphone` and `usb_speaker`, and are not applied to the +//| samples unless you ask for it. Either follow ``host_gain`` yourself, for +//| instance by assigning it to an `audiomixer.MixerVoice` level, or set +//| ``apply_host_volume`` to `True` to have the board scale the samples, which is +//| how a plain sound card behaves. +//| //| To enable this mode, you must configure the audio format in ``boot.py`` and then //| use the `usb_microphone` singleton instance in ``code.py``. //| @@ -110,7 +119,9 @@ static mp_obj_t usb_audio_enable(size_t n_args, const mp_obj_t *pos_args, mp_map mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - mp_int_t sample_rate = mp_arg_validate_int_range(args[ARG_sample_rate].u_int, 1, USB_AUDIO_MAX_SAMPLE_RATE, MP_QSTR_sample_rate); + // The microphone hands the host one millisecond of audio at a time, so a + // rate below 1 kHz would give it nothing to send. + mp_int_t sample_rate = mp_arg_validate_int_range(args[ARG_sample_rate].u_int, 1000, USB_AUDIO_MAX_SAMPLE_RATE, MP_QSTR_sample_rate); mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, USB_AUDIO_N_CHANNELS, MP_QSTR_channel_count); bool microphone = args[ARG_microphone].u_bool; bool speaker = args[ARG_speaker].u_bool; diff --git a/shared-module/usb_audio/__init__.c b/shared-module/usb_audio/__init__.c index 16f5952bc6a..31ae3d7d30b 100644 --- a/shared-module/usb_audio/__init__.c +++ b/shared-module/usb_audio/__init__.c @@ -12,6 +12,7 @@ #include "shared-module/usb_audio/USBSpeaker.h" #include "shared-module/usb_audio/usb_audio_descriptors.h" +#include #include #include "py/misc.h" @@ -50,9 +51,119 @@ uint8_t usb_audio_channel_count; bool usb_audio_microphone_enabled; bool usb_audio_speaker_enabled; -// Audio control state surfaced to the host. One extra entry for the master channel 0. -static int8_t usb_audio_mute[USB_AUDIO_N_CHANNELS + 1]; -static int16_t usb_audio_volume[USB_AUDIO_N_CHANNELS + 1]; +// Mute and volume as the host set them, per feature unit and per channel, +// channel 0 being the master. The single-direction functions have one feature +// unit, USB_AUDIO_ENTITY_FEATURE_UNIT; the headset has that one for its +// speaker and USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT for its microphone. Volume +// is in 1/256 dB, as on the wire. The gain each works out to, master and +// channel in series and mute folded in, is kept in Q15 so the sample paths +// only multiply. +#define USB_AUDIO_N_FEATURE_UNITS (2) +#define USB_AUDIO_SPEAKER_FEATURE_UNIT_SLOT (0) +// The volume range offered to the host: 0 dB, which is the default and +// unity, down to this. Nothing above unity, so the host's slider cannot clip. +#define USB_AUDIO_VOLUME_MIN_DB (-60) +#define USB_AUDIO_GAIN_UNITY (1 << 15) +static int8_t usb_audio_mute[USB_AUDIO_N_FEATURE_UNITS][USB_AUDIO_N_CHANNELS + 1]; +static int16_t usb_audio_volume[USB_AUDIO_N_FEATURE_UNITS][USB_AUDIO_N_CHANNELS + 1]; +static int32_t usb_audio_gain[USB_AUDIO_N_FEATURE_UNITS][USB_AUDIO_N_CHANNELS] = { + { USB_AUDIO_GAIN_UNITY, USB_AUDIO_GAIN_UNITY }, + { USB_AUDIO_GAIN_UNITY, USB_AUDIO_GAIN_UNITY }, +}; + +// Whether each unit's gain is applied to the samples here rather than left to +// the application. Off by default; see usb_audio_set_apply_host_volume(). +static bool usb_audio_apply[USB_AUDIO_N_FEATURE_UNITS]; + +// Which of the two feature units an entity id names, or -1 for none. +static int usb_audio_feature_unit_slot(uint8_t entity_id) { + if (entity_id == USB_AUDIO_ENTITY_FEATURE_UNIT) { + return USB_AUDIO_SPEAKER_FEATURE_UNIT_SLOT; + } + if (entity_id == USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT) { + return 1; + } + return -1; +} + +// The unit that controls the microphone: its own in a headset, the only one +// otherwise. +static int usb_audio_microphone_feature_unit_slot(void) { + return usb_audio_speaker_enabled ? 1 : USB_AUDIO_SPEAKER_FEATURE_UNIT_SLOT; +} + +static void usb_audio_update_gain(int slot) { + for (size_t ch = 0; ch < USB_AUDIO_N_CHANNELS; ch++) { + int32_t gain = 0; + if (!usb_audio_mute[slot][0] && !usb_audio_mute[slot][ch + 1]) { + // Master and channel attenuate in series, so their dB add. + int32_t db256 = usb_audio_volume[slot][0] + usb_audio_volume[slot][ch + 1]; + if (db256 >= 0) { + gain = USB_AUDIO_GAIN_UNITY; + } else { + gain = (int32_t)(USB_AUDIO_GAIN_UNITY * powf(10.0f, (float)db256 / (256.0f * 20.0f))); + } + } + usb_audio_gain[slot][ch] = gain; + } +} + +// The slot a direction's controls live in. A headset has a unit per direction; +// a single-direction function has just the one, which both names resolve to. +static int usb_audio_direction_slot(usb_audio_direction_t dir) { + if (dir == USB_AUDIO_DIR_MICROPHONE) { + return usb_audio_microphone_feature_unit_slot(); + } + return USB_AUDIO_SPEAKER_FEATURE_UNIT_SLOT; +} + +// The host writes these from the USB control-request callback while Python reads +// them from the VM. Each is a single aligned scalar, so a read cannot tear on any +// port CircuitPython targets, and a value one frame stale does not matter for a +// volume slider - no locking. +// +// Both report the EFFECTIVE setting for the first channel, master and channel in +// series, the same combination usb_audio_update_gain() uses. Reporting the master +// alone would read 0 dB and unmuted forever on Windows, which drives the +// per-channel controls and leaves the master untouched (measured). +bool usb_audio_host_mute(usb_audio_direction_t dir) { + const int slot = usb_audio_direction_slot(dir); + return usb_audio_mute[slot][0] != 0 || usb_audio_mute[slot][1] != 0; +} + +mp_float_t usb_audio_host_volume(usb_audio_direction_t dir) { + // Stored in 1/256 dB, as on the wire. + const int slot = usb_audio_direction_slot(dir); + return (usb_audio_volume[slot][0] + usb_audio_volume[slot][1]) / (mp_float_t)256.0; +} + +mp_float_t usb_audio_host_gain(usb_audio_direction_t dir) { + return usb_audio_gain[usb_audio_direction_slot(dir)][0] / (mp_float_t)USB_AUDIO_GAIN_UNITY; +} + +bool usb_audio_apply_host_volume(usb_audio_direction_t dir) { + return usb_audio_apply[usb_audio_direction_slot(dir)]; +} + +void usb_audio_set_apply_host_volume(usb_audio_direction_t dir, bool apply) { + usb_audio_apply[usb_audio_direction_slot(dir)] = apply; +} + +// Scales interleaved wire-format frames in place by a unit's gains. +static void usb_audio_apply_gain(int16_t *samples, size_t n_samples, int slot) { + if (!usb_audio_apply[slot]) { + return; // the application applies the host's gain, or ignores it + } + const int32_t left = usb_audio_gain[slot][0]; + const int32_t right = usb_audio_gain[slot][1]; + if (left == USB_AUDIO_GAIN_UNITY && right == USB_AUDIO_GAIN_UNITY) { + return; + } + for (size_t i = 0; i + 1 < n_samples; i += 2) { + samples[i] = (int16_t)((samples[i] * left) >> 15); + samples[i + 1] = (int16_t)((samples[i + 1] * right) >> 15); + } +} bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count, bool microphone, bool speaker) { if (tud_connected()) { @@ -149,14 +260,20 @@ void usb_audio_setup_singletons(void) { speaker; } -// Hand-rolled UAC2 speaker (host -> board) descriptor WITHOUT an async -// feedback endpoint. This mirrors TinyUSB's TUD_AUDIO20_SPEAKER_STEREO_FB_DESCRIPTOR -// (lib/tinyusb/src/device/usbd.h) but drops the trailing feedback endpoint, so -// the streaming alt-setting declares a single OUT endpoint (_nEPs = 0x01). The -// entity IDs match the mic descriptor (see usb_audio_descriptors.h); only the -// terminal roles reverse: the input terminal is the USB-streaming side and the -// output terminal is the desktop speaker, and the AS interface links the input -// terminal (0x01). Async feedback for true clock matching is a later step. +// Hand-rolled UAC2 speaker (host -> board) descriptor. This mirrors TinyUSB's +// TUD_AUDIO20_SPEAKER_STEREO_FB_DESCRIPTOR (lib/tinyusb/src/device/usbd.h) but +// drops the trailing feedback endpoint, so the streaming alt-setting declares +// a single OUT endpoint (_nEPs = 0x01). The entity IDs match the mic descriptor +// (see usb_audio_descriptors.h); only the terminal roles reverse: the input +// terminal is the USB-streaming side and the output terminal is the desktop +// speaker, and the AS interface links the input terminal (0x01). +// +// The OUT endpoint is adaptive, not asynchronous: an asynchronous sink has to +// tell the host its rate through a feedback endpoint, and Windows' +// usbaudio2.sys refuses the function (Code 10) when there is none, while an +// adaptive sink takes the host's rate as given, which is what the receive ring +// in USBSpeaker.c does, padding or dropping when the output's own clock +// drifts from it. #define USB_AUDIO_SPEAKER_DESCRIPTOR(_itfnum, _stridx, _nBytesPerSample, _nBitsUsedPerSample, _epout, _epsize) \ /* Standard Interface Association Descriptor (IAD) */ \ TUD_AUDIO20_DESC_IAD(/*_firstitf*/ _itfnum, /*_nitfs*/ 0x02, /*_stridx*/ 0x00), \ @@ -185,11 +302,11 @@ void usb_audio_setup_singletons(void) { /* Class-Specific AS Isochronous Audio Data Endpoint Descriptor(4.10.1.2) */ \ TUD_AUDIO20_DESC_CS_AS_ISO_EP(/*_attr*/ AUDIO20_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, /*_ctrl*/ AUDIO20_CTRL_NONE, /*_lockdelayunit*/ AUDIO20_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, /*_lockdelay*/ 0x0000) -// Hand-rolled UAC2 microphone (board -> host) descriptor WITHOUT an async -// feedback endpoint. This mirrors TinyUSB's TUD_AUDIO20_MIC_ONE_CH_DESCRIPTOR -// (lib/tinyusb/src/device/usbd.h) but drops the trailing feedback endpoint, so -// the streaming alt-setting declares a single IN endpoint (_nEPs = 0x00). Async -// feedback for true clock matching is a later step. +// Hand-rolled UAC2 microphone (board -> host) descriptor. This mirrors +// TinyUSB's TUD_AUDIO20_MIC_ONE_CH_DESCRIPTOR (lib/tinyusb/src/device/usbd.h) +// but drops the trailing feedback endpoint, so the streaming alt-setting +// declares a single IN endpoint. An asynchronous source needs no feedback: it +// sends at its own rate and the host follows. #define USB_AUDIO_MIC_DESCRIPTOR(_itfnum, _stridx, _nBytesPerSample, _nBitsUsedPerSample, _epin, _epsize) \ /* Standard Interface Association Descriptor (IAD) */ \ TUD_AUDIO20_DESC_IAD(/*_firstitf*/ _itfnum, /*_nitfs*/ 0x02, /*_stridx*/ 0x00), \ @@ -227,9 +344,9 @@ void usb_audio_setup_singletons(void) { // must use distinct entity IDs (USB_AUDIO_HS_ENTITY_*; see usb_audio_descriptors.h) // because they live in the same AudioControl interface, and they share one clock // source. The function spans three interfaces: AudioControl (_itfnum), the -// speaker AudioStreaming interface (_itfnum + 1, OUT endpoint), and the mic -// AudioStreaming interface (_itfnum + 2, IN endpoint). Neither stream has an -// async feedback endpoint, matching the single-direction descriptors. +// speaker AudioStreaming interface (_itfnum + 1, adaptive OUT endpoint), and +// the mic AudioStreaming interface (_itfnum + 2, asynchronous IN endpoint), as +// in the single-direction descriptors. #define USB_AUDIO_HEADSET_DESCRIPTOR(_itfnum, _stridx, _nBytesPerSample, _nBitsUsedPerSample, _epout, _epin, _epsize) \ /* Standard Interface Association Descriptor (IAD) -- 3 interfaces */ \ TUD_AUDIO20_DESC_IAD(/*_firstitf*/ _itfnum, /*_nitfs*/ 0x03, /*_stridx*/ 0x00), \ @@ -262,7 +379,7 @@ void usb_audio_setup_singletons(void) { TUD_AUDIO20_DESC_CS_AS_INT(/*_termid*/ USB_AUDIO_HS_ENTITY_SPK_INPUT_TERMINAL, /*_ctrl*/ AUDIO20_CTRL_NONE, /*_formattype*/ AUDIO20_FORMAT_TYPE_I, /*_formats*/ AUDIO20_DATA_FORMAT_TYPE_I_PCM, /*_nchannelsphysical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO20_CHANNEL_CONFIG_NON_PREDEFINED, /*_stridx*/ 0x00), \ /* Type I Format Type Descriptor(2.3.1.6 - Audio Formats) */ \ TUD_AUDIO20_DESC_TYPE_I_FORMAT(_nBytesPerSample, _nBitsUsedPerSample), \ - /* Standard AS Isochronous Audio Data Endpoint Descriptor(4.10.1.1) */ \ + /* Standard AS Isochronous Audio Data Endpoint Descriptor(4.10.1.1) -- adaptive, see USB_AUDIO_SPEAKER_DESCRIPTOR */ \ TUD_AUDIO20_DESC_STD_AS_ISO_EP(/*_ep*/ _epout, /*_attr*/ (uint8_t)((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_ADAPTIVE | (uint8_t)TUSB_ISO_EP_ATT_DATA), /*_maxEPsize*/ _epsize, /*_interval*/ 0x01), \ /* Class-Specific AS Isochronous Audio Data Endpoint Descriptor(4.10.1.2) */ \ TUD_AUDIO20_DESC_CS_AS_ISO_EP(/*_attr*/ AUDIO20_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, /*_ctrl*/ AUDIO20_CTRL_NONE, /*_lockdelayunit*/ AUDIO20_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, /*_lockdelay*/ 0x0000), \ @@ -435,7 +552,7 @@ size_t usb_audio_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *de // sits in the audiosample source the output backend pulls from. static void usb_audio_speaker_task(void) { // One USB packet of scratch; we loop until ep_out_ff is empty. - static uint8_t chunk[CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX]; + static int16_t chunk[CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX / sizeof(int16_t)]; while (tud_audio_available() > 0) { uint16_t got = tud_audio_read(chunk, sizeof(chunk)); @@ -448,7 +565,8 @@ static void usb_audio_speaker_task(void) { if (got > sizeof(chunk)) { got = sizeof(chunk); } - usb_audio_usbspeaker_background_drain(chunk, got); + usb_audio_apply_gain(chunk, got / sizeof(int16_t), USB_AUDIO_SPEAKER_FEATURE_UNIT_SLOT); + usb_audio_usbspeaker_background_drain((const uint8_t *)chunk, got); } } @@ -490,6 +608,10 @@ static void usb_audio_microphone_task(void) { want = filled; underran = true; } + if (filled > 0) { + usb_audio_apply_gain(usb_audio_mic_samples, want / sizeof(int16_t), + usb_audio_microphone_feature_unit_slot()); + } if (tud_audio_write((uint8_t *)usb_audio_mic_samples, (uint16_t)want) == 0) { break; // FIFO unexpectedly full / host not ready @@ -585,24 +707,32 @@ bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p // Only current-value requests are supported. TU_VERIFY(p_request->bRequest == AUDIO20_CS_REQ_CUR); - // A headset exposes a feature unit per direction; the speaker's id matches the - // single-direction USB_AUDIO_ENTITY_FEATURE_UNIT, the mic adds a second one. - // Mute/volume state is shared across them. - if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT || - entityID == USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT) { + int slot = usb_audio_feature_unit_slot(entityID); + if (slot >= 0) { if (channelNum > USB_AUDIO_N_CHANNELS) { return false; } switch (ctrlSel) { case AUDIO20_FU_CTRL_MUTE: TU_VERIFY(p_request->wLength == sizeof(audio20_control_cur_1_t)); - usb_audio_mute[channelNum] = ((audio20_control_cur_1_t *)pBuff)->bCur; + usb_audio_mute[slot][channelNum] = ((audio20_control_cur_1_t *)pBuff)->bCur; + usb_audio_update_gain(slot); return true; - case AUDIO20_FU_CTRL_VOLUME: + case AUDIO20_FU_CTRL_VOLUME: { TU_VERIFY(p_request->wLength == sizeof(audio20_control_cur_2_t)); - usb_audio_volume[channelNum] = ((audio20_control_cur_2_t *)pBuff)->bCur; + int16_t volume = ((audio20_control_cur_2_t *)pBuff)->bCur; + // Hosts stay inside the range they were given, but keep what + // is applied inside it whatever arrives. + if (volume > 0) { + volume = 0; + } else if (volume < USB_AUDIO_VOLUME_MIN_DB * 256) { + volume = USB_AUDIO_VOLUME_MIN_DB * 256; + } + usb_audio_volume[slot][channelNum] = volume; + usb_audio_update_gain(slot); return true; + } default: return false; @@ -639,25 +769,25 @@ bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p } // Feature unit (mute/volume) for either the speaker or mic chain. - if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT || - entityID == USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT) { + int slot = usb_audio_feature_unit_slot(entityID); + if (slot >= 0) { if (channelNum > USB_AUDIO_N_CHANNELS) { return false; } switch (ctrlSel) { case AUDIO20_FU_CTRL_MUTE: - return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_mute[channelNum], sizeof(usb_audio_mute[channelNum])); + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_mute[slot][channelNum], sizeof(usb_audio_mute[slot][channelNum])); case AUDIO20_FU_CTRL_VOLUME: switch (p_request->bRequest) { case AUDIO20_CS_REQ_CUR: - return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_volume[channelNum], sizeof(usb_audio_volume[channelNum])); + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_volume[slot][channelNum], sizeof(usb_audio_volume[slot][channelNum])); case AUDIO20_CS_REQ_RANGE: { audio20_control_range_2_n_t(1) ret; ret.wNumSubRanges = 1; - ret.subrange[0].bMin = -90 * 256; // -90 dB - ret.subrange[0].bMax = 90 * 256; // +90 dB + ret.subrange[0].bMin = USB_AUDIO_VOLUME_MIN_DB * 256; + ret.subrange[0].bMax = 0; ret.subrange[0].bRes = 256; // 1 dB steps return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &ret, sizeof(ret)); } diff --git a/shared-module/usb_audio/__init__.h b/shared-module/usb_audio/__init__.h index 5730e6ef60b..85d8d0e5e07 100644 --- a/shared-module/usb_audio/__init__.h +++ b/shared-module/usb_audio/__init__.h @@ -33,6 +33,35 @@ bool usb_audio_streaming(void); // it shares a headset function with a microphone. bool usb_audio_speaker_streaming(void); +// The two directions a feature unit can control. A headset has one unit per +// direction; a single-direction function has one, and both names resolve to it. +typedef enum { + USB_AUDIO_DIR_SPEAKER, + USB_AUDIO_DIR_MICROPHONE, +} usb_audio_direction_t; + +// What the host has asked for on a direction's feature unit. These are what the +// host set, not what the board did with it: by default nothing is applied to the +// samples and the application decides (see usb_audio_set_apply_host_volume). +// +// All three report the effective setting for the first channel, with the 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 would always read +// unchanged. usb_audio_host_gain() is that as a linear factor with mute folded in +// as zero, so it can be assigned straight to an audiomixer.MixerVoice level. +// Before the host sets anything they read False, 0.0 dB and 1.0. +bool usb_audio_host_mute(usb_audio_direction_t dir); +mp_float_t usb_audio_host_volume(usb_audio_direction_t dir); +mp_float_t usb_audio_host_gain(usb_audio_direction_t dir); + +// Whether the board scales the samples of this direction by the host's gain +// itself. False by default: a UAC2 device that declares the control is expected +// to honour it, but doing it silently in the background surprises applications +// that manage their own levels, so the default leaves it to the application and +// this turns on the classic sound-card behaviour. +bool usb_audio_apply_host_volume(usb_audio_direction_t dir); +void usb_audio_set_apply_host_volume(usb_audio_direction_t dir, bool apply); + // Negotiated audio format, valid when usb_audio_enabled() is true. extern uint32_t usb_audio_sample_rate; extern uint8_t usb_audio_channel_count; diff --git a/supervisor/shared/usb/tusb_config.h b/supervisor/shared/usb/tusb_config.h index ac63542b8e4..4a39e0a34c5 100644 --- a/supervisor/shared/usb/tusb_config.h +++ b/supervisor/shared/usb/tusb_config.h @@ -133,8 +133,7 @@ extern "C" { // OUT endpoint (host -> board speaker). Compiled into the class driver // unconditionally; whether it actually enumerates is decided by the emitted -// descriptor (still mic-only until the speaker descriptor lands). Mirrors the -// IN sizing above. +// descriptor, which usb_audio.enable() chooses. Mirrors the IN sizing above. #define CFG_TUD_AUDIO_ENABLE_EP_OUT 1 // wMaxPacketSize, sized for the highest supported sample rate. #define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX TUD_AUDIO_EP_SIZE(TUD_OPT_HIGH_SPEED, USB_AUDIO_MAX_SAMPLE_RATE, USB_AUDIO_N_BYTES_PER_SAMPLE, USB_AUDIO_N_CHANNELS)