When I first had a look at implementing an Audio USB device for the RP2040 with TinyUSB, I could make things work, but taking up the task to make this work in Arduino was too big.
Then Adafruit released their TinyUSB for Arduino that was quite nicely integrated into the RP2040 Arduino core of Earle Phil Hower.
I was hoping that Adafruit would provide some audio implementation with examples.
Since this did not happen, I decided to give it a try: Here I am documenting my experiences.
Design Goals for my new Adafruit_USBD_Audio class:
The microcontroller should be able to be an USB Audio Device:
- provide data access via callbacks
- configure audio info via begin method
- implement a Speaker (audio sink)
- implement a Microphone (audio source)
- provide all potential TinyUSB audio calback methods so that we can easily create a subclass and overwrite them
I was considering of implementing this as a subclass of Stream, but in the end I decided that this might be better done in my AudioTools project.
Device descriptors (the Microphone)
The first challenge was to come up with the device descriptors. I used the ones from the TinyUSB project as starting point. I wanted to have a 2 channels microphone, so I extended the examples for this case and tested it with the original build environment with a Rasperry Pico RP2040.
Next I implemented an Audio Device C++ class in my Adafruit_TinyUSB_Arduino fork that generates the descriptor dynamically and implemented the tinyusb callbacks as virtual methods.
Device descriptors (the Speaker)
TinyUSB was providing this 2 channel speaker example, but this was giving compile errors. So I needed to first do some corrections to make it compile. After my corrections, it was working, so I integrated it into my C++ Aduio class as well.
Testing/Debugging Tools
USB development is quite tricky since it is very difficult to track what’s happening. Using a debugger simply does not work because of the strict timing requirements and even adding logging commands turns out to be too slow.
I was using the following debugging/testing approch (in Linux):
- I used lsusb to compare my new generated device descriptor with the orignal one and first made sure that they are abolutely equal
- I used dmesg to check if the usb generated errors
- I added some digitalWrites in the callback functions and used a logic analyser to check if and how often some functions were called.
- I used the stacktrace of the debugger to pin down crashes
- I did some extensive code reviews to make sure that the orignal code was matching the ported C++ code.
- I used Audacity to test the recording and output functionality. Fortunately there is a Rescan Audio devices in the Transport menu
Problems with CDC (using the Microphone example)
Initially I deactivated CDC and the functionality was working great. But as soon as I activated CDC (Serial support) things initially were working, but when testing, just changing the sample rate in Audacity was crashing the microcontroller.
Here the debugger came to my help and so I found that I was not the first with this issue and that there was even an open pull request that addressed the problem. After applying it to my fork, the issue was gone!
Problems with CDC (using the Speaker example)
When I was implementing the Speaker it was also working w/o CDC and activating CDC was immediately crashing. Here dmesg was giving the hint:
[29056.169071] usb 1-1.3: config 1 interface 3 altsetting 1 has a duplicate endpoint with address 0x82, skipping
I messed up the endpoints in the device descriptor and after correcting this, things started to work as well.
Current Status
It seems to work quite nicely now. Here is a simple Arduino example microphone test sketch:
#include "Adafruit_TinyUSB.h"
Adafruit_USBD_Audio usb;
size_t readCB(uint8_t* data, size_t len, Adafruit_USBD_Audio& ref) {
int16_t* data16 = (int16_t*)data;
size_t samples = len / sizeof(int16_t);
size_t result = 0;
// generate random stereo data
for (int j = 0; j < samples; j+=2) {
data16[j] = random(-32000, 32000);
data16[j+1] = random(-32000, 32000);;
result += sizeof(int16_t)*2;
}
return result;
}
void setup() {
Serial.begin(115200);
// Start USB device as Microphone
usb.setReadCallback(readCB);
usb.begin(44100, 2, 16);
if (TinyUSBDevice.mounted()) {
TinyUSBDevice.detach();
delay(10);
TinyUSBDevice.attach();
}
}
void loop() {
// optionally use LED do display status
usb.updateLED();
}
Dependencies
https://github.com/pschatzmann/Adafruit_TinyUSB_Arduino
Consult the Wiki to try it out in your environment.
5 Comments
Gough Lui · 9. November 2024 at 4:43
Dear Phil,
This is quite an interesting and useful development. Not being a USB expert myself, I’ve had very little luck getting my head around the intricacies of all the descriptor formats, endpoints etc. You’ve definitely made things a lot easier to get started, especially if we don’t need to modify the more intricate parts of the setup (i.e. multi-rate support, volume mixers).
Nevertheless, I’ve been trying your “Audio” branch of TinyUSB from your GitHub. So far, I’ve come across an interesting issue which I hope is something minor you could help fix. I can build and run both the microphone and speaker demos just fine, Windows 11 detects the audio device correctly and they perform as expected.
But if I build and run the headset (i.e. both speaker + mic) demo, Windows 11 gives me a Code 10 Device Cannot Start with the error “STATUS_DEVICE_DATA_ERROR” on the USB Composite Device. I suspect this is implying there is something wrong with the structure of some USB descriptor or something about it that it doesn’t like but I can’t seem to find any more information in the event logs or anything. I confirmed this with two different Windows 11 computers, one AMD and one Intel chipset. I tried an older computer with Windows 10 and even older one with Windows 7, same error, so it seems to be a compatibility issue with Microsoft’s USB generic drivers.
Being desperate, I tried changing the sample rate from 44100 to 48000 with no change. I also tried disabling the USB-CDC with usb.setCDCActive(0); and removing all Serial.* calls but it didn’t make any difference. Do you have any other suggestions?
Oddly enough, trying the headset demo on Android (USB-OTG) and it’s working just fine there as well as in Ubuntu.
In case it matters, I am using a Wiznet W5500-EVB-Pico and a Raspberry Pi RP2040 Pico with the same results. Build is against Earl Philhower core, version 4.2.0 under Arduino IDE 2.3.3 on Windows 11.
Thanks,
Gough.
Jacob Schmidt · 24. October 2024 at 22:48
This is awesome! I have used your A2DP libraries with ESP32 to great success and am excited to try them on RP2040.
After fresh installs of the arduino_audio_tools, rp2040-A2DP, and libsbc, libs.
I tried the a2dp-sink example on the PicoW.
I got one compile error (it couldn’t find the the AudioCodecs directory in the audio_tools lib) and fixed it by copying that directory into the RP2040-A2DP-main src directory
The second compile error I got was the type “AudioBaseInfo” in A2DPCodecs.h, which I see from your updating you have just changed to “AudioInfo”, but possibly there was an instance that was missed?
Thank you for your work on this, it is very much appreciated!
pschatzmann · 25. October 2024 at 2:30
This post is about USB: AudioTools have always been working on an RP2040 and now I am supporting USB as well
I have started with the A2DP project for the RP2040, but never completed it. That’s maybe something for the cold winter days…
Curtis · 15. October 2024 at 15:34
Hey there, this is really impressive. I’ve taken a look at the branch and the examples but I’m a bit new to USB and trying to figure out how you would interface the speaker example with Arduino-Audio-Tools. How, for example, would you take the USB data and turn it into a stream?
pschatzmann · 15. October 2024 at 15:39
After I have everything working, I will extend the AudioTools with an USBAudio Stream.
For the time beeing you can use use the regular Arduino Print/Stream API in the callback.
E.g. on an I2SStream you would just do something like i2s.write(data, len);