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.