Initial Commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# audio-fx-plugin-spessasynth spec delta
|
||||
|
||||
## 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 of `process()` 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.js` is statically analysed for imports of `spessasynth_lib`
|
||||
- **THEN** zero static imports are found; only one dynamic `import("spessasynth_lib")` exists, inside `soundfont-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 `programChange` to program 0 arrives on channel 0
|
||||
- **THEN** subsequent `noteOn` events 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 `noteOn` of 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/volume` from 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 is `init`ed then `restore(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 `MeterProvider` is sampled
|
||||
- **THEN** `spessasynth_voices_active` reads 3
|
||||
|
||||
**Verified by:** `src/telemetry/telemetry.test.ts`
|
||||
Reference in New Issue
Block a user