Skip to main content

audio-anomaly

Find the knock, the new whine, the dropout or the clipping in a machine or environment recording without any labelled faults, and without flagging the healthy hum that makes up the rest of it.

Install

pip install audio-anomaly

Quickstart

import numpy as np, audio_anomaly

t = np.arange(4 * 16000) / 16000                                   # 4 s of machine hum at 16 kHz
hum = 0.3 * np.sin(2 * np.pi * 120 * t) + 0.01 * np.random.default_rng(0).standard_normal(t.size)
hum[32000:32480] += np.random.default_rng(1).standard_normal(480) * np.exp(-np.arange(480) / 60)  # a knock at 2 s
print(audio_anomaly.detect(hum, sample_rate=16000).summary())
audio-anomaly: 1 anomaly in the audio (4.00 s at 16000 Hz)
Normal means: the recording's own typical sound (no reference given)
Flagged 0.2% of the recording (1 burst); highest frame score 14.9, threshold 3.0.

Anomalies, in time order:
  1. burst at 2.000-2.010 s, score 14.9
     Broadband burst (an impact, knock or click) at 2.000 s lasting 0.010 s: 29.8 dB above the surrounding sound in 27 of 28 bands.

What the kinds mean:
  burst       a short broadband sound: an impact, knock, click or rattle

Notes:
  - at 16000 Hz the frequency bands reach up to 7.09 kHz; sound between that and the 8.00 kHz Nyquist limit is not monitored, so record at a higher sample rate to watch higher frequencies

Take the knock out and the same call answers no anomalies, with the highest frame score it saw (1.6) against the threshold (3.0), so you can see how much headroom a healthy recording had.

With a known-good recording of the same machine, every frame is judged against that instead of against the recording itself:

report = audio_anomaly.detect("pump_today.wav", reference="pump_healthy.wav")

What it does

Each recording is cut into half-overlapping frames (50 ms by default), and each frame into up to 32 log-spaced frequency bands from 30 Hz to 12 kHz. Every band level is the power in that band in dBFS, so a tone reads the same at any sample rate. The top band has to end below 95% of the Nyquist frequency, so at lower sample rates the bands stop sooner: with 50 ms frames they reach 3.67 kHz at 8 kHz, 7.09 kHz at 16 kHz and 9.23 kHz at 22.05 kHz, and full 12 kHz from 32 kHz up. Nothing above that is monitored, and the report says so in a note. Then it looks for:

  • burst - a short broadband sound: an impact, knock, click or rattle. Each frame is compared with the sound just before and just after it, so a knock stands out wherever it happens, and the first frames of a change that then persists are not mistaken for one. Burst edges are refined to the millisecond. Given a reference recording of a machine that makes impacts of its own (a press, a reciprocating pump), their typical strength and how much it varies are learned from it band by band (it needs at least three), and a burst is then reported only when it clearly exceeds them. A saved profile holds only the spectrum, so for such machines pass the reference recording itself.
  • tonal - narrowband energy that was not there before: a bearing whine, a squeal, a new hum. The band levels are smoothed over 0.6 s, which a knock cannot move, and compared with normal. A change has to hold at or above the threshold for 0.3 s, and then lasts while it stays above three quarters of it, so a band hovering at the threshold does not flicker in and out. A tone is a band standing out from the rest, so it is judged above any rise the whole spectrum shares: a gain change or a harder-working machine is a level change, never a tone. The report names the frequency, always inside the band it quotes.
  • level_rise - the sound got louder across much of the spectrum and stayed louder: a machine running rough. It must hold, like a tone.
  • level_drop - the overall level fell well below normal, again held with the same hysteresis.
  • dropout - the signal cut out: digital silence in the middle of the sound, or runs of exact zeros as short as 5 ms that no frame average would notice.
  • clipping - samples flattened against the full-scale rail, measured per channel before the mix to mono and against the format's own rail: +127/128 for 8-bit WAV, +32767/32768 for 16-bit, 1.0 for float. (A fixed 0.995 cut-off would never see 8-bit clipping at all, since 127/128 = 0.992.)

What "normal" is. Given a reference, it is the reference's typical spectrum, band by band. Without one, it is learned from the recording itself: the quieter three quarters of each band. That is why a whine that appears halfway through is still caught with no reference: the normal level of its band is set by the half without it. A change that covers more than about three quarters of a recording is, by then, its typical sound - give a reference for a machine that may already be faulty.

Why healthy machinery stays quiet. Every score is a robust z-score (median and MAD, never mean and standard deviation) and must be backed by at least 2 dB of real change per unit, so sensitivity=3.0 means "3 sigma and 6 dB". A perfectly steady hum barely wobbles, so its sigma is tiny; without the dB floor a 0.3 dB flutter would read as a ten-sigma event. Spreads are measured in ways the anomalies themselves cannot inflate, and widened by their own uncertainty when a recording is short. Across 700 synthetic healthy machines (8-44.1 kHz, 1-10 s, 50-900 Hz hum, three noise levels, slow amplitude wobble, pink noise, 25-100 ms frames), each judged on its own and against a second healthy recording of the same machine, the highest score without a reference was 2.4; against a reference, one run in 1,400 crossed the threshold (3.2, in the lowest band of a pink-noise recording). Against a reference, a whole-spectrum gain change of up to 5 dB raises nothing but a note, and a larger one is a single level change that the note explains.

The awkward cases. Pure silence reports no anomalies (and says it is silent) rather than flagging everything. A recording shorter than one frame returns an empty report. Leading and trailing silence is noted, not called a dropout. Stereo is mixed to mono. NaN samples are read as silence and counted. A reference of a different length or sample rate is fine: only its typical spectrum is used, mapped onto this recording's bands, and bands it cannot reach are judged against the recording itself - the notes say which. A recording 3 dB or more louder or quieter than its reference across the whole spectrum gets a note, since that is usually the microphone, its gain or its position rather than the machine. A WAV file that was cut short is analysed as far as it goes, with a warning giving the length its header promised; one whose RIFF size field is wrong is read chunk by chunk; a damaged one raises a ValueError naming the file. Your array is never modified, and the same input always gives the same report.

Only numpy is required: the FFT is numpy.fft, and WAV files are read with the standard library (wave, plus a small reader for the float files wave refuses).

API

detect(audio, *, sample_rate=None, reference=None, sensitivity=3.0, frame_ms=50) -> AudioAnomalyReport

argument meaning
audio a .wav path, a numpy array ((n,) or (n, channels)), or (samples, sample_rate)
sample_rate samples per second; required for a bare array
reference a known-good recording in any of those forms, or a profile from spectral_profile
sensitivity score a frame must reach to be anomalous; higher flags less. Try 4-5 for noisy environments
frame_ms analysis frame length in milliseconds; frames overlap by half

AudioAnomalyReport

member what it gives you
.anomalies list[Event] in time order
.scores numpy array, one score per frame; at or above sensitivity is anomalous
.frame_times the centre of each frame in seconds, aligned with .scores
.anomaly_ratio share of the recording's duration covered by anomalies, 0-1
.loudest(n=5) the n highest-scoring events, strongest first
.counts, .of_kind(kind), .has_anomalies, .max_score at a glance
.summary() the human report, plain ASCII punctuation, safe to pipe anywhere
.to_dict() JSON-safe dict of everything
.notes, .warnings what was assumed, converted or fallen back to
.mode, .compared_against "self" or "reference", and what normal meant, in words

Event(start_s, end_s, kind, score, message) is a frozen dataclass; message is one plain sentence, for example "New tone near 3.10 kHz from 1.98 s to the end: 24.1 dB above normal in the 2.81 kHz-3.21 kHz band."

Monitor(reference, *, sample_rate=None, sensitivity=3.0, frame_ms=50).check(audio) -> AudioAnomalyReport

The reference is analysed once; each check compares a new recording with it.

monitor = audio_anomaly.Monitor("pump_healthy.wav")
for path in todays_recordings:
    report = monitor.check(path)
    if report.has_anomalies:
        print(path, report.loudest(1)[0])

spectral_profile(audio, *, sample_rate=None, frame_ms=50) -> numpy.ndarray

The typical spectrum of a recording: an (n_bands, 3) array with columns frequency_hz, level_db (the average spectrum, dBFS) and spread_db (how much each band wanders). Save it and pass it back as reference later; a profile made with a different frame_ms is recognised and its own frame length is used, with a note.

np.save("pump.npy", audio_anomaly.spectral_profile("pump_healthy.wav"))
report = audio_anomaly.detect("pump_today.wav", reference=np.load("pump.npy"))

CLI

audio-anomaly pump.wav
audio-anomaly pump_today.wav --reference pump_healthy.wav
audio-anomaly pump_healthy.wav --save-profile pump.npy
audio-anomaly pump_today.wav --reference pump.npy --sensitivity 4
audio-anomaly pump.wav --json > findings.json
audio-anomaly pump.wav --output findings.json --fail-on-anomaly

--save-profile writes .npy, or .csv for any other extension, and --reference reads either back. --fail-on-anomaly exits with status 1 when anything was found, for scripts and scheduled checks. Any failure - an unreadable or damaged recording, an --output path that cannot be written - prints audio-anomaly: error: ... and exits with status 2, never 1, so a scheduled check cannot mistake a failure for a finding. Run audio-anomaly --help for the rest.

Limits

  • Level drops that recur at a stable period are read as beating, not independent faults. Two close frequencies (two motors just out of sync, say) produce a level that dips to near-zero on a completely regular cycle, and each dip looks identical to a real drop from a local, before/after comparison - even against a clean reference of the same machine, since that comparison is inherently local. Three or more drops at a near-constant interval and depth are reported once, as a note describing the period, rather than as repeated alarms.
  • This means a genuinely periodic mechanical fault - a loose part striking on every rotation, say - can look the same as beating and gets the same treatment: reported once as a note, not as repeated anomalies. Nothing in a level trace alone can tell physical beating apart from a real fault that happens to recur at a stable interval; that needs either a healthy reference recorded at the same speed and load, or knowledge of the machine's own RPM. The note is never suppressed, so a recurring pattern is always visible in the report - it is just not escalated to N separate alarms.

License

MIT

Metadata

Release files for audio-anomaly 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for audio-anomaly 0.1.0
File Size Uploaded
audio_anomaly-0.1.0.tar.gz 58.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for audio-anomaly 0.1.0
File Interpreter ABI Platform
audio_anomaly-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.5 kB

Release files / audio_anomaly-0.1.0.tar.gz

Download URL audio_anomaly-0.1.0.tar.gz
Size 58.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e7976603a19fbf97b10e77fb34b81468b8b5b2e2110b65d010bab151641e8a14
BLAKE2b-256 checksum
How to use checksums
f99be2318bdfdff50bc9f7b97cab07ecf699d1485afcb10d2a7a8da88230332e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / audio_anomaly-0.1.0-py3-none-any.whl

Download URL audio_anomaly-0.1.0-py3-none-any.whl
Size 49.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
56f4c5d79f17fffe4097a087ae3e70db95accfbcde3c7d17551109191a3c5fa2
BLAKE2b-256 checksum
How to use checksums
dc1090749546d462a9a4c6cb5bf55fd9f1ebd25b25be02f5a733dc6a5cfe40f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page