- audio-health-nano
- Prior art β this task is established, and this is a small variant of it
- Measured β ESC-50, split by the dataset's own folds
- The cross-corpus test, and what it exposes
- Which heads to use
- Scope
- Known failure modes
- Usage
- Deployment note: cap the ONNX Runtime thread pool
- Training
- Verification
- Provenance
- Prior art β this task is established, and this is a small variant of it
audio-health-nano
47,094 parameters. 188 KB. Six audio faults, one forward pass, multi-label.
clipped Β· dropout Β· dc_offset Β· hum Β· bandwidth_loss Β· hiss
Prior art β this task is established, and this is a small variant of it
Audio defect detection is a solved problem at full model scale. The direct reference point is Higham et al., A No-Reference Model for Detecting Audio Artifacts using Pretrained Audio Neural Networks, WACV workshops 2022 (Amazon) β 1-second segments classified as No Defect / Hum / Hiss / Distortion / Clicks at 0.986 balanced accuracy, using a modified PANN-CNN6 (~4.8M parameters) on log-mel spectrograms. Broadcast QC tools have detected hum, clipping and dropouts for decades, some of it patented.
Their 0.986 is the reference for what a full-size, pretrained model does. This does not match it and does not claim to. What differs:
| Higham et al. 2022 | this model | |
|---|---|---|
| parameters | ~4.8M (PANN-CNN6) | 47K, ~100Γ smaller |
| pretraining | AudioSet-pretrained | none |
| output | single-label, 5 classes | multi-label, 6 faults β real recordings carry several at once |
| faults | hum, hiss, distortion, clicks | adds bandwidth loss and DC offset |
| reporting | balanced accuracy | per-fault margin over a single-threshold baseline, plus a cross-corpus transfer test |
Use theirs if you want accuracy. Use this if you need something that runs in 188 KB on a CPU thread, and you want to see which heads actually earn a model.
Measured β ESC-50, split by the dataset's own folds
| fault | accuracy | majority | lift | best scalar | margin | recall |
|---|---|---|---|---|---|---|
dc_offset |
0.905 | 0.711 | +0.195 | 0.718 | +0.187 | 0.925 |
hum |
0.877 | 0.689 | +0.189 | 0.686 | +0.191 | 0.755 |
hiss |
0.901 | 0.697 | +0.204 | 0.886 | +0.015 | 0.984 |
bandwidth_loss |
0.905 | 0.714 | +0.192 | 0.882 | +0.023 | 0.684 |
clipped |
0.831 | 0.687 | +0.144 | 0.736 | +0.095 | 0.838 |
dropout |
0.793 | 0.695 | +0.098 | 0.750 | +0.043 | 0.719 |
hiss and bandwidth_loss barely beat a single threshold. Both are broad spectral-shape
changes that one statistic captures well. The model is convenient for sharing a forward pass with
the others, not necessary for those two.
The cross-corpus test, and what it exposes
Trained on ESC-50, evaluated on 20 s of real office ambience from a webcam microphone β a genuinely different source. The scalar baseline here is a threshold fitted on ESC-50 and transferred, which is what a deployed threshold actually is.
| fault | ESC-50 | rig microphone | transferred scalar | margin |
|---|---|---|---|---|
dropout |
0.824 | 0.899 | 0.763 | +0.136 |
clipped |
0.812 | 0.875 | 0.848 | +0.028 |
bandwidth_loss |
0.906 | 0.914 | 0.912 | +0.002 |
hiss |
0.908 | 0.904 | 0.894 | +0.010 |
dc_offset |
0.912 | 0.799 | 0.696 | +0.104 |
hum |
0.837 | 0.433 | 0.693 | β0.260 |
hum scoring below chance is a labelling artefact, not a model failure
The office recording already contains mains hum. Measured against its own broadband noise floor:
| frequency | peak above floor |
|---|---|
| 50 Hz | +28.8 dB |
| 60 Hz | +27.7 dB |
| 100 Hz | +33.2 dB |
| 120 Hz | +27.0 dB |
| 150 Hz | +30.9 dB |
| 180 Hz | +30.6 dB |
Every "no hum" example in that test is a recording with 30 dB of hum in it. The head is detecting hum that is genuinely present, against a label saying it is absent. In any environment with mains equipment, the negative class for hum does not exist, and a cross-corpus number measured there is meaningless.
This generalises: an operation the acquisition environment already performs cannot be detected as applied-versus-not. The same effect kills sharpening and denoising detectors on webcam frames, because the camera's own pipeline already sharpens and denoises.
Which heads to use
dropoutβ the only head passing a strict cross-corpus gate. Use it.dc_offset,clippedβ strong in-corpus, positive but modest across corpora. Use with a validation set from your own material.humβ use on media files where a clean negative exists. Do not evaluate it on live room audio from a mains-powered environment.hiss,bandwidth_lossβ a threshold does nearly as well. Use them because they come free with the forward pass, not because they need a network.
Scope
For: media QC triage, archive ingest checks, flagging recordings for human review, and monitoring capture chains on hardware too small for a pretrained audio network.
Not for:
- Not speech quality, intelligibility, or MOS estimation.
- Not a judgement about content or speakers. It sees a 64Γ64 spectrogram.
- Not a replacement for a full-size detector where accuracy matters more than size β see the prior art above.
- Not calibrated for music mastering or any perceptual quality decision.
Known failure modes
- Faults are synthesised onto real recordings. Real clipping involves the whole analogue chain; real dropouts have codec-specific concealment.
- 1 second at 16 kHz. Content above 8 kHz is invisible; faults shorter than a few ms may be missed.
dropoutrecall is 0.719 in-corpus β it misses about a quarter of cases while being the most transferable head.- Environment contamination, as above.
- No pretraining, so no AudioSet-style semantic prior. It reads spectrogram structure only.
Usage
import numpy as np, cv2, onnxruntime as ort
from scipy import signal as sg
SR = 16000
FAULTS = ["clipped", "dropout", "dc_offset", "hum", "bandwidth_loss", "hiss"]
def spec(x): # x: 1 s of mono float audio at 16 kHz
f, t, S = sg.stft(x, SR, nperseg=256, noverlap=192)
P = np.log10(np.abs(S) + 1e-8)
P = np.clip((P + 8) / 8.0, -1, 3) # ABSOLUTE level preserved -- see note below
return cv2.resize(P.astype(np.float32), (64, 64), interpolation=cv2.INTER_AREA)
so = ort.SessionOptions()
so.intra_op_num_threads = 1
so.add_session_config_entry("session.intra_op.allow_spinning", "0")
sess = ort.InferenceSession("audio_health.onnx", sess_options=so,
providers=["CPUExecutionProvider"])
probs = 1 / (1 + np.exp(-sess.run(None, {"spectrogram": spec(x)[None, None]})[0][0]))
print({f: round(float(p), 3) for f, p in zip(FAULTS, probs)})
Do not per-window standardise the spectrogram. Sibling models in this family do, and it deletes
absolute level β which is exactly what clipping is. With standardisation the clipped head sits at
0.503, chance; keeping absolute level takes it to 0.828. The scaling above is deliberate.
Deployment note: cap the ONNX Runtime thread pool
ONNX Runtime sizes its intra-op pool to the core count and those workers spin-wait between
inferences, burning about 1.9 cores to serve 0.28 ms inferences. With intra_op_num_threads=1 and
allow_spinning=0, measured idle CPU dropped from 192% to 16.5% of one core, throughput unchanged.
Training
7,000 train windows from ESC-50 folds 1β3, 2,400 test from folds 4β5 Β· faults sampled independently at p=0.30 so mixtures occur naturally Β· log-STFT (nperseg 256, noverlap 192), absolute level scaled to a fixed range, resized to 64Γ64 Β· 4 conv layers (16β32β48β64) Β· BCE with positive weighting capped at 10Γ Β· Adam 3e-3, 20 epochs.
Same architecture as resoajoe/alarm-nano, resoajoe/camera-health-nano and
resoajoe/blockgrid-nano, unchanged.
Verification
ONNX vs PyTorch, both CPU, 256 inputs: max relative logit difference 2.8e-07, 100% threshold agreement.
Provenance
ESC-50 (public, CC BY-NC) plus 20 s of the author's own office ambience. No speech, no identifiable people, no personal data.