Basic Pitch Transcriber — User Guide

A Praat-to-Python polyphonic transcription workflow that sends a selected Sound to Spotify's Basic Pitch model, post-processes the detected note events, writes MusicXML notation, and returns editable note data to Praat as Table, TextGrid, Strings, statistics, and visualization objects.

Author: Shai Cohen Affiliation: Department of Music, Bar-Ilan University, Israel Praat front end: v0.6 (2026) Python engine: v0.2 (2026) License: MIT License Repo: GitHub
Contents:

What this does

Basic Pitch Transcriber converts a selected Praat Sound into symbolic note events and notation. It does not attempt to infer an instrument label or separate instruments into timbral tracks. Instead, it uses Basic Pitch as an instrument-agnostic polyphonic note transcription model and then adds AudioTools-specific post-processing and notation logic.

Selected Praat Sound → temporary WAV → Basic Pitch inference → note-event filtering / repair → voice-layer allocation → optional sustain extension → MusicXML notation → Praat Table / TextGrid / Strings / visualization

The principal symbolic output is MusicXML. The Praat-side Table and TextGrid are editing and inspection surfaces built from the same detected note list.

This is automatic music transcription, not score recognition and not a guarantee of an authoritative musical score. Detection errors, octave errors, missed inner voices, false positives, early note endings and quantization choices can all affect the result.

Architecture

Praat front end

BasicPitchTranscriber.praat handles:

Python engine

basic_pitch_transcriber.py handles:

  1. Basic Pitch inference;
  2. normalization and sorting of note events;
  3. optional repeated-onset merging;
  4. voice allocation and polyphony statistics;
  5. optional sustain-to-next-onset extension;
  6. MusicXML notation through music21 or the built-in writer;
  7. temporary MIDI generation;
  8. stats.txt and notes.csv for the Praat front end.
The Python engine binds Basic Pitch's predict() arguments by name after inspecting the installed function signature. This is deliberate compatibility code for API changes across Basic Pitch releases rather than a fixed positional call.

Requirements & setup

Python packages

The engine explicitly requires:

music21 is optional but recommended because it provides the more complete notation backend. The script's own installation hint is:

pip install basic-pitch music21

Python executable discovery

The Praat front end currently tries:

PlatformExecutable search
Windowspython
Linux / other Unixpython3
macOS/opt/homebrew/bin/python3, then a Python-framework path, then /usr/local/bin/python3, then python3.

Python engine location

The front end looks for:

1. preferencesDirectory$/plugin_AudioTools/py/basic_pitch_transcriber.py 2. basic_pitch_transcriber.py beside the running Praat script

The plugin copy has priority. The Info window prints the exact engine path used on every run, which is useful when a stale plugin copy and a newly edited local copy both exist.

Praat 7 trust

Praat 7 requires permission for temporary-file writing, deletion and subprocess calls. The script requests full trust automatically when needed. Praat 6.x bypasses that guard.

Quick start

  1. Select exactly one Sound object.
  2. Run BasicPitchTranscriber.praat.
  3. Leave Preset = Custom if you want the three sensitivity values to be used exactly as entered.
  4. Choose the transcription sensitivity, pitch range and Melodia option.
  5. Choose whether repeated model-window onsets should be repaired.
  6. Optionally extend note endings with Sustain_to_next_onset_percent.
  7. Set the score tempo and quantization grid.
  8. Use Notation_backend = auto unless you specifically need to force one backend.
  9. Choose which MusicXML/Praat outputs you want.
  10. Run. Praat remains busy while the Python model is processing.
The first run may take longer because the Python environment/model may need initialization. This is a synchronous subprocess: Praat waits for the Python engine to finish.

Sensitivity presets

The default preset is Custom. A named preset overwrites exactly three fields:

All pitch-range, merge, sustain, notation and output settings remain as entered by the user.

PresetOnsetFrameMin noteIntended use
Custom0.500.30127.7 msUse the form values exactly.
Solo instrument (clean)0.600.40120 msCleaner monophonic or near-monophonic material; more conservative onset/frame activation.
Polyphonic / piano0.500.3058 msBasic Pitch-style defaults with shorter events allowed.
Sensitive0.300.2080 msUse when notes are being missed; expect more false positives.
Sparse0.750.55250 msHigh-confidence skeletal reduction.
Praat remembers form values between runs. If a named preset is selected, those remembered sensitivity values are intentionally replaced. The Info window reports the before/after values whenever the preset actually changes them.

Basic Pitch detection controls

Onset threshold

This is a Basic Pitch model-activation threshold, not dB. Lower values accept weaker onset activations and generally increase the number of detected note attacks.

Frame threshold

This is the activation threshold used while a note remains active. Lower values allow weaker frame activity to continue a note for longer and can also increase residual detections.

Minimum note length

Given in milliseconds. The Python engine passes it to Basic Pitch, which converts the requested duration to model frames internally. Notes at or below the resulting minimum duration are removed during decoding.

Pitch range

Minimum_frequency_Hz and Maximum_frequency_Hz can restrict the model output. A value of 0 means “use the model's default full range” for that boundary.

If both bounds are positive and minimum ≥ maximum, the Python engine ignores both bounds and reports a warning rather than applying an invalid range.

Melodia trick

When enabled, Basic Pitch performs its additional Melodia-style post-processing pass over residual frame activation after the normal onset-driven pass. In practical terms, it can recover note activity that remains strongly present even when a clean onset was not accepted.

Effective clamps

ParameterEffective range
Onset threshold0.05–0.95
Frame threshold0.05–0.95
Minimum note lengthat least 10 ms
Negative frequency boundsclamped to 0 = no explicit bound
The amplitude stored for each detected note is Basic Pitch's note-event activation amplitude in the range used by the model. It is not waveform peak level and not dB.

Repeated-onset repair

Basic Pitch analyses audio in overlapping inference windows. A sustained note can occasionally be re-decoded as a new onset near the model-window grid. AudioTools provides three post-processing choices for same-pitch events.

ModeExact behavior
offKeep the Basic Pitch note list unchanged.
window artifacts only (safe)Merge only when the next same-pitch onset lies near the model-window grid, is temporally contiguous with the preceding note, and is not substantially louder.
any gap under N msMerge any same-pitch continuation whose gap is ≤ Merge_gap_ms. This is deliberately aggressive and can collapse genuine tremolo, ostinati or repeated notes.

Safe-mode conditions

The model hop is derived from the installed Basic Pitch package when possible, with an engine fallback of approximately 1.64009 s. For a candidate repeated onset:

near model grid: |nextStart - round(nextStart/hop)×hop| ≤ 30 ms contiguous: nextStart - currentEnd ≤ 30 ms not substantially louder: nextAmplitude ≤ currentAmplitude × 1.15

All three tests must pass.

Merge_gap_ms does not control the default safe mode. The form's 120 ms value is used only by any gap under N ms. Safe window-artifact repair uses its own fixed 30 ms tolerance in the Python engine.

Sustain to next onset

Sustain_to_next_onset_percent addresses a different problem from repeated-onset repair: Basic Pitch can end a decaying note early when frame activation falls below the frame threshold.

After voice allocation, every note is allowed to extend toward the next onset in its own non-overlapping layer:

room = nextOnsetInLayer - detectedEnd newEnd = detectedEnd + sustainPercent/100 × room

The ceiling uses the earliest next onset across both the uncapped notation-layer allocation and the TextGrid-layer allocation, preserving non-overlap in both representations.

Sustain changes note end times. It does not invent new onsets, pitches or note counts.

Score tempo & quantization

Score_tempo_BPM is not tempo detection

The script does not estimate beat, meter or tempo from the audio. Score_tempo_BPM is supplied by the user and defines how detected seconds are mapped into symbolic quarter-note positions.

quarterLengthsPerSecond = Score_tempo_BPM / 60 symbolicPosition = detectedSeconds × quarterLengthsPerSecond

The tempo is clamped to 20–400 BPM.

If the entered tempo does not match the musical tempo of the source, the MusicXML can still be internally valid, but its rhythmic notation will represent the wrong metrical interpretation of the detected times.

Quantization grid

Both notation backends support:

Starts and ends are snapped to the chosen notation grid. A note that would collapse to zero duration after quantization is expanded to at least one grid unit.

Quantization affects the MusicXML notation. The Praat note Table and TextGrid retain the note-event times in seconds after merge/sustain processing rather than snapping them to the score grid.

Meter

The notation pipeline is fixed to 4/4. There is no meter-detection or meter-selection control in the current script.

Notation backends

Auto

Uses music21 when it is installed; otherwise uses the self-contained built-in MusicXML writer.

music21 — preferred

The engine first quantizes the note intervals and then allocates them greedily into non-overlapping notation layers on the quantized time base. Each layer becomes a separate music21.stream.Part named Voice 1, Voice 2, and so on.

“Voice” in this backend means a non-overlapping note layer represented as a separate MusicXML Part. It is not an inferred musical hand, instrument, performer, or contrapuntal voice identity.

Built-in writer — fallback

The built-in writer creates valid MusicXML 4.0 without requiring music21, but it is intentionally a simpler chordal reduction.

For genuine polyphony with independently moving voices, install music21. The built-in writer prioritizes a valid, openable score over preserving independent asynchronous voice durations.

Outputs

1. MusicXML file

When Save_musicxml_to_file is enabled, the complete MusicXML is copied to the user-specified path. The path must be supplied explicitly.

2. MusicXML in the Info window

Print_musicxml_to_info is enabled by default. The complete XML text is appended to the Info window in one operation.

3. Praat Strings object

Create_musicxml_Strings is enabled by default and creates:

<SoundName>_musicxml

Each string is one raw MusicXML file line. Saving the Strings object as raw text reproduces the score text, and the object can be passed directly to AudioTools workflows such as OM Score Transformer.

4. Praat note Table

When detected notes exist, the per-note CSV is automatically read back as a Table named:

<SoundName>_bpnotes

Columns:

ColumnMeaning
startNote start in seconds relative to the exported audio start.
endNote end after merge/sustain processing.
midiInteger MIDI note number.
ampBasic Pitch note activation amplitude.
voice0-based TextGrid layer index, or −1 when beyond the TextGrid layer cap.
namePitch name such as C4 or F#5.

5. Praat TextGrid

When Create_note_TextGrid is enabled, the script creates one interval tier per allocated voice layer and labels note intervals with pitch names.

Object name: <SoundName>_bpnotes Tier names: voice1 voice2 ... voiceN

The Table and TextGrid can therefore have the same visible base name but are different Praat object types.

6. MIDI

The Python engine writes a MIDI file as part of its internal Basic Pitch/notation workflow, but the current Praat front end treats it as a temporary file and deletes it during cleanup. There is no user-facing “Save MIDI” control in the present form.

Pitch bends

The symbolic note list used by the MusicXML, Table and TextGrid retains integer MIDI pitch, start, end and amplitude. Basic Pitch's optional pitch-bend trajectory is not carried into these Praat/MusicXML outputs.

Visualization

The Picture view is an analysis summary, not a rendered score page.

  1. Input waveform — the selected Praat Sound on its original time domain.
  2. Piano roll — detected notes shown as horizontal rectangles; vertical position = MIDI pitch, horizontal extent = note duration.
  3. Amplitude color — quiet model activations appear blue; stronger activations become warmer/redder.
  4. Polyphony over time — simultaneous-note count on a 240-point uniform time grid.
  5. Pitch-class distribution — detected MIDI notes collapsed into 12 pitch classes.
  6. Note-duration distribution — ten logarithmic duration bins between the detected minimum and maximum note durations.
  7. Summary — note count, pitch range, max polyphony, voice layers, thresholds, minimum note length, Melodia setting, score bars, tempo, grid, backend, XML size, duration statistics and timing.
The piano-roll color encodes Basic Pitch note amplitude. It is not a calibrated acoustic loudness display.

Time-axis steps are rounded to a readable 1/2/5 × 10k sequence rather than using arbitrary duration fractions.

Limits & important distinctions

TextGrid voice cap versus score voices

The Python engine allocates at most 8 TextGrid voice layers. Notes requiring a ninth or later simultaneous layer are marked unplaced for the TextGrid and reported.

The MusicXML notation allocation is separate and uncapped. Notes omitted from the TextGrid because of the 8-layer display/edit cap are still eligible for the score.

Per-note dump cap

The note CSV/Table is limited to the first 2000 notes. If the transcription exceeds that limit:

Tempo is supplied, not detected

The script performs note transcription but no beat tracking, downbeat tracking, meter estimation or tempo estimation.

Discrete pitches, not continuous pitch notation

The MusicXML score is based on integer MIDI-note events. It does not encode Basic Pitch's continuous pitch-bend trajectories.

Source channel count

The selected Praat Sound is exported as WAV with its original channel count. The AudioTools front end does not itself create separate transcriptions per channel or preserve channel identity as score parts.

No audio resynthesis

The script does not synthesize the MusicXML/MIDI back into a new Praat Sound. Play_input_sound simply plays the original selected Sound.

Temporary files

The workflow uses temporary WAV, MusicXML, MIDI, CSV, stats and log files. AudioTools removes these after the Praat objects and any requested external MusicXML copy have been created.

Error reporting

The Python engine mirrors console messages to a log file. If transcription fails, Praat prints that engine log into the Info window before raising the final error, so the Python traceback is visible without relying on an external terminal.

Further reading