git:20260716.42a79cc to git:20260925.4486774

63 added, 31 removed. Audit A to A.

---
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 CREPE (full model + viterbi) on the recorded audio; basic-pitch for double-stops; includes note segmentation, cleanup rules, and real-time plugin alternatives.
+ 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 is the offline **CREPE** pipeline (validated: CREPE `full`+viterbi beats pyin on
- bass and vocals). For in-Live plugin options (NeuralNote, Jam Origin) read
- `references/realtime-options.md`.
+ 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 — CREPE → MIDI
+ ## 2. Default pipeline — pyin → MIDI
```python
- import crepe, librosa, numpy as np, pretty_midi
+ import librosa, numpy as np, pretty_midi
- y, sr = librosa.load(AUDIO, sr=16000, mono=True)
- time, freq, conf, _ = crepe.predict(y, sr, model_capacity='full', viterbi=True, step_size=10)
+ 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)
- CONF, MIN_LEN, GAP = 0.55, 0.08, 0.05 # confidence gate, min note s, merge gap s
- midi_f = 69 + 12*np.log2(freq/440.0)
+ 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 t, m, c in zip(time, midi_f, conf):
- p = int(round(m)) if c >= CONF and np.isfinite(m) else None
- if cur and p == cur['p'] and t - cur['end'] <= GAP:
- cur['end'] = t
- else:
- if cur and cur['end'] - cur['start'] >= MIN_LEN:
- notes.append(cur)
- cur = {'p': p, 'start': t, 'end': t} if p is not None else 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)
+ inst = pretty_midi.Instrument(program=33) # Electric Bass (finger)
for n in notes:
- if 26 <= n['p'] <= 67: # octave sanity: drop-D D1 to 24th-fret G4
- inst.notes.append(pretty_midi.Note(velocity=96, pitch=n['p'],
- start=n['start'], end=n['end']))
+ 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: `CONF` up to 0.6–0.7 for noisy takes; `MIN_LEN` down to 0.05 for fast
- lines; `step_size=10` ms is fine for bass.
+ 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). CREPE
+ - **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?** CREPE is monophonic and will pick one note — rerun
+ - **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 → lower `CONF` gate is letting flicker through;
- wildly fewer → gate too high or the take is quieter than expected.
+ (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 crepe 0.0.16 + basic-pitch 0.4.0 + pretty_midi (verified 2026-07-16):
+ - 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 (pyin —
- superseded by CREPE).
+ - Prior art: `scripts/transcribe_bass_pyin.py` in the tooling root — the pyin pipeline
+ above, outputting beat-based JSON for Live.