voicequal
Real-time audio quality assessment for voice apps.
Answers the question every voice app eventually has to answer: "is this recording clean enough to process?"
voicequal analyzes audio with a handful of cheap acoustic metrics and
returns a tier — excellent, good, fair, or poor — plus the
numbers behind the decision, a per-second breakdown of where it went
wrong, one line of advice, and DNSMOS-style P.835 scores from a
distilled model that ships in the wheel. Pure numpy/scipy, CPU-only,
nothing to download.
$ voicequal listen
[15:26:37] EXCELLENT room= 45.5 dBA HNR=18.4dB
[15:26:53] CHANGE GOOD room= 55.0 dBA
[15:26:59] CHANGE FAIR room= 60.4 dBA
[15:27:09] CHANGE POOR room= 71.4 dBA
[15:27:53] CHANGE EXCELLENT room= 45.5 dBA
Install
pip install voicequal # core library
pip install 'voicequal[mic]' # + live-mic support
pip install 'voicequal[neural]' # + DNSMOS reference backend (onnxruntime)
Quick start
Analyze a file
from voicequal import assess
result = assess("recording.wav")
print(result.quality) # "good"
print(result.background_db) # 52.3
print(result.snr) # 24.1
print(result.reason) # "25<snr<=35 with moderate room: good"
Real-time streaming
from voicequal import LiveDetector
detector = LiveDetector()
detector.on_change(lambda result: print(f"→ {result.quality}"))
while streaming:
chunk = get_audio_chunk() # any float32 numpy array
detector.push(chunk)
Reference neural scores (optional)
from voicequal.neural import DNSMOS
scores = DNSMOS().score_file("recording.wav") # Microsoft DNSMOS, P.835
print(scores.sig, scores.bak, scores.ovrl) # 1..5 each
Command line
voicequal assess my_recording.wav # one-shot file report
voicequal listen # live mic streaming
voicequal listen --calibrate # first-time mic calibration
voicequal listen --sensitive # stricter thresholds
First listen run auto-calibrates the mic (10 seconds — sit quiet
then make noise). Calibration is saved to ~/.voicequal/.
P.835 scores, advice, and per-second breakdown
from voicequal import assess, advise
from voicequal.models import predict_mos
from voicequal.pipeline import worst_segments
r = assess("recording.wav")
print(advise(r).headline) # "Loud room is drowning the voice"
print(advise(r).actions) # ("Move away from the noise source", ...)
for seg in worst_segments(r, n=2): # which seconds hurt most
print(f"{seg.start_s:.0f}-{seg.end_s:.0f}s {seg.snr_estimate:.1f} dB {seg.quality}")
mos = predict_mos(samples, 16000) # DNSMOS-style SIG/BAK/OVRL, pure numpy
As an MCP tool
pip install 'voicequal[mcp]'
voicequal-mcp # stdio server exposing assess_file / assess_base64_wav
See the docs for the Claude Desktop and Claude Code config snippet.
How it works
voicequal estimates the mixing SNR (how much louder the voice is than the noise) without a clean reference, then reads a tier off a short ladder. Two cheap estimators cover the two kinds of voice audio, and the pipeline takes the larger:
- HNR, the harmonic-to-noise ratio from the normalised autocorrelation peak in the pitch range (Boersma, 1993). Right for sustained singing; under-reads on speech, whose consonants and pauses are unvoiced.
- Energy SNR, loud 25 ms blocks versus the quietest 30% of blocks. Right for speech, which has pauses; collapses on sustained singing, which has none.
Each fails low, so max(HNR, energy SNR) picks the one whose assumption
held. The spectral peak-vs-floor SNR that v0.1.x used is off by nearly
30 dB on sung vowels in noise and is now informational only.
room < 60 dBA → excellent (quiet room, nothing to fix)
snr_estimate ≥ 18.5 dB → excellent (voice dominates noise)
13.5–18.5 dB → good
10–13.5 dB → fair
< 10 dB → poor (noise dominates)
The metrics reported on every result:
| Metric | What it captures | Role |
|---|---|---|
| SNR estimate | max(HNR, energy SNR), the mixing-SNR estimate in dB | Drives tier |
| HNR | Periodic (voice) energy vs aperiodic (noise) energy, in dB | Feeds estimate |
| Energy SNR | Loud 25 ms blocks vs quiet blocks, in dB | Feeds estimate |
| Background dBA | Room loudness, median of recent RMS | Quiet-room gate |
| SNR (spectral) | How much louder the peak bin is than the noise floor | Informational |
| Spectral flatness | How "noise-like" (chaotic) vs "tonal" (structured) it is | Informational |
| Spectral concentration | Share of energy in the top-3 FFT bins | Informational |
| Temporal variance | Is noise sustained (fan) or transient (a passing car) | Informational |
| Clipping ratio | Fraction of samples at or above full scale | Informational |
Older decision paths remain available through assess_quality(): pass
only hnr for the v0.2.0 HNR ladder, or neither hnr nor
snr_estimate for the v0.1.x spectral-SNR logic.
Streaming is stabilized with a hysteresis buffer — a new tier has to persist for 3 frames before it's announced, so single-frame blips don't cause flicker. Room loudness is the median of the last ~3 s of RMS values (tracks sustained noise, ignores single-frame silences), and both SNR estimators are aggregated over the same window.
Benchmark
Two public test sets, one for each kind of voice audio. Both regenerate or download from a single script each; every number below comes from those scripts.
| Set | What it is | Clips | Ground truth |
|---|---|---|---|
| VoiceBank-DEMAND (speech) | Read English sentences + real environment noise at 17.5 / 12.5 / 7.5 / 2.5 dB | 824 | Mixing SNR computed from each clean/noisy pair |
| VocalSet + MUSAN (singing) | Sung scales (many techniques) + environment noise at 20 / 10 / 5 dB, plus clean and noise-only clips | 200 | Manifest SNR from the mix |
python benchmarks/speech/run_voicebank_demand.py # needs voicequal[neural] + pyarrow
python benchmarks/run_benchmark.py
Speech: VoiceBank-DEMAND, 824 clips
Tiers are scored against the four nominal SNR levels (17.5 dB → excellent … 2.5 dB → poor). That mapping is a convention, not human ratings, so read the exact figure as "did it land in the right SNR band".
| v0.2.0 (HNR only) | v0.3.0 | |
|---|---|---|
| Exact band | 27.9% | 80.0% |
| Within one band | 63.3% | 99.2% |
| Tier rank vs true SNR (Spearman) | +0.12 | +0.90 |
How each estimator tracks the true mixing SNR on the same clips (Spearman rank correlation):
| Estimator | Correlation | Notes |
|---|---|---|
voicequal snr_estimate |
+0.90 | max(HNR, energy SNR); reads ~6 dB high, the ladder absorbs it |
| voicequal energy SNR | +0.91 | the component doing the work on speech |
| DNSMOS OVRL | +0.57 | neural P.835 predictor, speech-trained |
| DNSMOS BAK | +0.52 | |
| voicequal HNR | +0.19 | under-reads: consonants and pauses are unvoiced |
| voicequal spectral SNR (v0.1.x) | −0.16 |
DNSMOS predicts human opinion, not SNR, so a lower correlation with SNR is not a defect. voicequal's estimate agrees with DNSMOS OVRL at +0.60.
| Path | Median latency, 2.3 s clip | Runtime |
|---|---|---|
| voicequal DSP | 15 ms | numpy + scipy |
| DNSMOS | 880 ms | onnxruntime, 1.1 MB model |
Distilled P.835 model vs DNSMOS
A 13k-parameter MLP on voicequal's features, trained to reproduce DNSMOS on VoiceBank-DEMAND train speakers and tested on the two unseen test speakers (1,648 clips). Ships in the wheel as 51 KB of numpy weights. Details and caveats in the docs.
| Target | Pearson vs DNSMOS | MAE (MOS) | Linear-from-SNR baseline |
|---|---|---|---|
| BAK | 0.93 | 0.19 | 0.76 |
| OVRL | 0.87 | 0.18 | 0.68 |
| SIG | 0.65 | 0.19 | 0.36 |
Inference after feature extraction: ~70 µs, versus ~180 ms for DNSMOS.
Singing: VocalSet + MUSAN, 200 clips
| Metric | v0.1.1 | v0.2.0 | v0.3.0 |
|---|---|---|---|
| Exact tier accuracy | 46.0% | 55.5% | 58.0% |
| Off-by-one accuracy | 82.0% | 89.5% | 86.0% |
| Spearman correlation | +0.496 | +0.662 | +0.645 |
| Category | Expected | v0.1.1 | v0.2.0 | v0.3.0 |
|---|---|---|---|---|
quiet_noise |
excellent | 97.5% | 100% | 100% |
clean_vocal |
excellent | 50.0% | 55.0% | 30.0% |
moderate_snr (20 dB) |
good | 50.0% | 32.5% | 45.0% |
loud_snr (10 dB) |
fair | 27.5% | 57.5% | 55.0% |
very_loud_snr (5 dB) |
poor | 5.0% | 32.5% | 60.0% |
Estimators against the manifest mixing SNR (120 mixed clips):
| Estimator | MAE | Spearman |
|---|---|---|
snr_estimate |
4.2 dB | +0.50 |
| HNR | 4.4 dB | +0.48 |
| Energy SNR | 6.0 dB | +0.19 |
| Spectral SNR (v0.1.x) | 28.1 dB | +0.22 |
Read this honestly
- Speech is now the strong case. Read speech has pauses, the energy SNR uses them, and the ladder lands in the right SNR band 80% of the time with almost no gross errors.
- Singing is harder and the numbers are modest. HNR is the only
estimator that works there, and it compresses at low SNR. The 5 dB
category climbed from 5% to 60% across two releases, but clean vocals
fell to 30%: VocalSet's breathy, lip-trill and vibrato techniques are
aperiodic by nature and now read
good. That is a real limitation of a periodicity-based estimator, not a threshold to tune away. - The ladder was fitted on these two sets. A single set of thresholds serves both; the fit favoured speech accuracy and singing off-by-one over singing exact.
- DNSMOS is blind to singing. It scores clean sung vowels around 1.1 out of 5, the same as white noise, so the neural comparison is speech-only. voicequal is, as far as we know, the only lightweight tool in this space that handles both.
- No human opinion scores yet. Both sets give SNR ground truth, not MOS. Correlation with listener ratings is the next thing to measure.
Limits — read this before using in production
- voicequal is calibrated for voice / recording quality. Whether a recording is clean enough to process, not whether it sounds subjectively pleasing to a human.
- Not a certified acoustic dB meter. Background dBA is a calibrated proxy using rough dB conversion, not the IEC 61672 A-weighted filter a real SPL meter uses.
- Different mics deliver different signal levels. Run
voicequal listen --calibrateonce per new machine or mic setup. - Assumes reasonable audio input. No echo cancellation or noise suppression built in. If your OS pre-processes mic audio (macOS Voice Isolation, browser noise suppression), your calibration will account for it — but detection accuracy will vary.
API reference
assess(path, target_sample_rate=16000, threshold_offset_db=0.0)
Analyze an audio file. Returns a FileAssessment with:
quality, reason, snr_estimate, hnr, energy_snr, background_db, snr,
spectral_flatness, spectral_concentration, clipping_ratio, temporal_variance, primary_score,
secondary_score, total_score, duration_seconds, sample_rate,
num_frames.
LiveDetector(sample_rate=16000, stability_frames=3, threshold_offset_db=0.0, db_offset=94.0)
Streaming detector. Methods:
push(samples)— append audio (any length, float32 numpy array)on_change(callback)— fire when the stable tier changesget_current()— snapshot the latestLiveAssessmentreset()— clear buffers and history
CLI
voicequal --version
voicequal assess <path>
voicequal listen [--calibrate] [--reset-calibration]
[--sensitive] [--stability-frames N]
[--heartbeat SECONDS]
Roadmap
See ROADMAP.md. Short version: fix the mixing-SNR blind spot (v0.2.0), emit ITU-T P.835-style SIG/BAK/OVRL scores and benchmark against DNSMOS (v0.3.0), then distil a ~1 MB ONNX quality model and add noise-type classification (v0.4.0).
Development
git clone https://github.com/jiya-singhal/voicequal
cd voicequal
poetry install --extras mic
poetry run pytest -v
poetry run ruff check . && poetry run ruff format --check .
poetry run mypy
Contributions welcome. Read CONTRIBUTING.md first, especially the rule about benchmark numbers for any change to the tier logic.
190+ tests, all under tests/. CI runs them on Python 3.10, 3.11, and 3.12. The library has no runtime dependencies
beyond numpy, scipy, soundfile, and rich (CLI). sounddevice is
optional (for live-mic support).
License
MIT. See LICENSE.
Metadata
Release files for voicequal 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| voicequal-0.4.0.tar.gz | 96.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| voicequal-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 192.6 kB
Release files / voicequal-0.4.0.tar.gz
| Download URL | voicequal-0.4.0.tar.gz |
|---|---|
| Size | 96.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0f4f62cc846f3c8a4482dc9d4665bf49159b854558edf33f3187a6cb788ed4f4
|
|
BLAKE2b-256 checksum How to use checksums |
6b508cd15f625a5af0383d53bef985f99610d66d2878eb96166c4bd394a73e2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.4.1 CPython/3.14.6 Darwin/25.6.0
|
Release files / voicequal-0.4.0-py3-none-any.whl
| Download URL | voicequal-0.4.0-py3-none-any.whl |
|---|---|
| Size | 96.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6fd25c646db88fe928260cea13f5bce7693ae0f4f36634d69c691682ac0bfd10
|
|
BLAKE2b-256 checksum How to use checksums |
463793d8628615cf574092d4547ab104cbaba366b5ef5c86407d4732ccc5d125
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.4.1 CPython/3.14.6 Darwin/25.6.0
|