# Audio Test Bundle

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.

## Contents

| 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.

## Install

```bash
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.

## Run

```bash
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:

```bash
ALSOFT_LOGLEVEL=3 mono ./altest.exe sound/SND200.wav 2>&1 | \
  grep -E "backend|Post-start|underrun|Error|resampler|Created device"
```

## Reading the result

| 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 won
- `Post-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:

```bash
sox in.wav -r 48000 out.wav
```

## Sound comes out the wrong device

Common on the tablet, where HDMI wins the default.

```bash
pactl list short sinks     # what exists right now
wpctl status               # default is marked with *
```

Set the default **by name**, not by node ID:

```bash
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:

```bash
systemctl --user restart wireplumber pipewire pipewire-pulse
```

## Rebuilding

```bash
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.