RipDoctor
Record a vinyl side, split it into tracks, tag them and file them in your music library.
What it does
- Records a side from a turntable attached to the machine, with a live meter and an auto-stop at the run-out. Or takes a WAV or FLAC you recorded elsewhere.
- Finds the track boundaries by looking for gaps in the 1-3 kHz band rather than at the full-band level, and by checking them against the track durations in MusicBrainz.
- Lets you correct them by ear. Each boundary can be auditioned as a short clip with a tick mixed in at the cut instant, so you hear whether the cut lands in the gap or on the music.
- Cuts, tags and files the tracks through beets, with cover art and album-mode ReplayGain.
- Archives the raw sides once it has confirmed the tracks reached the library.
You drive it from a web page on the machine that holds the library, or from the command line.
Install
pip install ripdoctor
You also need ffmpeg, ffprobe, flac and metaflac on the system, plus
arecord to record. Run ripdoctor doctor and it will tell you which are
missing and what to install for each.
Configuring beets
RipDoctor does not tag or file anything itself - it hands finished tracks to beets, which matches them against MusicBrainz, writes the tags, fetches cover art, computes ReplayGain and decides the path each file goes to.
beets needs its own configuration, and a fresh install has none. Without one
it runs with no plugins: no cover art, no ReplayGain, and files go to ~/Music
rather than your library. The import still appears to work, which is what makes
this worth saying twice.
beet config -p prints where beets expects its file. Create it with at least:
directory: /srv/music # where finished albums go
plugins: fetchart embedart replaygain
import:
move: yes # move out of review/ rather than copying
fetchart:
auto: yes
minwidth: 500
embedart:
auto: yes
replaygain:
backend: ffmpeg
auto: yes
albums: yes # album mode - vinyl is mastered as a side
directory must match the library setting in RipDoctor's own config; they are
two names for the same place and nothing else keeps them in step. ripdoctor doctor reports when they disagree, which plugins beets will actually load, and
prints a starter config if there is none.
If you already have a beets setup, point RipDoctor at it with BEETSDIR rather
than writing a second one.
Getting started
ripdoctor doctor # what is missing, and what to install
ripdoctor devices # find your capture device
ripdoctor config # check where audio and the library live
Set the pool and library directories, the capture device, rate and format in
your config file - ripdoctor config prints the path.
Then, before you commit twenty minutes to a side:
ripdoctor probe # record 20 seconds and say what arrived
This tells music from silence from an empty input, so a wrong input costs twenty seconds rather than a whole side.
Then start the interface:
ripdoctor serve
On first run it generates a login and prints it:
first run - a login was generated
user: ripdoctor
password: <shown once>
Store it now; it is not recoverable.
The password is hashed on the way to disk, so that is the only time you see it.
--user sets a different name on first run.
By default it listens on 127.0.0.1:8080 - that machine only. To reach it from
a laptop, set the address and port in RipDoctor's config file:
bind = "0.0.0.0"
port = 8084
or pass --bind and --port to try it once. Then open
http://<the machine>:8084.
It asks for a login whichever way it is bound: it can write to your pool and run ffmpeg, so the password is the thing that makes it safe to be reachable at all. There is no TLS - put it behind a reverse proxy if it leaves your own network.
From there, one record goes like this:
- Rip side A, flip the record, rip side B.
- First pass - search MusicBrainz, pick the release, and it lays the tracklist across the sides and fits the boundaries.
- Check by ear. Look at the delta column for boundaries that disagree with the catalogue, and audition those.
- Cut tracks into the review directory.
- Import - beets tags them, fetches cover art and files them.
- Archive - the raw sides are moved aside once the tracks are confirmed in the library.
The web interface
The bar under the controls is what you watch while cueing the needle. Arm-up
handling rumble is loud full-band but dead in 1-3 kHz; a silent groove is quiet
in both; music is loud in both. Drop the needle when the band bar reads groove,
not below it.
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.
In the cut panel, the 1-3 kHz gap curve is drawn under the waveform with the detection threshold on it. Ghost markers show where the catalogue says each cut should fall; the distance between a ghost and a real marker is how far the two disagree.
To keep it running across reboots, install it as a service.
packaging/ripdoctor.service is a systemd unit
to copy and edit - the paths in it are examples, and the comments say what each
one is for. Set BEETSDIR in it if beets keeps its configuration somewhere
other than the default; a variable exported in your shell is not inherited by a
service, which is an easy way to end up with no plugins.
Why 1-3 kHz
Splitting a side into tracks is usually done by full-band silence detection: pick a threshold in decibels, call anything quieter a gap, cut there. On most records this works.
On a sparse pressing it cannot. A real inter-track gap and a quiet passage inside a song both sit near -43 dB full-band, and no threshold separates them. Tuning it trades one failure for the other. On one album this produced two tracks 29 seconds too long and 22 seconds too short.
Measured in 1-3 kHz, the same two things separate by about 20 dB. Vinyl's noise - plinth rumble, arm handling, warp - is bass-heavy, and a quiet pressing has 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 keys off that.
These are measurements from one signal chain, not universal constants.
docs/method.md explains each number and where it came from, and
ripdoctor measure compares your chain against them.
What it is not
- Not a library manager. It writes tagged files into a directory and stops. Whatever serves or syncs your music watches that directory; RipDoctor never calls it.
- Not a restoration tool. No click removal, no declicking, no noise reduction.
- Not fully automatic. The last judgement about where a cut goes is yours.
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
Two things it will not do:
Nothing is cut until the 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.
The raw sides are not cleared until the record is provably in the library.
ripdoctor archive asks beets where it filed things and counts what arrived.
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 without
ffmpeg, a sound card or any audio files. tests/test_architecture.py enforces
that boundary.
docs/decisions.md records what was decided and why.
On Python 3.14 the 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. Run it again. CI uses 3.11 to 3.13.
Prior work
VinylFlow (MIT) covers adjacent ground and is worth a look 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.
The differences are scope, not quality. VinylFlow does not record, so capture happens elsewhere and the file is uploaded to it. It previews the resulting track; RipDoctor is built to judge the boundary itself. It 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.2.0.tar.gz.
File metadata
- Download URL: ripdoctor-0.2.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 |
f4fa5cfdbefadcc27bcd052f1edb866857966bf9bc02bbe1c3b1fe239f91660f
|
|
| MD5 |
67f4383ae07a7c0c5922e77d31c93f5d
|
|
| BLAKE2b-256 |
3ac2298359b22483c5622bfff115ee8343d8ab9e2ca8463b1ecf9d278e90573f
|
Provenance
The following attestation bundles were made for ripdoctor-0.2.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.2.0.tar.gz -
Subject digest:
f4fa5cfdbefadcc27bcd052f1edb866857966bf9bc02bbe1c3b1fe239f91660f - Sigstore transparency entry: 2760891576
- Sigstore integration time:
-
Permalink:
bspeelm/RipDoctor@c20a9ec297f664bc6a31fd0f413c86bac5e347e9 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bspeelm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c20a9ec297f664bc6a31fd0f413c86bac5e347e9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ripdoctor-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ripdoctor-0.2.0-py3-none-any.whl
- Upload date:
- Size: 189.3 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 |
586af3673690ab07459e28c49f5047e928b51c7971cffde5111fdf5609c4009f
|
|
| MD5 |
456cb8f02b58eebc495a7386befe7726
|
|
| BLAKE2b-256 |
3cd5fe0c4df7f5dfc1f1cde501542953f819d8f8bb34c00cdd141c8782b7c5b3
|
Provenance
The following attestation bundles were made for ripdoctor-0.2.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.2.0-py3-none-any.whl -
Subject digest:
586af3673690ab07459e28c49f5047e928b51c7971cffde5111fdf5609c4009f - Sigstore transparency entry: 2760891624
- Sigstore integration time:
-
Permalink:
bspeelm/RipDoctor@c20a9ec297f664bc6a31fd0f413c86bac5e347e9 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bspeelm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c20a9ec297f664bc6a31fd0f413c86bac5e347e9 -
Trigger Event:
push
-
Statement type: