Standalone harness for verifying the OpenAL playback path used by the EARS
Linux build. Plays a WAV twice — once through paplay (reference), once
through OpenAL via OpenTK — so a bad file, a bad parser and a bad audio
backend can be told apart.
| File | Purpose |
|---|---|
altest.exe | the test program |
OpenTK.dll | managed OpenAL binding |
OpenTK.dll.config | dllmap: openal32.dll → libopenal.so.1 |
alsoft.conf | OpenAL Soft settings (backend, resampler, buffers) |
sound/*.wav | test assets |
src/altest.cs, src/WavFile.cs | sources, only needed to rebuild |
OpenTK.dll and OpenTK.dll.config must sit next to altest.exe. Mono
resolves assemblies relative to the executable, not the working directory.
A missing .config produces DllNotFoundException: openal32.dll on Linux,
which reads like nonsense until you know what it means.
sudo apt install mono-runtime libmono-system-drawing4.0-cil \
libopenal1 openal-info pulseaudio-utils sox
libmono-system-drawing4.0-cil is easy to miss. OpenTK's net45 build
references System.Drawing even though the audio path never touches it.
Add mono-mcs only if rebuilding on this machine.
cd audiotest export ALSOFT_CONF=$PWD/alsoft.conf soxi sound/SND200.wav # what the file actually is paplay sound/SND200.wav # reference playback mono ./altest.exe sound/SND200.wav mono ./altest.exe sound/SND200.wav 0.5 # optional gain, 0..1
ALSOFT_CONF is required. Without it OpenAL only reads ~/.alsoftrc, which
won't exist on a fresh machine, and the bundled settings are ignored silently.
Verbose mixer log:
ALSOFT_LOGLEVEL=3 mono ./altest.exe sound/SND200.wav 2>&1 | \ grep -E "backend|Post-start|underrun|Error|resampler|Created device"
| Symptom | Meaning |
|---|---|
| Clean in both | audio path is good |
Clean in paplay, bad in altest | backend or resampler config — check the log |
| Bad in both | source file, or the sound device itself |
| Bad at gain 1.0, clean at 0.5 | clipping; cap gain at 0.85 in AudioEngine.SetGain |
DllNotFoundException | OpenTK.dll.config missing next to the exe |
Key log lines:
Initialized backend "pulse" — which backend wonPost-start: Stereo, Float32, 44100hz, 882 / 2646 buffer — negotiated rate
and buffer. Small buffers on a slow transport cause periodic ticks.Created device ... "RDP Sink" — under WSL this is the WSLg bridge, not
real hardware. Jitter here is usually the transport and disappears on the
target device. Don't tune against it.Assets are 11025 Hz mono. Against a 44100 or 48000 Hz device everything is
resampled; bsinc24 in alsoft.conf handles this well. If a future device
negotiates a rate that isn't an integer multiple, pre-converting the assets
removes the question entirely:
sox in.wav -r 48000 out.wav
Common on the tablet, where HDMI wins the default.
pactl list short sinks # what exists right now wpctl status # default is marked with *
Set the default by name, not by node ID:
pactl set-default-sink alsa_output.platform-cht-bsw-rt5645.HiFi__Speaker__sink
Node IDs are not stable. UCM destroys and recreates the built-in sink every time the headphone jack is plugged or unplugged, and the new node gets a new ID. An ID captured a minute ago is probably already dead — this looks like a sink "disappearing" but is normal behaviour. HDMI keeps a stable ID precisely because nothing switches it, which is why it tends to win by default.
The durable fix for a kiosk is to remove HDMI from the running: pavucontrol
→ Configuration → HDMI card → Off. This persists across reboots
(WirePlumber stores card profiles in ~/.local/state/wireplumber/). With
HDMI gone, whichever built-in sink exists is the only candidate and jack
switching just works.
Note: under PipeWire, /etc/pulse/default.pa and ~/.config/pulse/default.pa
are ignored. An old config there will look like it should work while doing
nothing. The live state is ~/.local/state/wireplumber/default-nodes.
If a sink seems missing, restart the stack before assuming hardware failure:
systemctl --user restart wireplumber pipewire pipewire-pulse
mcs src/altest.cs src/WavFile.cs -r:OpenTK.dll -out:altest.exe
Keep altest.cs out of the main program's *.cs glob — two Main methods
in one build is a duplicate-entry-point error.