Developer Docs

Audio API

Load a SoundFont or WAV sample and synthesise audio in real time alongside the tab β€” from note playback to custom metronome clicks.

Overview

The audio API lets a plugin synthesise audio using either a SoundFont 2 (.sf2) file or a pre-recorded WAV sample. SoundFonts are best for note-by-note synthesis; WAV samples are ideal for one-shot clicks, percussion, and metronomes where you want a very specific timbre.

Choose the right source: use SoundFonts when you need many playable notes; use WAV files when you want a specific short sample like a castanet click. WAV playback also supports semitone pitch shifts, so the same file can sound different on beat 1 without bundling multiple versions.

Methods

plugin.start_metronome(bpm: number, beats_per_measure: number [, accent_midi: number [, beat_midi: number [, velocity: number [, duration_ms: number]]]]) β†’ id

Starts a host-managed click track for manual practice mode and returns a metronome ID. Beat 1 in each measure uses accent_midi; all remaining beats use beat_midi.

Parameter Type Description
bpm number Tempo in beats per minute.
beats_per_measure number Time-signature numerator, such as 4 for 4/4 or 3 for 3/4.
accent_midi number MIDI note for beat 1. Defaults to 84.
beat_midi number MIDI note for non-accented beats. Defaults to 76.
velocity number MIDI-style velocity (0–127). Defaults to 100.
duration_ms number Click duration in milliseconds. Defaults to 70.
local metronome_id = plugin.start_metronome(120, 4)

plugin.on("playback.start", function(_)
  plugin.stop_metronome(metronome_id)
end)
plugin.stop_metronome(id: number)

Stops a metronome created by plugin.start_metronome. Calling it with an unknown or already-stopped ID is a no-op.

local id = plugin.start_metronome(90, 3)
plugin.stop_metronome(id)
plugin.load_wav(path: string) β†’ sample_id | nil, string

Loads a WAV file and returns a reusable sample handle. This is ideal for metronome clicks, percussion, and short one-shot sounds.

local click_id, err = plugin.load_wav(plugin.plugin_dir() .. "/castanet-hard_G.wav")
if not click_id then
  error(err)
end
plugin.play_wav(sample_id: number [, gain: number [, semitone_shift: number]])

Plays a previously loaded WAV sample immediately. Use semitone_shift to pitch the same sample up or down.

plugin.play_wav(click_id, 1.0, 0)   -- original pitch
plugin.play_wav(click_id, 1.0, 5)   -- up a fourth
plugin.play_wav(click_id, 0.8, -12) -- down an octave
plugin.start_wav_metronome(bpm: number, beats_per_measure: number, sample_id: number [, accent_semitones: number [, beat_semitones: number [, gain: number]]]) β†’ id

Starts a host-managed metronome that uses a WAV sample instead of MIDI notes. This is especially useful when you want beat 1 to reuse the same sample at a different pitch.

local id = plugin.start_wav_metronome(120, 4, click_id, 5, 0, 1.0)
plugin.stop_metronome(id)
plugin.load_soundfont(path: string) β†’ true | nil, string

Loads an SF2 file from the given absolute path. On success returns true. On failure returns nil and an error string. Calling this again replaces the previously loaded SoundFont β€” but only after the new file is fully parsed.

Parameter Type Description
path string Absolute path to an SF2 file
local ok, err = plugin.load_soundfont(plugin.plugin_dir() .. "/guitar.sf2")
if not ok then
  print("[MyPlugin] soundfont load failed: " .. err)
end
plugin.play_note(midi_note: number [, velocity: number [, duration_ms: number]])

Triggers playback of a MIDI note. The call returns immediately; playback happens asynchronously. Each concurrent call is independent β€” chords play without any extra logic on your part. Has no effect if no SoundFont has been loaded.

Parameter Type Description
midi_note number MIDI note number (0–127). Middle C is 60.
velocity number Playback velocity (0.0–1.0). Defaults to 0.8.
duration_ms number Note duration in milliseconds. Defaults to 500.
-- Play middle C for half a second at full velocity.
plugin.play_note(60, 1.0, 500)

-- Play with default velocity and duration.
plugin.play_note(60)
plugin.stop_all_notes()

Immediately silences all notes currently being synthesised by this plugin. Useful when the user closes the file or stops playback.

plugin.on("file.close", function(_)
  plugin.stop_all_notes()
end)
plugin.play_note_at(midi_note: number [, velocity: number [, duration_ms: number [, delay_ms: number]]])

Like plugin.play_note, but delays playback by delay_ms milliseconds before triggering the note. The call returns immediately β€” the delay and note synthesis happen on a background goroutine so the Lua VM and UI remain responsive. This makes it easy to play a sequence of notes (like a scale) with even timing from a single Lua call per note.

Parameter Type Description
midi_note number MIDI note number (0–127). Middle C is 60.
velocity number Playback velocity (0–127 MIDI scale). Defaults to 100.
duration_ms number Note duration in milliseconds. Defaults to 500.
delay_ms number Milliseconds to wait before playing. Defaults to 0 (play immediately).
-- Play an Am pentatonic scale ascending (12 notes, 200 ms apart).
local scale = 72
for i, midi in ipairs(scale) do
  plugin.play_note_at(midi, 80, 450, (i - 1) * 200)
end

MIDI Note Numbers

The note event gives you string and fret numbers, not MIDI notes. To convert, you need the open-string tuning of each string. Standard guitar tuning (low to high): E2, A2, D3, G3, B3, E4 β†’ MIDI 40, 45, 50, 55, 59, 64. GP string numbers are reversed: string 1 = thinnest (high e), string 6 = thickest (low E).

-- Standard guitar tuning: index 1 (thinnest) β†’ index 6 (thickest)
local STANDARD_TUNING = 40

local function fret_to_midi(string_num, fret)
  local open = STANDARD_TUNING[string_num]
  if not open then return nil end
  return open + fret
end

plugin.on("note", function(e)
  if e.tie_destination or e.dead_note then return end

  local midi = fret_to_midi(e.string, e.fret)
  if midi then
    -- Convert GP ticks to milliseconds.
    -- At the song's BPM: ms_per_tick = 60000 / (bpm * 960)
    local duration_ms = 400  -- safe default; tune to song tempo
    plugin.play_note(midi, 0.8, duration_ms)
  end
end)

Complete Example

A fully working plugin that loads a SoundFont and plays every note during playback. Bundle a guitar.sf2 file next to main.lua to use this as-is.

-- Standard guitar tuning (GP string 1 = thinnest = high e).
local STANDARD_TUNING = 40

local status_label
local win

local function fret_to_midi(string_num, fret)
  local open = STANDARD_TUNING[string_num]
  if not open then return nil end
  return open + fret
end

function plugin.on_init()
  status_label = plugin.new_label("Loading soundfont…")
  win = plugin.new_window("SoundFont Player")
  plugin.window_set_content(win, plugin.new_vbox(status_label))
  plugin.window_show(win)

  plugin.add_toolbar_item("SoundFont Player", function()
    plugin.window_show(win)
  end)

  -- Load the bundled SoundFont.
  local sf_path = plugin.plugin_dir() .. "/guitar.sf2"
  local ok, err = plugin.load_soundfont(sf_path)
  if ok then
    plugin.set_label_text(status_label, "Ready β€” open a file and press β–Ά")
  else
    plugin.set_label_text(status_label, "Error: " .. (err or "unknown"))
    return
  end

  -- Play each note as it fires during playback.
  plugin.on("note", function(e)
    -- Skip ties (note is already ringing) and dead notes (percussive mutes).
    if e.tie_destination or e.dead_note then return end

    local midi = fret_to_midi(e.string, e.fret)
    if not midi then return end

    -- Estimate duration from GP ticks.
    -- A quarter note is 960 ticks; adjust the constant for the song's BPM.
    local duration_ms = math.max(80, math.floor(e.duration / 4))

    -- Reduce velocity for ghost notes and palm-muted notes.
    local vel = 0.8
    if e.ghost_note  then vel = 0.3 end
    if e.palm_mute   then vel = vel * 0.6 end
    if e.accentuated_note       then vel = math.min(1.0, vel * 1.2) end
    if e.heavy_accentuated_note then vel = 1.0 end

    plugin.play_note(midi, vel, duration_ms)
  end)

  -- Stop all sound when the file closes.
  plugin.on("file.close", function(_)
    plugin.stop_all_notes()
    plugin.set_label_text(status_label, "Ready β€” open a file and press β–Ά")
  end)
end

function plugin.on_shutdown()
  plugin.stop_all_notes()
end

Tip: The duration field in the note event is in GP ticks (quarter = 960). To get milliseconds: ms = ticks / 960 * (60000 / bpm). Read plugin.current_song().tempo in the file.open handler to cache the BPM.