Load a SoundFont or WAV sample and synthesise audio in real time alongside the tab β from note playback to custom metronome clicks.
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.
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
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)
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.