# ExMidi
Elixir MIDI library — message construction, binary encode/decode, Standard MIDI File (SMF) read/write, SYX support, and streaming parser. Inspired by midi (Erlang) & mido (Python) lib.
All MIDI messages are represented as `{:midi, payload}` tuples, providing a unified API across all modules.
---
## Installation
Add `ex_midi` to your `mix.exs`:
```elixir
def deps do
[
{:ex_midi, "~> 0.1.0"}
]
end
```
Then run:
```sh
mix deps.get
```
---
## Quick Start
```elixir
alias ExMidi.MidiMsg
alias ExMidi.MidiBin
alias ExMidi.MidiMessage
alias ExMidi.MidiFile
alias ExMidi.MidiParser
```
### Message construction (tuple-based)
```elixir
msg = MidiMsg.note_on(1, 60, 100)
# => {:midi, {:note_on, [channel: 1, pitch: 60, velocity: 100]}}
msg = MidiMsg.note_on(60, 100)
# => {:midi, {:note_on, [pitch: 60, velocity: 100]}}
```
### Binary encode/decode
```elixir
MidiBin.encode(MidiMsg.note_on(1, 60, 100))
# => <<144, 60, 100>>
MidiBin.decode(<<144, 60, 100>>)
# => {:midi, {:note_on, [channel: 1, pitch: 60, velocity: 100]}}
```
### Frozen message struct (rich API)
```elixir
msg = MidiMessage.new(:note_on, channel: 1, pitch: 60, velocity: 100)
MidiMessage.to_bytes(msg)
# => <<144, 60, 100>>
MidiMessage.to_hex(msg)
# => "90 3C 64"
MidiMessage.copy(msg, channel: 2)
# => MidiMessage with channel=2
```
### Streaming parser
```elixir
parser = MidiParser.new()
parser = MidiParser.feed_bytes(parser, [144, 60, 100])
{msg, parser} = MidiParser.parse(parser)
# => {:midi, {:note_on, [channel: 1, pitch: 60, velocity: 100]}}
```
### MIDI File read/write
```elixir
# Read
midi = MidiFile.read("song.mid")
# Inspect
MidiFile.format(midi) # 0, 1, or 2
MidiFile.division(midi) # ticks per quarter note
MidiFile.tracks(midi) # list of tracks
# Playback simulation
for msg <- MidiFile.play(midi), do: IO.inspect(msg)
```
### SYX (System Exclusive) files
```elixir
alias ExMidi.MidiSyx
# Read
messages = MidiSyx.read_file("patch.syx")
# Write (binary format)
MidiSyx.write_file("patch.syx", messages)
# Write (plaintext hex format)
MidiSyx.write_file("patch.syx", messages, plaintext: true)
```
---
## Message Types
### Channel Voice
| Function | Description |
|---|---|
| `MidiMsg.note_on/2, /3` | Note on (pitch, velocity) |
| `MidiMsg.note_off/1, /2, /3` | Note off |
| `MidiMsg.aftertouch/1, /2` | Channel aftertouch |
| `MidiMsg.poly_aftertouch/2, /3` | Polyphonic aftertouch |
| `MidiMsg.pitch_bend/1, /2, /3` | Pitch bend |
| `MidiMsg.program_change/1, /2` | Program change |
| `MidiMsg.cc/2, /3` | Control change |
### Control Change (Channel Mode)
| Message | Control Value |
|---|---|
| All Sound Off | `MidiMsg.cc(1, 120, 0)` |
| Reset All Controllers | `MidiMsg.cc(1, 121, 0)` |
| Local Control Off | `MidiMsg.cc(1, 122, 0)` |
| All Notes Off | `MidiMsg.cc(1, 123, 0)` |
| Omni Mode Off | `MidiMsg.cc(1, 124, 0)` |
| Omni Mode On | `MidiMsg.cc(1, 125, 0)` |
| Poly Mode On | `MidiMsg.cc(1, 127, 0)` |
Use `MidiMsg.cc(channel, control, value)` for generic CC messages, or
`MidiBin.encode/1` on the `{:mode, ...}` tuple for strict mode messages.
### System Common
| Function | Description |
|---|---|
| `MidiMsg.sys_ex/1` | System Exclusive |
| `:tune_request` | Tune Request |
### Real-Time
| Function | Description |
|---|---|
| `MidiMsg.rt_clock/0` | MIDI clock (24 per quarter) |
| `MidiMsg.rt_start/0` | Start |
| `MidiMsg.rt_continue/0` | Continue |
| `MidiMsg.rt_stop/0` | Stop |
| `MidiMsg.rt_tick/0` | Tick |
| `MidiMsg.rt_reset/0` | Reset |
### Meta Messages
| Function | Description |
|---|---|
| `MidiMsg.text/1` | Text event |
| `MidiMsg.copyright/1` | Copyright notice |
| `MidiMsg.track_sequence_name/1` | Track/sequence name |
| `MidiMsg.instrument/1` | Instrument name |
| `MidiMsg.lyric/1` | Lyric |
| `MidiMsg.marker/1` | Marker |
| `MidiMsg.cuepoint/1` | Cue point |
| `MidiMsg.tempo_bpm/1` | Tempo (BPM) |
| `MidiMsg.time_sig/4` | Time signature |
| `MidiMsg.keysig/4` | Key signature |
| `MidiMsg.smpte/5` | SMPTE offset |
| `MidiMsg.sequencer_data/1` | Sequencer-specific data |
| `MidiMsg.sequence_number/1` | Sequence number |
| `MidiMsg.device/1` | MIDI device port |
| `MidiMsg.program/1` | Program name |
| `MidiMsg.undefined/1` | Undefined meta event |
---
## Module Overview
| Module | Description |
|---|---|
| `ExMidi` | Top-level API with `version/0` and `versions/0` |
| `ExMidi.MidiMsg` | Lightweight tuple-based message constructors |
| `ExMidi.MidiMessage` | Frozen struct with `copy/2`, `to_bytes/1`, `to_hex/1`, `to_map/1` |
| `ExMidi.MidiBin` | Binary encode/decode between tuples and wire format |
| `ExMidi.MidiParser` | Streaming byte-by-byte MIDI parser |
| `ExMidi.MidiFile` | Standard MIDI File read/write (Formats 0, 1, 2) |
| `ExMidi.MidiSyx` | SYX file read/write (binary and plaintext hex) |
| `ExMidi.MidiUtil` | Utilities: note names, quantize, BPM conversion, text output |
| `ExMidi.MidiLib` | Library version information |
---
## Development
```sh
# Setup
mix setup
# Run tests
mix test
# Code quality
mix quality
```
---
## License
Apache-2.0