Python parser and MIDI generator for the Alda music programming language
Project description
aldakit
A zero-dependency Python parser and MIDI generator for the Alda music programming language[^1].
[^1]: Includes a rich repl and native MIDI support via bundled prompt-toolkit and libremidi respectively.
Installation
Requires Python 3.10+
pip install aldakit
Or with uv:
uv add aldakit
Quick Start
Command Line
# Evaluate inline code
aldakit -e "piano: c d e f g"
# Interactive REPL
aldakit repl
# Play an Alda file (examples available in the repository)
aldakit examples/twinkle.alda
# Export to MIDI file
aldakit examples/bach-prelude.alda -o bach.mid
Python API
import aldakit
# Play directly
aldakit.play("piano: c d e f g")
# Save to MIDI file
aldakit.save("piano: c d e f g", "output.mid")
# Play from file
aldakit.play_file("song.alda")
# List available MIDI ports
print(aldakit.list_ports())
For more control, use the Score class:
from aldakit import Score
score = Score("""
piano:
(tempo 120)
o4 c4 d e f | g a b > c
""")
# Play with options
score.play(port="FluidSynth", wait=False)
# Save to file
score.save("output.mid")
# Access internals
print(f"Duration: {score.duration}s")
print(score.ast) # Parsed AST
print(score.midi) # MIDI sequence
MIDI Import
Import existing MIDI files and work with them as Alda:
from aldakit import Score
# Import a MIDI file
score = Score.from_midi_file("recording.mid")
# Or use from_file (auto-detects .mid/.midi)
score = Score.from_file("song.mid")
# View as Alda source
print(score.to_alda())
# piano:
# o4 c4 d e f | g a b > c
# Play the imported MIDI
score.play()
# Export to Alda file
score.save("song.alda")
# Re-export to MIDI
score.save("output.mid")
# Import with custom quantization grid
# Default is 0.25 (16th notes), use 0.5 for 8th notes
score = Score.from_midi_file("recording.mid", quantize_grid=0.5)
Features:
- Multi-track MIDI files (each channel becomes a separate part)
- Tempo detection and preservation
- General MIDI instrument mapping
- Chord detection for simultaneous notes
- Configurable timing quantization
Real-Time MIDI Transcription
Record MIDI input from a keyboard or controller:
import aldakit
# List available MIDI input ports
print(aldakit.list_input_ports())
# Record for 10 seconds from the first available port
score = aldakit.transcribe(duration=10)
# Play back what was recorded
score.play()
# Export to Alda source
print(score.to_alda())
# Record with options
score = aldakit.transcribe(
duration=30,
port_name="My MIDI Keyboard",
instrument="piano",
tempo=120,
quantize_grid=0.25, # Quantize to 16th notes
)
For more control, use TranscribeSession:
from aldakit.midi.transcriber import TranscribeSession
session = TranscribeSession(quantize_grid=0.25, default_tempo=120)
# Set a callback for note events (optional)
session.on_note(lambda pitch, vel, on: print(f"Note: {pitch}, vel={vel}, on={on}"))
# Start recording
session.start()
# Poll periodically (in a loop or timer)
import time
for _ in range(100):
session.poll()
time.sleep(0.1)
# Stop and get the recorded notes
seq = session.stop()
print(seq.to_alda())
Programmatic Composition
Build music programmatically using the compose module:
from aldakit import Score
from aldakit.compose import part, note, rest, chord, seq, tempo, volume
# Create a score from compose elements
score = Score.from_elements(
part("piano"),
tempo(120),
note("c", duration=4),
note("d"),
note("e"),
chord("c", "e", "g", duration=2),
)
score.play()
# Builder pattern with method chaining
score = (
Score.from_elements(part("violin"))
.with_tempo(90)
.add(note("g", duration=8), note("a"), note("b"))
)
# Note transformations
c = note("c", duration=4)
c_sharp = c.sharpen() # C#
c_up_octave = c.transpose(12) # Up one octave
# Repeat syntax
pattern = seq(note("c"), note("d"), note("e"))
repeated = pattern * 4 # Repeat 4 times
# Export to Alda source
print(score.to_alda()) # "violin: (tempo 90) g8 a b"
Available compose elements:
- Notes:
note("c", duration=4, octave=5, accidental="+", dots=1) - Rests:
rest(duration=4),rest(ms=500) - Chords:
chord("c", "e", "g"),chord(note("c"), note("e", accidental="+")) - Sequences:
seq(note("c"), note("d")),Seq.from_alda("c d e") - Parts:
part("piano"),part("violin", alias="v1") - Attributes:
tempo(120),volume(80),octave(5),panning(50) - Dynamics:
pp(),p(),mp(),mf(),f(),ff() - Advanced:
cram(),voice(),voice_group(),var(),var_ref(),marker(),at_marker()
Scales and Chords
Build melodies and harmonies using music theory helpers:
from aldakit import Score
from aldakit.compose import part, tempo
from aldakit.compose import (
# Scale functions
scale, scale_notes, scale_degree, mode,
relative_minor, relative_major,
# Chord builders
major, minor, dim, aug, maj7, min7, dom7,
arpeggiate, invert_chord, voicing,
)
# Get scale pitches
c_major = scale("c", "major") # ['c', 'd', 'e', 'f', 'g', 'a', 'b']
a_blues = scale("a", "blues") # ['a', 'c', 'd', 'd+', 'e', 'g']
# Generate scale as playable notes
melody = scale_notes("c", "pentatonic", duration=8)
# Key relationships
rel_min = relative_minor("c") # 'a' (C major -> A minor)
rel_maj = relative_major("a") # 'c' (A minor -> C major)
# Build chords
c_maj = major("c") # C E G
a_min7 = min7("a") # A C E G
g_dom7 = dom7("g", inversion=1) # B D F G (first inversion)
# Arpeggiate a chord
arp = arpeggiate(maj7("c"), pattern=[0, 1, 2, 3, 2, 1], duration=16)
# Custom voicing (spread chord across octaves)
spread = voicing(major("c"), [3, 4, 5]) # C3 E4 G5
# Create a I-IV-V-I progression
pitches = scale("c", "major")
progression = [
major(pitches[0], duration=2), # C major (I)
major(pitches[3], duration=2), # F major (IV)
major(pitches[4], duration=2), # G major (V)
major(pitches[0], duration=1), # C major (I)
]
score = Score.from_elements(
part("piano"),
tempo(100),
*progression,
)
score.play()
Available scales: major, minor, harmonic-minor, melodic-minor, pentatonic, blues, chromatic, whole-tone, dorian, phrygian, lydian, mixolydian, locrian, japanese, arabic, hungarian-minor, spanish, bebop-dominant, bebop-major
Available chords: major, minor, dim, aug, sus2, sus4, maj7, min7, dom7, dim7, half_dim7, min_maj7, aug7, maj6, min6, dom9, maj9, min9, add9, power
Transformers
Transform sequences with pitch and structural operations:
from aldakit.compose import (
note, seq,
transpose, invert, reverse, shuffle,
augment, diminish, fragment, loop, interleave,
pipe,
)
# Create a motif
motif = seq(note("c", duration=8), note("d", duration=8), note("e", duration=8))
# Pitch transformers
up_fourth = transpose(motif, 5) # Transpose up 5 semitones
inverted = invert(motif) # Invert intervals around first note
backwards = reverse(motif) # Retrograde
# Structural transformers
longer = augment(motif, 2) # Double durations (8th -> quarter)
shorter = diminish(motif, 2) # Halve durations (8th -> 16th)
first_two = fragment(motif, 2) # Take first 2 elements
repeated = loop(motif, 4) # Repeat 4 times (explicit)
# Chain transformations with pipe
result = pipe(
motif,
lambda s: transpose(s, 5),
reverse,
lambda s: augment(s, 2),
)
# All transforms preserve to_alda() export
print(result.to_alda())
MIDI Transformers
For post-MIDI-generation processing, use MIDI-level transformers that operate on absolute timing:
from aldakit import Score
from aldakit.midi.transform import (
quantize, humanize, swing, stretch,
accent, crescendo, normalize,
filter_notes, trim, merge,
)
# Get MIDI sequence from a score
score = Score("piano: c d e f g a b > c")
midi_seq = score.midi
# Timing transformers
quantized = quantize(midi_seq, grid=0.25, strength=0.8) # Snap to quarter-note grid
humanized = humanize(midi_seq, timing=0.02, velocity=10) # Add subtle variations
swung = swing(midi_seq, grid=0.5, amount=0.3) # Apply swing feel
# Velocity transformers
accented = accent(midi_seq, pattern=[1.0, 0.5, 0.5, 0.5]) # 4/4 accent pattern
crescendo_seq = crescendo(midi_seq, start_vel=50, end_vel=100)
normalized = normalize(midi_seq, target=100)
# Filtering and combining
filtered = filter_notes(midi_seq, lambda n: n.pitch >= 60) # Keep notes >= middle C
trimmed = trim(midi_seq, start=0.0, end=2.0) # First 2 seconds
merged = merge(midi_seq, another_seq) # Combine sequences
Note: MIDI transformers operate on absolute timing (seconds) and cannot be converted back to Alda notation.
Generative Functions
Create algorithmic compositions with generative functions:
from aldakit import Score
from aldakit.compose import part, tempo
from aldakit.compose.generate import (
random_walk, euclidean, markov_chain, lsystem, cellular_automaton,
shift_register, turing_machine,
)
# Random walk melody
melody = random_walk("c", steps=16, intervals=[-2, -1, 1, 2], duration=8, seed=42)
# Euclidean rhythms (e.g., Cuban tresillo: 3 hits over 8 steps)
rhythm = euclidean(hits=3, steps=8, pitch="c", duration=16)
# Markov chain
chain = markov_chain({
"c": {"d": 0.5, "e": 0.3, "g": 0.2},
"d": {"e": 0.6, "c": 0.4},
"e": {"c": 0.5, "g": 0.5},
"g": {"c": 1.0},
})
markov_melody = chain.generate(start="c", length=16, duration=8, seed=42)
# L-System (Fibonacci pattern)
from aldakit.compose import note, rest
fib = lsystem(
axiom="A",
rules={"A": "AB", "B": "A"},
iterations=5,
note_map={"A": note("c", duration=8), "B": note("e", duration=8)},
)
# Cellular automaton (Rule 110)
automaton = cellular_automaton(rule=110, width=8, steps=4, pitch_on="c", duration=16)
# Shift register (LFSR) - classic analog sequencer
lfsr = shift_register(16, bits=4, scale=["c", "e", "g", "b"], duration=16)
# Turing Machine - evolving loop (probability=0 for locked, higher for chaos)
turing = turing_machine(32, bits=8, probability=0.1, seed=42)
# Combine into a score
score = Score.from_elements(
part("piano"),
tempo(120),
*melody.elements,
)
score.play()
CLI Reference
aldakit [-h] [--version] [-e CODE] [-o FILE] [--port NAME]
[--stdin] [--parse-only] [--no-wait] [-v]
{repl,ports,input-ports,transcribe,play} [file]
Subcommands
| Command | Description |
|---|---|
repl |
Interactive REPL with syntax highlighting and auto-completion |
ports |
List available MIDI output ports |
input-ports |
List available MIDI input ports |
transcribe |
Record MIDI input and output Alda code |
play |
Play an Alda file or code (default behavior) |
Options
| Option | Description |
|---|---|
file |
Alda file to play (use - for stdin) |
-e, --eval CODE |
Evaluate Alda code directly |
-o, --output FILE |
Save to MIDI file instead of playing |
--port NAME |
MIDI output port name |
--stdin |
Read from stdin (blank line to play) |
--parse-only |
Print AST without playing |
--no-wait |
Don't wait for playback to finish |
-v, --verbose |
Verbose output |
Examples
# Interactive REPL with syntax highlighting
aldakit repl
# List available MIDI ports
aldakit ports
# List available MIDI input ports
aldakit input-ports
# Play with verbose output
aldakit -v examples/jazz.alda
# Read from stdin
echo "piano: c d e f g" | aldakit -
# Parse and show AST
aldakit --parse-only -e "piano: c/e/g"
# Export to MIDI file
aldakit examples/twinkle.alda -o twinkle.mid
# Record MIDI input for 10 seconds (default)
aldakit transcribe
# Record for 30 seconds with verbose note display
aldakit transcribe -d 30 -v
# Record with Alda-style note display
aldakit transcribe -d 10 -v --alda-notes
# Record and save to file
aldakit transcribe -o recording.alda
aldakit transcribe -o recording.mid
# Record and play back
aldakit transcribe --play
# Record with custom settings
aldakit transcribe -d 20 -t 90 -i guitar -q 0.5 --play
Interactive REPL
The REPL provides an interactive environment for composing and playing Alda code:
aldakit repl
Features:
- Syntax highlighting
- Auto-completion for instruments (3+ characters)
- Command history (persistent across sessions)
- Multi-line paste (use platform-specific paste: ctrl-v, shift-ctrl-v, cmd-v, etc.)
- Multi-line input (Alt+Enter)
- MIDI playback control (Ctrl+C to stop)
REPL Commands:
:help- Show help:quit- Exit REPL:ports- List MIDI ports:instruments- List available instruments:tempo [BPM]- Show/set default tempo:stop- Stop playback
Alda Syntax Reference
Notes and Rests
piano:
c d e f g a b # Notes
r # Rest
c4 d8 e16 # With duration (4=quarter, 8=eighth, etc.)
c4. d4.. # Dotted notes
c500ms d2s # Milliseconds and seconds
Accidentals
c+ # Sharp
c- # Flat
c_ # Natural
c++ # Double sharp
Octaves
o4 c # Set octave to 4
> c # Octave up
< c # Octave down
Chords
c/e/g # C major chord
c1/e/g # Whole note chord
c/e/g/>c # With octave change
Ties and Slurs
c1~1 # Tied notes (duration adds)
c4~d~e~f # Slurred notes (legato)
Parts
piano: c d e
violin "v1": c d e # With alias
violin/viola/cello "strings": # Multi-instrument
c d e
Attributes
(tempo 120) # Set tempo (BPM)
(tempo! 120) # Global tempo
(vol 80) # Volume (0-100)
(volume 80)
(quant 90) # Quantization/legato (0-100)
(panning 50) # Pan (0=left, 100=right)
# Dynamic markings
(pp) (p) (mp) (mf) (f) (ff)
Variables
riff = c8 d e f g4
piano:
riff riff > riff
Repeats
c*4 # Repeat note 4 times
[c d e]*4 # Repeat sequence
[c d e f]*8 # 8 times
Cram (Tuplets)
{c d e}4 # Triplet in quarter note
{c d e f g}2 # Quintuplet in half note
{c {d e} f}4 # Nested cram
Voices
piano:
V1: c4 d e f
V2: e4 f g a
V0: # End voices
Markers
piano:
c d e f
%chorus
g a b > c
violin:
@chorus # Jump to chorus marker
e f g a
Supported Instruments
All 128 General MIDI instruments are supported. Common examples:
piano,acoustic-grand-pianoviolin,viola,cello,contrabassflute,oboe,clarinet,bassoontrumpet,trombone,french-horn,tubaacoustic-guitar,electric-guitar-clean,electric-basschoir,strings,brass-section
See midi/types.py for the complete mapping.
MIDI Backend
aldakit uses libremidi via nanobind for cross-platform MIDI I/O:
- Low-latency realtime playback
- Virtual MIDI port support (AldaPyMIDI), makes it easy to just send to your DAW.
- Pure Python MIDI file writing (no external dependencies)
- Cross-platform: macOS (CoreMIDI), Linux (ALSA), Windows (WinMM)
- Supports hardware and software/virtual MIDI ports (FluidSynth, IAC Driver, etc.)
import aldakit
# List available ports
print(aldakit.list_ports())
# Play to virtual port (visible in DAWs like Ableton Live)
aldakit.play("piano: c d e f g")
# Play to a specific port
aldakit.play("piano: c d e f g", port="FluidSynth")
# Save to MIDI file
aldakit.save("piano: c d e f g", "output.mid")
MIDI Playback Setup
Virtual Port (Recommended)
When no hardware MIDI ports are available, aldakit creates a virtual port named "AldaPyMIDI". This port is visible to DAWs and other MIDI software:
- Start the REPL:
aldakit repl - In your DAW (Ableton Live, Logic Pro, etc.), look for "AldaPyMIDI" in MIDI input settings
- Play code in the REPL - notes will be sent to your DAW
Software Synthesizer (FluidSynth)
For high-quality General MIDI playback without hardware, use FluidSynth:
# Install FluidSynth (macOS)
brew install fluidsynth
# Install FluidSynth (Debian/Ubuntu)
sudo apt install fluidsynth
# Download a SoundFont (e.g., FluidR3_GM.sf2)
# eg. sudo apt install fluid-soundfont-gm
# Place in ~/Music/sf2/
# Start FluidSynth with CoreMIDI (macOS)
fluidsynth -a coreaudio -m coremidi ~/Music/sf2/FluidR3_GM.sf2
# In another terminal, start aldakit
aldakit repl
# aldakit> piano: c d e f g
A helper script is available in the repository:
# Set the SoundFont directory (add to your shell profile)
export ALDAPY_SF2_DIR=~/Music/sf2
# Run with default SoundFont (FluidR3_GM.sf2)
python scripts/fluidsynth-gm.py
# Or specify a SoundFont directly
python scripts/fluidsynth-gm.py /path/to/soundfont.sf2
# List available SoundFonts
python scripts/fluidsynth-gm.py --list
Hardware MIDI
Connect a USB MIDI interface or synthesizer, then:
# List available ports
aldakit ports
# Play to a specific port
aldakit --port "My MIDI Device" examples/twinkle.alda
MIDI File Export
If you don't have MIDI playback set up, export to a file:
# Save to MIDI file
aldakit examples/twinkle.alda -o twinkle.mid
# Open with default app
open twinkle.mid
Development
Setup
git clone https://github.com/shakfu/aldakit.git
cd aldakit
make # Build the libremidi extension
Run Tests
make test
# or
uv run pytest tests/ -v
Architecture
License
MIT
See Also
- Alda - The original Alda language and reference implementation
- Alda Cheat Sheet - Syntax reference
- Extending aldakit - Design document for programmatic API
- libremidi - A modern C++ MIDI 1 / MIDI 2 real-time & file I/O library. Supports Windows, macOS, Linux and WebMIDI.
- nanobind - a tiny and efficient C++/Python bindings
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aldakit-0.1.4.tar.gz.
File metadata
- Download URL: aldakit-0.1.4.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9feee216706d81bfe16b3a1e7ff50f24cb7d2f9a49f30aa4efe73e6e573d19a3
|
|
| MD5 |
297e5ef7d48a18b545e1dac1bf521a8f
|
|
| BLAKE2b-256 |
a96098b866a4a10e5026a5db4ebef8988bb504c00174db22bc4f756b0e62292e
|
File details
Details for the file aldakit-0.1.4-cp314-cp314-macosx_15_0_arm64.whl.
File metadata
- Download URL: aldakit-0.1.4-cp314-cp314-macosx_15_0_arm64.whl
- Upload date:
- Size: 641.2 kB
- Tags: CPython 3.14, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ebb1f2ca40e39b06e5e789c69d6ef551fcb7b480bb97cf6b7065f989e89e6c1
|
|
| MD5 |
f9d22bc6a4f45a2a75a771f98cebd540
|
|
| BLAKE2b-256 |
61efd634a2c1c5ac283a1539eb643fa099937dcbaa898272edfe622e8ab3938d
|
File details
Details for the file aldakit-0.1.4-cp313-cp313-macosx_15_0_arm64.whl.
File metadata
- Download URL: aldakit-0.1.4-cp313-cp313-macosx_15_0_arm64.whl
- Upload date:
- Size: 641.1 kB
- Tags: CPython 3.13, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14d02abc953f62e09b796c79acdc7c4d72274824de7d98bcccdd370cdee5afc6
|
|
| MD5 |
4694e29cafb179ff89032369ce93ccbf
|
|
| BLAKE2b-256 |
d2532e5c88487f598a4a9366ba6db43e7e91e686c420dccf7ff8b011c96886fb
|
File details
Details for the file aldakit-0.1.4-cp312-cp312-macosx_15_0_arm64.whl.
File metadata
- Download URL: aldakit-0.1.4-cp312-cp312-macosx_15_0_arm64.whl
- Upload date:
- Size: 641.2 kB
- Tags: CPython 3.12, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b14240cd795ce83f5df68cafd8aa436c2d674b4d809b7dc6ba9898b494a19f51
|
|
| MD5 |
f0af86a53cfa04917ca86e429013e595
|
|
| BLAKE2b-256 |
caf0703da5021d23757c816575b318ba9185b62e5fb4c7947badae5cbff44a7a
|
File details
Details for the file aldakit-0.1.4-cp311-cp311-macosx_15_0_arm64.whl.
File metadata
- Download URL: aldakit-0.1.4-cp311-cp311-macosx_15_0_arm64.whl
- Upload date:
- Size: 642.2 kB
- Tags: CPython 3.11, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3e77d637e004f3a8e09b2f69de003afbe4d1dbfdcceab8e8e8e0f06091f0df4
|
|
| MD5 |
4d5c2aaa6f48468a9cdb999f78682272
|
|
| BLAKE2b-256 |
4c4352a5bbd5f8eb7344c293b1ce1d897f6a1d2c622c2ec53fd53c8d9a764672
|
File details
Details for the file aldakit-0.1.4-cp310-cp310-macosx_15_0_arm64.whl.
File metadata
- Download URL: aldakit-0.1.4-cp310-cp310-macosx_15_0_arm64.whl
- Upload date:
- Size: 642.3 kB
- Tags: CPython 3.10, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b27dc3cd8a784857a7059dc91c72bfea1b3b4b81fdeb410ee1ab204c7c5cff9
|
|
| MD5 |
c764f1f919a0b20dc4544370de0d9828
|
|
| BLAKE2b-256 |
a1813a039c8c8921d1745c9c1b0097ca74ff11de2825ca540b8a12ebae86a2cc
|