Pi 5: USB audio playback silently drops one packet every ~5 s with a 128-frame buffer (UA Volt 2 Max, UAC2 explicit feedback)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 35/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Ambito
- audio-video-rtc, embedded-iot, operating-systems
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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
readiand onewriteiof 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(Debian1: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(Genesys05e3:0610inside 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_paramshalves 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=0still gave 496 xruns in 10 s. For 32 x 4 and 64 x 2 under the loopback, see the table above. ksoftirqdthreads 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;
}
- Lingua principale
- C
- Stelle
- 13.3k
- Fork
- 5.5k
- Merge medio
- 1g 4h
- PR unite (30g)
- 21
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di raspberrypi/linux
-
drm/vc4: hdmi: vc4_hdmi_hpd_irq_thread() checks !force_hotplug globally instead of per-connector bitmaskForse già presa @popcornmix l’ha presa 12 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
raspberrypi/linux#7724 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 63/100
raspberrypi/linux#7718 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
raspberrypi/linux#7415 · 2 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
rp1-cfe doesn't forward V4L2_EVENT_SOURCE_CHANGE event from csi-2 sensor driver to userspace appAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
raspberrypi/linux#7399 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
raspberrypi/linux#7357 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di raspberrypi/linux
Issue simili
-
good first issue help wanted program
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
hffgzktbcc-blip/ounce-core#297 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
coolsnowwolf/lede#14283 · 1 commento ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
OpenVisualCloud/SVT-JPEG-XS#130 ·
I maintainer di solito rispondono entro 1 giorno