Skip to main content

lib/ex_midi/midi_util.ex

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