Skip to content
1 change: 1 addition & 0 deletions shared-bindings/usb_audio/USBMicrophone.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "shared-bindings/usb_audio/USBMicrophone.h"
#include "shared-bindings/util.h"
#include "shared-module/usb_audio/__init__.h"
#include "shared-module/usb_audio/usb_audio_descriptors.h"

//| class USBMicrophone:
//| """Streams an audio sample to the host computer as a USB Audio Class microphone.
Expand Down
22 changes: 9 additions & 13 deletions shared-bindings/usb_audio/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
//| 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 1 IN endpoint and 2 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
//| This mode requires 2 IN endpoints and 2 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`.
//|
Expand All @@ -32,7 +32,7 @@
//|
//| # boot.py
//| import usb_audio
//| usb_audio.enable(sample_rate=16000, channel_count=1, bits_per_sample=16)
//| usb_audio.enable(sample_rate=16000, bits_per_sample=16)
//|
//| .. code-block:: py
//|
Expand Down Expand Up @@ -61,9 +61,8 @@
//| pass
//| mic.stop()
//|
//| The ``sample_rate`` and ``channel_count`` of the sample played must match the
//| values passed to `enable`, and the sample must be 16-bit signed; otherwise
//| ``play`` raises a ``ValueError``.
//| The ``sample_rate`` and ``channel_count`` of the sample played must match the values passed to `enable`,
//| and the sample must be 16-bit signed; otherwise ``play`` raises a ``ValueError``.
//|
//| This interface is experimental and may change without notice even in stable
//| versions of CircuitPython."""
Expand All @@ -84,7 +83,6 @@
//| def enable(
//| sample_rate: int = 16000,
//| channel_count: int = 1,
//| bits_per_sample: int = 16,
//| microphone: bool = True,
//| speaker: bool = False,
//| ) -> None:
Expand All @@ -93,7 +91,7 @@
//| This function may only be used from ``boot.py``.
//|
//| :param int sample_rate: Samples per second of the streamed audio.
//| :param int channel_count: Number of channels. Only mono (1) is supported initially.
//| :param int channel_count: Number of channels. Either mono (1) or stereo (2) is supported.
//| :param int bits_per_sample: Bits per signed PCM sample. Only 16 is supported initially.
//| :param bool microphone: Present a microphone (audio flows board -> host). Enabled by default.
//| :param bool speaker: Present a speaker (audio flows host -> board).
Expand All @@ -103,11 +101,10 @@
//|
//|
static mp_obj_t usb_audio_enable(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_sample_rate, ARG_channel_count, ARG_bits_per_sample, ARG_microphone, ARG_speaker };
enum { ARG_sample_rate, ARG_channel_count, ARG_microphone, ARG_speaker };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_sample_rate, MP_ARG_INT, { .u_int = 16000 } },
{ MP_QSTR_channel_count, MP_ARG_INT, { .u_int = 1 } },
{ MP_QSTR_bits_per_sample, MP_ARG_INT, { .u_int = 16 } },
{ MP_QSTR_microphone, MP_ARG_BOOL, { .u_bool = true } },
{ MP_QSTR_speaker, MP_ARG_BOOL, { .u_bool = false } },
};
Expand All @@ -116,15 +113,14 @@ static mp_obj_t usb_audio_enable(size_t n_args, const mp_obj_t *pos_args, mp_map

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);
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);
mp_int_t bits_per_sample = mp_arg_validate_int(args[ARG_bits_per_sample].u_int, USB_AUDIO_BITS_PER_SAMPLE, MP_QSTR_bits_per_sample);
bool microphone = args[ARG_microphone].u_bool;
bool speaker = args[ARG_speaker].u_bool;

if (!microphone && !speaker) {
mp_raise_ValueError(MP_ERROR_TEXT("At least one of microphone and speaker must be enabled"));
}

if (!shared_module_usb_audio_enable(sample_rate, channel_count, bits_per_sample, microphone, speaker)) {
if (!shared_module_usb_audio_enable(sample_rate, channel_count, microphone, speaker)) {
mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot change USB devices now"));
}

Expand Down
41 changes: 36 additions & 5 deletions shared-module/usb_audio/USBMicrophone.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include "shared-bindings/usb_audio/USBMicrophone.h"
#include "shared-module/usb_audio/__init__.h"
#include "shared-module/usb_audio/usb_audio_descriptors.h"
#include "shared-module/audiocore/__init__.h"

// Only one microphone may feed the single USB IN endpoint at a time. This points
Expand Down Expand Up @@ -53,7 +54,7 @@ void common_hal_usb_audio_usbmicrophone_play(usb_audio_usbmicrophone_obj_t *self
if (audiosample_get_channel_count(sample_base) != usb_audio_channel_count) {
mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_channel_count);
}
if (audiosample_get_bits_per_sample(sample_base) != usb_audio_bits_per_sample) {
if (audiosample_get_bits_per_sample(sample_base) != USB_AUDIO_BITS_PER_SAMPLE) {
mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_bits_per_sample);
}
if (!sample_base->samples_signed) {
Expand Down Expand Up @@ -101,15 +102,33 @@ bool common_hal_usb_audio_usbmicrophone_get_paused(usb_audio_usbmicrophone_obj_t
return self->playing && self->paused;
}

static inline uint32_t copy16lsb(uint32_t val) {
#if (defined(__ARM_ARCH_7EM__) && (__ARM_ARCH_7EM__ == 1))
return __PKHBT(val, val, 16);
#else
val &= 0x0000ffff;
return val | (val << 16);
#endif
}

static inline uint32_t copy16msb(uint32_t val) {
#if (defined(__ARM_ARCH_7EM__) && (__ARM_ARCH_7EM__ == 1))
return __PKHTB(val, val, 16);
#else
val &= 0xffff0000;
return val | (val >> 16);
#endif
}

size_t usb_audio_usbmicrophone_background_fill(uint8_t *out, size_t max_bytes) {
usb_audio_usbmicrophone_obj_t *self = active_microphone;
if (self == NULL || !self->playing || self->paused || self->sample == MP_OBJ_NULL) {
return 0;
}

// The negotiated USB format is 16-bit signed mono PCM. For this step the
// The negotiated USB format is 16-bit signed PCM. For this step the
// bound sample is assumed to already be in that format (e.g. a 16-bit signed
// mono audiocore.RawSample), so its bytes are copied straight through.
// audiocore.RawSample), so its bytes are copied straight through.
size_t filled = 0;
while (filled < max_bytes) {
if (self->buffer_length == 0) {
Expand Down Expand Up @@ -138,8 +157,20 @@ size_t usb_audio_usbmicrophone_background_fill(uint8_t *out, size_t max_bytes) {
}
}

size_t n = MIN(self->buffer_length, max_bytes - filled);
memcpy(out + filled, self->buffer, n);
size_t n;
if (MP_LIKELY(usb_audio_channel_count == USB_AUDIO_N_CHANNELS)) {
n = MIN(self->buffer_length, max_bytes - filled);
memcpy(out + filled, self->buffer, n);
} else {
n = MIN(self->buffer_length << 1, max_bytes - filled);
uint32_t *word_buffer = (uint32_t *)self->buffer;
uint32_t *word_out = (uint32_t *)(out + filled);
for (size_t i = 0; i < n / sizeof(uint32_t); i += 2) {
uint32_t v = word_buffer[i >> 1];
word_out[i] = copy16lsb(v);
word_out[i + 1] = copy16msb(v);
}
}
self->buffer += n;
self->buffer_length -= n;
filled += n;
Expand Down
39 changes: 30 additions & 9 deletions shared-module/usb_audio/USBSpeaker.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ void common_hal_usb_audio_usbspeaker_construct(usb_audio_usbspeaker_obj_t *self)
// format we present is 16-bit signed LE PCM, which is exactly what the
// CircuitPython audio pipeline carries, so no conversion is needed.
self->base.sample_rate = usb_audio_sample_rate;
self->base.bits_per_sample = usb_audio_bits_per_sample;
self->base.bits_per_sample = USB_AUDIO_BITS_PER_SAMPLE;
self->base.channel_count = usb_audio_channel_count;
self->base.samples_signed = true;
self->base.single_buffer = false;
self->base.max_buffer_length = USB_AUDIO_SPEAKER_OUTPUT_BUFFER_SIZE;

self->ring_head = 0;
self->ring_tail = 0;
self->ring_count = 0;
Expand Down Expand Up @@ -193,16 +193,29 @@ audioio_get_buffer_result_t usb_audio_usbspeaker_get_buffer(usb_audio_usbspeaker

uint32_t half = self->base.max_buffer_length / 2;
uint8_t *out = self->output_buffer + half * self->output_index;
int16_t *word_out = (int16_t *)out;
int16_t *word_ring = (int16_t *)self->ring;
self->output_index = 1 - self->output_index;

// Consumer side of the SPSC ring (runs in the output backend's refill ISR).
// It is never preempted by the producer, so no interrupt guard is required.
size_t to_copy = MIN(self->ring_count, (size_t)half);

size_t first = MIN(to_copy, USB_AUDIO_SPEAKER_RING_SIZE - self->ring_tail);
memcpy(out, &self->ring[self->ring_tail], first);
if (to_copy > first) {
memcpy(out + first, &self->ring[0], to_copy - first);
if (MP_LIKELY(usb_audio_channel_count == USB_AUDIO_N_CHANNELS)) {
memcpy(out, &self->ring[self->ring_tail], first);
if (to_copy > first) {
memcpy(out + first, &self->ring[0], to_copy - first);
}
} else {
for (size_t i = self->ring_tail / USB_AUDIO_N_BYTES_PER_SAMPLE; i < first / USB_AUDIO_N_BYTES_PER_SAMPLE; i += 2) {
word_out[i >> 1] = word_ring[i];
}
if (to_copy > first) {
for (size_t i = 0; i < (to_copy - first) / USB_AUDIO_N_BYTES_PER_SAMPLE; i += 2) {
word_out[(first / USB_AUDIO_N_BYTES_PER_SAMPLE) + (i >> 1)] = word_ring[i];
}
}
}
self->ring_tail = (self->ring_tail + to_copy) % USB_AUDIO_SPEAKER_RING_SIZE;
self->ring_count -= to_copy;
Expand All @@ -211,17 +224,25 @@ audioio_get_buffer_result_t usb_audio_usbspeaker_get_buffer(usb_audio_usbspeaker
// Underrun: pad the remainder with silence. Samples are signed, so
// silence is 0. This is the consume-side of the pacing failure mode
// tracked in the usb-audio-artifact-pacing memory: we never spin.
memset(out + to_copy, 0, half - to_copy);
if (MP_LIKELY(usb_audio_channel_count == USB_AUDIO_N_CHANNELS)) {
memset(out + to_copy, 0, half - to_copy);
} else {
memset(out + (to_copy >> 1), 0, (half - to_copy) >> 1);
}
}

// Mono only for v1, so the single-channel offset is always 0; computed the
// same way as audiocore.RawSample so stereo (interleaved ring) can extend it.
// Computed the same way as audiocore.RawSample so stereo (interleaved ring)
// can extend it.
if (single_channel_output) {
out += (channel % self->base.channel_count) * (self->base.bits_per_sample / 8);
}

*buffer = out;
*buffer_length = half;
if (MP_LIKELY(usb_audio_channel_count == USB_AUDIO_N_CHANNELS)) {
*buffer_length = half;
} else {
*buffer_length = half >> 1;
}
// A live USB stream is infinite; never report DONE or the backend would stop.
return GET_BUFFER_MORE_DATA;
}
2 changes: 1 addition & 1 deletion shared-module/usb_audio/USBSpeaker.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// comfortably inside the ring.
#define USB_AUDIO_SPEAKER_FRAMES_PER_BUFFER (128)

// Bytes per audio frame in the negotiated UAC2 format (mono 16-bit for v1).
// Bytes per audio frame in the negotiated UAC2 format (stereo 16-bit).
#define USB_AUDIO_SPEAKER_BYTES_PER_FRAME (USB_AUDIO_N_BYTES_PER_SAMPLE * USB_AUDIO_N_CHANNELS)

// Full owned double-buffer: two halves of FRAMES_PER_BUFFER frames each. Matches
Expand Down
Loading
Loading