defmodule ExMidi.MidiUtil do
@moduledoc """
Utility functions for MIDI data.
"""
@microsecs_per_minute 60_000_000
@note_names ~w(C C# D D# E F F# G G# A A# B)
@doc "Returns the list of note names."
def note_names, do: @note_names
@doc "Returns the length of a note type in beats."
def note_length(:whole), do: 4.0
def note_length(:half), do: 2.0
def note_length(:quarter), do: 1.0
def note_length(:eighth), do: 0.5
def note_length(:"8th"), do: 0.5
def note_length(:sixteenth), do: 0.25
def note_length(:"16th"), do: 0.25
def note_length(:thirtysecond), do: 0.125
def note_length(:"thirty second"), do: 0.125
def note_length(:"32nd"), do: 0.125
def note_length(:sixtyfourth), do: 0.0625
def note_length(:"sixty fourth"), do: 0.0625
def note_length(:"64th"), do: 0.0625
@doc "Convert BPM to microseconds per quarter note."
def bpm_to_mpq(bpm), do: @microsecs_per_minute / bpm
@doc "Convert microseconds per quarter note to BPM."
def mpq_to_bpm(mpq), do: @microsecs_per_minute / mpq
@doc "Quantize a track's event delta times to the nearest multiple of boundary."
def quantize({:track, events}, boundary) do
{new_events, _} =
Enum.map_reduce(events, 0, fn event, beats_from_start ->
quantized_event(event, beats_from_start, boundary)
end)
{:track, new_events}
end
def quantize([], _boundary), do: []
def quantize(events, boundary) when is_list(events) do
{new_events, _} =
Enum.map_reduce(events, 0, fn event, beats_from_start ->
quantized_event(event, beats_from_start, boundary)
end)
new_events
end
defp quantized_event({name, delta_time, values}, beats_from_start, boundary) do
new_delta = quantized_delta_time(beats_from_start, delta_time, boundary)
{{name, new_delta, values}, beats_from_start + delta_time}
end
defp quantized_delta_time(beats_from_start, delta_time, boundary) do
diff = div(beats_from_start + delta_time, boundary)
if diff >= boundary / 2 do
delta_time - diff
else
delta_time - diff + boundary
end
end
@doc "Convert a MIDI note number to a string like \"C4\"."
def note_to_string(num) do
note = rem(num, 12)
octave = div(num, 12)
Enum.at(@note_names, note) <> "#{octave - 1}"
end
@doc "Convert a sequence to text representation."
def seq_to_text(seq), do: seq_to_text(seq, false)
def seq_to_text({:seq, _, tracks}, show_chan_events) do
seq_to_text({:seq, tracks}, show_chan_events)
end
def seq_to_text({:seq, _header, _meta_track, tracks}, show_chan_events) do
seq_to_text({:seq, tracks}, show_chan_events)
end
def seq_to_text({:seq, tracks}, show_chan_events) do
Enum.each(tracks, &track_to_text(&1, show_chan_events))
:ok
end
@doc "Convert a track to text representation."
def track_to_text(track), do: track_to_text(track, false)
def track_to_text(track, show_chan_events) do
IO.puts("\n*** Track start ***\n")
{:track, events} = track
Enum.each(events, &event_to_text(&1, show_chan_events))
:ok
end
@doc "Convert an event to text representation."
def event_to_text(event), do: event_to_text(event, false)
def event_to_text({name, _} = event, show_chan_events) do
event_to_text(event, name, show_chan_events)
end
def event_to_text({name, _, _} = event, show_chan_events) do
event_to_text(event, name, show_chan_events)
end
defp event_to_text(event, name, show_chan_events) do
chan_events = [:off, :on, :poly_press, :controller, :program, :chan_press, :pitch_bend]
is_chan_event = name in chan_events
cond do
name == :track_end -> :ok
show_chan_events or not is_chan_event -> IO.inspect(event)
true -> IO.inspect(name)
end
end
end