Pitchline
Product & engineering plan · v1.0

Private audio analysis

Find the pitch behind the track.

A focused web app that accepts one MP3 and returns its musical key, tempo, and tuning reference—then forgets the file ever existed.

01 / SCOPE

Define the promise

Keep version one deliberately narrow: a single track goes in, a small and honest result comes out.

Input

One local MP3

Drag, drop, or choose a file. Recommended limit: 50 MB and 15 minutes. Validate both extension and decoded media—not only the browser-provided MIME type.

Output

Three primary values

Key and mode (C♯ minor), tempo (128 BPM), and tuning reference (A4 ≈ 447.8 Hz). Add a confidence label to every estimate.

Non-goals

No library management

No history, login, playlists, cloud storage, metadata editing, sharing links, payment system, or uploaded-file playback from the server in v1.

Terminology: label the third result “Tuning reference,” not “detune.” Also show the offset from concert pitch, such as +30.4 cents vs A=440 Hz. That is more precise and easier for musicians to interpret.
02 / SHAPE

Use a stateless request pipeline

Server-side processing is the best first version: it behaves consistently across phones and desktops, keeps DSP code straightforward, and fits cleanly in one Coolify container.

Chosen approach

FastAPI serves both the UI and analysis API

The browser streams a multipart upload. The API writes to a unique temporary directory, FFmpeg decodes it to mono PCM, Python analyzes it, and the directory is removed before the request completes.

  • One repository and one deployable image
  • No cross-origin configuration
  • Simple local development and logs

Why not browser-only yet?

WebAssembly could remove the upload entirely, but MP3 decoding, memory pressure, worker setup, and behavior on lower-powered mobile devices make it a riskier MVP. Treat it as a later privacy/performance experiment.

BrowserValidate basics and upload
→
APIEnforce size, type, timeout
→
Temporary workspaceDecode → analyze → JSON
→
CleanupDelete on success or failure

Nothing is written to a volume. Container logs must never include filenames, audio bytes, or request bodies.

Toolchain

Use one reproducible Python workflow

Make this a uv project with a committed pyproject.toml, uv.lock, and supported Python version. Use Ruff for formatting and linting, and Ty for static type checking.

uv sync uv run ruff format --check . uv run ruff check . uv run ty check

The same checks run locally and in CI before a change is mergeable. Runtime and development dependencies are declared in the project metadata; the lockfile is updated intentionally with dependency changes.

03 / DSP

Estimate tuning first

Tuning and key are coupled. Correcting the pitch-class representation with the estimated tuning reference makes the later key estimate more reliable.

1 · Prepare

Decode once

  • FFmpeg → mono float PCM
  • Use a fixed sample rate (22.05 or 44.1 kHz)
  • Trim long leading/trailing silence
  • Reject unreadable or near-silent audio
2 · Tune

Find the shared pitch offset

  • Estimate spectral peaks over many frames
  • Map peaks to nearest 12-TET notes
  • Use a robust circular histogram of cents
  • Ignore noisy/percussive frames
3 · Key

Build tuning-aware chroma

  • Generate CQT/chroma with offset correction
  • Aggregate stable harmonic frames
  • Compare against 24 major/minor profiles
  • Return top result, runner-up, confidence
4 · Tempo

Track onsets and beats

  • Compute onset-strength envelope
  • Estimate global tempo and beat positions
  • Score half/double-time alternatives
  • Prefer a musically useful range
5 · Guard

Measure confidence

  • Frame agreement for tuning
  • Gap between key candidates
  • Beat regularity for BPM
  • Use “low confidence,” never fake precision
6 · Return

Round only for display

  • Key: pitch class + mode
  • BPM: one decimal internally
  • A4: one decimal Hz
  • Cents: signed, one decimal
t = tuning offset in cents
A4 Hz = 440 × 2(t / 1200)

example: t = +30.4
A4 ≈ 447.8 Hz
Important limitation: the estimate assumes the recording is approximately in 12-tone equal temperament with one shared global offset. Chorus, tape drift, stretched samples, non-Western tuning, sparse percussion, or intentionally detuned instruments can make “A = ___ Hz” undefined or unstable. Show Unable to estimate reliably when confidence is low.
04 / UX

One calm screen

The interface should feel like a utility, not a dashboard. Make privacy visible at the moment of upload.

StateWhat the user seesBehavior
EmptyLarge drop zone, “Choose MP3,” size limit, and “Your file is deleted immediately after analysis.”Accept one file; keyboard and touch accessible.
ReadyFilename, human-readable size, Analyze button, Remove button.Client-side type/size check saves an unnecessary upload.
AnalyzingIndeterminate progress with rotating messages: uploading, decoding, finding tempo, measuring tuning.Disable duplicate submits; allow cancellation with AbortController.
ResultThree large result cards plus confidence, cents offset, and “Analyze another.”Do not put results in the URL or browser persistence.
ErrorPlain-language reason: too large, unsupported/corrupt file, timed out, or not enough musical pitch.Keep the selected file only when retrying makes sense.
05 / CONTRACT

Keep the API tiny

Two endpoints are enough. Avoid job queues until real traffic or request-duration limits prove they are necessary.

POST /api/analyze

Request: multipart/form-data with a single file field. Response:

{ "key": { "tonic": "C#", "mode": "minor", "confidence": 0.84 }, "tempo": { "bpm": 128.0, "confidence": 0.91, "alternatives": [64.0, 256.0] }, "tuning": { "a4_hz": 447.8, "cents_from_440": 30.4, "confidence": 0.77 }, "analysis_seconds": 3.2 }

GET /health

Return 200 {"status":"ok"}. Do not run a sample analysis in the health check; it should only confirm that the HTTP process is ready.

Error shape

{ "error": { "code": "UNSUPPORTED_AUDIO", "message": "This MP3 could not be decoded." } }

Use 400 for malformed requests, 413 for size limits, 415 for unsupported content, 422 for analyzable audio with no reliable musical estimate, 429 for throttling, and 500 for unexpected failures.

06 / SAFETY

Make “no storage” enforceable

Privacy is an implementation property, not only a sentence in the interface.

Per-request temp directoryCreate with an unpredictable ID; never use the original filename as a path.
Guaranteed cleanupRemove the directory in finally, including decode errors, cancellation, and timeouts.
No persistent mountDo not attach a Coolify volume. Temporary container filesystem only.
No content loggingLog timing, status, and anonymous request ID only—not names, bytes, or results.
Layered upload limitsEnforce at proxy and application level; stream reads instead of trusting Content-Length.
Bounded processingFFmpeg timeout, request timeout, limited concurrency, CPU/memory limits, non-root container.
Safe media parsingCall FFmpeg with an argument array, never a shell string; ship a patched base image.
No browser persistenceNo localStorage, IndexedDB, service-worker cache of responses, or third-party analytics.
07 / BUILD

Ship in four passes

A vertical slice first, then accuracy and hardening. The sequence keeps every phase runnable.

Walking skeleton

App + toolchain + container
  • Initialize a uv project
  • Configure Ruff and Ty
  • FastAPI serves static UI
  • Upload endpoint and limits
  • FFmpeg decode
  • Health endpoint
  • Guaranteed temp cleanup

Analysis core

DSP + fixtures
  • Tuning estimate and formula
  • Tuning-aware key profiles
  • Beat tracking
  • Confidence heuristics
  • Known-answer test clips

Product finish

States + accessibility
  • Drop zone and validation
  • Loading/cancel/error states
  • Responsive result cards
  • Privacy copy
  • Keyboard and screen-reader pass

Production

Coolify + operations
  • Non-root multi-stage image
  • Install from uv.lock
  • Run Ruff and Ty in CI
  • Resource/concurrency limits
  • Health check and HTTPS
  • Rate limiting
  • Cleanup and load tests
08 / QUALITY

Test against known answers

Audio algorithms fail gracefully only when the test set includes awkward music, not just clean electronic loops.

Accuracy corpus

  • Generate tonal clips at A=432, 440, 444, and 448 Hz
  • Pitch-shift full mixes by known ±10/25/40 cents
  • Include major/minor keys across all pitch classes
  • Include tempos from 60–200 BPM plus half-time genres
  • Add live drums, ambient intros, key changes, silence, and percussion-only files

Suggested acceptance targets

  • Tuning: within ±2 cents on stable synthetic/reference clips
  • Key: ≥80% exact tonic + mode on the curated tonal set
  • Tempo: within ±1 BPM, accepting a declared ½×/2× equivalent
  • Cleanup: zero temp files after every success/failure/cancel test
  • Resilience: predictable response under max file size and concurrent load
Calibrate before promising: these are engineering targets, not guaranteed real-world accuracy. Publish accuracy wording only after running the fixed corpus and reviewing failures by genre.
09 / COOLIFY

Deploy one Dockerfile

Connect the Git repository as a Dockerfile application. The service must listen on 0.0.0.0, expose its internal port, and require no persistent storage.

Image

Build predictably

Install locked Python dependencies with uv, install FFmpeg in the runtime stage, run as an unprivileged user, and keep build tools out of the final image.

Runtime

Bound the workload

Start with 1–2 analysis workers, a 60–120 second timeout, and explicit CPU/memory limits. Scale horizontally only after observing timings.

Routing

Check readiness

Expose the app port, attach the domain and HTTPS, and configure /health. Ensure curl or wget exists if the container health check uses it.

SettingStarting valueReason
Upload size50 MBEnough for common compressed tracks while containing abuse.
Track duration15 minutesReject unusually expensive inputs after probing metadata.
Analysis timeout90 secondsFail predictably instead of pinning a worker indefinitely.
Concurrent analyses2 per containerDSP is CPU-heavy; tune after measuring the actual host.
Retention0 seconds after requestCleanup runs immediately on every exit path; no scheduled sweeper is the primary control.
10 / NOTES

Implementation references

Primary documentation supporting the analysis and deployment choices.

Tooling & deployment

  • uv — project management, dependency resolution, and lockfile workflow.
  • Ruff — formatter and linter.
  • Ty — static type checker.
  • Coolify Dockerfile deployment — build context, exposed port, and 0.0.0.0 binding.
  • Coolify health checks — routing behavior and required in-image health-check client.

Commit pyproject.toml and uv.lock. Dependency upgrades, Ruff configuration, and Ty configuration should land through tested pull requests.

Definition of done

A user can upload a valid MP3, receive understandable key/BPM/tuning results and confidence, and analyze another track. All paths—including cancellation and failure—remove temporary audio. The app passes Ruff and Ty checks plus the reference corpus, runs within declared limits, and deploys from one repository to Coolify with no database or persistent volume.

Ready
to build