Custom USB Audio (UAC x2 Input Interface) `

So I don’t know if anyone else implemented this but I had trouble getting USB audio running with tinyusb (I need only recording for now). So I asked my good friends (Claude, Gemini, Kimi and others…) for some help. This is a basic version based on ST Middleware audio class with some enhancements. It shows up as a stereo input device (I use it with Reaper DAW and audio device in exclusive mode).
There was a big issue with audible glitches due to clock drift, since ST did no implement any drift control loop and apparantly there is no feedback endpoint for input devices.

It works great for a first try, but there may be some residual glitches, especially when I run NAM models with IR (thats almost 85% load).

Let me know if and how its working for you.

Drop ‘.c’ and ‘.h’ files in your main folder and add this to makefile:

C_SOURCES += \ usbd_audio.c \ usbd_audio_if.c \ usbd_conf.c \ usbd_desc.c

main: (usb buffer is in D2 RAM for caching issues, be sure to define it)

/**
  ******************************************************************************
  * @file    UsbAudio.cpp
  * @brief   Daisy Seed UAC1 Microphone — main application
  *
  * This makes the Daisy Seed appear as a standard USB microphone on any PC.
  * No custom driver needed — UAC1 is supported natively by Windows, macOS,
  * and Linux.  The device will show up in your DAW as an audio input.
  *
  * Signal flow:
  *   Analog In (3.5mm or line)
  *     → Daisy PCM3060 ADC (24-bit, 48 kHz)
  *       → AudioCallback (float, non-interleaved)
  *         → convert float→int16, interleave L/R
  *           → AudioIF_PushSamples() → ring buffer
  *             → PeriodicTC() every 1ms (USB DataIn)
  *               → USB host (PC) → DAW input
  *
  * NOTE: With the USB port in use as an audio device the libDaisy USB serial
  * logger is unavailable.  Debug via the UART pins or a J-Link probe instead.
  ******************************************************************************
  */
#define DSY_RAM_D2 __attribute__((section(".heap")))
#include "daisy_seed.h"
#include "usbd_audio_if.h"
#include "daisysp.h"

using namespace daisy;
using namespace daisysp;
using namespace std;



/* C linkage for the USB device library init function in usbd_conf.c */
extern "C" {
    #include "usbd_core.h"
    #include "usbd_desc.h"
    #include "usbd_audio.h"

    extern USBD_HandleTypeDef hUsbDeviceFS;
}

using namespace daisy;

/* -------------------------------------------------------------------------- */
/* Globals                                                                      */
/* -------------------------------------------------------------------------- */
DaisySeed hw;

Oscillator testTone;

uint32_t last_led_time = 0;
uint32_t led_tick = 0;

/* USB device handle — defined here, referenced by usbd_conf.c via extern.
  

usb_audio.zip (16.2 KB)

 Some versions of libDaisy already define hUsbDeviceFS; if you get a
   duplicate-definition link error, remove this line and add:
       extern USBD_HandleTypeDef hUsbDeviceFS;
   at the top of this file instead.                                           */
//USBD_HandleTypeDef hUsbDeviceFS;

/* -------------------------------------------------------------------------- */
/* Audio callback                                                               */
/* -------------------------------------------------------------------------- */
void AudioCallback(AudioHandle::InputBuffer  in,
                   AudioHandle::OutputBuffer out,
                   size_t                    size)
{
    /* Convert the Daisy's non-interleaved float buffers to interleaved int16.
       The Daisy codec delivers 24-bit audio internally; float range is ±1.0f.
       We scale to int16 range (±32767) for the USB stream.                  */

    /* Use a local stack buffer: size*2 int16 samples, size ≤ 480 (10ms max)
       Stack usage = 480*2*2 = 1920 bytes — safe on the H7's 128 KB DTCM.   */
    int16_t usb_buf[size * 2];

    

    for(size_t i = 0; i < size; i++)
    {
        float temp = testTone.Process();
        float l = in[0][i];
        float r = temp;//in[1][i];

        /* Clamp to [-1, 1] then scale */
        if(l >  1.0f) l =  1.0f;
        if(l < -1.0f) l = -1.0f;
        if(r >  1.0f) r =  1.0f;
        if(r < -1.0f) r = -1.0f;

        usb_buf[i * 2 + 0] = (int16_t)(l * 32767.0f);
        usb_buf[i * 2 + 1] = (int16_t)(r * 32767.0f);

        /* Pass audio through to the hardware outputs (optional monitor) */
        out[0][i] = l;
        out[1][i] = r;
    }

    /* Push into the USB ring buffer.  This is called from an ISR so it must
       be fast — memcpy only, no blocking.                                    */
    AudioIF_PushSamples(usb_buf, (uint32_t)size);
}

/* -------------------------------------------------------------------------- */
/* USB device initialisation                                                    */
/* -------------------------------------------------------------------------- */
static void InitUSBAudio(void)
{
    /* Initialise the USB device stack on the FS (internal) port              */
    USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);

    /* Register the UAC1 audio class                                           */
    USBD_RegisterClass(&hUsbDeviceFS, &USBD_AUDIO);

    /* Register our application-layer callbacks                                */
    USBD_AUDIO_RegisterInterface(&hUsbDeviceFS, &USBD_AUDIO_fops);

    /* Start — after this the device enumerates on the host                   */
    USBD_Start(&hUsbDeviceFS);
}

/* -------------------------------------------------------------------------- */
/* main                                                                         */
/* -------------------------------------------------------------------------- */
int main(void)
{
    /* Initialise the Daisy Seed hardware (clocks, codec, DMA, etc.)          */
    hw.Init();

    /* Set sample rate to 48 kHz — must match USBD_AUDIO_FREQ in usbd_audio.h */
    hw.SetAudioSampleRate(SaiHandle::Config::SampleRate::SAI_48KHZ);

    /* Set a small block size (48 samples = 1 ms) so each audio callback      *
     * produces exactly one USB packet.  This keeps the ring buffer simple    *
     * and avoids fractional-packet logic.                                    */
    hw.SetAudioBlockSize(48);

    testTone.Init(48000.0f);
    testTone.SetWaveform(Oscillator::WAVE_SIN);
    testTone.SetFreq(440.0f);
    testTone.SetAmp(0.1f);

    /* Bring up USB audio BEFORE starting the audio engine so the ring buffer *
     * is ready when the first audio callback fires.                          */
    InitUSBAudio();

    /* Start the audio engine — AudioCallback will now fire every 1 ms        */
    hw.StartAudio(AudioCallback);

    /* Everything else happens in interrupt context.  Main loop is idle.      */
    for(;;)
    {
        /* Optionally blink the LED to show the firmware is running            */
        /* hw.SetLed(true); HAL_Delay(500); hw.SetLed(false); HAL_Delay(500); */

    }
}