TuningFork.Midi (TuningFork v0.1.11)

Copy Markdown View Source

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)

Summary

Functions

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

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

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

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.

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

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

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

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

Turn a parsed file into a score.

Write score to path as a Standard MIDI File.

Types

event()

@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()}

t()

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

Functions

drum_channels(midi, opts \\ [])

@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(score, opts \\ [])

@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(midi)

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

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

mini(midi, opts \\ [])

@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(midi)

@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(arg1)

@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.

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

parse!(binary)

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

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

read!(path)

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

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

to_score(midi, opts \\ [])

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

Turn a parsed file into a score.

Tempo changes become the score's tempo map, note lengths become envelope decay, velocity scales the synth's gain, and pitch bend becomes a frequency multiplier and a :freq curve. Control changes other than pitch bend 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
  • :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
  • :beats — how long the score is, default however long the file runs, rounded up

write!(score, path, opts \\ [])

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

Write score to path as a Standard MIDI File.

Takes encode/2's options.