@signalsandsorcery/plugin-sdk 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # @signalsandsorcery/plugin-sdk
2
+
3
+ Plugin SDK for building custom generator plugins for [Signals & Sorcery](https://signalsandsorcery.com) — an AI-powered music production workstation.
4
+
5
+ Plugins extend the Loop Workstation with custom input generators that create MIDI patterns, manage audio samples, generate AI audio textures, or combine all three. Each plugin gets its own accordion section in the workstation UI and a scoped `PluginHost` API for interacting with tracks, MIDI, audio, and more.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @signalsandsorcery/plugin-sdk
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ Full documentation is available at [signalsandsorcery.com/plugin-sdk](https://signalsandsorcery.com/plugin-sdk/):
16
+
17
+ - [Getting Started](https://signalsandsorcery.com/plugin-sdk/getting-started.html) — Directory structure, manifest, installation, debugging
18
+ - [API Reference](https://signalsandsorcery.com/plugin-sdk/api-reference.html) — Complete PluginHost API with type signatures and examples
19
+ - [Tutorial: Euclidean Rhythm Generator](https://signalsandsorcery.com/plugin-sdk/tutorial.html) — Build a working plugin from scratch
20
+
21
+ ## Reference Plugins
22
+
23
+ These built-in plugins serve as reference implementations:
24
+
25
+ | Plugin | Type | Description | Source |
26
+ |--------|------|-------------|--------|
27
+ | Synth Generator | `midi` | AI-powered MIDI generation with Surge XT presets | [sas-synth-plugin](https://github.com/shiehn/sas-synth-plugin) |
28
+ | Sample Player | `sample` | Sample library browser with time-stretching | [sas-sample-plugin](https://github.com/shiehn/sas-sample-plugin) |
29
+ | Audio Texture | `audio` | AI audio texture generation | [sas-audio-plugin](https://github.com/shiehn/sas-audio-plugin) |
30
+
31
+ ## What's in the SDK
32
+
33
+ ### Types
34
+
35
+ The core plugin contract — everything you need to implement a generator plugin:
36
+
37
+ ```typescript
38
+ import type {
39
+ GeneratorPlugin, // Interface your plugin class implements
40
+ PluginHost, // Scoped API surface (tracks, MIDI, audio, LLM, etc.)
41
+ PluginUIProps, // Props passed to your React component
42
+ PluginManifest, // plugin.json schema
43
+ MusicalContext, // Key, mode, BPM, bars, chords
44
+ MidiClipData, // MIDI clip payload
45
+ PluginMidiNote, // Individual MIDI note
46
+ PluginTrackHandle, // Track identity returned by createTrack()
47
+ PluginError, // Typed error class with error codes
48
+ } from '@signalsandsorcery/plugin-sdk';
49
+ ```
50
+
51
+ ### UI Components
52
+
53
+ Pre-built components that match the host app's visual style:
54
+
55
+ | Component | Description |
56
+ |-----------|-------------|
57
+ | `TrackRow` | Full-featured track row with prompt input, generate/shuffle/copy buttons, mute/solo, volume/pan, FX drawer, instrument drawer, and progress overlay |
58
+ | `VolumeSlider` | Compact horizontal volume slider (0-1) with dB tooltip |
59
+ | `PanSlider` | Compact horizontal pan slider (-1 to +1) with double-click to center |
60
+ | `FxToggleBar` | Per-track FX control panel with 6 categories, 5 presets each, and dry/wet sliders |
61
+ | `SorceryProgressBar` | Animated progress bar with time-based pacing for long operations |
62
+ | `InstrumentDrawer` | Searchable grid of available VST3/AU instrument plugins |
63
+
64
+ ```typescript
65
+ import { TrackRow, VolumeSlider, PanSlider, FxToggleBar, SorceryProgressBar } from '@signalsandsorcery/plugin-sdk';
66
+ ```
67
+
68
+ ### Hooks
69
+
70
+ ```typescript
71
+ import { useSceneState } from '@signalsandsorcery/plugin-sdk';
72
+
73
+ // Maintains separate state per scene — preserved across scene switches
74
+ const [prompts, setPrompts, setPromptsForScene] = useSceneState(activeSceneId, {});
75
+ ```
76
+
77
+ ### Constants
78
+
79
+ ```typescript
80
+ import {
81
+ VALID_INSTRUMENT_ROLES, // ['bass', 'kick', 'snare', 'lead', 'pad', ...]
82
+ PLUGIN_SDK_VERSION, // '1.0.0'
83
+ FX_CATEGORIES, // ['eq', 'compressor', 'chorus', 'phaser', 'delay', 'reverb']
84
+ FX_PRESET_CONFIGS, // Preset definitions for all 6 FX categories
85
+ } from '@signalsandsorcery/plugin-sdk';
86
+ ```
87
+
88
+ ## Quick Start
89
+
90
+ ### 1. Create the manifest (`plugin.json`)
91
+
92
+ ```json
93
+ {
94
+ "id": "@my-org/my-plugin",
95
+ "displayName": "My Plugin",
96
+ "version": "1.0.0",
97
+ "description": "A custom generator plugin",
98
+ "generatorType": "midi",
99
+ "main": "index.js",
100
+ "minHostVersion": "1.0.0",
101
+ "capabilities": {
102
+ "requiresLLM": true,
103
+ "requiresSurgeXT": true
104
+ }
105
+ }
106
+ ```
107
+
108
+ Generator types: `midi` | `audio` | `sample` | `hybrid`
109
+
110
+ ### 2. Implement the plugin class
111
+
112
+ ```typescript
113
+ import type { GeneratorPlugin, PluginHost, PluginUIProps, PluginSettingsSchema, MusicalContext } from '@signalsandsorcery/plugin-sdk';
114
+ import { MyPanel } from './components/Panel';
115
+
116
+ export class MyPlugin implements GeneratorPlugin {
117
+ readonly id = '@my-org/my-plugin';
118
+ readonly displayName = 'My Plugin';
119
+ readonly version = '1.0.0';
120
+ readonly description = 'A custom generator plugin';
121
+ readonly generatorType = 'midi' as const;
122
+
123
+ private host: PluginHost | null = null;
124
+
125
+ async activate(host: PluginHost): Promise<void> {
126
+ this.host = host;
127
+ }
128
+
129
+ async deactivate(): Promise<void> {
130
+ this.host = null;
131
+ }
132
+
133
+ getUIComponent() {
134
+ return MyPanel;
135
+ }
136
+
137
+ getSettingsSchema(): PluginSettingsSchema | null {
138
+ return null;
139
+ }
140
+
141
+ // Optional lifecycle hooks
142
+ async onSceneChanged(sceneId: string | null): Promise<void> { }
143
+ onContextChanged(context: MusicalContext): void { }
144
+ }
145
+ ```
146
+
147
+ ### 3. Build the UI
148
+
149
+ ```tsx
150
+ import type { PluginUIProps } from '@signalsandsorcery/plugin-sdk';
151
+
152
+ export function MyPanel({ host, activeSceneId, isAuthenticated, isConnected }: PluginUIProps) {
153
+ const handleGenerate = async () => {
154
+ if (!activeSceneId) {
155
+ host.showToast('warning', 'No Scene', 'Select a scene first');
156
+ return;
157
+ }
158
+
159
+ // Create a track
160
+ const track = await host.createTrack({ name: 'My Track', role: 'lead', loadSynth: true });
161
+
162
+ // Get musical context
163
+ const context = await host.getMusicalContext();
164
+
165
+ // Write MIDI
166
+ await host.writeMidiClip(track.id, {
167
+ startTime: 0,
168
+ endTime: (context.bars * 4 * 60) / context.bpm,
169
+ tempo: context.bpm,
170
+ notes: [
171
+ { pitch: 60, startBeat: 0, durationBeats: 1, velocity: 100 },
172
+ { pitch: 64, startBeat: 1, durationBeats: 1, velocity: 90 },
173
+ { pitch: 67, startBeat: 2, durationBeats: 1, velocity: 85 },
174
+ { pitch: 72, startBeat: 3, durationBeats: 1, velocity: 100 },
175
+ ],
176
+ });
177
+
178
+ host.showToast('success', 'Done', 'Pattern generated');
179
+ };
180
+
181
+ return (
182
+ <div>
183
+ <button onClick={handleGenerate} disabled={!isConnected}>
184
+ Generate
185
+ </button>
186
+ </div>
187
+ );
188
+ }
189
+ ```
190
+
191
+ ### 4. Install the plugin
192
+
193
+ Place the compiled plugin in:
194
+
195
+ ```
196
+ ~/.signals-and-sorcery/plugins/my-plugin/
197
+ plugin.json
198
+ index.js
199
+ ...
200
+ ```
201
+
202
+ Restart Signals & Sorcery. The plugin appears in the workstation accordion.
203
+
204
+ ## PluginHost API Overview
205
+
206
+ All methods are available on the `host` object your plugin receives in `activate()` and via `PluginUIProps.host`. Methods marked with **ownership** can only modify tracks the calling plugin created.
207
+
208
+ ### Track Management
209
+ | Method | Description |
210
+ |--------|-------------|
211
+ | `createTrack(options)` | Create a track with name, role, synth, instrument |
212
+ | `deleteTrack(trackId)` | Delete an owned track |
213
+ | `getPluginTracks()` | List all tracks this plugin owns in the active scene |
214
+ | `getTrackInfo(trackId)` | Detailed track state (mute, volume, pan, plugins) |
215
+ | `adoptSceneTracks()` | Re-claim unowned tracks matching generator type |
216
+ | `setTrackMute/Volume/Pan/Solo/Name` | Track property setters |
217
+ | `shufflePreset(trackId)` | Randomize Surge XT preset (keeps MIDI) |
218
+ | `duplicateTrack(trackId)` | Clone track with MIDI + new preset |
219
+
220
+ ### MIDI Operations
221
+ | Method | Description |
222
+ |--------|-------------|
223
+ | `writeMidiClip(trackId, clip)` | Write MIDI notes (replaces existing) |
224
+ | `clearMidi(trackId)` | Clear all MIDI from a track |
225
+ | `postProcessMidi(notes, options)` | Quantize, swing, scale enforcement, humanization |
226
+ | `auditionNote(trackId, pitch, velocity, durationMs)` | Preview a single note |
227
+
228
+ ### Audio Operations
229
+ | Method | Description |
230
+ |--------|-------------|
231
+ | `writeAudioClip(trackId, filePath, position?)` | Place audio file on track |
232
+ | `generateAudioTexture(request)` | AI audio generation from text prompt |
233
+
234
+ ### Plugin/Synth Operations
235
+ | Method | Description |
236
+ |--------|-------------|
237
+ | `loadSynthPlugin(trackId, pluginName)` | Load VST3/AU plugin |
238
+ | `setPluginState/getPluginState` | Save/restore base64-encoded preset data |
239
+ | `getTrackPlugins(trackId)` | List loaded plugins |
240
+ | `getAvailableInstruments()` | Get scanned VST3/AU instruments |
241
+ | `setTrackInstrument(trackId, pluginId)` | Change instrument (preserves MIDI) |
242
+
243
+ ### FX Operations
244
+ Six categories in signal chain order: `eq` > `compressor` > `chorus` > `phaser` > `delay` > `reverb`
245
+
246
+ | Method | Description |
247
+ |--------|-------------|
248
+ | `getTrackFxState(trackId)` | Get enabled/preset/dryWet per category |
249
+ | `toggleTrackFx(trackId, category, enabled)` | Enable/disable FX category |
250
+ | `setTrackFxPreset(trackId, category, presetIndex)` | Set FX preset (0-4) |
251
+ | `setTrackFxDryWet(trackId, category, value)` | Set dry/wet mix (0.0-1.0) |
252
+
253
+ ### Scene Context
254
+ | Method | Description |
255
+ |--------|-------------|
256
+ | `getGenerationContext(excludeTrackId?)` | Full context + concurrent track MIDI data |
257
+ | `getMusicalContext()` | Key, mode, BPM, bars, genre, chords |
258
+ | `getActiveSceneId()` | Current scene ID (synchronous) |
259
+ | `getSceneList()` | All scenes in the project |
260
+
261
+ ### Transport & Events
262
+ | Method | Description |
263
+ |--------|-------------|
264
+ | `onTrackStateChange(listener)` | Real-time mute, solo, volume, pan updates |
265
+ | `onTransportEvent(listener)` | Play, stop, BPM change, position |
266
+ | `onDeckBoundary(listener)` | Loop boundary events (bar, beat, loopCount) |
267
+ | `onSceneChange(listener)` | Scene selection changes |
268
+ | `onEngineReady(listener)` | Engine finished loading tracks |
269
+ | `getTransportState()` | Current playback state snapshot |
270
+
271
+ ### LLM Access
272
+ | Method | Description |
273
+ |--------|-------------|
274
+ | `generateWithLLM(request)` | Generate text/JSON (metered, requires auth) |
275
+ | `isLLMAvailable()` | Check auth + gateway reachability |
276
+
277
+ ### Preset System
278
+ | Method | Description |
279
+ |--------|-------------|
280
+ | `getPresetCategories(pluginName)` | Available Surge XT categories |
281
+ | `getRandomPreset(category)` | Random preset from category |
282
+ | `getPresetByName(category, name)` | Specific preset lookup |
283
+ | `classifyPresetCategory(description)` | LLM-based text-to-category |
284
+
285
+ ### Scene Composition
286
+ | Method | Description |
287
+ |--------|-------------|
288
+ | `composeScene(options)` | Bulk LLM arrangement generation |
289
+ | `onComposeProgress(listener)` | Progress events (planning, generating, complete) |
290
+
291
+ ### Data Persistence
292
+ | Method | Description |
293
+ |--------|-------------|
294
+ | `getSceneData/setSceneData/getAllSceneData/deleteSceneData` | Per-scene key-value storage |
295
+ | `getProjectData/setProjectData` | Project-wide storage |
296
+ | `settings.get/set/getAll/onChange` | Global settings (cross-project) |
297
+ | `getDataDirectory()` | Plugin's isolated data directory path |
298
+
299
+ ### Plugin Presets
300
+ | Method | Description |
301
+ |--------|-------------|
302
+ | `getPluginPresets(category?)` | Get saved presets for this plugin |
303
+ | `savePluginPreset(options)` | Save a preset (name, category, data) |
304
+ | `deletePluginPreset(id)` | Delete a preset |
305
+
306
+ ### File System & Network
307
+ | Method | Description |
308
+ |--------|-------------|
309
+ | `showOpenDialog/showSaveDialog` | Native file dialogs (requires `fileDialog` capability) |
310
+ | `downloadFile/importFile` | Download/copy files to plugin data directory |
311
+ | `httpRequest(options)` | HTTP requests (requires `network` capability with `allowedHosts`) |
312
+
313
+ ### Secure Storage
314
+ | Method | Description |
315
+ |--------|-------------|
316
+ | `storeSecret/getSecret/deleteSecret` | OS keychain encryption, per-plugin scoped |
317
+
318
+ ### Sample Library
319
+ | Method | Description |
320
+ |--------|-------------|
321
+ | `getSamples/getSampleById` | Query sample library |
322
+ | `importSamples(filePaths)` | Import audio files |
323
+ | `createSampleTrack/deleteSampleTrack` | Manage sample tracks |
324
+ | `getPluginSampleTracks()` | List owned sample tracks |
325
+ | `timeStretchSample(sampleId, targetBpm)` | Time-stretch to target BPM |
326
+
327
+ ### Notifications & Progress
328
+ | Method | Description |
329
+ |--------|-------------|
330
+ | `showToast(type, title, message?)` | Toast notification (info/success/warning/error) |
331
+ | `setProgress(trackId, progress)` | Track progress bar (0-100, -1 to hide) |
332
+ | `setStatusMessage(message)` | Accordion header status text |
333
+ | `confirmAction(title, message)` | Confirmation dialog |
334
+
335
+ ## Error Codes
336
+
337
+ All errors are `PluginError` instances with a typed `code` property:
338
+
339
+ | Code | Description |
340
+ |------|-------------|
341
+ | `NOT_OWNED` | Tried to modify a track not owned by this plugin |
342
+ | `TRACK_NOT_FOUND` | Track ID doesn't exist in engine |
343
+ | `TRACK_LIMIT_EXCEEDED` | Plugin has too many tracks (default: 16 per scene) |
344
+ | `NO_ACTIVE_SCENE` | No scene is selected |
345
+ | `ENGINE_ERROR` | Audio engine call failed |
346
+ | `INVALID_MIDI` | Malformed MIDI data |
347
+ | `FILE_NOT_FOUND` | Referenced file doesn't exist |
348
+ | `INVALID_FORMAT` | Unsupported audio format |
349
+ | `PLUGIN_NOT_FOUND` | VST/AU plugin not installed |
350
+ | `LLM_BUDGET_EXCEEDED` | Over daily token limit |
351
+ | `LLM_UNAVAILABLE` | LLM gateway unreachable |
352
+ | `NOT_AUTHENTICATED` | User not logged in |
353
+ | `TIMEOUT` | Operation timed out |
354
+ | `CANCELLED` | User cancelled the operation |
355
+ | `INCOMPATIBLE` | Plugin requires newer SDK version |
356
+ | `CAPABILITY_DENIED` | Plugin lacks required capability in manifest |
357
+ | `SECRET_NOT_FOUND` | Secret key doesn't exist |
358
+
359
+ ## Security Model
360
+
361
+ - **Ownership scoping** — Plugins can only modify tracks they created (enforced at runtime)
362
+ - **Capability gating** — Network and file system access require manifest declarations
363
+ - **Secret isolation** — Each plugin's secrets are encrypted and scoped per plugin
364
+ - **Track limits** — 16 tracks per plugin per scene (configurable)
365
+
366
+ ## License
367
+
368
+ MIT
package/dist/index.d.mts CHANGED
@@ -138,6 +138,17 @@ interface PluginHost {
138
138
  writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;
139
139
  /** Clear all MIDI from a track this plugin owns. */
140
140
  clearMidi(trackId: string): Promise<void>;
141
+ /**
142
+ * Export all tracks owned by this plugin in the active scene as a ZIP bundle
143
+ * of Standard MIDI Files (one .mid per track, named after each track with
144
+ * collision-avoidance suffixes). Prompts the user for a save location.
145
+ *
146
+ * Tracks with no MIDI data are skipped. Returns the path written, or
147
+ * `{ canceled: true }` if the user dismissed the save dialog.
148
+ *
149
+ * @since SDK 1.1.0
150
+ */
151
+ exportTracksAsMidiBundle(options?: ExportMidiBundleOptions): Promise<ExportMidiBundleResult>;
141
152
  /**
142
153
  * Run the host's MIDI post-processing pipeline on raw notes.
143
154
  * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.
@@ -163,6 +174,10 @@ interface PluginHost {
163
174
  getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;
164
175
  /** Change the instrument plugin on a track. Preserves MIDI data. */
165
176
  setTrackInstrument(trackId: string, pluginId: string): Promise<void>;
177
+ /** Open the instrument plugin's native editor GUI as a floating window. */
178
+ showInstrumentEditor(trackId: string): Promise<void>;
179
+ /** Close the instrument plugin's editor window. */
180
+ hideInstrumentEditor(trackId: string): Promise<void>;
166
181
  /** Get the FULL generation context for the active scene. */
167
182
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
168
183
  /** Get lightweight musical context (no concurrent track MIDI data). */
@@ -267,6 +282,24 @@ interface PluginHost {
267
282
  logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;
268
283
  /** Start a timer. Returns a stop function that logs the duration. */
269
284
  startTimer(name: string): () => void;
285
+ /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */
286
+ splitStems(trackId: string): Promise<PluginStemSplitResult>;
287
+ /** Check if the stem splitter binary is available. */
288
+ isStemSplitterAvailable(): Promise<boolean>;
289
+ }
290
+ /** Stem type identifiers */
291
+ type StemType = 'vocals' | 'drums' | 'bass' | 'other';
292
+ /** Result of splitting an audio track into stems */
293
+ interface PluginStemSplitResult {
294
+ /** Created stem tracks with audio loaded (all auto-muted) */
295
+ stems: PluginStemTrackInfo[];
296
+ }
297
+ /** Information about a single stem track created by stem splitting */
298
+ interface PluginStemTrackInfo {
299
+ /** The stem type (vocals, drums, bass, other) */
300
+ stemType: StemType;
301
+ /** Track handle for the new stem track */
302
+ track: PluginTrackHandle;
270
303
  }
271
304
  interface ExportedPluginData {
272
305
  pluginId: string;
@@ -377,6 +410,31 @@ interface MidiWriteResult {
377
410
  /** Actual bars covered */
378
411
  bars: number;
379
412
  }
413
+ /**
414
+ * Options for {@link PluginHost.exportTracksAsMidiBundle}.
415
+ * @since SDK 1.1.0
416
+ */
417
+ interface ExportMidiBundleOptions {
418
+ /** Default ZIP filename suggested in the save dialog (without extension). */
419
+ defaultName?: string;
420
+ }
421
+ /**
422
+ * Result of {@link PluginHost.exportTracksAsMidiBundle}.
423
+ * @since SDK 1.1.0
424
+ */
425
+ type ExportMidiBundleResult = {
426
+ success: true;
427
+ filePath: string;
428
+ trackCount: number;
429
+ skippedCount: number;
430
+ } | {
431
+ success: false;
432
+ canceled: true;
433
+ } | {
434
+ success: false;
435
+ canceled?: false;
436
+ error: string;
437
+ };
380
438
  interface PostProcessOptions {
381
439
  /** Snap notes to grid (default: true) */
382
440
  quantize?: boolean;
@@ -902,14 +960,20 @@ interface SDKTrackRowProps {
902
960
  instrumentsLoading?: boolean;
903
961
  /** Re-scan for instruments */
904
962
  onRefreshInstruments?: () => void;
963
+ /** Which stage the instrument drawer is in */
964
+ instrumentDrawerStage?: 'instruments' | 'editor';
965
+ /** Called when user clicks "Open Editor" */
966
+ onShowEditor?: () => void;
967
+ /** Called when user wants to go back from editor view */
968
+ onBackToInstruments?: () => void;
905
969
  }
906
- declare function TrackRow({ track, prompt, runtimeState, fxDetailState, fxDrawerOpen, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, instrumentDrawerOpen, onToggleInstrumentDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, }: SDKTrackRowProps): React.ReactElement;
970
+ declare function TrackRow({ track, prompt, runtimeState, fxDetailState, fxDrawerOpen, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, instrumentDrawerOpen, onToggleInstrumentDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, instrumentDrawerStage, onShowEditor, onBackToInstruments, }: SDKTrackRowProps): React.ReactElement;
907
971
 
908
972
  /**
909
- * InstrumentDrawer — Sliding drawer for selecting instrument plugins (VST3/AU).
973
+ * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.
910
974
  *
911
- * Appears below the track controls when the "P" button is toggled.
912
- * Shows a searchable grid of available instrument plugins.
975
+ * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
976
+ * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
913
977
  */
914
978
 
915
979
  interface InstrumentDrawerProps {
@@ -923,8 +987,16 @@ interface InstrumentDrawerProps {
923
987
  onSelect: (pluginId: string) => void;
924
988
  /** Called when user clicks refresh to re-scan plugins */
925
989
  onRefresh: () => void;
990
+ /** Which stage the drawer is in */
991
+ stage?: 'instruments' | 'editor';
992
+ /** Called when user clicks "Open Editor" */
993
+ onShowEditor?: () => void;
994
+ /** Called when user wants to go back from editor view to instrument list */
995
+ onBackToInstruments?: () => void;
996
+ /** Name of the selected instrument (for display in editor header) */
997
+ selectedInstrumentName?: string | null;
926
998
  }
927
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, }: InstrumentDrawerProps): React.ReactElement;
999
+ declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
928
1000
 
929
1001
  /**
930
1002
  * VolumeSlider Component
@@ -1133,4 +1205,4 @@ declare function sliderToDb(slider: number): number;
1133
1205
  */
1134
1206
  declare function dbToSlider(db: number): number;
1135
1207
 
1136
- export { type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, type CreateTrackOptions, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, type DeckBoundaryEvent, type DeckBoundaryListener, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, type GeneratorPlugin, type GeneratorType, type InstrumentDescriptor, InstrumentDrawer, type InstrumentDrawerProps, type LLMGenerationRequest, type LLMGenerationResult, type MidiClipData, type MidiWriteResult, type MixInterpolation, type MusicalContext, PLUGIN_SDK_VERSION, PanSlider, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginStatus, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackRuntimeState, type PluginTransportState, type PluginUIProps, type PostProcessOptions, type SDKTrackRowProps, SLIDER_UNITY, type SavePluginPresetOptions, type SceneChangeListener, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type TrackFxDetailState, type TrackFxState, TrackRow, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, VALID_INSTRUMENT_ROLES, VolumeSlider, calculateTimeBasedTarget, dbToSlider, sliderToDb, useSceneState };
1208
+ export { type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, type CreateTrackOptions, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, type DeckBoundaryEvent, type DeckBoundaryListener, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, type ExportMidiBundleOptions, type ExportMidiBundleResult, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, type GeneratorPlugin, type GeneratorType, type InstrumentDescriptor, InstrumentDrawer, type InstrumentDrawerProps, type LLMGenerationRequest, type LLMGenerationResult, type MidiClipData, type MidiWriteResult, type MixInterpolation, type MusicalContext, PLUGIN_SDK_VERSION, PanSlider, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginStatus, type PluginStemSplitResult, type PluginStemTrackInfo, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackRuntimeState, type PluginTransportState, type PluginUIProps, type PostProcessOptions, type SDKTrackRowProps, SLIDER_UNITY, type SavePluginPresetOptions, type SceneChangeListener, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type StemType, type TrackFxDetailState, type TrackFxState, TrackRow, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, VALID_INSTRUMENT_ROLES, VolumeSlider, calculateTimeBasedTarget, dbToSlider, sliderToDb, useSceneState };
package/dist/index.d.ts CHANGED
@@ -138,6 +138,17 @@ interface PluginHost {
138
138
  writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;
139
139
  /** Clear all MIDI from a track this plugin owns. */
140
140
  clearMidi(trackId: string): Promise<void>;
141
+ /**
142
+ * Export all tracks owned by this plugin in the active scene as a ZIP bundle
143
+ * of Standard MIDI Files (one .mid per track, named after each track with
144
+ * collision-avoidance suffixes). Prompts the user for a save location.
145
+ *
146
+ * Tracks with no MIDI data are skipped. Returns the path written, or
147
+ * `{ canceled: true }` if the user dismissed the save dialog.
148
+ *
149
+ * @since SDK 1.1.0
150
+ */
151
+ exportTracksAsMidiBundle(options?: ExportMidiBundleOptions): Promise<ExportMidiBundleResult>;
141
152
  /**
142
153
  * Run the host's MIDI post-processing pipeline on raw notes.
143
154
  * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.
@@ -163,6 +174,10 @@ interface PluginHost {
163
174
  getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;
164
175
  /** Change the instrument plugin on a track. Preserves MIDI data. */
165
176
  setTrackInstrument(trackId: string, pluginId: string): Promise<void>;
177
+ /** Open the instrument plugin's native editor GUI as a floating window. */
178
+ showInstrumentEditor(trackId: string): Promise<void>;
179
+ /** Close the instrument plugin's editor window. */
180
+ hideInstrumentEditor(trackId: string): Promise<void>;
166
181
  /** Get the FULL generation context for the active scene. */
167
182
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
168
183
  /** Get lightweight musical context (no concurrent track MIDI data). */
@@ -267,6 +282,24 @@ interface PluginHost {
267
282
  logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;
268
283
  /** Start a timer. Returns a stop function that logs the duration. */
269
284
  startTimer(name: string): () => void;
285
+ /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */
286
+ splitStems(trackId: string): Promise<PluginStemSplitResult>;
287
+ /** Check if the stem splitter binary is available. */
288
+ isStemSplitterAvailable(): Promise<boolean>;
289
+ }
290
+ /** Stem type identifiers */
291
+ type StemType = 'vocals' | 'drums' | 'bass' | 'other';
292
+ /** Result of splitting an audio track into stems */
293
+ interface PluginStemSplitResult {
294
+ /** Created stem tracks with audio loaded (all auto-muted) */
295
+ stems: PluginStemTrackInfo[];
296
+ }
297
+ /** Information about a single stem track created by stem splitting */
298
+ interface PluginStemTrackInfo {
299
+ /** The stem type (vocals, drums, bass, other) */
300
+ stemType: StemType;
301
+ /** Track handle for the new stem track */
302
+ track: PluginTrackHandle;
270
303
  }
271
304
  interface ExportedPluginData {
272
305
  pluginId: string;
@@ -377,6 +410,31 @@ interface MidiWriteResult {
377
410
  /** Actual bars covered */
378
411
  bars: number;
379
412
  }
413
+ /**
414
+ * Options for {@link PluginHost.exportTracksAsMidiBundle}.
415
+ * @since SDK 1.1.0
416
+ */
417
+ interface ExportMidiBundleOptions {
418
+ /** Default ZIP filename suggested in the save dialog (without extension). */
419
+ defaultName?: string;
420
+ }
421
+ /**
422
+ * Result of {@link PluginHost.exportTracksAsMidiBundle}.
423
+ * @since SDK 1.1.0
424
+ */
425
+ type ExportMidiBundleResult = {
426
+ success: true;
427
+ filePath: string;
428
+ trackCount: number;
429
+ skippedCount: number;
430
+ } | {
431
+ success: false;
432
+ canceled: true;
433
+ } | {
434
+ success: false;
435
+ canceled?: false;
436
+ error: string;
437
+ };
380
438
  interface PostProcessOptions {
381
439
  /** Snap notes to grid (default: true) */
382
440
  quantize?: boolean;
@@ -902,14 +960,20 @@ interface SDKTrackRowProps {
902
960
  instrumentsLoading?: boolean;
903
961
  /** Re-scan for instruments */
904
962
  onRefreshInstruments?: () => void;
963
+ /** Which stage the instrument drawer is in */
964
+ instrumentDrawerStage?: 'instruments' | 'editor';
965
+ /** Called when user clicks "Open Editor" */
966
+ onShowEditor?: () => void;
967
+ /** Called when user wants to go back from editor view */
968
+ onBackToInstruments?: () => void;
905
969
  }
906
- declare function TrackRow({ track, prompt, runtimeState, fxDetailState, fxDrawerOpen, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, instrumentDrawerOpen, onToggleInstrumentDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, }: SDKTrackRowProps): React.ReactElement;
970
+ declare function TrackRow({ track, prompt, runtimeState, fxDetailState, fxDrawerOpen, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, instrumentDrawerOpen, onToggleInstrumentDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, instrumentDrawerStage, onShowEditor, onBackToInstruments, }: SDKTrackRowProps): React.ReactElement;
907
971
 
908
972
  /**
909
- * InstrumentDrawer — Sliding drawer for selecting instrument plugins (VST3/AU).
973
+ * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.
910
974
  *
911
- * Appears below the track controls when the "P" button is toggled.
912
- * Shows a searchable grid of available instrument plugins.
975
+ * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
976
+ * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
913
977
  */
914
978
 
915
979
  interface InstrumentDrawerProps {
@@ -923,8 +987,16 @@ interface InstrumentDrawerProps {
923
987
  onSelect: (pluginId: string) => void;
924
988
  /** Called when user clicks refresh to re-scan plugins */
925
989
  onRefresh: () => void;
990
+ /** Which stage the drawer is in */
991
+ stage?: 'instruments' | 'editor';
992
+ /** Called when user clicks "Open Editor" */
993
+ onShowEditor?: () => void;
994
+ /** Called when user wants to go back from editor view to instrument list */
995
+ onBackToInstruments?: () => void;
996
+ /** Name of the selected instrument (for display in editor header) */
997
+ selectedInstrumentName?: string | null;
926
998
  }
927
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, }: InstrumentDrawerProps): React.ReactElement;
999
+ declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
928
1000
 
929
1001
  /**
930
1002
  * VolumeSlider Component
@@ -1133,4 +1205,4 @@ declare function sliderToDb(slider: number): number;
1133
1205
  */
1134
1206
  declare function dbToSlider(db: number): number;
1135
1207
 
1136
- export { type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, type CreateTrackOptions, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, type DeckBoundaryEvent, type DeckBoundaryListener, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, type GeneratorPlugin, type GeneratorType, type InstrumentDescriptor, InstrumentDrawer, type InstrumentDrawerProps, type LLMGenerationRequest, type LLMGenerationResult, type MidiClipData, type MidiWriteResult, type MixInterpolation, type MusicalContext, PLUGIN_SDK_VERSION, PanSlider, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginStatus, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackRuntimeState, type PluginTransportState, type PluginUIProps, type PostProcessOptions, type SDKTrackRowProps, SLIDER_UNITY, type SavePluginPresetOptions, type SceneChangeListener, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type TrackFxDetailState, type TrackFxState, TrackRow, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, VALID_INSTRUMENT_ROLES, VolumeSlider, calculateTimeBasedTarget, dbToSlider, sliderToDb, useSceneState };
1208
+ export { type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, type CreateTrackOptions, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, type DeckBoundaryEvent, type DeckBoundaryListener, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, type ExportMidiBundleOptions, type ExportMidiBundleResult, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, type GeneratorPlugin, type GeneratorType, type InstrumentDescriptor, InstrumentDrawer, type InstrumentDrawerProps, type LLMGenerationRequest, type LLMGenerationResult, type MidiClipData, type MidiWriteResult, type MixInterpolation, type MusicalContext, PLUGIN_SDK_VERSION, PanSlider, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginStatus, type PluginStemSplitResult, type PluginStemTrackInfo, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackRuntimeState, type PluginTransportState, type PluginUIProps, type PostProcessOptions, type SDKTrackRowProps, SLIDER_UNITY, type SavePluginPresetOptions, type SceneChangeListener, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type StemType, type TrackFxDetailState, type TrackFxState, TrackRow, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, VALID_INSTRUMENT_ROLES, VolumeSlider, calculateTimeBasedTarget, dbToSlider, sliderToDb, useSceneState };