Pi 5: USB audio playback silently drops one packet every ~5 s with a 128-frame buffer (UA Volt 2 Max, UAC2 explicit feedback)

Abierto
#7,731 4 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
c, cpp, linux

Línea de trabajo

Start with the supplied loopback.cpp and the snd-usb-audio implementation, especially data_ep_set_params and the playback/capture URB handling for 16- and 128-frame periods. Reproduce the packet loss with the documented ALSA loopback commands, then compare the Raspberry Pi xHCI scheduling path with another host or mainline kernel. Done means the root cause is identified, a fix or diagnostic change is covered by a reproducible test, and the relevant kernel paths are documented.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Describe the bug

On a Raspberry Pi 5, a UA Volt 2 Max (UAC2, async playback with explicit feedback) loses one playback packet
(6 frames at 48 kHz) about every 5.05 s when the ALSA buffer is 128 frames (32 x 4 or 64 x 2). No xrun is reported.
ALSA's frame counts then show the playback running ~25 ppm slower than the capture, so a duplex program's capture
backlog grows until it overruns (about every 2 minutes). With buffers of 192 frames or more (32 x 6, 64 x 3, 128 x 2)
a cabled loopback stays exact to the sample for 10 minutes. The same happens with lowlatency=0 (64 x 2), so it
follows the buffer size, not the low-latency playback mode.

A second, separate symptom: with 16-frame periods both directions lose 1 packet in 8 (41.8k / 42.7k frames/s
instead of 48k), also without an xrun.

Steps to reproduce the behaviour

Output 1 is cabled to Input 1. The test program (loopback.cpp, below, ~230 lines, ALSA only) opens hw:CARD=Max,DEV=0 for capture and playback:

  • interleaved S32, the same period and period count for both streams;
  • snd_pcm_link, the playback buffer filled with silence, then both started together;
  • a loop of one readi and one writei of a period each.

It writes a 1 kHz sine indexed by the playback frame count. In the capture it measures the sine's phase over each
10 ms window (exactly ten cycles, so no leakage) and converts it to a delay in samples, with a precision of about
0.01 sample. A constant delay means a clean path. A step means frames were lost or repeated. A smooth ramp would
mean the DAC and ADC run on separate clocks.

g++ -O2 -std=c++17 loopback.cpp -o loopback -lasound
sudo chrt -f 80 ./loopback drift 600 32 6   # seconds, period, periods
sudo chrt -f 80 ./loopback lat 20 32 6      # round trip of a pulse every 0.25 s
Device (s)

Raspberry Pi 5

System
Raspberry Pi reference 2026-10-06 (pi-gen b2cd98a9ee08a31ef7d0110ed09f2492bbd972ec, stage2), Debian GNU/Linux 13 (trixie)
Firmware: 2026/09/25 15:59:09, version 1accd665 (release) (embedded)
Linux 6.18.50+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.18.50-1+rpt1 (2026-09-11) aarch64
config.txt: arm_freq=2800, over_voltage_delta=50000 (overclocked; not retested at stock clocks)
  • Raspberry Pi 5 Model B Rev 1.1, Debian 13 (trixie) Lite, kernel 6.18.50+rpt-rpi-2712 (Debian 1:6.18.50-1+rpt1, 2026-09-11)

  • Universal Audio Volt 2 Max, 2b5a:0050, USB 2.0 high speed, connected directly to a Pi USB port (both controllers tried, same result)

  • /proc/asound/Max/stream0: playback EP 0x03 (ASYNC, 8 ch S32_LE), sync EP 0x81, implicit feedback: no; capture EP 0x82 (ASYNC, 12 to 18 ch); data packet interval 125 us; device at 48 kHz, internal clock

  • The test program runs as SCHED_FIFO 80, pinned to core 2 (taskset -c 2). The CPU is never the limit: the work per period is a copy.

  • The Volt's own internal hub: the audio function enumerates as usb 1-2.1 / 3-1.1 (Genesys 05e3:0610 inside the interface, not an external hub)

Logs

Nothing in dmesg while the packets are lost (only enumeration and module load / unload lines).

Additional context
Results (snd-usb-audio defaults, lowlatency=1)
period x periods buffer run delay drift steps after start-up
256 x 8 2048 10 min 0.00 ppm none
32 x 6 192 10 min 0.00 ppm none (off a straight line: rms 0.03 sample)
32 x 8, 64 x 3, 64 x 4, 128 x 2, 128 x 4 192 to 512 30 s each within 0.1 ppm none
32 x 4 128 90 s +24.8 ppm +5 to 6 samples every 5.05 s (3.38, 8.43, 13.48, 18.52 s, ...)
64 x 2 128 30 s +24.3 ppm same pattern
32 x 2, 32 x 3 64, 96 10 s n/a 678 / 548 xruns
  • At 32 x 4, each step comes with a discontinuity in the waveform (that window's residual off the sine is +6 to +12 dB).
  • Each step is one 125 us packet (6 frames). There is often a smaller one about 0.24 s later.
  • No xrun is reported for these losses.
  • ALSA's own frame counts agree with the steps: over a run the playback consumes ~25 ppm fewer frames per second than the capture delivers.
  • The DAC and ADC share one clock: at larger buffers there is no drift at all.
  • Round trip at 32 x 6: 371.9 frames (7.75 ms), the same for every pulse. At 32 x 4 it varies from 338 to 362 frames, moving with the losses.
Same test with lowlatency=0 (module reloaded)
period x periods buffer result
32 x 4, 32 x 6 128, 192 playback xrun within 20 ms of the start, every time
64 x 2 128 +21.6 ppm: the same 5-sample steps every ~5.1 s (11.96, 17.08 s, ...)
32 x 8, 32 x 12, 64 x 3, 128 x 2, 256 x 8 192 to 2048 flat: 0.000 ppm, no steps in 30 s

So the loss is not specific to the low-latency playback path: it follows the 128-frame buffer in both modes. With
lowlatency=0 the playback delay is about twice the buffer (snd_pcm_delay 378..384 frames at 64 x 3, against 203..256
with lowlatency=1).

Re-run with the program in the repository (lowlatency=1): at 32 x 4, steps of +5 to 6 samples at 3.43, 8.56, 13.68,
18.82, 23.96, 29.09 and 34.23 s (every 5.13 s), each followed by a ~0.7-sample step 0.69 s later. At 32 x 6, 40 s with
0.0000 samples off a straight line.

16-frame periods (counted from frames per second, not looped back)
  • At 16 x N the capture gets 41.8k and the playback 42.7k frames per second: 1 packet in 8 is missing, with no xrun.
  • In the 6.18 source, data_ep_set_params halves the capture URB's packet count until it fits the period. With 16-frame periods (maximum packet 9 frames) each capture URB is a single packet.
  • A Neural DSP Quad Cortex (152a:880a, implicit feedback) on the same Pi and port runs 16 x 3 at a full 48k frames per second.
Already tried, no change
  • implicit_fb=1 (with the 18-channel capture altset): about 70 xruns/s at 32 x 4.
  • At 32 x 3, lowlatency=0 still gave 496 xruns in 10 s. For 32 x 4 and 64 x 2 under the loopback, see the table above.
  • ksoftirqd threads at SCHED_FIFO, the xHCI interrupts moved to another core: the 16-frame loss rate stays the same.
  • The xHCI isochronous scheduling threshold is 1 microframe (HCSPARAMS2 = 0x140000f1).

Not tried: another host (x86, mainline kernel), so it is open whether this is in snd-usb-audio or in the Pi 5's
xHCI (RP1). The ALSA frames-per-second figures over short runs (30 to 40 s) depend on the start-up and are not used
above; over 10 minutes at 32 x 6 the two streams agree within 2.2 ppm.

The test program is below. Build with g++ -O2 -std=c++17 loopback.cpp -o loopback -lasound.

loopback.cpp (the test program)
// loopback: a USB interface's first output cabled to its first input; checks what ALSA's frame counts cannot.
//   loopback lat   [seconds] [period] [periods] [device]: a pulse every 0.25 s, the round trip in frames
//   loopback drift [seconds] [period] [periods] [device]: a 1 kHz sine written by the playback's frame count; its
//     phase in the capture over 10 ms windows (ten whole cycles: no leakage) gives the delay to far below a sample.
//     A flat delay is a clean path; steps are frames lost or repeated somewhere, a smooth ramp a clock of its own.
// Streams opened as pi_host opens them: interleaved S32, linked, the playback buffer filled with silence, then started
// together. Defaults: lat 20 s 32 x 6, drift 600 s 32 x 6, hw:CARD=Max,DEV=0 (UA Volt 2 Max), capture / playback
// channel 0. Build: g++ -O2 -std=c++17 pi/loopback.cpp -o loopback -lasound. Stop pi_host first: with it running
// the cable feeds the amp's output back into its input.
#include <alsa/asoundlib.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <string>
#include <vector>

static const unsigned rate = 48000;

struct Pcm
{
    snd_pcm_t* h = nullptr;
    unsigned channels = 0;
    snd_pcm_uframes_t period = 0, buffer = 0;
    std::vector<int32_t> raw;

    bool open (const char* dev, snd_pcm_stream_t stream, snd_pcm_uframes_t wantPeriod, unsigned periods)
    {
        if (snd_pcm_open (&h, dev, stream, 0) < 0) return false;
        snd_pcm_hw_params_t* hw;
        snd_pcm_hw_params_alloca (&hw);
        snd_pcm_hw_params_any (h, hw);
        snd_pcm_hw_params_set_access (h, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
        if (snd_pcm_hw_params_set_format (h, hw, SND_PCM_FORMAT_S32_LE) < 0) return false;
        snd_pcm_hw_params_get_channels_min (hw, &channels); // the fewest the device takes (the Volt: 12 in, 8 out)
        snd_pcm_hw_params_set_channels (h, hw, std::max (channels, 2u));
        snd_pcm_hw_params_set_rate (h, hw, rate, 0);
        int dir = 0;
        period = wantPeriod;
        snd_pcm_hw_params_set_period_size_near (h, hw, &period, &dir);
        snd_pcm_hw_params_set_periods_near (h, hw, &periods, &dir);
        if (snd_pcm_hw_params (h, hw) < 0) return false;
        snd_pcm_hw_params_get_channels (hw, &channels);
        snd_pcm_hw_params_get_period_size (hw, &period, &dir);
        snd_pcm_hw_params_get_buffer_size (hw, &buffer);
        snd_pcm_sw_params_t* sw;
        snd_pcm_sw_params_alloca (&sw);
        snd_pcm_uframes_t boundary = 0;
        snd_pcm_sw_params_current (h, sw);
        snd_pcm_sw_params_get_boundary (sw, &boundary);
        snd_pcm_sw_params_set_avail_min (h, sw, period);
        snd_pcm_sw_params_set_start_threshold (h, sw, boundary); // started by hand, both together
        if (snd_pcm_sw_params (h, sw) < 0) return false;
        raw.assign (period * channels, 0);
        return true;
    }
    int read()
    {
        for (snd_pcm_uframes_t done = 0; done < period;)
        {
            const auto n = snd_pcm_readi (h, raw.data() + done * channels, period - done);
            if (n < 0) return (int) n;
            done += (snd_pcm_uframes_t) n;
        }
        return 0;
    }
    int write()
    {
        for (snd_pcm_uframes_t done = 0; done < period;)
        {
            const auto n = snd_pcm_writei (h, raw.data() + done * channels, period - done);
            if (n < 0) return (int) n;
            done += (snd_pcm_uframes_t) n;
        }
        return 0;
    }
    float get (size_t i) const { return (float) (raw[i * channels] * (1.0 / 2147483648.0)); }
    void put (size_t i, double x) { raw[i * channels] = (int32_t) std::lrint (std::clamp (x, -1.0, 1.0) * 2147483647.0); }
};

static double now()
{
    timespec t;
    clock_gettime (CLOCK_MONOTONIC, &t);
    return (double) t.tv_sec + 1e-9 * (double) t.tv_nsec;
}

int main (int argc, char** argv)
{
    setvbuf (stdout, nullptr, _IOLBF, 0);
    const std::string mode = argc > 1 ? argv[1] : "drift";
    const bool lat = mode == "lat";
    if (! lat && mode != "drift") { std::printf ("usage: loopback lat|drift [seconds] [period] [periods] [device]\n"); return 1; }
    const double seconds = argc > 2 ? std::atof (argv[2]) : (lat ? 20.0 : 600.0);
    const unsigned wantPeriod = argc > 3 ? (unsigned) std::atoi (argv[3]) : 32;
    const unsigned periods = argc > 4 ? (unsigned) std::atoi (argv[4]) : 6;
    const char* dev = argc > 5 ? argv[5] : "hw:CARD=Max,DEV=0";

    Pcm in, out;
    if (! in.open (dev, SND_PCM_STREAM_CAPTURE, wantPeriod, periods) || ! out.open (dev, SND_PCM_STREAM_PLAYBACK, wantPeriod, periods)
        || in.period != out.period)
    { std::printf ("cannot open %s with equal periods\n", dev); return 1; }
    const int P = (int) in.period;
    std::printf ("%s: %s, period %d, buffer %lu / %lu (in / out), %u / %u channels\n", mode.c_str(), dev, P, in.buffer, out.buffer, in.channels, out.channels);
    const bool linked = snd_pcm_link (in.h, out.h) == 0;
    snd_pcm_prepare (in.h);
    snd_pcm_prepare (out.h);
    for (snd_pcm_uframes_t f = 0; f + out.period <= out.buffer; f += out.period) out.write();
    const long long prefill = (long long) (out.buffer / out.period * out.period);
    if (linked) snd_pcm_start (in.h); else { snd_pcm_start (out.h); snd_pcm_start (in.h); }
    const double t0 = now();

    const long long total = (long long) (seconds * rate);
    std::vector<float> cap; // lat: every captured frame
    if (lat) cap.reserve ((size_t) total + (size_t) P);
    long long inN = 0, outN = 0;
    long delayMin = 1L << 30, delayMax = 0, backlogMax = 0;
    float peak = 0.0f; // after the first second (the converters start with a spike)
    const int W = 480;
    double I = 0.0, Q = 0.0, E = 0.0;
    int wn = 0;
    std::vector<double> D, R; // drift: delay (samples, unwrapped) and residual off the sine (dB) per window
    double sinT[48], cosT[48];
    for (int k = 0; k < 48; ++k) { sinT[k] = std::sin (2.0 * M_PI * k / 48.0); cosT[k] = std::cos (2.0 * M_PI * k / 48.0); }

    while (inN < total)
    {
        if (in.read() < 0) { std::printf ("capture xrun at %.2f s\n", inN / (double) rate); break; }
        for (int i = 0; i < P; ++i)
        {
            const float x = in.get ((size_t) i);
            if (inN > rate) peak = std::max (peak, std::abs (x));
            if (lat) { cap.push_back (x); continue; }
            const int k = (int) ((inN + i) % 48);
            I += x * cosT[k]; Q += x * sinT[k]; E += (double) x * x;
            if (++wn == W)
            {
                double phi = std::atan2 (-I, Q) / (2.0 * M_PI) * 48.0; // the sine's delay, mod 48 samples
                if (! D.empty()) phi += 48.0 * std::round ((D.back() - phi) / 48.0);
                D.push_back (phi);
                const double fit = 2.0 / W * (I * I + Q * Q);
                R.push_back (10.0 * std::log10 (std::max (1e-30, (E - fit) / std::max (1e-30, fit))));
                I = Q = E = 0.0;
                wn = 0;
            }
        }
        for (int i = 0; i < P; ++i)
        {
            const long long n = lat ? inN + i : outN + i; // lat: output frame i answers input frame i, as pi_host does
            out.put ((size_t) i, lat ? ((n >= rate && n % 12000 == 6000) ? 0.5 : 0.0) : 0.25 * sinT[n % 48]);
        }
        inN += P;
        if (out.write() < 0) { std::printf ("playback xrun at %.2f s\n", inN / (double) rate); break; }
        outN += P;
        snd_pcm_sframes_t d = 0;
        if (snd_pcm_delay (out.h, &d) == 0 && inN > rate) { delayMin = std::min (delayMin, (long) d); delayMax = std::max (delayMax, (long) d); }
        const long backlog = (long) snd_pcm_avail (in.h);
        if (inN > rate) backlogMax = std::max (backlogMax, backlog);
        if (! lat && D.size() > 1 && inN % (rate * 60) < (long long) P)
            std::printf ("%4.0f s: delay %+.3f samples since the start\n", inN / (double) rate, D.back() - D.front());
    }

    // ALSA's view: frames moved per second of the system clock, each stream
    const double t1 = now();
    snd_pcm_sframes_t d = 0;
    snd_pcm_delay (out.h, &d);
    const snd_pcm_sframes_t av = snd_pcm_avail (in.h);
    const double capRate = (double) (inN + av) / (t1 - t0), playRate = (double) (outN + prefill - d) / (t1 - t0);
    std::printf ("in peak %.1f dBFS, playback delay %ld..%ld, capture backlog max %ld\n", 20.0 * std::log10 (std::max (1e-9f, peak)), delayMin, delayMax, backlogMax);
    std::printf ("ALSA: capture %.3f, playback %.3f frames/s (playback %+.1f ppm against capture)\n", capRate, playRate, (playRate / capRate - 1.0) * 1e6);

    if (lat)
    {
        // each pulse: the peak of |x| within 100 ms (a parabola through the top three)
        std::vector<double> rt;
        for (long long n = 6000 + rate; n + 4800 < (long long) cap.size(); n += 12000)
        {
            long long m = n;
            for (long long j = n; j < n + 4800; ++j) if (std::abs (cap[j]) > std::abs (cap[m])) m = j;
            const double a = std::abs (cap[m - 1]), b = std::abs (cap[m]), c = std::abs (cap[m + 1]);
            if (b < 0.001) continue;
            rt.push_back ((double) (m - n) + ((a - 2 * b + c) != 0.0 ? 0.5 * (a - c) / (a - 2 * b + c) : 0.0));
        }
        if (rt.empty()) { std::printf ("no pulses came back (cable? output level?)\n"); return 1; }
        std::sort (rt.begin(), rt.end());
        std::printf ("round trip: %zu pulses, min %.2f, median %.2f, max %.2f frames (median %.3f ms)\n", rt.size(), rt.front(),
                     rt[rt.size() / 2], rt.back(), rt[rt.size() / 2] / rate * 1000.0);
        return 0;
    }

    // drift: the windows after the first second (start-up), a line through their delays, and the steps between them
    const size_t k0 = (size_t) (rate / W), n = D.size();
    if (n < k0 + 10) { std::printf ("too short\n"); return 1; }
    double sx = 0, sy = 0, sxx = 0, sxy = 0;
    for (size_t k = k0; k < n; ++k) { const double x = (double) k; sx += x; sy += D[k]; sxx += x * x; sxy += x * D[k]; }
    const double m = (double) (n - k0), slope = (m * sxy - sx * sy) / (m * sxx - sx * sx), icpt = (sy - slope * sx) / m;
    double rms = 0.0, off = 0.0, worst = -300.0;
    int steps = 0, glitches = 0;
    for (size_t k = k0; k < n; ++k)
    {
        const double e = D[k] - (icpt + slope * k);
        rms += e * e;
        off = std::max (off, std::abs (e));
        worst = std::max (worst, R[k]);
        if (R[k] > -60.0) ++glitches;
        if (k > k0 && std::abs (D[k] - D[k - 1]) > 0.3 && steps++ < 20)
            std::printf ("  step %+.3f samples at %.2f s (residual %.1f dB)\n", D[k] - D[k - 1], k * W / (double) rate, R[k]);
    }
    std::vector<double> r (R.begin() + (long) k0, R.end());
    std::sort (r.begin(), r.end());
    std::printf ("after the first second: delay %+.3f ppm (%+.3f samples/s), off the line rms %.4f / max %.4f samples\n",
                 slope / W * 1e6, slope / W * rate, std::sqrt (rms / m), off);
    std::printf ("%d steps over 0.3 samples between 10 ms windows; residual median %.1f dB, worst %.1f dB, %d windows over -60 dB\n",
                 steps, r[r.size() / 2], worst, glitches);
    return 0;
}
Lenguaje dominante
C
Estrellas
13.3k
Forks
5.5k
Merge medio
1 d 4 h
PR fusionados (30 d)
21

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de raspberrypi/linux

Todos los issues de raspberrypi/linux

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.