Repository navigation
Description
Summary
After any client of the virtual sink has played and stopped, JamesDSP pins one
CPU core at 100% and audio breaks up until the application is restarted. A
freshly started instance is fine, so it looks like the problem "appears over
time" - in fact it starts with the first digital silence that reaches the DSP.
Root cause is denormal float arithmetic in the effect chain; PR #304 fixes it.
Environment
- JamesDSP
2.7.0-81-g3ea7d73(Pipewire flavour, built from master) - PipeWire 1.6.11 / WirePlumber 0.5.19, 48 kHz, 256-frame quantum
- Qt 6.11.3, Gentoo Linux, x86_64 (Haswell Xeon)
- Default preset: bass +6 dB, tube preamp 130, compander, stereo wide 46,
master limiter on; output device USB (C-Media) withAudioOutputUseDefault=false jamesdsp_sinkis the default sink,cavaalso taps its monitor
Steps to reproduce
- Start
jamesdsp --trayand give it a clean start (no streams yet). - Play anything through the default sink (
jamesdsp_sink), e.g.pw-play file.wav. - Let playback finish and stop (~2 s later is enough).
- Watch
pw-topand the threads of the process:
R 34 256 48000 5.3ms 14.2us 1.00 0.00 0 S16LE 2 48000 alsa_output...C-Media...
R 96 0 0 6.2us 4.9us 0.00 0.00 0 F32P 2 48000 + jamesdsp_sink
R 107 0 0 4.8ms +++ 0.91 +++ 2249 + jdsp_@PwJamesDspPlugin_JamesDsp
The filter node is permanently over budget (BUSY +++) and its ERR counter
grows by roughly 100 per second, i.e. every graph cycle is an xrun and the
node is immediately rescheduled. top -H -p $(pgrep -x jamesdsp) shows the
data-loop thread at 100%:
PID USER PR NI %CPU TIME+ COMMAND
5322 jam -84 0 99.6 1:48.52 data-loop
Expected
The filter keeps running in real time; idle CPU stays at a few percent.
Actual
One core is burnt indefinitely, audio is distorted / dropping out, and the only
way out is restarting JamesDSP (the next stream that stops re-triggers it).
Measurements
Probe on pw_filter_get_dsp_buffer (block size is always 256 frames; the graph
is driven at 187.5 Hz, so a healthy filter should see ~188 calls/s):
| state | block calls/s | CPU of data-loop |
|---|---|---|
| right after start, input never carried a signal | ~750 | 10% |
| after a stream played and stopped | ~600 | 100% |
| after a stream played and stopped, with a continuous -80 dBFS keep-alive stream | ~600 | 7-8% |
The third row is the key: as long as the input never becomes pure digital
silence, the CPU stays low. A fresh, exactly-zero DSP state is fast too - only
the state that has decayed from a real signal is slow.
Root cause
The DSP state (bass / tube / compander / limiter / ...) decays into the
denormal float range once the input is all zeros. Denormal arithmetic is about
an order of magnitude slower on x86, so processing 256 frames no longer fits
into the 5.3 ms quantum: every cycle ends in an xrun and the graph reschedules
the node immediately, which is what eats the core.
JamesDSP never enables flush-to-zero / denormals-are-zero, unlike most audio
DSPs. (Curiously the vendored EEL2 interpreter, used only by liveprog, does
carry its own denormal handling in nseel-compiler.c - the native effect chain
has nothing equivalent.)
Ruled out (all tested, no effect):
node.passive/node.always-processon the sink and the filterpactl suspend-sink jamesdsp_sink- graph wiring itself (links are exactly as intended, buffers are always valid,
pw_filter_get_dsp_buffernever returns NULL) - it is not a PipeWire bug: the hot frames are inside the JamesDSP binary (not
in the graph/scheduler code), and anLD_PRELOADshim that only setsMXCSR
fixes it without touching PipeWire at all
Workarounds for other users
Either restart JamesDSP after the audio stops, or run it with FTZ/DAZ enabled:
/* jdsp-ftz.c -> gcc -shared -fPIC -o jdsp-ftz.so jdsp-ftz.c -ldl */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <pthread.h>
#include <stdlib.h>
#include <xmmintrin.h>
static void enable_ftz(void) { _mm_setcsr(_mm_getcsr() | 0x8040u); }
__attribute__((constructor)) static void init(void) { enable_ftz(); }
struct tramp { void *(*fn)(void *); void *arg; };
static void *run(void *p)
{
struct tramp t = *(struct tramp *)p;
free(p);
enable_ftz();
return t.fn(t.arg);
}
int pthread_create(pthread_t *th, const pthread_attr_t *a, void *(*fn)(void *), void *arg)
{
static int (*real)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
if (!real)
real = (void *)dlsym(RTLD_NEXT, "pthread_create");
struct tramp *t = malloc(sizeof(*t));
if (!t) return real(th, a, fn, arg);
t->fn = fn; t->arg = arg;
int r = real(th, a, run, t);
if (r) free(t);
return r;
}LD_PRELOAD=/path/to/jdsp-ftz.so jamesdsp --trayWith that, four play/stop cycles in a row keep the thread at 7-8% and the error
counter at 0.
Fix
PR #304: enable MXCSR FTZ|DAZ once per audio processing thread, in
PwJamesDspPlugin::process() (PipeWire flavour) and in
gst_jamesdsp_transform_ip() (the GStreamer element used by the PulseAudio
flavour, which has the same latent problem).
Activity