Skip to main content

README.md

# 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