mlua-pulse 0.1.0

Lua-friendly music composition and audio export bindings built on tunes and mlua
Documentation
local pulse = require("pulse")

local title = "solo-piano"
local bpm = 78.0
local beat = 60.0 / bpm
local export_options = {
  sample_rate = 44100,
  normalize = "rms",
  target_db = -19.0,
  true_peak_db = -1.0,
  midi_velocity_gain = 2.0,
  midi_min_velocity = 0.62,
}

local function rep(value, count)
  local values = {}
  for index = 1, count do
    values[index] = value
  end
  return values
end

local function degree(scale, value)
  local unique = #scale - 1
  local zero = value - 1
  local octave = math.floor(zero / unique)
  local position = (zero % unique) + 1
  return pulse.transpose(scale[position], octave * 12)
end

local function line(scale, degrees)
  local notes = {}
  for index, value in ipairs(degrees) do
    notes[index] = degree(scale, value)
  end
  return notes
end

local function voicing(root, kind, shifts)
  local source = pulse.chord(root, kind)
  local chord = {}
  for index, shift in ipairs(shifts) do
    chord[index] = pulse.transpose(source[((index - 1) % #source) + 1], shift)
  end
  return chord
end

local function piano_phrase(chords, bass_notes, melody_scale, melody_degrees, lead_instrument)
  return pulse.phrase()
    :add(
      pulse.sequence()
        :chords(chords)
        :durations(rep(beat * 2.0, #chords))
        :instrument("acoustic_piano")
        :volume(0.7)
        :pan(-0.08)
        :velocity(0.58)
        :effect("reverb", "room")
    )
    :add(
      pulse.sequence()
        :notes(bass_notes)
        :durations(rep(beat, #bass_notes))
        :instrument("acoustic_piano")
        :volume(0.74)
        :pan(-0.2)
        :velocity(0.62)
    )
    :add(
      pulse.sequence()
        :notes(line(melody_scale, melody_degrees))
        :durations(rep(beat / 2.0, #melody_degrees))
        :instrument(lead_instrument)
        :volume(0.42)
        :pan(0.18)
        :velocity(0.5)
        :at(beat)
        :effect("delay", { time = beat * 1.5, feedback = 0.18, mix = 0.14 })
    )
end

local c_ionian = pulse.scale("C4", "ionian")
local a_aeolian = pulse.scale("A3", "minor")
local f_lydian = pulse.scale("F3", "lydian")
local g_mixolydian = pulse.scale("G3", "mixolydian")

local intro = piano_phrase({
  voicing("C3", "maj7", {0, 0, 12, 12}),
  voicing("G2", "dominant7", {0, 12, 0, 12}),
  voicing("A2", "min7", {0, 12, 0, 12}),
  voicing("F2", "maj7", {0, 12, 0, 12}),
}, line(pulse.scale("C2", "ionian"), {1, 5, 8, 5, 6, 3, 4, 1}), c_ionian,
  {3, 5, 8, 10, 9, 8, 6, 5, 4, 6, 8, 9, 8, 6, 5, 3}, "electric_piano")
  :repeat_times(2)

local theme = piano_phrase({
  voicing("A2", "min7", {0, 12, 0, 12}),
  voicing("E2", "dominant7", {0, 12, 0, 12}),
  voicing("F2", "maj7", {0, 12, 0, 12}),
  voicing("C3", "maj7", {0, 0, 12, 12}),
}, line(pulse.scale("A1", "minor"), {1, 5, 8, 5, 6, 3, 5, 1}), a_aeolian,
  {1, 3, 5, 8, 10, 8, 6, 5, 4, 6, 8, 11, 10, 8, 6, 5}, "acoustic_piano")
  :repeat_times(2)

local bridge = piano_phrase({
  voicing("F2", "maj7", {0, 12, 0, 12}),
  voicing("D2", "min7", {0, 12, 0, 12}),
  voicing("G2", "dominant7", {0, 12, 0, 12}),
  voicing("C3", "maj7", {0, 0, 12, 12}),
}, line(pulse.scale("F1", "lydian"), {1, 5, 8, 5, 6, 3, 5, 1}), f_lydian,
  {1, 3, 5, 7, 8, 10, 12, 10, 9, 8, 6, 5, 4, 3, 2, 1}, "electric_piano")
  :repeat_times(2)

local reprise = pulse.phrase()
  :add(
    pulse.sequence()
      :chords({
        voicing("C3", "maj7", {0, 0, 12, 12}),
        voicing("A2", "min7", {0, 12, 0, 12}),
        voicing("F2", "maj7", {0, 12, 0, 12}),
        voicing("G2", "dominant7", {0, 12, 0, 12}),
      })
      :durations(rep(beat * 2.0, 4))
      :instrument("acoustic_piano")
      :volume(0.68)
      :pan(-0.08)
      :velocity(0.52)
      :effect("reverb", "chamber")
  )
  :add(
    pulse.sequence()
      :notes(line(g_mixolydian, {8, 7, 6, 5, 4, 3, 2, 1, 3, 5, 8, 10, 8, 5, 3, 1}))
      :durations(rep(beat / 2.0, 16))
      :instrument("acoustic_piano")
      :volume(0.44)
      :pan(0.14)
      :velocity(0.48)
      :at(beat)
  )

local song = pulse.song()
  :tempo(bpm)
  :add_phrase(intro)
  :add_phrase(theme)
  :add_phrase(bridge)
  :add_phrase(reprise)
  :master_effect("reverb", "hall")
  :master_effect("eq", "warm")
  :master_effect("limiter", "transparent")

assert(title == "solo-piano")
song:export_wav("output/solo-piano.wav", export_options)
song:export_midi("output/solo-piano.mid", export_options)