# `TuningFork.Midi`
[🔗](https://github.com/jaman/tuning_fork/blob/v0.1.12/tuning_fork/lib/tuning_fork/midi.ex#L1)

Reads a Standard MIDI File, and turns one into a `TuningFork.Score`.

    "song.mid"
    |> TuningFork.Midi.read!()
    |> TuningFork.Midi.to_score(synths: %{0 => lead, 1 => bass}, default: pad)
    |> TuningFork.Score.render(44_100)

# `event`

```elixir
@type event() ::
  {:note_on, 0..15, 0..127, 0..127}
  | {:note_off, 0..15, 0..127, 0..127}
  | {:program, 0..15, 0..127}
  | {:control, 0..15, 0..127, 0..127}
  | {:pitch_bend, 0..15, integer()}
  | {:tempo, pos_integer()}
  | {:track_name, binary()}
  | {:sysex, binary()}
```

# `t`

```elixir
@type t() :: %TuningFork.Midi{
  division: pos_integer(),
  format: 0..2,
  tracks: [[{non_neg_integer(), event()}]]
}
```

# `drum_channels`

```elixir
@spec drum_channels(t(), keyword()) :: [0..15]
```

Which channels hold drums, resolving `:auto` against the file.

Takes `to_score/2`'s `:drum_channels` option and returns the channel list it would use.
With `:auto`, channel 9 is always included, and another channel is included only when it
has no program change, every note on it is in 27..87, and at least half its distinct note
numbers are in 35..59.

# `encode`

```elixir
@spec encode(TuningFork.Score.t(), keyword()) :: binary()
```

A `TuningFork.Score` as a Standard MIDI File, ready to write.

    iex> score = TuningFork.Score.new(bpm: 120, beats: 4)
    iex> <<"MThd", _rest::binary>> = TuningFork.Midi.encode(score)

Writes format 0, one track, 480 ticks a beat. A note becomes a note on and a note off
separated by the voice's duration; a voice's pitch becomes the nearest semitone and its gain
becomes velocity. The score's tempo is the first event. Nothing else about a voice is
written.

## Options

  * `:channel` — 1 to 16, default 1
  * `:name` — a track name to write into the file

# `merged`

```elixir
@spec merged(t()) :: [{non_neg_integer(), event()}]
```

Every event in the file, in tick order, as `{tick, event}`, with the tracks merged.

# `mini`

```elixir
@spec mini(t(), keyword()) :: {non_neg_integer(), String.t()}
```

One channel of the file as Strudel mini-notation: a `<…>` of bars, each a `[…]` of
steps — a note, a chord `[c4,e4]`, or a rest `~`, held with `@n` — and how many bars
it is, so a recorded piece can be played through the kit's instruments.

    {bars, mini} = TuningFork.Midi.mini(midi, channel: 0, steps_per_beat: 4)
    Strudel.pattern(~s|note("#{mini}").s("gm_piano")|)

Onsets snap to the grid leaning late — an onset up to three quarters of a step after
a step is on it, so grace notes and rounding that push a file's notes late leave
them on their steps; lengths round; a note reaching past the next onset or the
bar's end is cut there; a length under a step is a step.

## Options

  * `:channel` — the channel to take, default `0`
  * `:beats_per_bar`, `:steps_per_beat` — the grid: `:beats_per_bar` default 4,
    `:steps_per_beat` default 4 (sixteenths)
  * `:from`, `:bars` — the first bar to take and how many; default from the start,
    to the last note
  * `:voice` — `:lowest` or `:highest` to take one note of every chord, default
    `:all`
  * `:on` — `:beats` to keep only the notes struck on a beat, for a bass line from
    a stride left hand; default `:steps`, every note
  * `:transpose` — semitones added to every note, default 0

# `notes`

```elixir
@spec notes(t()) :: [map()]
```

The notes in the file, paired up and measured in beats, ordered by beat.

Each note is `%{beat:, beats:, note:, velocity:, channel:, program:}`, where `:beat` and
`:beats` are floats, `:program` is the channel's program at the time the note started, and
the rest come from the file. A second note on for a pitch already sounding closes the
first; a note left open at the end of the file is given one beat.

# `parse`

```elixir
@spec parse(binary()) :: {:ok, t()} | {:error, term()}
```

Parse a MIDI file of format 0, 1 or 2 with ticks-per-beat division.

Tracks come back with absolute ticks rather than the deltas the file stores. Chunks that are
not tracks are skipped, a note on at velocity zero is returned as a note off, and aftertouch
is dropped.

A system exclusive message is returned whole as `{:sysex, bytes}`, from its `0xF0` to its
closing `0xF7`, at the tick it starts on. A message the file splits across a `0xF0` packet
and following `0xF7` packets is joined into one. An `0xF7` packet that continues no open
message is dropped.

Fails with `{:error, :not_a_midi_file}`, `{:error, {:unsupported_format, format}}`,
`{:error, :smpte_division_unsupported}` or `{:error, :zero_division}`.

# `parse!`

```elixir
@spec parse!(binary()) :: t()
```

Parse a MIDI file, raising `ArgumentError` where `parse/1` would report an error.

# `read!`

```elixir
@spec read!(Path.t()) :: t()
```

Read and parse a MIDI file from disk. Raises as `parse!/1` does.

# `to_score`

```elixir
@spec to_score(t(), keyword()) :: TuningFork.Score.t()
```

Turn a parsed file into a score.

Tempo changes become the score's tempo map. A note's length is how long its key is held:
the synth's envelope runs at its own speeds and is let go at the note's end, as
`TuningFork.Envelope.gated/2` describes; drums keep their own length. Velocity scales the
synth's gain, and so do the channel's volume (controller 7) and expression (controller
11), as General MIDI has them: volume 100 and expression 127 until the file sets them, a
factor of volume / 127 × expression / 127, held as a `:gain` curve when either changes
while the note sounds. Pan (controller 10) sets the voice's `:pan`. Pitch bend becomes a
frequency multiplier and a `:freq` curve. Curves run over the voice's whole length,
release included. Other control changes are dropped.

## Options

  * `:synths` — `%{channel => Voice.t()}`
  * `:drums` — `%{note_number => Voice.t()}`, used on the percussion channels
  * `:drum_channels` — which channels those are, default `[9]`. `:auto` to guess, as
    `drum_channels/2` does
  * `:default` — what anything unclaimed is played with, default `TuningFork.Voice.new/1`
    with an envelope that sounds at full level for as long as the note is held
  * `:bpm` — play at this tempo, ignoring the file's own tempo and every change in it
  * `:quantize` — note lengths rounded to this many seconds, default 0.01. `nil` for none
  * `:bend_range` — semitones a full pitch bend covers, default 2
  * `:bend_points` — most breakpoints kept per bend curve, default 8
  * `:level_points` — most breakpoints kept per volume and expression curve, default 32
  * `:beats` — how long the score is, default however long the file runs, rounded up

# `write!`

```elixir
@spec write!(TuningFork.Score.t(), Path.t(), keyword()) :: :ok
```

Write `score` to `path` as a Standard MIDI File.

Takes `encode/2`'s options.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
