Skip to main content

voicequal

CI PyPI Python Docs License: MIT

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 --calibrate once 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 changes
  • get_current() — snapshot the latest LiveAssessment
  • reset() — 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)

Source distribution for voicequal 0.4.0
File Size Uploaded
voicequal-0.4.0.tar.gz 96.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for voicequal 0.4.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.1.1

2 release files

0.1.0

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