Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

midi_sfx

Safe one-shot SFX helper layered over the midi_player WASM plugin. Centralises the probe-and-play pattern the five-games scenes (TD / FPS / twin-stick) used to duplicate. Probes _G.midi once at module load, caches a direct reference to midi.play_note for the hot path, and exposes an introspection API so scenes can branch their audio design before they wire callers up.

The midi plugin namespace is a WASM-backed proxy table whose missing methods throw on access (see #3027). On a fresh checkout the staged artifact may also lag behind the source and silently miss methods that new scene code expects (#3026). The combination produced a particularly nasty failure mode: scene callers wrote the obvious if type(midi.play_note) ~= "function" then return end guard, which itself raised on the bare field access — killing the coroutine and leaving the soundtrack playing while gameplay scripts hung.

Exports

  • M.capabilities() -> CapabilityMap — shallow copy of the probed capability table. Keys are the function-valued exports of _G.midi at module-load time; empty when the plugin is absent.
  • M.has(method: string) -> boolean — non-allocating predicate equivalent to capabilities()[method] == true.
  • M.sfx(note: number, velocity?: number, duration?: number, channel?: number) -> boolean — play a one-shot note. Never throws. Returns true if dispatched, false if play_note is unavailable (with a one-time log.warn).

Types:

  • CapabilityMap = { [string]: boolean }

Usage

local Midi = require("@builtin::modules.midi_sfx")

if Midi.has("play_note") then
    -- design SFX-heavy audio
end

-- Play a one-shot note. Never throws.
Midi.sfx(60, 100, 0.12, 14)  -- note, velocity, duration_s, channel

Notes

  • The probe runs once at module load. If the plugin is rebuilt during a session, restart_instance is required to pick up new methods.
  • The pcall-around-field-read in the probe is belt-and-braces — once a name appears in pairs(_G.midi) the strict-throw shouldn't fire, but defensive access against the plugin proxy is cheap.
  • sfx defaults: velocity = 100, duration = 0.12s, channel = 14. The plugin call itself is pcall-wrapped — sfx() never raises.
  • Soundtrack-level callers (midi.load_song / play / stop / pause / resume) don't need this helper — those scenes already guard with if type(_G.midi) ~= "table" then return end and the methods are stable across plugin versions.