Developer Docs

Events Reference

All events fired by Tabatura — their timing, payload shape, and example handlers.

Subscribing to Events

Call plugin.on(event, handler) to subscribe. It returns a numeric subscription ID you can pass to plugin.off to unsubscribe. Most plugins subscribe in plugin.on_init and never need to unsubscribe.

-- Subscribe
local id = plugin.on("note", function(e)
  -- handle event
end)

-- Unsubscribe (rarely needed)
plugin.off("note", id)

Keep event handlers short — avoid slow or blocking work inside them, as they run on the same thread that drives the UI.

All Events

"file.open"

Fired once after a GP file is fully parsed and loaded.

Field Type Description
song.title string Song title
song.artist string Artist name
song.album string Album name
song.tempo number Tempo in BPM
song.time_signature string e.g. "4/4"
song.key_signature string e.g. "C Major"
plugin.on("file.open", function(e)
  local s = e.song
  print(s.title .. " by " .. s.artist)
  print("Tempo: " .. s.tempo .. " BPM  |  " .. s.time_signature)
end)
"file.close"

Fired when the user closes the current file. No payload — e is an empty table.

No payload — the event table is empty.

plugin.on("file.close", function(_)
  -- Reset any UI state.
  plugin.set_label_text(my_label, "No file open")
end)
"playback.start"

Fired when the user starts playback. Useful for arming UI state, switching from manual practice mode to song-synced behavior, or caching the current song metadata at the moment playback begins.

Field Type Description
song.title string Song title
song.artist string Artist name
song.album string Album name
song.tempo number Tempo in BPM
song.time_signature string e.g. "4/4"
song.key_signature string e.g. "C Major"
plugin.on("playback.start", function(e)
  local song = e.song
  plugin.set_label_text(status_label,
    "Playback started at " .. song.tempo .. " BPM")
end)
"playback.stop"

Fired when playback stops, either because the user stopped it or because the song finished naturally.

Field Type Description
completed boolean true when playback reached the end of the song; false when it was stopped early
plugin.on("playback.stop", function(e)
  if e.completed then
    plugin.set_label_text(status_label, "Song finished")
  else
    plugin.set_label_text(status_label, "Playback stopped")
  end
end)
"render"

Fired after every view refresh (scroll, resize, zoom, etc.). No payload. Use sparingly — this can fire frequently.

No payload — the event table is empty.

plugin.on("render", function(_)
  -- e.g. sync an overlay position after the tab redraws.
end)
"beat"

Fired once per beat during playback, before note events for that beat. Useful for progress bars or beat counters.

Field Type Description
beat_number number 1-based global beat counter across the whole song
total_beats number Total number of beats in the song
plugin.on("beat", function(e)
  local pct = e.beat_number / e.total_beats * 100
  plugin.set_label_text(progress_label,
    string.format("Beat %d / %d  (%.0f%%)",
      e.beat_number, e.total_beats, pct))
end)
"beat.in_measure"

Fired once per GP beat slot during playback, carrying beat-level effect data. A GP beat slot is one rhythmic unit (eighth note, sixteenth, etc.) — not necessarily a musical beat. Use beat.metronome_pulse instead if you need to fire exactly once per musical beat.

Field Type Description
measure number 1-based measure number
beat_in_measure number 1-based beat slot index within the measure
numerator number Time-signature numerator for this measure (e.g. 4 for 4/4, 5 for 5/8)
denominator number Time-signature denominator for this measure (e.g. 4 for 4/4, 8 for 5/8)
beat_tick number Accumulated GP ticks from the start of the measure to this beat slot. Quarter note = 960 ticks. Use beat_tick % (960 * 4 / denominator) == 0 to detect musical beat boundaries.
duration number Beat slot duration in GP ticks. Quarter note = 960
fade_in boolean Beat has a fade-in effect
slap number 0 = none, 1 = slap, 2 = pop, 3 = tap
stroke_down number Downstroke speed in ticks (0 = no stroke)
stroke_up number Upstroke speed in ticks (0 = no stroke)
pick_stroke number 0 = none, 1 = up, 2 = down
repeat_open boolean This measure opens a repeat section
repeat_close number >0: measure closes a repeat N times; 0 = no close
repeat_alternative number Volta bitmask — bit 0 = 1st ending, bit 1 = 2nd, …
tremolo_bar table? Whammy-bar envelope. Present only when the beat has a tremolo bar effect. Shape: { type, value, points: [{position, value, vibrato}, …] }
plugin.on("beat.in_measure", function(e)
  if e.repeat_open then
    print("Repeat section starts at measure " .. e.measure)
  end
  if e.repeat_close > 0 then
    print("Repeat closes (x" .. e.repeat_close .. ")")
  end
end)
"beat.metronome_pulse"

Fired exactly once per musical beat during playback — N times per measure where N is the time signature numerator. Unlike beat.in_measure (which fires once per GP beat slot), this event fires at the correct musical beat boundaries even when notes span multiple beats. The app splits long note sleeps internally to guarantee accurate timing.

Field Type Description
measure number 1-based measure number
musical_beat number 1-based beat number within the measure (1 = downbeat, fires first)
numerator number Time-signature numerator (e.g. 4 for 4/4, 5 for 5/8)
denominator number Time-signature denominator (e.g. 4 for 4/4, 8 for 5/8)
plugin.on("beat.metronome_pulse", function(e)
  -- e.musical_beat == 1 is the downbeat (accent it)
  local is_downbeat = e.musical_beat == 1
  play_click(is_downbeat)
  plugin.set_label_text(status_label,
    "Beat " .. e.musical_beat .. " of " .. e.numerator .. "/" .. e.denominator)
end)
"note"

Fired for every individual note during playback. A single beat can produce multiple note events simultaneously (one per sounding string). Note events fire after the beat and beat.in_measure events for the same position.

Field Type Description
track number 0-based track index
measure number 1-based measure number
beat number 1-based beat within the measure
string number GP string number. 1 = thinnest (high e), 6 = thickest (low E) for standard guitar
fret number Fret number (0 = open string)
duration number Beat duration in GP ticks (shared with beat events)
fade_in boolean Beat has a fade-in effect
slap number 0 = none, 1 = slap, 2 = pop, 3 = tap
stroke_down number Downstroke speed in ticks (0 = no stroke)
stroke_up number Upstroke speed in ticks (0 = no stroke)
hammer_on boolean Note is played as a hammer-on
pull_off boolean Note is played as a pull-off
slide_type number 0 = none, 1 = shift, 2 = legato, 3 = in from below, 4 = in from above, 5 = out downward, 6 = out upward
vibrato boolean Note has vibrato
harmonic number 0 = none, 1 = natural, 2 = artificial, 3 = pinch, 4 = tapped, 5 = semi, 6 = feedback
palm_mute boolean Palm mute applied
let_ring boolean Let ring applied
staccato boolean Staccato articulation
tapping boolean Right-hand tapping
dead_note boolean Muted/dead note (x)
ghost_note boolean Ghost note (parenthesised)
tie_origin boolean This note ties forward to the next beat
tie_destination boolean This note is the destination of a tie
tremolo_pick number Rapid-pick speed: 8, 16, or 32 (0 = none)
accentuated_note boolean Standard accent (>)
heavy_accentuated_note boolean Heavy accent (^)
bend table? Pitch-bend envelope. Shape: { type, value, points: [{position, value, vibrato}, …] }. Omitted when absent.
trill table? Trill data: { fret: number, duration: number }. Omitted when absent.
grace_note table? Grace note: { fret, dynamic, duration, on_beat }. Omitted when absent.
tremolo_bar table? Whammy-bar envelope (same shape as in beat.in_measure). Omitted when absent.
plugin.on("note", function(e)
  -- Skip ties to avoid re-triggering already-ringing notes.
  if e.tie_destination then return end

  -- Build a human-readable description of the note.
  local parts = {
    string.format("Track %d | M%d B%d | Str %d Fret %d",
      e.track + 1, e.measure, e.beat, e.string, e.fret)
  }
  if e.hammer_on  then parts[#parts+1] = "H"  end
  if e.pull_off   then parts[#parts+1] = "P"  end
  if e.palm_mute  then parts[#parts+1] = "PM" end
  if e.let_ring   then parts[#parts+1] = "LR" end

  plugin.set_label_text(my_label, table.concat(parts, "  "))
end)