5.0 KiB
Capability Spec: audio-fx-plugin-spessasynth
Package: @suaveplan/audio-fx-plugin-spessasynth
Status: ACTIVE
ADDED Requirements
Requirement: SpessaSynthPlugin implements AudioFxPlugin
SpessaSynthPlugin SHALL implement AudioFxPlugin from @suaveplan/audio-fx-plugin-base, exposing init, process, dispose, getParameters, snapshot, restore. It is a source plugin (no audio input; output is the synth's rendered PCM).
Scenario: Render a single noteOn into a buffer
- WHEN
init({ soundFontUrl: "fixtures/gm.sf2", sampleRate: 48_000 })resolves and the consumer feeds a{ type: "noteOn", channel: 0, note: 60, velocity: 100 }MIDI message followed by 48,000 samples ofprocess()calls - THEN the produced output has non-zero RMS (audible voice rendered)
Verified by: tests/integration.test.ts::"renders a noteOn"
Requirement: Lazy SF2 loading via dynamic import
init() SHALL import("spessasynth_lib") lazily so consumers who never load this plugin pay no cost.
Scenario: Bundle without import does not eagerly load SpessaSynth
- WHEN the package's
dist/index.jsis statically analysed for imports ofspessasynth_lib - THEN zero static imports are found; only one dynamic
import("spessasynth_lib")exists, insidesoundfont-loader.ts
Verified by: tests/bundle.test.ts::"spessasynth_lib is lazy-only"
Requirement: SoundFont parse failures surface typed errors
A corrupt SF2/SF3 file SHALL reject init() with SpessaSynthError("SPESSASYNTH.SOUNDFONT_PARSE_FAILED") carrying the underlying parse error on cause.
Scenario: Invalid SF2 bytes
- WHEN
init({ soundFontBytes: new Uint8Array([0,0,0,0]), sampleRate: 48_000 })is called - THEN it rejects with code
SPESSASYNTH.SOUNDFONT_PARSE_FAILED
Verified by: src/loader/soundfont-loader.test.ts
Requirement: MIDI routing — 16 channels + program change + NRPN/RPN
The plugin SHALL route every channel-voice message to the synth, program-change messages to the corresponding SF2 preset (bank + program), and NRPN/RPN events to the registered handlers (per-note tuning, master fine-tune).
Scenario: Program-change selects piano patch on channel 0
- WHEN the synth is initialised with GM and a
programChangeto program 0 arrives on channel 0 - THEN subsequent
noteOnevents on channel 0 render with the "Acoustic Grand Piano" preset
Scenario: NRPN per-note tuning
- WHEN an NRPN with paramHigh=0x65, paramLow=0x4c (per-note tuning) is received followed by a
noteOnof note 60 - THEN the rendered fundamental matches the tuned frequency (verified via FFT peak detection on the output)
Verified by: tests/midi-routing.test.ts
Requirement: Parameter surface with chain-driven automation
The plugin SHALL expose 16-channel × 5-parameter (volume, pan, program, reverbSend, chorusSend) + master gain via getParameters(). Every parameter SHALL be drivable by the host chain's automation lanes.
Scenario: Automate channel 0 volume from 0.0 to 1.0 over 1 second
- WHEN the host chain schedules a volume automation on
channel/0/volumefrom 0 to 1 over 48,000 samples - THEN the rendered output envelope tracks the schedule (RMS at sample 24,000 is approximately half RMS at sample 48,000, within 5%)
Verified by: tests/automation.test.ts
Requirement: Voice management
The plugin SHALL expose getActiveVoiceCount(): number, setMaxPolyphony(n: number): void, and allNotesOff(channel?: number): void. When polyphony is exceeded, the oldest-released or quietest voice is stolen per the SF2 spec.
Scenario: Polyphony cap steals voices
- WHEN
setMaxPolyphony(4)is called and 5 simultaneous noteOn events are sent - THEN
getActiveVoiceCount()equals 4, not 5
Verified by: src/voice-manager/voice-manager.test.ts
Requirement: Snapshot / restore round-trips state
Calling snapshot() SHALL produce a JSON-serialisable object capturing: soundfont URL, per-channel program selection, per-channel parameter values, master gain, polyphony cap. restore(snapshot) SHALL bring a fresh plugin instance to identical state.
Scenario: snapshot/restore round-trip
- WHEN plugin A is configured (load SF2, set channel-0 program 24, set channel-3 pan 0.5),
snap = A.snapshot(), and plugin B isinited thenrestore(snap) - THEN B's
getParameters()matches A's exactly
Verified by: src/plugin/spessasynth-plugin.test.ts
Requirement: OTel instrumentation
Every noteOn SHALL increment spessasynth_voices_triggered_total. getActiveVoiceCount() SHALL be reflected in gauge spessasynth_voices_active. Every process() call SHALL record spessasynth_process_duration_us.
Scenario: Voice gauge tracks live count
- WHEN 3 simultaneous noteOn events are active and the in-memory
MeterProvideris sampled - THEN
spessasynth_voices_activereads 3
Verified by: src/telemetry/telemetry.test.ts