Builtin Library
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.midiat module-load time; empty when the plugin is absent.M.has(method: string) -> boolean— non-allocating predicate equivalent tocapabilities()[method] == true.M.sfx(note: number, velocity?: number, duration?: number, channel?: number) -> boolean— play a one-shot note. Never throws. Returnstrueif dispatched,falseifplay_noteis unavailable (with a one-timelog.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_instanceis required to pick up new methods. - The
pcall-around-field-read in the probe is belt-and-braces — once a name appears inpairs(_G.midi)the strict-throw shouldn't fire, but defensive access against the plugin proxy is cheap. sfxdefaults:velocity = 100,duration = 0.12s,channel = 14. The plugin call itself ispcall-wrapped —sfx()never raises.- Soundtrack-level callers (
midi.load_song/play/stop/pause/resume) don't need this helper — those scenes already guard withif type(_G.midi) ~= "table" then return endand the methods are stable across plugin versions.