Skip to content
Back to skills

Bass Transcribe

ASecurity

Use when the user has played bass or another single-note instrument (or sung a line) and wants the take as a MIDI file, an Ableton Live clip, or a tab/chart.

  • 3 stars
  • 0 votes
  • 1 copy
  • 4 views
  • Added September 5, 2026
ai-agentspythongoswiftexpressgit

Works with

  • cli
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add PerIPan/music-workflow-skills --skill bass-transcribe --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bass Transcribe?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bass Transcribe
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/peripan-bass-transcribe/badge)](https://www.skillsdirectory.com/skills/peripan-bass-transcribe)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: bass-transcribe
description: Use when the user has played bass or another single-note instrument (or sung a line) and wants the take as a MIDI file, an Ableton Live clip, or a tab/chart.
---

# 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. CREPE `full` runs at ~1× real time; for
a quick preview, **SwiftF0** (`swift-f0==0.3.0`, ONNX, ~250× real time) agrees with it within
50 cents on 97–99% of frames both call voiced — but it voices only 27–80% of the frames
CREPE does, dropping exactly the quiet, sparse singing CREPE was chosen for. A sung line is never the
tonic source: on a drone track the singer never held the tonic, and the melody's longest
pitch class was the 2nd.

## 2c. Octave cross-check (when a line looks wrong, or on drone material)

Run a second tracker, band-limited to the same range, and flag where the two disagree by
more than 50 cents — don't auto-correct; listen to those spots.

```python
import crepe.core as cc
y16, _ = librosa.load(AUDIO, sr=16000, mono=True)
act = cc.get_activation(y16, 16000, model_capacity='tiny', step_size=10, verbose=0)
cents = np.linspace(0, 7180, 360) + 1997.3794084376191           # CREPE bin → cents
act[:, (cents < 1200 * np.log2(40 / 10)) | (cents > 1200 * np.log2(220 / 10))] = 0
f_crepe = 10 * 2 ** (cc.to_viterbi_cents(act) / 1200)            # band-limited Viterbi
t_crepe = np.arange(len(f_crepe)) * 0.01
# compare with pyin's f0 on t_crepe where both are voiced; flag |Δ| > 50 cents
```

Zeroing the out-of-band bins *before* Viterbi keeps the decoder inside the bass range
(filtering afterwards can't). `tiny` is ~17× faster than `full`. On separated bass stems
this flagged ~4% of frames on a busy line — almost all exact-octave splits — and ~9% on a
drone, where the disagreements also include harmonic locks, not just octaves.

## 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 → Live beats. For a take
   recorded to Live's click, `beats = sec × BPM / 60` is exact; for a take played free or
   a song stem, map through the beat grid (`ableton-mcp` → "Timing: seconds → Live
   beats"). Size the clip in **BARS** (round up to 1/2/4/8…), push over the raw-TCP bulk
   path (or `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 `(bar, cell)`
   aggregation as in its Phase 4.

## 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

Run with the analysis interpreter listed in the `song-analysis` skill's
`references/environment-setup.md` → "This machine" (librosa, crepe, basic-pitch,
pretty_midi).

Files in this skill

  • SKILL.md4.2 KB
  • references/realtime-options.md1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.