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:

FieldTypeDefaultDescription
namestringauto-generatedDisplay name for the track
rolestringMusical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx'
loadSynthbooleanfalseLoad a synth plugin immediately
synthNamestring'Surge XT'Which synth to load (ignored if loadSynth is false)
metadataRecord<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:

FieldTypeDescription
idstringEngine track ID
namestringDisplay name
dbIdstringDatabase row ID
rolestringMusical role
mutedbooleanIs track muted?
soloedbooleanIs track soloed?
volumenumberVolume (0.0 – 1.0)
pannumberPan (-1.0 left to 1.0 right)
pluginsPluginSynthInfo[]Loaded synth plugins
hasMidibooleanHas MIDI clips?
hasAudiobooleanHas 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


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:

FieldTypeDescription
startTimenumberClip start time in seconds
endTimenumberClip end time in seconds
temponumberBPM for beat/time conversion
notesPluginMidiNote[]Array of MIDI notes

PluginMidiNote:

FieldTypeDescription
pitchnumberMIDI pitch 0–127
startBeatnumberStart position in quarter-note beats (0 = clip start)
durationBeatsnumberDuration in quarter-note beats
velocitynumberVelocity 1–127
channelnumberMIDI 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:

FieldTypeDefaultDescription
quantizebooleantrueSnap notes to grid
quantizeGridstring'1/16'Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T'
quantizeStrengthnumber75Quantize strength 0–100
swingnumber0Swing amount 0–100
humanizenumber0Timing/velocity variation 0–100
enforceScalebooleanfalseEnforce diatonic scale (uses scene key/mode)
clampRegister[number, number]Clamp note pitches to [low, high] range
removeOverlapsbooleantrueRemove 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:

FieldTypeDefaultDescription
promptstringText description of the desired audio
durationSecondsnumberscene lengthDuration in seconds
bpmnumberproject BPMTarget BPM

Returns: PluginAudioTextureResult with filePath and durationSeconds.


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:

FieldTypeDescription
indexnumberPlugin slot index
namestringPlugin name
typestring'VST3', 'AudioUnit', or 'Internal'
enabledbooleanWhether 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:

FieldTypeDescription
chordProgressionobjectKey (tonic, mode), chordsWithTiming, genre
concurrentTracksPluginConcurrentTrackInfo[]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:

FieldTypeDescription
keystringTonic: 'C', 'D', 'Eb', 'F#', etc.
modestring'major', 'minor', 'dorian', 'mixolydian', etc.
bpmnumberBeats per minute (20–960)
barsnumberScene length in bars
genrestring | nullGenre hint: 'Drum & Bass', 'Lo-fi Hip Hop', etc.
timeSignaturestring'4/4', '3/4', '6/8'
chordProgressionPluginChordTiming[]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:

FieldTypeDescription
idstringScene UUID
namestringScene name
isMutedbooleanWhether the scene is muted

Transport & Events

onTransportEvent(listener)

Subscribe to transport state changes (play, stop, BPM changes).

onTransportEvent(listener: TransportEventListener): UnsubscribeFn

TransportEvent:

FieldTypeDescription
typestring'play', 'stop', 'pause', 'bpmChange', 'positionChange'
bpmnumberCurrent BPM (on bpmChange)
positionnumberPosition in seconds
isPlayingbooleanWhether 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:

FieldTypeDescription
deckIdstring'loop-a' or 'loop-b'
barnumberCurrent bar number (1-based)
beatnumberCurrent beat within bar (1-based)
loopCountnumberHow 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:

FieldTypeDescription
isPlayingbooleanTransport is playing
isPausedbooleanTransport is paused
bpmnumberCurrent BPM
positionnumberPosition in seconds
timeSignaturestringe.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:

FieldTypeDefaultDescription
systemstringSystem prompt (instructions, role, output format)
userstringUser prompt (the actual request)
maxTokensnumberhost defaultMax tokens for response (host may cap)
responseFormatstring'text''text' or 'json'

Returns:

FieldTypeDescription
contentstringResponse text (parse as JSON if responseFormat was 'json')
tokensUsednumberTokens consumed
modelstringModel 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:

FieldTypeDescription
namestringPreset name
categorystringOptional category
dataRecord<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:

FieldTypeDescription
titlestringDialog title
defaultPathstringStarting directory
filtersArray<{ name, extensions }>File type filters
multiSelectionsbooleanAllow selecting multiple files
directoriesbooleanAllow 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:

FieldTypeDefaultDescription
urlstringFull URL (host must be in allowedHosts)
methodstring'GET''GET', 'POST', 'PUT', 'DELETE', 'PATCH'
headersRecord<string, string>Request headers
bodystring | Record<string, unknown>Request body
timeoutMsnumber30000Timeout 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:

FieldTypeDescription
bpmnumberFilter by BPM
key{ tonic, mode? }Filter by musical key
categorystringFilter by category
searchQuerystringText 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>

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>;
}
CodeDescription
NOT_OWNEDTried to modify a track not owned by this plugin
TRACK_NOT_FOUNDTrack ID doesn't exist in engine
TRACK_LIMIT_EXCEEDEDPlugin has too many tracks (default: 16 per scene)
NO_ACTIVE_SCENENo scene is selected
ENGINE_ERRORTracktion engine call failed
INVALID_MIDIMalformed MIDI data
FILE_NOT_FOUNDAudio file doesn't exist
INVALID_FORMATUnsupported audio format
PLUGIN_NOT_FOUNDVST/AU plugin not installed
LLM_BUDGET_EXCEEDEDOver token limit
LLM_UNAVAILABLEGateway unreachable
NOT_AUTHENTICATEDUser not logged in
TIMEOUTOperation timed out
CANCELLEDUser cancelled the operation
INCOMPATIBLEPlugin requires newer SDK version
CAPABILITY_DENIEDPlugin lacks required capability in manifest
SECRET_NOT_FOUNDSecret key doesn't exist
import { PluginError } from '@sas/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);
    }
  }
}