bass-transcribe · git:20260925.4486774 · 2026-09-25 · sha256 ac94a10279c1dbe9
bass-transcribe git:20260925.4486774A
Immutable. This exact content never changes and is served at /api/v1/blob/ac94a10279c1dbe9.
---
name: bass-transcribe
description: Use when the user has played bass (or another monophonic instrument) and wants the take transcribed — to a MIDI file, an Ableton Live clip, or a tab/chart. Default pipeline is pyin on the recorded audio (CREPE for vocals and expressive leads); basic-pitch for double-stops; includes note segmentation, cleanup rules, and real-time plugin alternatives.
---
# Bass Transcribe — played take → MIDI / Live clip / chart
Default for bass is the offline **pyin** pipeline. For vocals and expressive leads (sax,
synth lead) switch to **CREPE** (§2b). For in-Live plugin options (NeuralNote, Jam Origin)
read `references/realtime-options.md`.
**Why pyin, not CREPE, for bass:** in a side-by-side A/B in Live on a separated bass stem,
the CREPE line was rejected by ear ("CREPE is not for bass") — it over-segments a
slow-moving line into spurious adjacent notes. Published evidence agrees: pYIN beats CREPE
on bass lines because CREPE's training set has almost no sub-bass examples (Araz, ISMIR
2021 LBD), and no public pitch benchmark covers the bass register. pyin is also ~30×
faster. On vocals the same A/B went the other way — CREPE caught quiet, legato notes pyin
dropped.
## 1. Get the audio
- **User points at a file** — wav/mp3/aiff of the take. Mono preferred; DI cleaner than mic.
- **Record in Live** — audio track, arm, record the take. MCP **cannot export audio**:
the user does File → Export, or point at the raw recording under the Set's
`Samples/Recorded/` folder.
- Ask for (or detect) the take's **BPM** if the output will go into a Live clip.
## 2. Default pipeline — pyin → MIDI
```python
import librosa, numpy as np, pretty_midi
y, sr = librosa.load(AUDIO, sr=22050, mono=True)
HOP = 256 # 11.6 ms
f0, voiced, vprob = librosa.pyin(y, sr=sr, fmin=40.0, fmax=220.0, # E1 … A3
frame_length=4096, hop_length=HOP)
t = librosa.times_like(f0, sr=sr, hop_length=HOP)
rms = librosa.feature.rms(y=y, frame_length=4096, hop_length=HOP)[0]
rms = rms / (rms.max() + 1e-9)
VPROB, MIN_LEN, GAP = 0.55, 0.10, 0.05 # voicing gate, min note s, bridgeable dropout s
pitch = np.where(voiced & (vprob >= VPROB),
np.round(librosa.hz_to_midi(np.nan_to_num(f0, nan=40.0))), -1).astype(int)
notes, cur = [], None
for i, (ti, p) in enumerate(zip(t, pitch)):
if cur and p == cur['p']:
cur['end'], cur['i1'] = ti, i # same pitch: extend
continue
if cur and p < 0 and ti - cur['end'] <= GAP:
continue # short dropout: bridge it
if cur and cur['end'] - cur['start'] >= MIN_LEN:
notes.append(cur) # pitch change / long gap: close
cur = {'p': p, 'start': ti, 'end': ti, 'i0': i, 'i1': i} if p >= 0 else None
if cur and cur['end'] - cur['start'] >= MIN_LEN:
notes.append(cur)
pm = pretty_midi.PrettyMIDI()
inst = pretty_midi.Instrument(program=33) # Electric Bass (finger)
for n in notes:
vel = int(np.clip(50 + 70 * rms[n['i0']:n['i1'] + 1].mean(), 35, 120))
inst.notes.append(pretty_midi.Note(velocity=vel, pitch=n['p'],
start=n['start'], end=n['end']))
pm.instruments.append(inst)
pm.write(OUT_MID)
```
Tuning knobs: `fmin=36` for drop-D (D1), `fmin=30` + `frame_length=8192` for a 5-string
low B; `fmax` up to ~400 Hz only for high solos (a wider range invites octave jumps);
`VPROB` up to 0.6–0.7 for noisy takes; `MIN_LEN` down to 0.06 for fast lines. Measured on
separated bass stems: ~13 s for a 2-minute song on an M3.
## 2b. Vocals and expressive leads — CREPE
Same segmentation, with CREPE's confidence in place of `vprob`:
```python
import crepe
y16, _ = librosa.load(AUDIO, sr=16000, mono=True)
t, freq, conf, _ = crepe.predict(y16, 16000, model_capacity='full', viterbi=True, step_size=10)
# pitch = where(conf >= 0.55, round(hz_to_midi(freq)), -1); then the loop above
```
`full` + viterbi is deterministic on CPU. Expect more notes than pyin, including breaths
and glottal onsets — post-filter sub-100 ms notes.
## 3. Cleanup rules
- **Drop blips** < 80 ms (tracker flicker, fret noise).
- **Octave sanity**: 4-string bass ≈ MIDI 26–67 (drop-D D1 … 24th-fret G4). Tracker
octave errors land a 5th/octave out — flag and correct outliers to the nearest
in-range octave.
- **Legato-join** same-pitch notes split by < 50 ms gaps.
- **Ghost notes / slides** show as short low-confidence fragments — either drop (chart
use) or keep (feel/groove analysis). Ask which the user wants.
- **Double-stops or chords played?** pyin is monophonic and will pick one note — rerun
with **basic-pitch** (`basic-pitch <out_dir> <audio> --save-midi`) for those passages.
## 4. Outputs
1. **`.mid` file** — the pipeline's direct product; drag into any DAW.
2. **Live clip** via the `ableton-mcp` skill: convert seconds → beats
(`beats = sec * BPM / 60`), size the clip in **BARS** (round up to 1/2/4/8…), push
with `add_notes_to_clip` in ≤140-note chunks (never two pushes to the same clip in
parallel).
3. **Tab / chart** — bass tab or bar chart generation: chart craft lives in the
`song-analysis` skill (`references/chart-and-lyrics.md`); pitch-class per half-bar
aggregation as in its Phase 3.
## 5. Verify before declaring done
Report note count, pitch range, and duration vs. take length. Sanity signals: a bass
take yields roughly 1–4 notes/second; a pitch histogram dominated by 2–5 pitch classes
(the song's roots). Wildly more notes → the `VPROB` gate is letting flicker through;
wildly fewer → gate too high, the take is quieter than expected, or the part is a drone
(few long notes is a legitimate answer).
## Local environment (this machine)
- Python with librosa + crepe 0.0.16 + basic-pitch 0.4.0 + pretty_midi:
`/Users/peripan/dev/abletonAI/audio-analysis/.venv-bp/bin/python`
- Prior art: `scripts/transcribe_bass_pyin.py` in the tooling root — the pyin pipeline
above, outputting beat-based JSON for Live.