Skip to content

Instantly share code, notes, and snippets.

@benagricola
Last active June 8, 2024 16:16
Show Gist options
  • Save benagricola/ce97696df9377a9f9c38045b89769a5b to your computer and use it in GitHub Desktop.
Save benagricola/ce97696df9377a9f9c38045b89769a5b to your computer and use it in GitHub Desktop.
Using MicroPython's new pure-python USB driver to send audio data to the host.
import array
import math
import time
import os
import usb.device
from uac2 import AudioInterface
print("Okay, configuring the audio interface now...")
# Constants
sample_rate = 48000
frequency = 440 # 440Hz
num_samples = 10 # number of samples to fit in 60 bytes
# Generate the sine wave
wave = [0.5 * math.sin(2 * math.pi * frequency * (i / sample_rate)) for i in range(num_samples)]
# Convert to 24-bit PCM
wave_pcm = [int(sample * (2**23 - 1)) for sample in wave]
# Convert to bytes and interleave left and right channels (they are the same because it's mono)
byte_array = bytearray()
for sample in wave_pcm:
# Convert sample to 24-bit little-endian
sample_bytes = sample.to_bytes(3, 'little')
# Add sample to left channel
byte_array.extend(sample_bytes)
# Add sample to right channel
byte_array.extend(sample_bytes)
print("Sample Len: {}".format(len(byte_array)))
class AudioTest(AudioInterface):
def __init__(self, sample):
self.sample = sample
super().__init__()
def send_sample(self):
self._tx.write(self.sample)
# Create an AudioInterface instance
print("Creating AudioTest instance...")
# Replace 'AudioInterface' with your actual AudioInterface class
m = AudioTest(sample=byte_array)
print("Initializing USB device...")
usb.device.get().init(m, builtin_driver=True)
while not m.is_open():
print("Waiting for USB host to configure the interface...")
time.sleep_ms(100)
print("USB host configured the interface")
while m.is_open():
m.send_sample()
m._tx_xfer()
print("Finished")
# MicroPython USB AUDIO INPUT module
# MIT license; Copyright (c) 2023 Paul Hamshere, 2023-2024 Angus Gratton
from micropython import const
import struct, time
from usb.device.core import Interface, Buffer
_EP_IN_FLAG = const(1 << 7)
_INTERFACE_CLASS_AUDIO = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_CONTROL = const(0x01)
_INTERFACE_SUBCLASS_AUDIO_STREAMING = const(0x02)
class AudioInterface(Interface):
# Base class to implement a USB Audio input device in Python.
def __init__(self, txlen=30):
# Arguments are size of transmit and receive buffers in bytes.
super().__init__()
self.ep_in = None # TX direction (device to host)
self._tx = Buffer(txlen)
def _tx_xfer(self):
# Keep an active IN transfer to send data to the host, whenever
# there is data to send.
if self.is_open() and not self.xfer_pending(self.ep_in) and self._tx.readable():
self.submit_xfer(self.ep_in, self._tx.pend_read(), self._tx_cb)
def _tx_cb(self, ep, res, num_bytes):
if res == 0:
self._tx.finish_read(num_bytes)
def desc_cfg(self, desc, itf_num, ep_num, strs):
# By this point, standard device and configuration descriptors have been added.
# Append the standard AudioControl interface descriptor
desc.interface(itf_num, 0, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_CONTROL)
# Append the class-specific AC interface header descriptor
desc.pack(
"<BBBHHBB",
9, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype MS_HEADER
0x0100, # BcdADC
0x0009, # wTotalLength
0x01, # bInCollection,
itf_num + 1, # baInterfaceNr - points to the Audio Streaming interface
)
# Append the input terminal descriptor
# Append input terminal name
idx = len(strs)+1
strs.append("MicroPython Audio Input")
desc.pack(
"<BBBBHBBHBB",
12, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x02, # bDescriptorSubtype INPUT_TERMINAL
0x01, # bTerminalID
0x0603,# wTerminalType - USB streaming
0x00, # bAssocTerminal - no association
0x02, # bNrChannels - 2 channels
0x0003, # wChannelConfig - left and right front speakers
0x00, # iChannelNames - no string
idx, # iTerminal - no string
)
# Next add the Audio Streaming interface descriptor
desc.interface(
itf_num + 1, 1, _INTERFACE_CLASS_AUDIO, _INTERFACE_SUBCLASS_AUDIO_STREAMING
)
# Append the class-specific AS interface descriptor
desc.pack(
"<BBBBBH",
7, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x01, # bDescriptorSubtype AS_GENERAL
0x01, # bTerminalLink - points to the Terminal ID of the Terminal to which the endpoint of this interface is connected
0x01, # bDelay - delay introduced by the data path
0x0001,# wFormatTag - PCM
)
# Append the Type I format type descriptor
desc.pack(
"<BBBBBBBB3B",
11, # bLength
0x24, # bDescriptorType CS_INTERFACE
0x02, # bDescriptorSubtype FORMAT_TYPE
0x01, # bFormatType - Type I
2, # bNrChannels - 2 channels
3, # bSubframeSize - 3 bytes per audio subframe
24, # bBitResolution - 24 bits per sample
1, # bSamFreqType - 1 discrete sampling frequencies
0x80, 0xBB, 0x00, # tSamFreq - 48000 Hz
)
self.ep_in = ep_num | _EP_IN_FLAG
# # rx side, USB "in" endpoint
_audio_endpoint(desc, self.ep_in)
def num_itfs(self):
return 2
def num_eps(self):
return 1
def on_open(self):
super().on_open()
# kick off any transfers that may have queued while the device was not open
self._tx_xfer()
def _audio_endpoint(desc, bEndpointAddress):
# Append the standard AS Isochronous Audio Data Endpoint descriptor
desc.pack(
"<BBBBHB",
7, # bLength
0x05, # bDescriptorType ENDPOINT
bEndpointAddress, # bEndpointAddress
0x01, # bmAttributes - Isochronous
0x0120, # wMaxPacketSize
0x01, # bInterval
)
# Append the class-specific AS Isochronous Audio Data Endpoint descriptor
desc.pack(
"<BBBBBH",
7, # bLength
0x25, # bDescriptorType CS_ENDPOINT
0x01, # bDescriptorSubtype EP_GENERAL
0x00, # bmAttributes
0x00, # bLockDelayUnits
0x0000, # wLockDelay
)
@benagricola
Copy link
Author

OK the updated code now is able to send audio data. It's not clean, there's clearly an issue with the sample rate vs buffer size vs transmit rate in USB but... AUDIO!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment