API Reference¶
This page provides auto-generated API documentation for the MIDI Markdown implementation.
Overview¶
The MMD codebase is organized into several key packages:
- Parser - Lark-based parsing and AST generation
- Core/IR - Intermediate Representation layer
- Codegen - Output format generation (MIDI, CSV, JSON)
- Runtime - Real-time MIDI playback
- Validation - Document and value validation
- Alias - Alias resolution and device libraries
- Expansion - Command expansion (variables, loops, sweeps)
- CLI - Command-line interface
Parser¶
MMDParser¶
MMDParser ¶
Main parser class for MIDI Markup Language.
Usage
parser = MMDParser() document = parser.parse_file('song.mmd')
or¶
document = parser.parse_string(mml_content)
Initialize the parser with the grammar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grammar_file
|
str | None
|
Path to the .lark grammar file. If None, uses the grammar from the parser package. |
None
|
Source code in src/midi_markdown/parser/parser.py
Functions¶
parse_file ¶
Parse an MMD file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filepath
|
str | Path
|
Path to the .mmd file |
required |
Returns:
| Type | Description |
|---|---|
MMDDocument
|
MMDDocument object containing the parsed content |
Source code in src/midi_markdown/parser/parser.py
parse_string ¶
Parse MML content from a string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content
|
str
|
MMD markup content |
required |
filename
|
str
|
Name for error reporting |
'<string>'
|
Returns:
| Type | Description |
|---|---|
MMDDocument
|
MMDDocument object |
Source code in src/midi_markdown/parser/parser.py
parse_interactive ¶
Parse MML text for REPL, handling incomplete input.
This method supports interactive parsing where input may be incomplete (e.g., user is still typing). It distinguishes between: - Incomplete input: Need more text (returns False, None) - Invalid but complete: Syntax error (returns True, Exception) - Valid and complete: Success (returns True, MMDDocument)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
MML source text (may be incomplete) |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Tuple of (complete, result): |
MMDDocument | Exception | None
|
|
tuple[bool, MMDDocument | Exception | None]
|
|
tuple[bool, MMDDocument | Exception | None]
|
|
Example
parser = MMDParser() complete, result = parser.parse_interactive("[00:01.0") assert not complete # Incomplete timing marker complete, result = parser.parse_interactive("[00:01.000]\n- cc 1.7.64") assert complete and isinstance(result, MMDDocument)
Source code in src/midi_markdown/parser/parser.py
AST Nodes¶
ast_nodes ¶
MIDI Markup Language (MML) AST Node Definitions
This module provides the data classes that represent the Abstract Syntax Tree (AST) for parsed MML documents. These are simple, flat data structures designed for ease of use and efficient processing.
Classes¶
Track
dataclass
¶
Represents a track in multi-track mode.
MML supports multi-track MIDI files where each track can be defined separately and merged during compilation.
Timing
dataclass
¶
Represents a timing specification in MML.
Timing can be one of four types: - absolute: [mm:ss.mmm] format (e.g., [01:23.250]) - musical: [bars.beats.ticks] format (e.g., [8.4.120]) - relative: [+value unit] format (e.g., [+1b], [+500ms]) - simultaneous: [@] - execute at same time as previous event
MIDICommand
dataclass
¶
MIDICommand(type: str, channel: int | None = None, data1: int | None = None, data2: int | None = None, params: dict[str, Any] = dict(), timing: Timing | None = None, source_line: int = 0)
Represents a MIDI command in the AST.
This is a flexible structure that can represent any MIDI command type: - Channel voice: note_on, note_off, cc, pc, pitch_bend, etc. - System: sysex - Meta: tempo, marker, time_signature, etc. - Alias: calls to user-defined aliases
SweepStatement
dataclass
¶
SweepStatement(start_time: Timing | None, end_time: Timing | None, interval: str, commands: list[str | MIDICommand], source_line: int = 0)
Represents a @sweep statement within an alias.
Used for generating automated parameter ramps with timing.
Example
@sweep from [0.0.0] to [+1000ms] every 50ms - cc {ch}.1.ramp(0, 127) @end
DefineStatement
dataclass
¶
Represents a @define statement within an alias.
Used for creating alias-local variables that can be referenced with ${VAR_NAME} syntax within the same alias.
Example
@define MIDI_VAL 100 - cc {ch}.7.${MIDI_VAL}
AliasDefinition
dataclass
¶
AliasDefinition(name: str, parameters: list[dict[str, Any]], commands: list[str | MIDICommand | Timing | DefineStatement | SweepStatement], description: str | None = None, computed_values: dict[str, str] = dict(), conditional_branches: list[ConditionalBranch] | None = None, is_macro: bool = False, has_conditionals: bool = False)
Represents an alias definition in MML.
Aliases can be: - Simple: Single command template with parameter substitution - Macro: Multiple commands with full command blocks - Conditional: Macro with @if/@elif/@else branches (Stage 7) - Advanced: With @define and @sweep statements
Core / Intermediate Representation¶
IR Data Structures¶
ir ¶
Intermediate Representation (IR) data structures.
The IR layer sits between the AST and output formats, enabling: - REPL: Interactive evaluation and inspection - Live playback: Real-time MIDI output - Diagnostics: Timing analysis, event queries - Multiple outputs: MIDI files, JSON, CSV, etc.
Classes¶
EventType ¶
Bases: Enum
MIDI event types for Intermediate Representation.
This enum covers all MIDI event types supported by MML: - Channel Voice Messages (NOTE_ON, NOTE_OFF, CC, PC, etc.) - System Common Messages (SYSEX, MTC, Song Position/Select) - Meta Events for MIDI files (TEMPO, TIME_SIGNATURE, MARKER, TEXT)
See Also
- MIDI 1.0 Specification: https://www.midi.org/specifications
- Standard MIDI Files Specification (SMF)
MIDIEvent
dataclass
¶
MIDIEvent(time: int, type: EventType, channel: int, data1: int = 0, data2: int = 0, time_seconds: float | None = None, metadata: dict[str, Any] | None = None)
Represents a single MIDI event in the Intermediate Representation.
The MIDIEvent is the core data structure in the IR layer, representing a single MIDI message with absolute timing. Events are created during compilation from AST and can be queried, filtered, and converted to various output formats.
Attributes:
| Name | Type | Description |
|---|---|---|
time |
int
|
Absolute time in ticks (PPQ-based, e.g., 480 ticks = 1 quarter note at PPQ=480) |
type |
EventType
|
Event type from EventType enum (NOTE_ON, CC, TEMPO, etc.) |
channel |
int
|
MIDI channel (1-16, or 0 for meta events with no channel) |
data1 |
int
|
First data byte (note number for notes, CC number for CC, etc.) |
data2 |
int
|
Second data byte (velocity for notes, CC value for CC, etc.) |
time_seconds |
float | None
|
Absolute time in seconds (computed from tempo map during IR creation) |
metadata |
dict[str, Any] | None
|
Optional metadata dict with source location, track name, etc. |
Example
Create a Note On event at 1 second (480 ticks at 120 BPM, PPQ=480)¶
event = MIDIEvent( ... time=480, ... type=EventType.NOTE_ON, ... channel=1, ... data1=60, # Middle C ... data2=80, # Velocity ... time_seconds=1.0, ... metadata={"source_line": 10, "track": "Main"} ... ) print(f"Note {event.data1} at {event.time_seconds}s") Note 60 at 1.0s
See Also
- IRProgram: Container for collections of MIDIEvent objects
- string_to_event_type: Convert string types to EventType enum
IRProgram
dataclass
¶
Intermediate representation of a compiled MML program.
The IRProgram is the central data structure after compilation, containing all MIDI events with computed timing. It sits between the AST (syntactic representation) and output formats (MIDI files, JSON, live playback).
This IR layer enables: - Query operations (events by time, type, channel) - Multiple output formats from single compilation - REPL with interactive inspection - Live playback with real-time scheduling - Diagnostic analysis (timing, event counts, duration)
Attributes:
| Name | Type | Description |
|---|---|---|
resolution |
int
|
PPQ (Pulses Per Quarter note), typically 480 or 960 |
initial_tempo |
int
|
Starting tempo in BPM (before any tempo changes) |
events |
list[MIDIEvent]
|
Sorted list of MIDIEvent objects (sorted by time) |
metadata |
dict[str, Any]
|
Dictionary with document metadata (title, author, etc.) |
Example
from midi_markdown.parser.parser import MMDParser from midi_markdown.core.compiler import compile_ast_to_ir parser = MMDParser() doc = parser.parse_file("examples/00_basics/00_hello_world.mmd") ir = compile_ast_to_ir(doc, ppq=480) print(f"Duration: {ir.duration_seconds:.2f}s") Duration: 2.00s print(f"Events: {ir.event_count}") Events: 4
Query events by type¶
notes = ir.events_by_type(EventType.NOTE_ON) print(f"Note events: {len(notes)}") Note events: 2
See Also
- compile_ast_to_ir: Main compilation function
- create_ir_program: Helper for creating IR from events
- MIDIEvent: Individual event structure
Attributes¶
Functions¶
Get events at specific time (within tolerance).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
float
|
Time in seconds |
required |
tolerance
|
float
|
Time window in seconds (default 10ms) |
0.01
|
Returns:
| Type | Description |
|---|---|
list[MIDIEvent]
|
List of events within time window |
Source code in src/midi_markdown/core/ir.py
Get events in time range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
float
|
Start time in seconds |
required |
end
|
float
|
End time in seconds |
required |
Returns:
| Type | Description |
|---|---|
list[MIDIEvent]
|
List of events in range [start, end] |
Source code in src/midi_markdown/core/ir.py
IR Compiler¶
compiler ¶
AST to IR compilation.
Converts parsed AST (from parser) into executable IR (intermediate representation). The IR can then be sent to various outputs: MIDI files, JSON, live playback, REPL.
Functions¶
compile_ast_to_ir ¶
Compile MML document AST to IR program.
This is the main entry point for compilation. It orchestrates: 1. Event generation from AST commands 2. Timing resolution (absolute, musical, relative) 3. Expansion (loops, sweeps, variables) 4. Validation (ranges, monotonicity) 5. Time computation (ticks → seconds using tempo map)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document
|
MMDDocument
|
Parsed MML document AST |
required |
ppq
|
int
|
Pulses per quarter note (MIDI resolution) |
480
|
Returns:
| Type | Description |
|---|---|
IRProgram
|
IRProgram ready for output or execution |
Example
from midi_markdown.parser.parser import MMDParser from midi_markdown.core import compile_ast_to_ir parser = MMDParser() doc = parser.parse_file("song.mmd") ir = compile_ast_to_ir(doc, ppq=480) print(f"Duration: {ir.duration_seconds}s, Events: {ir.event_count}")
Source code in src/midi_markdown/core/compiler.py
Code Generation¶
MIDI File Generation¶
midi_file ¶
MIDI file generation from IR program.
Generates Standard MIDI Files from compiled IR programs. Pure function approach - returns bytes instead of writing files.
Functions¶
generate_midi_file ¶
Generate Standard MIDI File from IR program.
This is a pure function that converts an IR program to MIDI file bytes. The caller is responsible for writing the bytes to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ir_program
|
IRProgram
|
Compiled IR program |
required |
midi_format
|
int
|
MIDI file format (0, 1, or 2) |
MIDI_FORMAT_MULTI_TRACK
|
Returns:
| Type | Description |
|---|---|
bytes
|
MIDI file as bytes |
Example
from midi_markdown.core import compile_ast_to_ir from midi_markdown.parser.parser import MMDParser parser = MMDParser() doc = parser.parse_file("song.mmd") ir = compile_ast_to_ir(doc) midi_bytes = generate_midi_file(ir) Path("output.mid").write_bytes(midi_bytes)
Source code in src/midi_markdown/codegen/midi_file.py
CSV Export¶
csv_export ¶
CSV export for MIDI events in midicsv-compatible format.
This module exports IRProgram data to the midicsv CSV format, which can be imported into spreadsheet programs, databases, or text processing tools.
Format specification: https://www.fourmilab.ch/webtools/midicsv/
Functions¶
export_to_csv ¶
Export IRProgram to midicsv-compatible CSV format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ir_program
|
IRProgram
|
Compiled IR program to export |
required |
include_header
|
bool
|
Include file header and footer records |
True
|
Returns:
| Type | Description |
|---|---|
str
|
CSV string in midicsv format |
Example
from midi_markdown.core import compile_ast_to_ir from midi_markdown.parser.parser import MMDParser parser = MMDParser() doc = parser.parse_file("song.mmd") ir = compile_ast_to_ir(doc) csv = export_to_csv(ir) print(csv) 0, 0, Header, 1, 1, 480 1, 0, Start_track 1, 0, Tempo, 500000 ...
Source code in src/midi_markdown/codegen/csv_export.py
JSON Export¶
json_export ¶
JSON export for MIDI events in complete and simplified formats.
This module exports IRProgram data to JSON in two formats: - Complete: Full MIDI data with exact timing (for programmatic use) - Simplified: Human-readable format with note names and readable times (for analysis)
Functions¶
export_to_json ¶
Export IRProgram to JSON format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ir_program
|
IRProgram
|
Compiled IR program to export |
required |
format
|
str
|
"complete" (full MIDI data) or "simplified" (human-readable) |
'complete'
|
pretty
|
bool
|
Pretty-print with indentation (default: True) |
True
|
Returns:
| Type | Description |
|---|---|
str
|
JSON string |
Example
from midi_markdown.core import compile_ast_to_ir from midi_markdown.parser.parser import MMDParser parser = MMDParser() doc = parser.parse_file("song.mmd") ir = compile_ast_to_ir(doc) json_complete = export_to_json(ir, format="complete") json_simple = export_to_json(ir, format="simplified")
Source code in src/midi_markdown/codegen/json_export.py
Runtime / Real-time Playback¶
MIDI I/O¶
midi_io ¶
MIDI I/O management for real-time MIDI output.
This module provides the MIDIOutputManager class for managing MIDI output port connections and sending MIDI messages using python-rtmidi.
Event Scheduler¶
scheduler ¶
Event scheduler for precise real-time MIDI playback.
This module provides the EventScheduler class for scheduling and playing MIDI events with sub-5ms timing precision using a hybrid sleep/busy-wait strategy.
Classes¶
EventScheduler ¶
Schedules and plays MIDI events with precise timing.
The EventScheduler uses a hybrid sleep/busy-wait strategy to achieve sub-5ms timing precision: - For delays > 10ms: Use time.sleep() (OS scheduler) - For delays < 10ms: Use busy-wait loop (tight loop)
Example
from midi_markdown.runtime.midi_io import MIDIOutputManager midi = MIDIOutputManager() midi.open_port(0) scheduler = EventScheduler(midi) events = [ ... ScheduledEvent(0.0, [0x90, 60, 80], {}), ... ScheduledEvent(1000.0, [0x80, 60, 0], {}), ... ] scheduler.load_events(events) scheduler.start()
... wait for completion ...¶
scheduler.stop() midi.close_port()
Initialize event scheduler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
midi_output
|
MIDIOutputManager
|
MIDIOutputManager instance for sending MIDI messages |
required |
Source code in src/midi_markdown/runtime/scheduler.py
Functions¶
Load events into scheduler queue.
Clears any existing events and loads the new event list into the internal PriorityQueue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events
|
list[ScheduledEvent]
|
List of ScheduledEvent objects (will be sorted by time_ms) |
required |
Example
scheduler.load_events([ ... ScheduledEvent(0.0, [0x90, 60, 80], {}), ... ScheduledEvent(500.0, [0x80, 60, 0], {}), ... ])
Source code in src/midi_markdown/runtime/scheduler.py
Start playback in separate thread.
Creates and starts a daemon thread that processes events from the queue. If already playing, this is a no-op.
Example
scheduler.load_events(events) scheduler.start()
Playback begins immediately in background thread¶
Source code in src/midi_markdown/runtime/scheduler.py
Pause playback (preserves position).
Records the current time so that resume() can adjust the time offset to account for the pause duration.
Example
scheduler.start()
... playback running ...¶
scheduler.pause()
Playback paused, position preserved¶
Source code in src/midi_markdown/runtime/scheduler.py
Resume from paused position.
Calculates the duration of the pause and adjusts the time offset so that playback continues from where it left off.
Example
scheduler.pause() time.sleep(2.0) # Paused for 2 seconds scheduler.resume()
Playback resumes, no timing drift¶
Source code in src/midi_markdown/runtime/scheduler.py
Stop playback and reset position.
Sets the stop flag and waits for the scheduler thread to finish (with 1-second timeout). Resets all timing state.
Example
scheduler.start()
... playback running ...¶
scheduler.stop()
Playback stopped, thread cleaned up¶
Source code in src/midi_markdown/runtime/scheduler.py
Seek to a specific time position.
Stops current playback, reloads events from target time onward, and adjusts timing state to resume from the new position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_time_ms
|
float
|
Target time in milliseconds from start |
required |
all_events
|
list[ScheduledEvent]
|
Complete list of all events (needed to reload from target time) |
required |
Example
scheduler.seek(5000.0, all_events) # Seek to 5 seconds
Source code in src/midi_markdown/runtime/scheduler.py
Tempo Tracker¶
tempo_tracker ¶
Tempo tracking for tick-to-millisecond conversion.
This module provides the TempoTracker class for converting between MIDI tick times and real-world millisecond times, accounting for tempo changes throughout a sequence.
Classes¶
TempoTracker ¶
Converts tick times to milliseconds using tempo map.
The TempoTracker maintains a tempo map that allows accurate conversion between MIDI tick times and real-world millisecond times, accounting for tempo changes throughout a sequence.
Example
tracker = TempoTracker(ppq=480, default_tempo=120.0) tracker.add_tempo_change(960, 140.0) # Change to 140 BPM at tick 960 tracker.build_tempo_map() print(tracker.ticks_to_ms(1440)) # Convert tick 1440 to milliseconds 1428.57...
Initialize tempo tracker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ppq
|
int
|
Pulses per quarter note (ticks per beat) |
required |
default_tempo
|
float
|
Initial tempo in BPM |
120.0
|
Source code in src/midi_markdown/runtime/tempo_tracker.py
Functions¶
Register a tempo change at specified tick.
Tempo changes can be added in any order - they will be sorted when build_tempo_map() is called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tick
|
int
|
Absolute tick time where tempo changes |
required |
tempo
|
float
|
New tempo in BPM |
required |
Example
tracker = TempoTracker(ppq=480) tracker.add_tempo_change(480, 90.0) tracker.add_tempo_change(960, 140.0)
Source code in src/midi_markdown/runtime/tempo_tracker.py
Calculate cumulative milliseconds for each tempo segment.
This method must be called after all tempo changes are added and before ticks_to_ms() or ms_to_ticks() are used. It: 1. Sorts segments by tick 2. Ensures a segment exists at tick 0 (uses default_tempo if needed) 3. Calculates cumulative milliseconds for each segment
Example
tracker = TempoTracker(ppq=480, default_tempo=120.0) tracker.add_tempo_change(960, 140.0) tracker.build_tempo_map()
Source code in src/midi_markdown/runtime/tempo_tracker.py
Convert absolute tick time to milliseconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ticks
|
int
|
Absolute tick time |
required |
Returns:
| Type | Description |
|---|---|
float
|
Time in milliseconds |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If tempo map not built (call build_tempo_map() first) |
Example
tracker = TempoTracker(ppq=480, default_tempo=120.0) tracker.build_tempo_map() tracker.ticks_to_ms(480) # 1 beat at 120 BPM = 500ms 500.0
Source code in src/midi_markdown/runtime/tempo_tracker.py
Convert milliseconds to absolute tick time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ms
|
float
|
Time in milliseconds |
required |
Returns:
| Type | Description |
|---|---|
int
|
Absolute tick time |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If tempo map not built (call build_tempo_map() first) |
Example
tracker = TempoTracker(ppq=480, default_tempo=120.0) tracker.build_tempo_map() tracker.ms_to_ticks(500.0) # 500ms at 120 BPM = 1 beat = 480 ticks 480
Source code in src/midi_markdown/runtime/tempo_tracker.py
Realtime Player¶
player ¶
Real-time MIDI playback engine.
This module provides the RealtimePlayer class for playing compiled MML programs in real-time through MIDI output ports.
Classes¶
RealtimePlayer ¶
High-level real-time MIDI playback from IRProgram.
The RealtimePlayer orchestrates MIDI I/O, tempo tracking, and event scheduling to play compiled MML programs in real-time through MIDI output devices.
Example
from midi_markdown.core.compiler import compile_mml from midi_markdown.runtime.player import RealtimePlayer ir = compile_mml("examples/00_basics/00_hello_world.mmd") player = RealtimePlayer(ir, "IAC Driver Bus 1") player.play()
... wait for completion ...¶
player.stop()
Initialize real-time player.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ir_program
|
IRProgram
|
Compiled IR program to play |
required |
port_name
|
str | int
|
MIDI output port name or index |
required |
Raises:
| Type | Description |
|---|---|
MIDIIOError
|
If MIDI port cannot be opened |
Source code in src/midi_markdown/runtime/player.py
Functions¶
Resume from paused position.
If not paused, this is a no-op. Time offset is adjusted to account for pause duration.
Stop playback and send All Notes Off.
Stops the scheduler, sends CC 123 (All Notes Off) on all 16 channels to prevent stuck notes, and resets playback position.
Source code in src/midi_markdown/runtime/player.py
Seek to a specific time position.
Stops current playback, sends All Notes Off to prevent stuck notes, and resumes playback from the target time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_time_ms
|
float
|
Target time in milliseconds from start |
required |
Example
player.seek(5000.0) # Seek to 5 seconds
Source code in src/midi_markdown/runtime/player.py
Seek forward or backward by a number of bars.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bar_offset
|
int
|
Number of bars to seek (positive=forward, negative=backward) |
required |
current_position_ms
|
float
|
Current playback position in milliseconds |
required |
Returns:
| Type | Description |
|---|---|
float
|
New position in milliseconds after seeking |
Example
new_pos = player.seek_bars(1, 5000.0) # Seek forward 1 bar new_pos = player.seek_bars(-2, 10000.0) # Seek backward 2 bars
Source code in src/midi_markdown/runtime/player.py
Seek forward or backward by a number of beats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beat_offset
|
int
|
Number of beats to seek (positive=forward, negative=backward) |
required |
current_position_ms
|
float
|
Current playback position in milliseconds |
required |
Returns:
| Type | Description |
|---|---|
float
|
New position in milliseconds after seeking |
Example
new_pos = player.seek_beats(4, 5000.0) # Seek forward 4 beats new_pos = player.seek_beats(-1, 10000.0) # Seek backward 1 beat
Source code in src/midi_markdown/runtime/player.py
Get total duration in milliseconds.
Returns:
| Type | Description |
|---|---|
float
|
Duration in milliseconds, or 0.0 if no events |
Source code in src/midi_markdown/runtime/player.py
Check if playback is complete.
Returns:
| Type | Description |
|---|---|
bool
|
True if scheduler is stopped, False otherwise |
TUI Components¶
state ¶
Thread-safe state management for TUI.
This module provides the TUIState class for managing UI state updates from multiple threads (main UI thread, scheduler thread, keyboard thread).
Classes¶
TUIState ¶
Thread-safe state container for TUI updates.
This class manages all state that needs to be shared between threads: - Main UI thread: Reads state for rendering - Scheduler thread: Updates position and events - Keyboard thread: Updates playback state
All public methods are thread-safe using a lock.
Initialize TUI state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
total_duration_ms
|
float
|
Total duration of playback in milliseconds |
required |
initial_tempo
|
float
|
Initial tempo in BPM |
required |
max_events
|
int
|
Maximum number of events to keep in history |
20
|
Source code in src/midi_markdown/runtime/tui/state.py
Update current playback position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position_ms
|
float
|
Current position in milliseconds |
required |
position_ticks
|
int
|
Current position in ticks |
required |
Source code in src/midi_markdown/runtime/tui/state.py
Update current tempo.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tempo
|
float
|
Tempo in BPM |
required |
Set playback state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
One of "stopped", "playing", "paused" |
required |
Add event to history buffer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_info
|
EventInfo
|
Event information to add |
required |
Get atomic snapshot of current state.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with all state fields |
Source code in src/midi_markdown/runtime/tui/state.py
components ¶
Rich UI components for TUI display.
This module provides reusable Rich components for rendering the TUI: - Header: File name, port, title - Progress bar: Visual playback progress - Event list: Scrolling MIDI event history - Status bar: Time, tempo, state - Controls: Keyboard shortcut help
display ¶
TUI Display Manager with Rich Live integration.
This module provides the TUIDisplayManager class that manages the live terminal display using Rich's Live context for flicker-free updates at 30 FPS.
Classes¶
TUIDisplayManager ¶
TUIDisplayManager(state: TUIState, file_name: str, port_name: str, title: str | None = None, refresh_rate: int = 30)
Manages live terminal display with Rich Live.
This class coordinates the display of all TUI components and manages the refresh loop at a target rate of 30 FPS (every ~33ms).
The display runs in the main thread while playback happens in a background thread, with state synchronized through TUIState.
Initialize TUI display manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
TUIState
|
TUIState instance for reading display data |
required |
file_name
|
str
|
Name of the MML file being played |
required |
port_name
|
str
|
MIDI output port name |
required |
title
|
str | None
|
Optional song title |
None
|
refresh_rate
|
int
|
Target refresh rate in FPS (default: 30) |
30
|
Source code in src/midi_markdown/runtime/tui/display.py
Start the live display.
This should be called before starting playback. The display will run until stop() is called.
Source code in src/midi_markdown/runtime/tui/display.py
Update the display with current state.
This should be called periodically (e.g., every 33ms for 30 FPS).
Source code in src/midi_markdown/runtime/tui/display.py
Run the display update loop until stopped.
This is a blocking call that continuously updates the display at the target refresh rate. Call this in the main thread while playback runs in a background thread.
Source code in src/midi_markdown/runtime/tui/display.py
Validation¶
Document Validator¶
document_validator ¶
Document structure and command validation.
Timing Validator¶
timing_validator ¶
Timing sequence validation.
Value Validator¶
value_validator ¶
MIDI value range validation.
Alias System¶
Alias Resolver¶
resolver ¶
Alias resolver for expanding device-specific commands.
The alias system is a core feature of MML that allows device-specific shortcuts.
Classes¶
AliasResolver ¶
Resolves and expands aliases to MIDI commands.
The resolver: 1. Validates alias usage 2. Binds arguments to parameters 3. Validates parameter ranges and types 4. Evaluates computed values (mathematical expressions) 5. Evaluates conditional branches (if/elif/else) 6. Expands aliases to base MIDI commands
Stage 1: Basic expansion (no nesting, no imports, no computation) Stage 2: Enhanced parameter types (note names, percentages, booleans) Stage 3: Nested aliases (aliases calling other aliases) Stage 6: Computed values (mathematical expressions in parameters) Stage 7: Conditional logic (@if/@elif/@else branches)
Initialize the alias resolver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aliases
|
dict[str, AliasDefinition] | None
|
Dictionary of alias definitions (name -> AliasDefinition) |
None
|
max_depth
|
int
|
Maximum allowed nesting depth (default: 10) |
10
|
Source code in src/midi_markdown/alias/resolver.py
Functions¶
resolve(alias_name: str, arguments: list[Any], timing: Any = None, source_line: int = 0, _expansion_stack: list[tuple[str, list[Any]]] | None = None, _depth: int = 0) -> list[MIDICommand]
Resolve an alias to MIDI commands with nesting support.
This method now supports nested aliases (aliases calling other aliases) with cycle detection and depth limiting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alias_name
|
str
|
The alias name to expand |
required |
arguments
|
list[Any]
|
Arguments passed to the alias |
required |
timing
|
Any
|
Timing information to apply to expanded commands |
None
|
source_line
|
int
|
Source line number for error reporting |
0
|
_expansion_stack
|
list[tuple[str, list[Any]]] | None
|
Internal - tracks current expansion path for cycle detection |
None
|
_depth
|
int
|
Internal - current expansion depth for depth limiting |
0
|
Returns:
| Type | Description |
|---|---|
list[MIDICommand]
|
List of expanded MIDI commands (fully resolved, no nested alias calls) |
Raises:
| Type | Description |
|---|---|
AliasError
|
If alias not found or parameters invalid |
AliasRecursionError
|
If circular dependency detected |
AliasMaxDepthError
|
If expansion exceeds max_depth |
Note
The _expansion_stack and _depth parameters are for internal use only and should not be passed by external callers.
Source code in src/midi_markdown/alias/resolver.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
Import Resolver¶
imports ¶
Import manager for loading device libraries and alias definitions.
This module handles: - Resolving import paths (relative/absolute) - Loading and parsing device library files - Detecting circular imports - Merging alias definitions from multiple sources
Alias Models¶
models ¶
Data models for alias expansion tracking.
This module provides data structures for tracking alias expansion state, including call chains and expansion nodes.
Conditionals¶
conditionals ¶
Conditional evaluation for alias branching logic (Stage 7).
This module provides the ConditionalEvaluator class that evaluates conditional expressions in alias definitions (@if/@elif/@else) and selects the appropriate branch to execute based on parameter values.
Computation¶
computation ¶
Safe computation engine for evaluating expressions in alias definitions.
This module provides secure evaluation of mathematical expressions in computed value blocks, with strict security controls to prevent code injection.
Command Expansion¶
Command Expander¶
expander ¶
Command Expander - Unified orchestrator for variable expansion, loops, and sweeps.
Phase 4: Coordinates all expansion operations and provides a clean interface for the compilation pipeline.
Classes¶
CommandExpander ¶
CommandExpander(ppq: int = 480, tempo: float = 120.0, time_signature: tuple[int, int] = (4, 4), source_file: str = '<unknown>')
Unified orchestrator for command expansion.
Coordinates: - Variable definitions (@define) via SymbolTable - Loop expansion via LoopExpander - Sweep expansion via SweepExpander - Variable substitution in commands - Event sorting and validation
Usage
expander = CommandExpander(ppq=480, tempo=120.0) expanded_events = expander.process_ast(ast_nodes)
Initialize command expander.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ppq
|
int
|
Pulses per quarter note (MIDI resolution) |
480
|
tempo
|
float
|
Default tempo in BPM |
120.0
|
time_signature
|
tuple[int, int]
|
Time signature as (numerator, denominator) tuple, e.g., (4, 4) for 4/4 time |
(4, 4)
|
source_file
|
str
|
Source filename for error reporting |
'<unknown>'
|
Source code in src/midi_markdown/expansion/expander.py
Functions¶
Process AST nodes and return expanded events.
Uses two-pass processing: 1. Pass 1: Collect all @define statements 2. Pass 2: Expand loops, sweeps, and commands with variable substitution
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ast_nodes
|
list[Any]
|
List of AST nodes from parser |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
List of expanded event dictionaries sorted by time |
Raises:
| Type | Description |
|---|---|
ExpansionError
|
If expansion fails |
Source code in src/midi_markdown/expansion/expander.py
Variables¶
variables ¶
Symbol table implementation for MML variables. Handles variable definition, lookup, and scoping.
Loops¶
loops ¶
Loop implementation for MIDI Markup Language.
Provides @loop directive for repeating event sequences with: - Iteration control (count) - Timing intervals (beats, ticks, ms, BBT) - Loop variables (LOOP_INDEX, LOOP_ITERATION, LOOP_COUNT) - Scoped variable resolution
Phase 3 of Variables Implementation.
Sweeps¶
sweeps ¶
Sweep implementation for MIDI Markup Language.
Provides @sweep directive for value interpolation with: - Multiple interpolation curves (linear, exponential, logarithmic, ease-in/out) - Time-based or step-based interpolation - Support for any MIDI value parameter
Phase 3 of Variables Implementation.
Utilities¶
Parameter Types¶
parameter_types ¶
Parameter Type Conversion Utilities
Provides functions to convert various parameter types (note names, percentages, booleans) to MIDI values. Used by both the parser and alias system.
Constants¶
constants ¶
MIDI Markup Language Constants
Defines all magic numbers and constants used throughout the MML implementation.
Attributes¶
CLI¶
Main Application¶
main ¶
CLI entry point for MIDI Markdown.
This module provides the main command-line interface using Typer. All command implementations are in the commands/ package.
Error Formatting¶
errors ¶
Rich error display formatting for the MIDI Markup Language compiler.
This module provides structured error display with: - Source code context with line numbers - Visual indicators (carets, colors) pointing to error locations - Error codes (E1xx parse, E2xx validation, E3xx type, E4xx file/import) - Helpful suggestions and "did you mean?" hints - Accessibility support (--no-color, --no-emoji flags)
Type Reference¶
Common Types¶
# Timing modes
TimingMode = Literal["absolute", "musical", "relative", "simultaneous"]
# Event types
EventType = Literal[
"note_on",
"note_off",
"cc",
"pc",
"pitch_bend",
"poly_pressure",
"channel_pressure",
"tempo",
"time_signature",
"marker",
"text",
"sysex"
]
# Command types in AST
CommandType = Literal[
"note_on",
"note_off",
"cc",
"pc",
"pitch_bend",
"poly_pressure",
"channel_pressure",
"tempo",
"time_signature",
"marker",
"text",
"sysex"
]
# Parameter types for aliases
ParameterType = Literal["int", "note", "percent", "bool", "enum"]
Data Structures¶
# MIDI Event (IR layer)
@dataclass
class MIDIEvent:
time: int # Absolute ticks
type: EventType # Event type
channel: int | None = None # 1-16 for channel events
data1: int | None = None # Note/CC number, etc.
data2: int | None = None # Velocity/value, etc.
metadata: dict[str, Any] = field(default_factory=dict)
# Timing
@dataclass
class Timing:
mode: str # "absolute", "musical", "relative", "simultaneous"
value: int | tuple[int, ...] # Absolute ticks or (bars, beats, ticks)
# MIDI Command (AST)
@dataclass
class MIDICommand:
type: str # Command type
channel: int # 1-16
data1: int | None = None # Note/CC number
data2: int | None = None # Velocity/value
duration: int | None = None # For notes with duration
Implementation Notes¶
Pipeline Overview¶
Input (.mmd)
↓
Parser → Parse to AST (Lark grammar + transformer)
↓
Import Resolver → Load device library aliases
↓
Alias Resolver → Expand alias calls to MIDI commands
↓
Command Expander → Expand variables, loops, sweeps, conditionals
↓
Validator → Check ranges, timing, values
↓
IR Compiler → Convert to Intermediate Representation
↓
IRProgram → Query-able event list with metadata
↓
┌───────────────────┬──────────────────────────┐
│ Codegen │ Runtime (Phase 3) │
│ - MIDI file │ - Real-time MIDI I/O │
│ - JSON export │ - Event scheduler │
│ - CSV export │ - Tempo tracker │
│ │ - TUI player │
└───────────────────┴──────────────────────────┘
↓ ↓
Output (.mid, .json) Live MIDI Output
Key Design Patterns¶
- Functional Core: Codegen functions are pure (no side effects)
- Immutable AST: AST nodes are frozen dataclasses
- Progressive Enhancement: Each pipeline stage adds information
- Type Safety: Full type hints with mypy validation
- Error Context: All errors track source location
Testing Strategy¶
- 1090+ tests (72.53% coverage)
- Unit tests: Component isolation
- Integration tests: Multi-component workflows
- E2E tests: Full compilation pipeline
- Fixtures: 37 test MMD files (25 valid, 12 invalid)
See Also¶
- Parser Design - Parser and lexer architecture details
- Language Specification - Complete MMD spec
- CLAUDE.md - Developer context