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 80+ presets and 21 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.