Playback and Export

This is the output layer. You’ve built your theory, composed your arrangement, shaped your sounds – now you need to hear it. PyTheory gives you four ways to get your music out: speakers, WAV files, MIDI files, and sheet music.

Use speakers for immediate feedback while you’re sketching and experimenting. Use WAV export when you want to share actual audio – post it, send it, drop it into a video. Use MIDI export when you want to bring your sketch into a real DAW and finish it with professional instruments, mixing, and mastering. Use ABC notation export when you want sheet music – rendered in the browser or shared as plain text. Each output serves a different stage of the creative process.

PyTheory can play audio through your speakers, save to WAV, export to MIDI, or generate sheet music as ABC notation. Everything is synthesized from waveforms – no samples or external audio files needed.

Note

Audio playback requires PortAudio to be installed on your system. On macOS: brew install portaudio. On Ubuntu: apt install libportaudio2.

play() – Single Tones and Chords

The simplest way to hear something:

from pytheory import Tone, Chord, play

play(Tone.from_string("A4"), t=1_000)          # A440 for 1 second
play(Chord.from_symbol("Am7"), t=2_000)         # chord for 2 seconds

Optional parameters for synth, envelope, and temperament:

from pytheory import Synth, Envelope

play(Tone.from_string("C4"), synth=Synth.SAW, envelope=Envelope.PLUCK, t=1_000)
play(Tone.from_string("C4"), temperament="pythagorean", t=1_000)

Synth-specific parameters are passed through as keyword arguments:

# Mellotron with flute tape
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="choir", t=2_000)

# Hard sync with custom slave ratio
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)

# Wavefolding with 4 folds
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=4.0)

# Drift oscillator with square shape
play(Tone.from_string("C4"), synth=Synth.DRIFT, shape="square")

play_score() – Full Arrangements

Plays a Score with all its parts and drums mixed together. Output is stereo — each part is panned according to its pan setting, drums are stereo-panned like a real kit, and reverb tails have natural stereo width. A master bus compressor/limiter (4:1 ratio, brick-wall at 0.95) is applied to prevent clipping and make the mix louder and punchier:

from pytheory import Score, Duration, Chord
from pytheory.play import play_score

score = Score("4/4", bpm=140)
score.drums("bossa nova", repeats=4)
chords = score.part("chords", synth="sine", envelope="pad")
for sym in ["Am", "Dm", "E7", "Am"]:
    chords.add(Chord.from_symbol(sym), Duration.WHOLE)
play_score(score)

The render pipeline respects the Score’s temperament and reference_pitch settings, so Baroque or microtonal scores play back at the correct tuning:

score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)

Press Ctrl+C at any time during playback to stop — PyTheory catches KeyboardInterrupt and stops audio cleanly.

See Sequencing for how to build scores and parts.

render_score() – Headless Rendering

Returns a raw audio buffer (numpy float32 array) without playing it. Useful for saving to WAV or further processing:

>>> from pytheory.play import render_score
>>> buf = render_score(score)   # numpy float32 array
>>> len(buf)
604800

save() – WAV Export

Render tones or chords to a WAV file. Works without speakers or PortAudio:

from pytheory import save, Chord, Tone, Synth

save(Tone.from_string("A4"), "a440.wav", t=1_000)
save(Chord.from_name("Am7"), "am7.wav", t=2_000)
save(
    Chord.from_name("C"),
    "c_triangle.wav",
    synth=Synth.TRIANGLE,
    temperament="meantone",
    t=3_000,
)

save_midi() – MIDI Export

MIDI export is probably the most useful feature here for working musicians. The idea is simple: sketch your ideas in Python – where iteration is fast, where you can use loops and randomness and music theory functions – and then export to MIDI. Open that MIDI file in Logic, Ableton, Reaper, FL Studio, or whatever you use, and now you’ve got your chord progressions, melodies, and bass lines on real tracks. Swap in your favorite soft synths, add real mixing, finish the track properly. Python is the sketchpad; the DAW is the canvas.

Export tones, chords, progressions, or full scores as Standard MIDI Files. MIDI files can be opened in any DAW, edited, transposed, and assigned to any instrument.

Simple export (single tone, chord, or progression):

from pytheory import save_midi, Key, Tone, Chord

save_midi(Tone.from_string("C4"), "middle_c.mid", t=1000)
save_midi(Chord.from_symbol("Am7"), "am7.mid")

chords = Key("C", "major").progression("I", "V", "vi", "IV")
save_midi(chords, "pop.mid", t=500, bpm=120)

Score-based export (with time signature, tempo, and parts):

from pytheory import Score, Duration, Key

score = Score("4/4", bpm=140)
for chord in Key("G", "major").progression("I", "IV", "V", "I"):
    score.add(chord, Duration.WHOLE)
score.save_midi("progression.mid")

to_abc() – ABC Notation / Sheet Music

ABC notation is a human-readable text format for music that tools can turn into staff notation and MIDI. It’s widely used for folk tunes, lead sheets, and quick sketches. PyTheory can export any Score as ABC notation – and optionally wrap it in an HTML page that renders sheet music right in the browser using abcjs.

Basic export:

from pytheory import Score, Duration, Key

score = Score("4/4", bpm=120)
lead = score.part("lead")
for chord in Key("C", "major").progression("I", "V", "vi", "IV"):
    lead.add(chord, Duration.WHOLE)

print(score.to_abc(title="Pop Chords", key="C"))

Output:

X:1
T:Pop Chords
M:4/4
Q:1/4=120
L:1/8
K:C
[CEG]8 | [GBd]8 | [Ace]8 | [FAc]8 |

Open sheet music in the browser with html=True:

html = score.to_abc(title="Pop Chords", key="C", html=True)

with open("chords.html", "w") as f:
    f.write(html)

import webbrowser
webbrowser.open("chords.html")

This generates a self-contained HTML page with an embedded <script> tag that loads abcjs from a CDN and renders the notation as SVG – no build steps, no dependencies, just open the file.

Multi-part scores automatically get V: (voice) directives so each instrument appears on its own staff. Bass parts (average note below C4) get bass clef automatically. Drum-only parts are skipped. Notes longer than one measure are split into tied notes across barlines.

Parameters:

  • title – Tune title for the T: header (default "Untitled").

  • key – ABC key signature string (default "C"). Use "Am" for A minor, "Bb" for B-flat major, "F#m" for F-sharp minor, etc.

  • html – If True, return a full HTML document instead of raw ABC (default False).

to_lilypond() – LilyPond Export

LilyPond is the gold standard for publication-quality music engraving. to_lilypond() generates complete LilyPond source files that you can compile to PDF:

score = Score("4/4", bpm=120)
lead = score.part("lead")
for note in ["C4", "D4", "E4", "F4"]:
    lead.add(note, Duration.QUARTER)

ly = score.to_lilypond(title="My Score", key="C", mode="major")

with open("score.ly", "w") as f:
    f.write(ly)

Then compile with lilypond score.ly to get a PDF. Multi-part scores get separate staves in a StaffGroup, bass clef is auto-detected, and long notes are split with ties across barlines.

Parameters:

  • title – Title for the \header block (default "Untitled").

  • key – Key signature root (default "C"). Use note names like "Bb", "F#", "Eb".

  • mode – LilyPond mode string (default "major"). Use "minor" for minor keys.

to_musicxml() – MusicXML Export

MusicXML is the interchange format for notation software. Export your score and open it in MuseScore, Sibelius, Finale, Dorico, or any other notation app:

xml = score.to_musicxml(title="My Score")

with open("score.musicxml", "w") as f:
    f.write(xml)

The output is a complete MusicXML 4.0 partwise document with proper time signatures, tempo markings, clef detection, tied notes across barlines, and chord notation. No external dependencies needed.

to_tab() – Guitar/Bass Tablature

Generate ASCII tablature from any Part or Score:

lead = score.part("lead")
lead.add("E4", Duration.QUARTER)
lead.add("B3", Duration.QUARTER)
lead.add("G3", Duration.QUARTER)
lead.add("D3", Duration.QUARTER)

print(lead.to_tab())

Output:

e|---0---------|
B|------0------|
G|---------0---|
D|------------0|
A|-------------|
E|-------------|

Works on Score too – it picks the first melodic part automatically:

print(score.to_tab())                          # auto-pick part
print(score.to_tab(part_name="bass"))           # specific part
print(score.to_tab(tuning="bass"))              # 4-string bass tab
print(score.to_tab(tuning="drop_d"))            # drop D guitar

Supports "guitar" (6-string standard), "bass" (4-string), "drop_d", or a custom list of MIDI note numbers for any tuning.

play_pattern() – Drum Patterns

Play a drum pattern through the speakers:

from pytheory import Pattern
from pytheory.play import play_pattern

play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)

See Drums for the full list of 100 presets and 37 fills.

play_progression() – Quick Chord Playback

Play a chord progression in sequence with a single call:

from pytheory import Key, play_progression

chords = Key("C", "major").progression("I", "V", "vi", "IV")
play_progression(chords, t=800)

Optional synth, envelope, and gap parameters:

from pytheory import Synth, Envelope

play_progression(chords, t=1000, synth=Synth.TRIANGLE, gap=200)
play_progression(chords, t=2000, envelope=Envelope.PAD)

That’s the workflow: hear it, tweak it, hear it again. When it sounds right, export to WAV or MIDI and take it somewhere bigger.

MIDI Import

Load any Standard MIDI File into a Score — then play it through PyTheory’s synth engine with effects, or analyze the theory:

from pytheory import Score
from pytheory.play import play_score

score = Score.from_midi("song.mid")

# See what's inside
for name, part in score.parts.items():
    print(f"{name}: {len(part.notes)} notes")

# Change the synth and add effects
score.parts["ch1"].synth = "saw"
score.parts["ch1"].reverb_mix = 0.3

play_score(score)

Each MIDI channel becomes a named Part (ch1, ch2, etc.). Channel 10 (drums) becomes drum hits. Tempo, time signature, note durations, and velocities are all preserved.

Download any MIDI file from the internet, load it, play it through the synth engine with reverb and delay. That’s the whole idea.

Audio Import — WAV → Score

The reverse direction: record yourself humming a melody, whistling a hook, or playing a bass line, and turn the recording back into notes you can edit, harmonize, quantize, export to MIDI, or print as sheet music:

from pytheory import Score

score = Score.from_wav("hum.wav", bpm=100, quantize=0.25)

for note in score.parts["melody"].notes:
    print(note.tone, note.beats)

score.save_midi("hum.mid")          # finish it in your DAW
print(score.to_abc(title="My Hum")) # or print the sheet music

Pitch tracking uses the YIN algorithm — the same approach as most monophonic tuners — implemented in pure numpy. Transcription is monophonic: one note at a time (voice, whistle, a single instrument line), not chords. Record melody and bass as separate takes.

For full mixes, pass split=True and you get a full arrangement sketch back — four parts plus the key:

score = Score.from_wav("song.wav", split=True, quantize=0.5)

score.parts["melody"]    # the lead line
score.parts["bass"]      # the bassline
score.parts["chords"]    # chord symbols (Am, F, C, G...)
score.parts["drums"]     # kick / snare / hat hits
score.detected_key       # e.g. <Key C major>

play_score(score)        # an instant cover version

Under the hood: harmonic-percussive separation splits sustained pitched material from drums (held notes are horizontal lines on a spectrogram, drum hits vertical ones — median filters tell them apart). The harmonic stem gets band-split YIN passes for bass and melody, plus a chromagram — the spectrum folded into 12 pitch classes — matched against major/minor triad templates per chord window. The percussive stem gets onset detection with each hit classified as kick, snare, or hat by where its energy lives. The key falls out of everything pitched via Key.detect().

What to expect: chords and bassline come out well (a rendered Am-F-C-G test mix comes back exactly), the melody as well as it dominates the mix, and the drum groove reads correctly even when individual edge hits get fuzzy. It’s a sketch to edit, not a record deal — but it turns “what is this song?” into an editable Score in one line.

Tempo is estimated automatically when you don’t pass bpm= — the onset pattern is autocorrelated to find the pulse (a rendered groove comes back exact; pulse-free rubato humming falls back to 120).

Tips for good transcriptions:

  • Tighten the pitch rangefmin=60, fmax=350 for a bass line, fmin=150, fmax=1200 for whistling. Less range to search means fewer octave mistakes.

  • Raise the floor above vocal fry — voice recordings often carry low creaky artifacts at note starts and ends that transcribe as quiet sub-bass blips. If you see suspiciously low, low-velocity notes at phrase boundaries, set fmin=100 (or higher) and they disappear.

  • Pin the tempo if you know it — auto-estimation reads the pulse from the onsets, but if you recorded against a metronome, pass that bpm= and remove the guesswork.

  • Quantize when you want a clean chartquantize=0.25 snaps to sixteenths; leave it off to keep the timing as performed.

Phone voice memos (.m4a), mp3s, and other formats load directly — they’re converted on the fly through afconvert (built into macOS) or ffmpeg, whichever is on your PATH.

There’s a CLI command too:

$ pytheory transcribe hum.m4a hum.mid --quantize 0.25
$ pytheory transcribe bass.wav --fmin 60 --fmax 350 --play
$ pytheory transcribe song.wav --split

See the Cookbook for the full voice-memo-to-sheet-music recipe.

PyTheory Studio — the Browser Front Door

All of the above, without writing any code:

$ pytheory studio

opens a local web app: drop in a recording (.wav, voice memo, .mp3) and the transcription renders as sheet music right on the page (via abcjs). Press play to hear it through PyTheory’s synths, download the MIDI for your DAW, and there’s a tuner at the bottom. Check “full mix” for the four-part bass/melody/chords/drums split.

Everything runs on your machine — the only thing fetched from the internet is the notation renderer. Your audio never leaves localhost.