RipDoctor
Capture a vinyl side, find the track boundaries, cut them where you want them.
Status: it has taken a record. One LP, start to finish on real hardware - both sides captured from the turntable, boundaries fitted and checked, tracks cut, tagged and filed with cover art and album-mode ReplayGain, and the raw sides archived only once the record was provably in the library. Driven from the page throughout, with nothing typed at a shell to get it through.
That is one record, on one machine, through one signal chain, by the person who
wrote it. It is the difference between "the tests pass" and "it works", and it
is not the same as "it works for you". See
docs/decisions.md for what has been decided and why, and
ripdoctor doctor for what this machine is missing.
The problem it solves
Splitting a recorded vinyl side into tracks is usually done by detecting silence: pick a threshold in decibels, call anything quieter a gap, cut there. On most records this works.
On a sparse pressing it cannot work, because there is no such threshold. A real inter-track gap and a quiet passage inside a song both sit near −43 dB full-band, and no number separates them. Tuning the threshold trades one failure for the other. On one album this produced two tracks that were 29 seconds too long and 22 seconds too short.
Restrict the measurement to 1–3 kHz and the same two things separate by about 20 dB. Vinyl's noise — plinth rumble, arm handling, warp — is bass-heavy, and on a quiet pressing there is nothing at all above 8 kHz. The 1–3 kHz band is where music is and noise is not.
| full band | 1–3 kHz | |
|---|---|---|
| music | −26 | −40 |
| arm up, being handled | −36 | −67 |
| silent groove | −64 | −85 |
| dead air, needle up | −89 | −93 |
Full band puts music and arm-rumble 10 dB apart. The band lane puts them 27 dB apart. Every boundary decision in this tool keys off that.
These are measurements from one signal chain, not universal constants. See docs/method.md for what each number means and where it came from.
That is the argument in one picture. The blue is the full-band waveform, where the gaps between tracks are barely dents. The orange line under it is the same side measured in 1-3 kHz, and every inter-track gap drops clean through the dashed threshold. Nine of them, on a side where full-band silence detection finds an ambiguous handful.
What it does
Records, if the turntable is attached to the machine running it, with a live meter showing the band level so you can see what the stylus is doing. It also accepts a WAV or FLAC you recorded somewhere else; neither path is second-class.
Fits boundaries by requiring two independent constraints to agree: the cut must fall inside a measured gap, and it must fall at the catalogue duration from the previous cut. Where they disagree, the cut is clamped and the disagreement is reported rather than absorbed.
Cuts each track with its own start and end, so the groove between tracks is discarded instead of being attached to whichever neighbour is nearer.
Verifies by ear, which is the part that matters. Automatic detection finds where music dominates; only a person finds where music is. So the tool does not ask you to scrub through a track and judge. It mixes a 1400 Hz tick at the exact cut instant and asks one question: does the tick land in the gap, or on the music?
What it is not
- Not a library manager. It writes tagged files into a directory and stops. Whatever you use to serve or sync your music watches that directory; RipDoctor never calls it.
- Not a restoration tool. No click removal, no declicking, no noise reduction.
- Not a cataloguing app.
- Not fully automatic, and not trying to be. The last judgement is yours.
Install
pip install ripdoctor
It pulls in beets, which does the tagging, the path formatting and the album-mode ReplayGain a finished record is filed by. Nothing else comes with it.
beets keeps its own configuration, and RipDoctor uses whatever it finds -
BEETSDIR and the default location are beets' own, and a service that sets one
passes it down like any other environment variable. Point it at the
configuration the rest of the machine uses: beets with no configuration is
beets with no plugins, which means no cover art and no ReplayGain, filing into
a library database nothing else reads. ripdoctor doctor reports when beets
would file a record somewhere other than the configured library. The single thing RipDoctor
adds is a layer that turns the import prompt back on, because under quiet: yes
beets never asks: it applies anything above the match threshold and skips
anything below it without a word, and there is then no such thing as a preview.
The system tools it needs are ffmpeg, ffprobe, flac and metaflac, plus
arecord if you want to record. ripdoctor doctor reports which of those are
missing and what to install for each.
Commands
ripdoctor doctor what this machine has, what it lacks, and what to do
ripdoctor config every resolved setting, including where the audio lives
ripdoctor devices capture devices this machine has, and what each accepts
ripdoctor probe record twenty seconds and say what arrived
ripdoctor measure compare this signal chain against the shipped thresholds
ripdoctor record record one side, metered, stopping itself at the run-out
ripdoctor serve run the web interface
ripdoctor salvage finish a capture an interrupted session left behind
ripdoctor name say what a record is, for one ripped before it was
ripdoctor lookup find a release in the catalogue, usable entries first
ripdoctor fit place track boundaries from a spec and measured levels
ripdoctor split cut a plan into tracks
ripdoctor check build tick clips, one per boundary, for listening to
ripdoctor import tag the cut tracks and place them in the library
ripdoctor archive confirm a record arrived before the raw sides are cleared
ripdoctor probe before a record, not after. It turns "drop the needle, wait
twenty minutes, find out it was the wrong input" into a twenty-second question,
and tells the four states apart: music, signal with nothing musical in it, an
empty room, or something merely quiet.
ripdoctor doctor is the one to run first. It never needs a working machine -
that is the point of it - and every problem it reports comes with what to do
about it.
The raw sides are never cleared until the record is provably somewhere else -
ripdoctor archive counts what actually arrived and says so, asking beets where
it filed things rather than guessing at a path.
Nothing is cut until a plan validates. A track that would end before it starts, overlap its neighbour, collide on a track number or run past the end of the side is refused with the reason, rather than written as an empty or duplicated file.
The web interface
ripdoctor serve is the same pipeline with a waveform in front of it, on the
machine that holds the library. packaging/ripdoctor.service
runs it as a service; the paths in it are examples and the comments say what
each one is for. It binds to loopback unless told otherwise, and
asks for a login either way: it can write to the pool and run ffmpeg, so a
password is the compensating control for being reachable at all.
What the page adds over the commands is the part that needs eyes and ears. The 1-3 kHz gap curve is drawn under the waveform with the detection threshold on it, so a boundary is placed against the measurement rather than against a number in a table. Ghost markers show where the catalogue says each cut should fall, and the distance between a ghost and a real one is the disagreement the fitter reported. Every boundary can be auditioned as a tick clip without leaving the page.
Recording runs on the server rather than in the browser, so the meter and the auto-stop survive a closed laptop. You can listen to the input while cueing the needle, which is the only way to tell that the arm is tracking before twenty minutes have gone by.
The bar under the controls is what you watch while cueing. Arm-up handling
rumble is loud full-band and dead in 1-3 kHz, a silent groove is quiet in both,
and music is loud in both - so where the band bar sits says which of those is
happening, before twenty minutes have gone by. Drop the needle when it reads
groove, not below it.
Phases
| Phase | State |
|---|---|
| 0 — scaffold, decisions, budgets, architecture tests | done |
| 1 — the detection and fitting core, and its tests | done |
| 2 — audio seam, config, doctor; offline CLI | done |
| 3 — capture, the live meter, auto-stop | done, and run against a turntable |
| 4 — web interface | done |
| 5 — tagging and import | done |
| 6 — documentation revision | done |
| 7 — validate on the server, then cut over | done; cut over 2026-09-07 |
Development
make check # lint, types, tests, budgets
make test
The core layer computes over decibel envelopes and touches nothing — no
subprocess, no filesystem, no clock — so the whole algorithm is testable with
no ffmpeg, no sound card and no audio files. That boundary is enforced by
tests/test_architecture.py, not by convention.
On Python 3.14 specifically, this suite is occasionally unreliable through no fault of its own - see ADR-019. A failure whose error is impossible (an unknown opcode, a NameError for something imported at the top of the file) is the interpreter, not the code. Run it again. CI uses 3.11 to 3.13 and is not affected.
main is protected by a ruleset kept in .github/rulesets/main.json: linear
history, no force-push, no deletion, and a pull request whose checks have all
passed. Nobody bypasses it, the author included - a gate with an exception for
the person most likely to be in a hurry is not a gate.
Size budgets are failing checks. The comment ratio is hard and is never raised; the others are the author's call. Never write less code to fit a number.
The documentation was written ahead of the code and revised once the code was
complete. What holds it true from here is a set of tests rather than a ratio:
every command the README names is a real subparser and every subparser is
named; every threshold appears in docs/method.md with the number the code
actually uses; every element the front end reaches for exists in the markup;
and every endpoint it calls is a route. See ADR-023 and ADR-037.
Prior work
VinylFlow (MIT) covers adjacent ground and is worth your attention if RipDoctor is not what you want. It takes a recording you already made and splits, tags and exports it, with an interactive waveform editor for adjusting boundaries.
The differences are in scope rather than quality. VinylFlow does not record, so capture happens elsewhere and the file is uploaded to it; RipDoctor records on the machine that holds the library, so the file is written once and never moves. VinylFlow previews the resulting track; RipDoctor is built to judge the boundary itself, which is a different question. VinylFlow splits on full-band silence detection with a tunable threshold, which is the approach this project exists because of.
Licence
MIT. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ripdoctor-0.1.0.tar.gz.
File metadata
- Download URL: ripdoctor-0.1.0.tar.gz
- Upload date:
- Size: 2.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8fe3778dbcdbf6481a74e500b305f18fba6cf73ca8d0d83eaf570aa325b2e8d
|
|
| MD5 |
b8e653649ce04c0e1eee9c335b2f03e3
|
|
| BLAKE2b-256 |
a0dc9e21a07cb9b6dae1a9256df81cef3bdf78b2af5e41e6803ee2de3f9da14f
|
Provenance
The following attestation bundles were made for ripdoctor-0.1.0.tar.gz:
Publisher:
release.yml on bspeelm/RipDoctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ripdoctor-0.1.0.tar.gz -
Subject digest:
b8fe3778dbcdbf6481a74e500b305f18fba6cf73ca8d0d83eaf570aa325b2e8d - Sigstore transparency entry: 2755029831
- Sigstore integration time:
-
Permalink:
bspeelm/RipDoctor@bda3844f95c3420adc50f2d021b5aa7839eb2c56 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bspeelm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bda3844f95c3420adc50f2d021b5aa7839eb2c56 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ripdoctor-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ripdoctor-0.1.0-py3-none-any.whl
- Upload date:
- Size: 187.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5657a1eebc31346d831793206247b1e68f4f597c019fdfcf11ff075931cb92c6
|
|
| MD5 |
284e0de0e09d992ac30c4b8caae84523
|
|
| BLAKE2b-256 |
d03794a88c2dbdcf07613377c75c1790affdc693f1f3930f25b48f47f7b24dff
|
Provenance
The following attestation bundles were made for ripdoctor-0.1.0-py3-none-any.whl:
Publisher:
release.yml on bspeelm/RipDoctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ripdoctor-0.1.0-py3-none-any.whl -
Subject digest:
5657a1eebc31346d831793206247b1e68f4f597c019fdfcf11ff075931cb92c6 - Sigstore transparency entry: 2755031082
- Sigstore integration time:
-
Permalink:
bspeelm/RipDoctor@bda3844f95c3420adc50f2d021b5aa7839eb2c56 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bspeelm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bda3844f95c3420adc50f2d021b5aa7839eb2c56 -
Trigger Event:
push
-
Statement type: