@signalsandsorcery/plugin-sdk 1.0.0 → 1.1.0

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
@@ -163,6 +163,10 @@ interface PluginHost {
163
163
  getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;
164
164
  /** Change the instrument plugin on a track. Preserves MIDI data. */
165
165
  setTrackInstrument(trackId: string, pluginId: string): Promise<void>;
166
+ /** Open the instrument plugin's native editor GUI as a floating window. */
167
+ showInstrumentEditor(trackId: string): Promise<void>;
168
+ /** Close the instrument plugin's editor window. */
169
+ hideInstrumentEditor(trackId: string): Promise<void>;
166
170
  /** Get the FULL generation context for the active scene. */
167
171
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
168
172
  /** Get lightweight musical context (no concurrent track MIDI data). */
@@ -267,6 +271,24 @@ interface PluginHost {
267
271
  logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;
268
272
  /** Start a timer. Returns a stop function that logs the duration. */
269
273
  startTimer(name: string): () => void;
274
+ /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */
275
+ splitStems(trackId: string): Promise<PluginStemSplitResult>;
276
+ /** Check if the stem splitter binary is available. */
277
+ isStemSplitterAvailable(): Promise<boolean>;
278
+ }
279
+ /** Stem type identifiers */
280
+ type StemType = 'vocals' | 'drums' | 'bass' | 'other';
281
+ /** Result of splitting an audio track into stems */
282
+ interface PluginStemSplitResult {
283
+ /** Created stem tracks with audio loaded (all auto-muted) */
284
+ stems: PluginStemTrackInfo[];
285
+ }
286
+ /** Information about a single stem track created by stem splitting */
287
+ interface PluginStemTrackInfo {
288
+ /** The stem type (vocals, drums, bass, other) */
289
+ stemType: StemType;
290
+ /** Track handle for the new stem track */
291
+ track: PluginTrackHandle;
270
292
  }
271
293
  interface ExportedPluginData {
272
294
  pluginId: string;
@@ -902,14 +924,20 @@ interface SDKTrackRowProps {
902
924
  instrumentsLoading?: boolean;
903
925
  /** Re-scan for instruments */
904
926
  onRefreshInstruments?: () => void;
927
+ /** Which stage the instrument drawer is in */
928
+ instrumentDrawerStage?: 'instruments' | 'editor';
929
+ /** Called when user clicks "Open Editor" */
930
+ onShowEditor?: () => void;
931
+ /** Called when user wants to go back from editor view */
932
+ onBackToInstruments?: () => void;
905
933
  }
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;
934
+ 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
935
 
908
936
  /**
909
- * InstrumentDrawer — Sliding drawer for selecting instrument plugins (VST3/AU).
937
+ * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.
910
938
  *
911
- * Appears below the track controls when the "P" button is toggled.
912
- * Shows a searchable grid of available instrument plugins.
939
+ * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
940
+ * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
913
941
  */
914
942
 
915
943
  interface InstrumentDrawerProps {
@@ -923,8 +951,16 @@ interface InstrumentDrawerProps {
923
951
  onSelect: (pluginId: string) => void;
924
952
  /** Called when user clicks refresh to re-scan plugins */
925
953
  onRefresh: () => void;
954
+ /** Which stage the drawer is in */
955
+ stage?: 'instruments' | 'editor';
956
+ /** Called when user clicks "Open Editor" */
957
+ onShowEditor?: () => void;
958
+ /** Called when user wants to go back from editor view to instrument list */
959
+ onBackToInstruments?: () => void;
960
+ /** Name of the selected instrument (for display in editor header) */
961
+ selectedInstrumentName?: string | null;
926
962
  }
927
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, }: InstrumentDrawerProps): React.ReactElement;
963
+ declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
928
964
 
929
965
  /**
930
966
  * VolumeSlider Component
@@ -1133,4 +1169,4 @@ declare function sliderToDb(slider: number): number;
1133
1169
  */
1134
1170
  declare function dbToSlider(db: number): number;
1135
1171
 
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 };
1172
+ 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 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
@@ -163,6 +163,10 @@ interface PluginHost {
163
163
  getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;
164
164
  /** Change the instrument plugin on a track. Preserves MIDI data. */
165
165
  setTrackInstrument(trackId: string, pluginId: string): Promise<void>;
166
+ /** Open the instrument plugin's native editor GUI as a floating window. */
167
+ showInstrumentEditor(trackId: string): Promise<void>;
168
+ /** Close the instrument plugin's editor window. */
169
+ hideInstrumentEditor(trackId: string): Promise<void>;
166
170
  /** Get the FULL generation context for the active scene. */
167
171
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
168
172
  /** Get lightweight musical context (no concurrent track MIDI data). */
@@ -267,6 +271,24 @@ interface PluginHost {
267
271
  logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;
268
272
  /** Start a timer. Returns a stop function that logs the duration. */
269
273
  startTimer(name: string): () => void;
274
+ /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */
275
+ splitStems(trackId: string): Promise<PluginStemSplitResult>;
276
+ /** Check if the stem splitter binary is available. */
277
+ isStemSplitterAvailable(): Promise<boolean>;
278
+ }
279
+ /** Stem type identifiers */
280
+ type StemType = 'vocals' | 'drums' | 'bass' | 'other';
281
+ /** Result of splitting an audio track into stems */
282
+ interface PluginStemSplitResult {
283
+ /** Created stem tracks with audio loaded (all auto-muted) */
284
+ stems: PluginStemTrackInfo[];
285
+ }
286
+ /** Information about a single stem track created by stem splitting */
287
+ interface PluginStemTrackInfo {
288
+ /** The stem type (vocals, drums, bass, other) */
289
+ stemType: StemType;
290
+ /** Track handle for the new stem track */
291
+ track: PluginTrackHandle;
270
292
  }
271
293
  interface ExportedPluginData {
272
294
  pluginId: string;
@@ -902,14 +924,20 @@ interface SDKTrackRowProps {
902
924
  instrumentsLoading?: boolean;
903
925
  /** Re-scan for instruments */
904
926
  onRefreshInstruments?: () => void;
927
+ /** Which stage the instrument drawer is in */
928
+ instrumentDrawerStage?: 'instruments' | 'editor';
929
+ /** Called when user clicks "Open Editor" */
930
+ onShowEditor?: () => void;
931
+ /** Called when user wants to go back from editor view */
932
+ onBackToInstruments?: () => void;
905
933
  }
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;
934
+ 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
935
 
908
936
  /**
909
- * InstrumentDrawer — Sliding drawer for selecting instrument plugins (VST3/AU).
937
+ * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.
910
938
  *
911
- * Appears below the track controls when the "P" button is toggled.
912
- * Shows a searchable grid of available instrument plugins.
939
+ * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
940
+ * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
913
941
  */
914
942
 
915
943
  interface InstrumentDrawerProps {
@@ -923,8 +951,16 @@ interface InstrumentDrawerProps {
923
951
  onSelect: (pluginId: string) => void;
924
952
  /** Called when user clicks refresh to re-scan plugins */
925
953
  onRefresh: () => void;
954
+ /** Which stage the drawer is in */
955
+ stage?: 'instruments' | 'editor';
956
+ /** Called when user clicks "Open Editor" */
957
+ onShowEditor?: () => void;
958
+ /** Called when user wants to go back from editor view to instrument list */
959
+ onBackToInstruments?: () => void;
960
+ /** Name of the selected instrument (for display in editor header) */
961
+ selectedInstrumentName?: string | null;
926
962
  }
927
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, }: InstrumentDrawerProps): React.ReactElement;
963
+ declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
928
964
 
929
965
  /**
930
966
  * VolumeSlider Component
@@ -1133,4 +1169,4 @@ declare function sliderToDb(slider: number): number;
1133
1169
  */
1134
1170
  declare function dbToSlider(db: number): number;
1135
1171
 
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 };
1172
+ 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 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.js CHANGED
@@ -125,24 +125,60 @@ function InstrumentDrawer({
125
125
  currentPluginId,
126
126
  isLoading,
127
127
  onSelect,
128
- onRefresh
128
+ onRefresh,
129
+ stage = "instruments",
130
+ onShowEditor,
131
+ onBackToInstruments,
132
+ selectedInstrumentName
129
133
  }) {
130
134
  const [search, setSearch] = (0, import_react.useState)("");
131
135
  const SURGE_XT_DEFAULT_ID = "Surge XT";
132
136
  const filtered = (0, import_react.useMemo)(() => {
133
- const all = instruments.filter(
137
+ let all = instruments.filter(
134
138
  (i) => i.name !== "Surge XT"
135
139
  );
136
- if (!search.trim()) return all;
137
- const q = search.toLowerCase();
138
- return all.filter(
139
- (i) => i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q)
140
- );
141
- }, [instruments, search]);
140
+ if (search.trim()) {
141
+ const q = search.toLowerCase();
142
+ all = all.filter(
143
+ (i) => i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q)
144
+ );
145
+ }
146
+ if (currentPluginId) {
147
+ const selectedIdx = all.findIndex((i) => i.pluginId === currentPluginId);
148
+ if (selectedIdx > 0) {
149
+ const [selected] = all.splice(selectedIdx, 1);
150
+ all.unshift(selected);
151
+ }
152
+ }
153
+ return all;
154
+ }, [instruments, search, currentPluginId]);
142
155
  const isDefaultSelected = currentPluginId === null;
143
156
  const isSelected = (pluginId) => {
144
157
  return pluginId === currentPluginId;
145
158
  };
159
+ if (stage === "editor") {
160
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2", children: [
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
163
+ "button",
164
+ {
165
+ onClick: () => onBackToInstruments?.(),
166
+ className: "px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors",
167
+ children: "\u2190 Back"
168
+ }
169
+ ),
170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-sas-muted font-medium truncate flex-1", children: selectedInstrumentName ?? "Plugin" })
171
+ ] }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
173
+ "button",
174
+ {
175
+ onClick: () => onShowEditor?.(),
176
+ className: "w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors",
177
+ children: "Open Plugin Editor"
178
+ }
179
+ )
180
+ ] });
181
+ }
146
182
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2", children: [
147
183
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
148
184
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1007,7 +1043,10 @@ function TrackRow({
1007
1043
  currentInstrumentPluginId,
1008
1044
  onInstrumentSelect,
1009
1045
  instrumentsLoading,
1010
- onRefreshInstruments
1046
+ onRefreshInstruments,
1047
+ instrumentDrawerStage,
1048
+ onShowEditor,
1049
+ onBackToInstruments
1011
1050
  }) {
1012
1051
  const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;
1013
1052
  const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);
@@ -1152,9 +1191,9 @@ function TrackRow({
1152
1191
  {
1153
1192
  "data-testid": "sdk-shuffle-button",
1154
1193
  onClick: onShuffle,
1155
- disabled: !hasMidi || isGenerating,
1156
- className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
1157
- title: hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
1194
+ disabled: !hasMidi || isGenerating || !!currentInstrumentPluginId,
1195
+ className: `w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${!hasMidi || isGenerating || !!currentInstrumentPluginId ? "bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
1196
+ title: currentInstrumentPluginId ? "Shuffle only works with default Surge XT" : hasMidi ? "Re-roll sound (keep MIDI)" : "Generate MIDI first",
1158
1197
  children: "Shuffle"
1159
1198
  }
1160
1199
  ),
@@ -1214,7 +1253,11 @@ function TrackRow({
1214
1253
  currentPluginId: currentInstrumentPluginId ?? null,
1215
1254
  isLoading: instrumentsLoading ?? false,
1216
1255
  onSelect: onInstrumentSelect,
1217
- onRefresh: onRefreshInstruments
1256
+ onRefresh: onRefreshInstruments,
1257
+ stage: instrumentDrawerStage,
1258
+ onShowEditor,
1259
+ onBackToInstruments,
1260
+ selectedInstrumentName: instrumentName
1218
1261
  }
1219
1262
  ) })
1220
1263
  ] });