API Reference
Complete reference for the PluginHost API — the scoped interface that plugins use to interact with Signals & Sorcery. Each plugin receives its own PluginHost instance with ownership-scoped access.
Track Management
All track methods are ownership-scoped — plugins can only modify tracks they created. Attempting to modify another plugin's track throws a NOT_OWNED error.
createTrack(options)
Create a new track in the active scene.
createTrack(options: CreateTrackOptions): Promise<PluginTrackHandle>
Parameters:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | auto-generated | Display name for the track |
role | string | — | Musical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx' |
loadSynth | boolean | false | Load a synth plugin immediately |
synthName | string | 'Surge XT' | Which synth to load (ignored if loadSynth is false) |
metadata | Record<string, unknown> | — | Plugin-specific metadata stored in the database |
Returns: PluginTrackHandle with id, name, dbId, and optional role.
Errors: NO_ACTIVE_SCENE, TRACK_LIMIT_EXCEEDED, ENGINE_ERROR
const track = await host.createTrack({
name: 'Bass Line',
role: 'bass',
loadSynth: true,
});
// track.id = engine track ID (use for all subsequent operations)
// track.dbId = database row ID
deleteTrack(trackId)
Delete a track previously created by this plugin.
deleteTrack(trackId: string): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND, ENGINE_ERROR
getPluginTracks()
Get all tracks this plugin owns in the active scene.
getPluginTracks(): Promise<PluginTrackHandle[]>
Returns an empty array if the plugin has no tracks or no scene is active.
getTrackInfo(trackId)
Get detailed info about a specific owned track.
getTrackInfo(trackId: string): Promise<PluginTrackInfo>
Returns:
| Field | Type | Description |
|---|---|---|
id | string | Engine track ID |
name | string | Display name |
dbId | string | Database row ID |
role | string | Musical role |
muted | boolean | Is track muted? |
soloed | boolean | Is track soloed? |
volume | number | Volume (0.0 – 1.0) |
pan | number | Pan (-1.0 left to 1.0 right) |
plugins | PluginSynthInfo[] | Loaded synth plugins |
hasMidi | boolean | Has MIDI clips? |
hasAudio | boolean | Has audio clips? |
Errors: NOT_OWNED, TRACK_NOT_FOUND
setTrackMute(trackId, muted)
setTrackMute(trackId: string, muted: boolean): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND
setTrackVolume(trackId, volume)
setTrackVolume(trackId: string, volume: number): Promise<void>
volume is linear: 0.0 (silent) to 1.0 (full).
Errors: NOT_OWNED, TRACK_NOT_FOUND
setTrackPan(trackId, pan)
setTrackPan(trackId: string, pan: number): Promise<void>
pan range: -1.0 (hard left) to 1.0 (hard right). 0.0 is center.
Errors: NOT_OWNED, TRACK_NOT_FOUND
setTrackName(trackId, name)
setTrackName(trackId: string, name: string): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND
adoptSceneTracks()
Adopt unowned tracks in the active scene that match this plugin's generator type. Useful when re-activating a plugin or restoring state — tracks that were previously created by a plugin of the same type but currently have no owner will be claimed.
adoptSceneTracks(): Promise<PluginTrackHandle[]>
Returns: Array of PluginTrackHandle for all newly adopted tracks. Returns an empty array if no matching unowned tracks are found.
Errors: NO_ACTIVE_SCENE, ENGINE_ERROR
// Re-adopt tracks on scene change
async onSceneChanged(sceneId: string | null): Promise<void> {
if (!sceneId) return;
const adopted = await this.host.adoptSceneTracks();
console.log(`Re-adopted ${adopted.length} tracks`);
}
setTrackSolo(trackId, solo)
setTrackSolo(trackId: string, solo: boolean): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND
// Solo a track to preview it in isolation
await host.setTrackSolo(track.id, true);
shufflePreset(trackId)
Randomly change the Surge XT preset on an owned track. Reads the track's existing MIDI notes to analyze the pitch range, then selects a random preset from a matching category (e.g., bass notes get bass presets). The current preset is excluded so you always get a different sound.
shufflePreset(trackId: string): Promise<ShufflePresetResult>
Returns:
| Field | Type | Description |
|---|---|---|
presetName | string | Name of the newly applied preset |
presetCategory | string | Category the preset was drawn from (e.g., 'basses-low') |
Errors: NOT_OWNED, PLUGIN_NOT_FOUND (no Surge XT on track), ENGINE_ERROR
const result = await host.shufflePreset(track.id);
host.showToast('info', 'New Preset', `${result.presetName} (${result.presetCategory})`);
duplicateTrack(trackId)
Create a copy of an owned track. Copies the track's MIDI data, role, and loads Surge XT on the new track. The new track is automatically owned by the calling plugin.
duplicateTrack(trackId: string): Promise<PluginTrackHandle>
Returns: PluginTrackHandle for the new track (name will be "<original>-copy").
Errors: NOT_OWNED, NO_ACTIVE_SCENE, TRACK_LIMIT_EXCEEDED, ENGINE_ERROR
const copy = await host.duplicateTrack(track.id);
// copy.id = new engine track ID
// copy.name = 'Bass Line-copy'
// Give the copy a different preset
await host.shufflePreset(copy.id);
FX Operations
Per-track FX processing with 6 categories in signal chain order: eq → compressor → chorus → phaser → delay → reverb. All FX methods are ownership-scoped.
getTrackFxState(trackId)
Get the detailed FX state for a track — enabled/disabled, preset index, and dry/wet level for each category.
getTrackFxState(trackId: string): Promise<PluginTrackFxDetailState>
PluginTrackFxDetailState is Record<string, PluginFxCategoryDetailState> — one entry per FX category.
PluginFxCategoryDetailState:
| Field | Type | Description |
|---|---|---|
enabled | boolean | Whether this FX category is active |
presetIndex | number | Current preset index (0–4) |
dryWet | number | Dry/wet mix level (0.0 – 1.0) |
Errors: NOT_OWNED, TRACK_NOT_FOUND
const fxState = await host.getTrackFxState(track.id);
console.log(fxState.reverb.enabled); // false
console.log(fxState.reverb.presetIndex); // 0
console.log(fxState.reverb.dryWet); // 0.33
toggleTrackFx(trackId, category, enabled)
Toggle an FX category on or off for a track.
toggleTrackFx(trackId: string, category: string, enabled: boolean): Promise<void>
Parameters:
| Field | Type | Description |
|---|---|---|
trackId | string | Engine track ID (must be owned) |
category | string | FX category: 'eq', 'compressor', 'chorus', 'phaser', 'delay', 'reverb' |
enabled | boolean | Whether to enable or disable the FX |
Errors: NOT_OWNED, TRACK_NOT_FOUND
// Enable reverb on a track
await host.toggleTrackFx(track.id, 'reverb', true);
setTrackFxPreset(trackId, category, presetIndex)
Set the FX preset for a category. Each category has 5 presets (index 0–4). Returns the new dry/wet value if the preset changes it.
setTrackFxPreset(trackId: string, category: string, presetIndex: number): Promise<{ dryWet?: number }>
Parameters:
| Field | Type | Description |
|---|---|---|
trackId | string | Engine track ID (must be owned) |
category | string | FX category |
presetIndex | number | Preset index (0–4) |
Returns: { dryWet?: number } — the new dry/wet level if the preset sets one.
Errors: NOT_OWNED, TRACK_NOT_FOUND
// Set reverb to preset 2 (e.g., "Hall")
const result = await host.setTrackFxPreset(track.id, 'reverb', 2);
if (result.dryWet !== undefined) {
console.log('New dry/wet:', result.dryWet);
}
setTrackFxDryWet(trackId, category, value)
Set the dry/wet mix level for an FX category.
setTrackFxDryWet(trackId: string, category: string, value: number): Promise<void>
Parameters:
| Field | Type | Description |
|---|---|---|
trackId | string | Engine track ID (must be owned) |
category | string | FX category |
value | number | Dry/wet level (0.0 fully dry – 1.0 fully wet) |
Errors: NOT_OWNED, TRACK_NOT_FOUND
// Set delay mix to 50%
await host.setTrackFxDryWet(track.id, 'delay', 0.5);
MIDI Operations
writeMidiClip(trackId, clip)
Write MIDI notes to a track. Replaces any existing MIDI on the track.
writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>
MidiClipData:
| Field | Type | Description |
|---|---|---|
startTime | number | Clip start time in seconds |
endTime | number | Clip end time in seconds |
tempo | number | BPM for beat/time conversion |
notes | PluginMidiNote[] | Array of MIDI notes |
PluginMidiNote:
| Field | Type | Description |
|---|---|---|
pitch | number | MIDI pitch 0–127 |
startBeat | number | Start position in quarter-note beats (0 = clip start) |
durationBeats | number | Duration in quarter-note beats |
velocity | number | Velocity 1–127 |
channel | number | MIDI channel 0–15 (default: 0) |
Returns: MidiWriteResult with notesInserted count and actual bars covered.
Errors: NOT_OWNED, TRACK_NOT_FOUND, INVALID_MIDI
await host.writeMidiClip(track.id, {
startTime: 0,
endTime: 8, // 8 seconds
tempo: 120,
notes: [
{ pitch: 60, startBeat: 0, durationBeats: 1, velocity: 100 },
{ pitch: 64, startBeat: 1, durationBeats: 1, velocity: 90 },
{ pitch: 67, startBeat: 2, durationBeats: 1, velocity: 95 },
{ pitch: 72, startBeat: 3, durationBeats: 0.5, velocity: 80 },
],
});
clearMidi(trackId)
Clear all MIDI from a track.
clearMidi(trackId: string): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND
postProcessMidi(notes, options)
Run the host's MIDI post-processing pipeline: quantize, swing, scale enforcement, register clamping, overlap removal, and humanization.
postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>
PostProcessOptions:
| Field | Type | Default | Description |
|---|---|---|---|
quantize | boolean | true | Snap notes to grid |
quantizeGrid | string | '1/16' | Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T' |
quantizeStrength | number | 75 | Quantize strength 0–100 |
swing | number | 0 | Swing amount 0–100 |
humanize | number | 0 | Timing/velocity variation 0–100 |
enforceScale | boolean | false | Enforce diatonic scale (uses scene key/mode) |
clampRegister | [number, number] | — | Clamp note pitches to [low, high] range |
removeOverlaps | boolean | true | Remove overlapping notes on same pitch/channel |
const raw = generateNotes();
const processed = await host.postProcessMidi(raw, {
quantize: true,
quantizeGrid: '1/8',
swing: 30,
humanize: 15,
enforceScale: true,
});
await host.writeMidiClip(track.id, { ...clip, notes: processed });
auditionNote(trackId, pitch, velocity, durationMs)
Play a single note on a track for preview. Fire-and-forget — does not record.
auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>
Audio Operations
writeAudioClip(trackId, filePath, position?)
Place an audio file on a track.
writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>
Errors: NOT_OWNED, TRACK_NOT_FOUND, FILE_NOT_FOUND, INVALID_FORMAT
generateAudioTexture(request)
Invoke the host's AI audio texture generation pipeline.
generateAudioTexture(request: PluginAudioTextureRequest): Promise<PluginAudioTextureResult>
PluginAudioTextureRequest:
| Field | Type | Default | Description |
|---|---|---|---|
prompt | string | — | Text description of the desired audio |
durationSeconds | number | scene length | Duration in seconds |
bpm | number | project BPM | Target BPM |
Returns: PluginAudioTextureResult with filePath and durationSeconds.
Stem Splitting
splitStems(trackId)
Split an audio track into separate stems (vocals, drums, bass, other). Creates new muted tracks for each stem.
| Param | Type | Description |
|---|---|---|
trackId | string | Engine track ID of the audio track to split |
Returns: PluginStemSplitResult
interface PluginStemSplitResult {
stems: PluginStemTrackInfo[];
}
interface PluginStemTrackInfo {
stemType: 'vocals' | 'drums' | 'bass' | 'other';
track: PluginTrackHandle;
}
const result = await host.splitStems(audioTrack.id);
for (const stem of result.stems) {
console.log(`Created ${stem.stemType} track: ${stem.track.id}`);
// Stems are auto-muted — unmute the ones you want
await host.setTrackMute(stem.track.id, false);
}
isStemSplitterAvailable()
Check if the stem splitter binary is available on the system.
Returns: Promise<boolean>
const available = await host.isStemSplitterAvailable();
if (!available) {
host.showToast('warning', 'Stem splitter not installed');
}
Plugin/Synth Operations
loadSynthPlugin(trackId, pluginName)
Load a VST3 or AudioUnit plugin onto a track.
loadSynthPlugin(trackId: string, pluginName: string): Promise<number>
Returns: Plugin index (for use with setPluginState, getPluginState, removePlugin).
Errors: NOT_OWNED, TRACK_NOT_FOUND, PLUGIN_NOT_FOUND
setPluginState(trackId, pluginIndex, stateBase64)
Set plugin state using base64-encoded preset data.
setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>
getPluginState(trackId, pluginIndex)
Get current plugin state as a base64-encoded string.
getPluginState(trackId: string, pluginIndex: number): Promise<string>
getTrackPlugins(trackId)
List plugins loaded on a track.
getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>
PluginSynthInfo:
| Field | Type | Description |
|---|---|---|
index | number | Plugin slot index |
name | string | Plugin name |
type | string | 'VST3', 'AudioUnit', or 'Internal' |
enabled | boolean | Whether the plugin is active |
removePlugin(trackId, pluginIndex)
Remove a plugin from a track.
removePlugin(trackId: string, pluginIndex: number): Promise<void>
isPluginAvailable(pluginName)
Check if a VST3/AU plugin is installed on the system.
isPluginAvailable(pluginName: string): Promise<boolean>
Scene Context
getGenerationContext(excludeTrackId?)
Get the full generation context for the active scene, including concurrent track MIDI data. Use excludeTrackId to omit the current track's data (common when generating for that track).
getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>
PluginGenerationContext:
| Field | Type | Description |
|---|---|---|
chordProgression | object | Key (tonic, mode), chordsWithTiming, genre |
concurrentTracks | PluginConcurrentTrackInfo[] | Other tracks with their MIDI, organized by chord |
const ctx = await host.getGenerationContext(myTrack.id);
// ctx.chordProgression.key = { tonic: 'C', mode: 'minor' }
// ctx.concurrentTracks[0].notesByChord[0].chord = 'Cm7'
getMusicalContext()
Lightweight musical context without concurrent track data.
getMusicalContext(): Promise<MusicalContext>
MusicalContext:
| Field | Type | Description |
|---|---|---|
key | string | Tonic: 'C', 'D', 'Eb', 'F#', etc. |
mode | string | 'major', 'minor', 'dorian', 'mixolydian', etc. |
bpm | number | Beats per minute (20–960) |
bars | number | Scene length in bars |
genre | string | null | Genre hint: 'Drum & Bass', 'Lo-fi Hip Hop', etc. |
timeSignature | string | '4/4', '3/4', '6/8' |
chordProgression | PluginChordTiming[] | Chord symbols with quarter-note timing |
getActiveSceneId()
Get the currently active scene ID. Returns null if no scene is selected.
getActiveSceneId(): string | null
getSceneList()
Get all scenes in the project.
getSceneList(): Promise<PluginSceneInfo[]>
PluginSceneInfo:
| Field | Type | Description |
|---|---|---|
id | string | Scene UUID |
name | string | Scene name |
isMuted | boolean | Whether the scene is muted |
Transport & Events
onTrackStateChange(listener)
Subscribe to real-time track state changes (mute, solo, volume, pan). The listener fires whenever the engine reports a state change for any track owned by this plugin. Returns an unsubscribe function.
onTrackStateChange(listener: TrackStateChangeListener): UnsubscribeFn
TrackStateChangeListener is (trackId: string, state: PluginTrackRuntimeState) => void.
PluginTrackRuntimeState:
| Field | Type | Description |
|---|---|---|
id | string | Engine track ID |
muted | boolean | Whether the track is muted |
solo | boolean | Whether the track is soloed |
volume | number | Volume level (0.0 – 1.0) |
pan | number | Pan position (-1.0 left – 1.0 right) |
const unsub = host.onTrackStateChange((trackId, state) => {
console.log(`Track ${trackId}: muted=${state.muted}, volume=${state.volume}`);
// Update your UI state accordingly
});
// Later: clean up
unsub();
onTransportEvent(listener)
Subscribe to transport state changes (play, stop, BPM changes).
onTransportEvent(listener: TransportEventListener): UnsubscribeFn
TransportEvent:
| Field | Type | Description |
|---|---|---|
type | string | 'play', 'stop', 'pause', 'bpmChange', 'positionChange' |
bpm | number | Current BPM (on bpmChange) |
position | number | Position in seconds |
isPlaying | boolean | Whether transport is playing |
const unsub = host.onTransportEvent((event) => {
if (event.type === 'bpmChange') {
console.log('New BPM:', event.bpm);
}
});
// Later: clean up
unsub();
onDeckBoundary(listener)
Subscribe to deck loop boundary events — fired when a deck loops back to the start.
onDeckBoundary(listener: DeckBoundaryListener): UnsubscribeFn
DeckBoundaryEvent:
| Field | Type | Description |
|---|---|---|
deckId | string | 'loop-a' or 'loop-b' |
bar | number | Current bar number (1-based) |
beat | number | Current beat within bar (1-based) |
loopCount | number | How many loops completed |
onSceneChange(listener)
Subscribe to scene change events.
onSceneChange(listener: SceneChangeListener): UnsubscribeFn
Listener receives the new scene ID (string) or null if no scene is active.
getTransportState()
Get a one-shot snapshot of the current transport state.
getTransportState(): Promise<PluginTransportState>
PluginTransportState:
| Field | Type | Description |
|---|---|---|
isPlaying | boolean | Transport is playing |
isPaused | boolean | Transport is paused |
bpm | number | Current BPM |
position | number | Position in seconds |
timeSignature | string | e.g., '4/4' |
LLM Access
LLM methods are metered and require authentication. Check availability before use.
generateWithLLM(request)
Generate text or JSON via the host's authenticated LLM service.
generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>
LLMGenerationRequest:
| Field | Type | Default | Description |
|---|---|---|---|
system | string | — | System prompt (instructions, role, output format) |
user | string | — | User prompt (the actual request) |
maxTokens | number | host default | Max tokens for response (host may cap) |
responseFormat | string | 'text' | 'text' or 'json' |
Returns:
| Field | Type | Description |
|---|---|---|
content | string | Response text (parse as JSON if responseFormat was 'json') |
tokensUsed | number | Tokens consumed |
model | string | Model that generated the response |
Errors: NOT_AUTHENTICATED, LLM_UNAVAILABLE, LLM_BUDGET_EXCEEDED
if (await host.isLLMAvailable()) {
const result = await host.generateWithLLM({
system: 'You are a music theory assistant. Return JSON.',
user: `Suggest a chord progression in ${context.key} ${context.mode}`,
responseFormat: 'json',
maxTokens: 500,
});
const chords = JSON.parse(result.content);
}
isLLMAvailable()
Check if LLM access is available (user authenticated and gateway reachable).
isLLMAvailable(): Promise<boolean>
Preset System
getPresetCategories(pluginName)
Get available preset categories for a synth plugin (e.g., Surge XT).
getPresetCategories(pluginName: string): Promise<string[]>
getRandomPreset(category)
Get a random preset from a category.
getRandomPreset(category: string): Promise<PluginPresetData | null>
getPresetByName(category, name)
Get a specific preset by name.
getPresetByName(category: string, name: string): Promise<PluginPresetData | null>
classifyPresetCategory(description)
Use LLM to classify a text description into a preset category.
classifyPresetCategory(description: string): Promise<string>
const category = await host.classifyPresetCategory('warm analog pad');
const preset = await host.getRandomPreset(category);
if (preset) {
await host.setPluginState(track.id, 0, preset.state);
}
Plugin Presets
Plugin-specific presets (distinct from synth presets). These store your plugin's custom configurations.
getPluginPresets(category?)
getPluginPresets(category?: string): Promise<PluginPresetInfo[]>
savePluginPreset(options)
savePluginPreset(options: SavePluginPresetOptions): Promise<PluginPresetInfo>
SavePluginPresetOptions:
| Field | Type | Description |
|---|---|---|
name | string | Preset name |
category | string | Optional category |
data | Record<string, unknown> | Preset data to store |
deletePluginPreset(id)
deletePluginPreset(id: string): Promise<void>
Data Persistence
Scene-Scoped Data
Per-scene data is tied to a specific scene. Use for track configurations, generation parameters, etc.
getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>
setSceneData(sceneId: string, key: string, value: unknown): Promise<void>
getAllSceneData(sceneId: string): Promise<Record<string, unknown>>
deleteSceneData(sceneId: string, key: string): Promise<void>
// Save pattern config for this scene
await host.setSceneData(sceneId, 'pattern', { steps: 16, pulses: 5 });
// Restore on scene change
const config = await host.getSceneData<PatternConfig>(sceneId, 'pattern');
Project-Scoped Data
Project-wide data persists across scenes.
getProjectData<T = unknown>(key: string): Promise<T | null>
setProjectData(key: string, value: unknown): Promise<void>
Global Settings
Global settings persist across projects. Managed via host.settings:
interface PluginSettingsStore {
get<T>(key: string, defaultValue: T): T;
set(key: string, value: unknown): void;
getAll(): Record<string, unknown>;
onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;
}
const density = host.settings.get<number>('density', 4);
host.settings.set('density', 8);
Data Directory
Get the absolute path to the plugin's isolated data directory on disk.
getDataDirectory(): string
File System
Requires the fileDialog capability in the manifest.
showOpenDialog(options)
Show a native file open dialog.
showOpenDialog(options: PluginFileDialogOptions): Promise<string[] | null>
Returns null if the user cancels.
PluginFileDialogOptions:
| Field | Type | Description |
|---|---|---|
title | string | Dialog title |
defaultPath | string | Starting directory |
filters | Array<{ name, extensions }> | File type filters |
multiSelections | boolean | Allow selecting multiple files |
directories | boolean | Allow selecting directories |
showSaveDialog(options)
Show a native file save dialog.
showSaveDialog(options: PluginFileDialogOptions): Promise<string | null>
downloadFile(url, filename, options?)
Download a file to the plugin's data directory.
downloadFile(url: string, filename: string, options?: PluginDownloadOptions): Promise<string>
Returns the absolute path to the downloaded file.
importFile(sourcePath, destFilename)
Copy a file into the plugin's data directory.
importFile(sourcePath: string, destFilename: string): Promise<string>
Network
Requires the network capability with allowedHosts in the manifest.
httpRequest(options)
Make an HTTP request to an allowed host.
httpRequest(options: PluginHttpRequestOptions): Promise<PluginHttpResponse>
PluginHttpRequestOptions:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Full URL (host must be in allowedHosts) |
method | string | 'GET' | 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' |
headers | Record<string, string> | — | Request headers |
body | string | Record<string, unknown> | — | Request body |
timeoutMs | number | 30000 | Timeout in milliseconds |
Returns: PluginHttpResponse with status, statusText, headers, and body.
Errors: CAPABILITY_DENIED (if host not in allowedHosts)
Secure Storage
Secrets are encrypted using the OS keychain (Electron safeStorage) and scoped per plugin. Plugin A cannot access plugin B's secrets.
storeSecret(key, value)
storeSecret(key: string, value: string): Promise<void>
getSecret(key)
getSecret(key: string): Promise<string | null>
deleteSecret(key)
deleteSecret(key: string): Promise<void>
Sample Library
getSamples(filter?)
Query the sample library.
getSamples(filter?: PluginSampleFilter): Promise<PluginSampleInfo[]>
PluginSampleFilter:
| Field | Type | Description |
|---|---|---|
bpm | number | Filter by BPM |
key | { tonic, mode? } | Filter by musical key |
category | string | Filter by category |
searchQuery | string | Text search |
getSampleById(id)
getSampleById(id: string): Promise<PluginSampleInfo | null>
importSamples(filePaths)
Import audio files into the sample library.
importSamples(filePaths: string[]): Promise<PluginSampleImportResult>
Returns: { imported: number, skipped: number, errors: string[] }
createSampleTrack(sampleId, options?)
Create a sample track in the active scene.
createSampleTrack(sampleId: string, options?: { name?: string }): Promise<PluginTrackHandle>
deleteSampleTrack(trackId)
deleteSampleTrack(trackId: string): Promise<void>
getPluginSampleTracks()
Get all sample tracks in the active scene, re-establishing ownership. Returns track handles paired with their sample metadata.
getPluginSampleTracks(): Promise<PluginSampleTrackInfo[]>
PluginSampleTrackInfo:
| Field | Type | Description |
|---|---|---|
track | PluginTrackHandle | Track handle (id, name, dbId, role) |
sample | PluginSampleInfo | Associated sample metadata |
volume | number | Track volume (0.0 – 1.0) |
pan | number | Track pan (-1.0 left – 1.0 right) |
const sampleTracks = await host.getPluginSampleTracks();
for (const st of sampleTracks) {
console.log(`${st.track.name} → ${st.sample.filename} (vol: ${st.volume})`);
}
Notifications & Progress
showToast(type, title, message?)
Show a toast notification.
showToast(type: 'info' | 'success' | 'warning' | 'error', title: string, message?: string): void
setProgress(trackId, progress)
Show a progress indicator on a track. Pass -1 to hide.
setProgress(trackId: string, progress: number): void
progress range: 0 to 100, or -1 to hide.
setStatusMessage(message)
Set a status message in the plugin's accordion header. Pass null to clear.
setStatusMessage(message: string | null): void
confirmAction(title, message)
Show a confirmation modal dialog. Returns true if the user confirms.
confirmAction(title: string, message: string): Promise<boolean>
Performance / Logging
logMetric(name, durationMs, metadata?)
Log a performance metric.
logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void
startTimer(name)
Start a timer. Returns a stop function that automatically calls logMetric().
startTimer(name: string): () => void
const stop = host.startTimer('pattern-generation');
const notes = generatePattern(steps, pulses);
stop(); // logs: "pattern-generation: 42ms"
Error Codes
All errors thrown by the host are PluginError instances with a typed code property:
class PluginError extends Error {
readonly code: PluginErrorCode;
readonly details?: Record<string, unknown>;
}
| Code | Description |
|---|---|
NOT_OWNED | Tried to modify a track not owned by this plugin |
TRACK_NOT_FOUND | Track ID doesn't exist in engine |
TRACK_LIMIT_EXCEEDED | Plugin has too many tracks (default: 16 per scene) |
NO_ACTIVE_SCENE | No scene is selected |
ENGINE_ERROR | Tracktion engine call failed |
INVALID_MIDI | Malformed MIDI data |
FILE_NOT_FOUND | Audio file doesn't exist |
INVALID_FORMAT | Unsupported audio format |
PLUGIN_NOT_FOUND | VST/AU plugin not installed |
LLM_BUDGET_EXCEEDED | Over token limit |
LLM_UNAVAILABLE | Gateway unreachable |
NOT_AUTHENTICATED | User not logged in |
TIMEOUT | Operation timed out |
CANCELLED | User cancelled the operation |
INCOMPATIBLE | Plugin requires newer SDK version |
CAPABILITY_DENIED | Plugin lacks required capability in manifest |
SECRET_NOT_FOUND | Secret key doesn't exist |
import { PluginError } from '@signalsandsorcery/plugin-sdk';
try {
await host.createTrack({ name: 'New Track' });
} catch (err) {
if (err instanceof PluginError) {
switch (err.code) {
case 'NO_ACTIVE_SCENE':
host.showToast('warning', 'Select a scene first');
break;
case 'TRACK_LIMIT_EXCEEDED':
host.showToast('error', 'Too many tracks', 'Delete some tracks first');
break;
default:
host.showToast('error', 'Error', err.message);
}
}
}