Audio analysis · Personal project

Record Checker

Hear the problem. Explore the evidence.

Mix feedback arrives as "the low end is muddy" with no way to find the moment it refers to. This ties every suggestion to a measurement and the seconds behind it.

Local prototype

Try the demo Explore the architecture Project details

Interactive sample

Glass Harbour

64 seconds · 120 BPM · A minor · Alternative pop

0:00 / 1:04

Interactive simulation using prepared findings; the Python engine does not run on this page.

Project details

My role

I built both halves: the Python analysis engine — detectors, ranking and report format, on NumPy, SciPy and SoundFile, with loudness following the ITU BS.1770-4 spec — and the browser workspace over it, in Next.js, React, TypeScript and WaveSurfer.js over the Web Audio API. No collaborators.

A personal project, built in my own time over 21 commits in October 2026. Claude is an optional layer that rewrites the report's prose and never produces a finding — the engine's output is the same with it switched off.

Project status

Local prototype. The portfolio demo uses prepared findings; evaluation against a labeled music collection is the next development step.

Validation and scope

  • No instrument names from a stereo mix, and no prescribed EQ moves like "−3.7 dB at 287 Hz".
  • No target curve and no quality score. It will not tell you −14 LUFS or a flat spectrum is required.
  • A choice you marked deliberate is pushed down, not deleted.
  • No guessing which section is the chorus, or claiming a key for a drum loop.
  • A Claude rewrite the evidence does not support is discarded for the deterministic report and a line saying why.

Built with

  • Python
  • NumPy
  • SciPy
  • Next.js
  • React
  • TypeScript
  • Web Audio API
  • Claude (optional layer)
Where the project stands

A personal project: 21 commits between 10 and 16 October 2026, running on a local machine with no accounts, no payments and no deployment.

The evaluation rubric is written and its gates are set, but it has not been run against a labelled song collection — so every quality figure the rubric defines is a target, not a result. That is the next piece of work, and until it is done the page reports no quality number at all.

How the demo audio and the prepared findings were made

"Glass Harbour" has no audio file behind it. Your browser builds all 64 seconds from an oscillator graph when the demo loads — 120 BPM, A minor, 32 bars — so nothing is downloaded and every render comes out identical. Noise comes from a seeded generator, which is what makes that true.

The three findings on this page were not detected. The problems were composed into the track on purpose and the write-ups are mine, so the demo shows what the output looks like and what it is for, not the engine running. The engine is Python and does not run in a browser.

Inside each finding, values tagged "setting" are the exact synthesis parameters that built the flaw. Values tagged "measured" are real measurements of this render, taken by the project's own deterministic check suite. On a real file, producing those measurements and the findings over them is precisely what the engine does instead.

What I'd do next
  • Run the labelled evaluation against real music. The rubric and gates are written, the song collection is not — so every quality number today is a target, not a result.
  • Exercise the Claude layer against the live API. Tested only against a fake client, the verifier has never rejected a real model response.
  • Validate section boundaries. Segmentation uses level and band-balance novelty alone, which is why sections stay A, B and C rather than claiming a chorus.
  • Reference-track comparison, which the tool currently declares and refuses rather than approximating.

From measurement to something you can hear

  1. Locate

    Feedback points to a specific passage.

  2. Inspect

    Measurements remain available behind each finding.

  3. Listen

    Play the passage and evaluate the suggestion.

Inside the analysis engine

Select a layer to follow audio into measurements, findings and a report.

Choose a layer of Record Checker to read about

Each layer may only call the one below it.

Off to the side: the core never imports these.

transports

cli · http api · mcp adapter

In
A request from outside — a command line invocation, an HTTP call, or an MCP tool call.
Does
Translates whichever of the three it is into one call on the service below.
Out
A service request, in the shared contract types.
Why it is its own layer
Three ways in, one code path. A transport can be added without the analysis knowing it exists.

service

analysis service · storage · tenancy · cache

In
A service request.
Does
Runs the analysis, stores the result, keeps tenants apart, and reuses a cached result when the same audio has been seen before.
Out
A stored, retrievable analysis.
Why it is its own layer
Everything about who asked and where the result is kept lives here, so nothing below this line knows about users or storage at all.

reporting

deterministic report · charts

In
Ranked findings and the measurements behind them.
Does
Assembles the report and its charts, the same way every time.
Out
The deterministic report — the output you get with no model involved.
Why it is its own layer
The report is finished before the optional language layer is ever offered it. If that layer is absent or wrong, this is still the answer.

ranking

rank · dedupe

In
Candidate findings from the detectors.
Does
Scores them, removes duplicates and near-duplicates, and cuts the list to at most three.
Out
At most three findings, in priority order.
Why it is its own layer
Detectors are free to be generous. Deciding what is worth saying is a different job from deciding what is true, so it is a different layer.

detectors

technical · musical

In
Measurements. Never audio.
Does
Reads the measurement set and proposes findings over specific intervals, each classified and carrying its two confidences.
Out
Candidate findings, each tied to the interval and the algorithm that produced it.
Why it is its own layer
This is the constraint the whole stack is arranged around. A detector that could reach the audio could produce a claim with no measurement behind it — and then nothing in the report could be checked.

measurement engine

engine · builder

In
DSP routines and a decoded buffer.
Does
Runs the routines over the track and over the intervals inside it, and assembles one typed measurement set.
Out
The measurement set every detector reads.
Why it is its own layer
One place decides what gets measured, so two detectors asking the same question get the same number.

dsp

loudness · peaks · amplitude · spectral · stereo

In
Audio buffers.
Does
The maths: loudness to ITU BS.1770-4, true and sample peaks, amplitude envelopes, spectral balance, stereo correlation and width.
Out
Numbers, with no interpretation attached.
Why it is its own layer
Pure functions with no opinion in them — and the only part of the system a published specification can be checked against.

audio i/o

probe · limits · decode · buffer · views

In
A file.
Does
Probes the format, checks it against limits, decodes it, and exposes buffers plus the views that address a slice of one.
Out
Typed buffers and interval views over them.
Why it is its own layer
The only layer that knows what a file is. Everything above works on buffers, which is what lets a single stem and a full mix go through the same code.

contracts

typed, shared by every layer

In
Nothing. It imports no layer.
Does
Defines the types every layer passes: measurements, findings, intervals, confidences, classifications.
Out
The vocabulary the layering rule is written in.
Why it is its own layer
Shared by all of them and depending on none of them. Without a common type language, 'each layer may only call the one below it' has nothing to be expressed in.

llm

injected — the core never imports it

In
A finished deterministic report.
Does
Rewrites the prose, after which a verifier checks the rewrite against the evidence.
Out
Better-reading prose, or the deterministic report plus a line explaining why the rewrite was discarded.
Why it is its own layer
It hangs off the side because the core never imports it. The tool installs four packages and runs with no API key, and the findings do not change when this is switched on.

listening

estimates, kept apart from the measurements

In
Measurements.
Does
Estimates musical context — tempo, key, where the sections change — from level and band-balance novelty.
Out
Context estimates, labelled as estimates.
Why it is its own layer
Kept out of the stack so an estimate can never be mistaken for a measurement. It is why sections stay A, B and C rather than claiming a chorus.

Key engineering decisions

Pick one to read it. The constraint, the implementation and the cost are behind each.

Choose an engineering decision to read

Detectors read measurements, never audio

Detectors are handed the measurement set and nothing else, so every finding walks back to the interval and the algorithm that produced it.

The constraint, what I built, and what it cost
The constraint
A finding has to be traceable back to the interval and the algorithm that produced it.
What I implemented
The detector layer is handed measurements and nothing else. Each layer may only call the one below it, and the rule is enforced by what each package imports rather than by convention.
What it cost
A detector that needs a new signal has to get it added to the measurement layer first. In return, every finding can be walked back to the numbers behind it.

Instead ofdetectors that reach into the audio buffer whenever they want something the measurements do not carry.

Evidence quality and interpretation confidence are recorded separately

Each finding carries two confidences rather than one score: how reliable the evidence is, and how confident the reading of it is.

The constraint, what I built, and what it cost
The constraint
The numbers can be solid while what they mean is not.
What I implemented
Every finding carries two confidences — how reliable the evidence is, and how confident the interpretation is — and is classified as a measured fact, an inferred issue, a creative option, or something already working. Anything inferred has to carry alternative explanations.
What it cost
More to compute and more to show than a single score would need. A single score averages away exactly the distinction the tool exists to make.

Instead ofone confidence percentage per finding.

Claude rewrites the prose and never produces the finding

The language layer rewrites the report's wording and is checked against the evidence. The findings are the same findings with it switched off.

The constraint, what I built, and what it cost
The constraint
The core has to run with no API key, no GPU and no model weights.
What I implemented
The language model is an injected layer the core never imports. It rewrites the report's prose, and a verifier then checks the rewrite against the evidence; if it is absent, fails, or says something the measurements do not support, the output is the deterministic report plus a line explaining why.
What it cost
The prose is plainer when the layer is off. The findings are the same findings either way, which is the point.

Instead ofasking a model to read the measurements and write the findings itself.

The frequency curve is a measurement, not an EQ display

The curve is drawn in absolute terms, and a band the transform cannot resolve is left as a gap rather than interpolated into something that looks like data.

The constraint, what I built, and what it cost
The constraint
A curve normalised to its own peak makes a quiet track and a loud one look identical.
What I implemented
The curve is drawn in absolute terms, so a quiet track looks quiet, and a band the transform cannot resolve is drawn as a gap rather than interpolated into something that looks like data.
What it cost
Less immediately pretty than a normalised curve. A gap is information; a smooth line across it is not.

Instead ofnormalising the curve and interpolating across unresolved bands.