diff --git a/src/lib/csound/engine.ts b/src/lib/csound/engine.ts index 2fa0913..0ff7054 100644 --- a/src/lib/csound/engine.ts +++ b/src/lib/csound/engine.ts @@ -1,23 +1,40 @@ import { Csound } from '@csound/browser'; +export type InstanceMode = 'persistent' | 'ephemeral'; + export interface CsoundEngineOptions { onMessage?: (message: string) => void; onError?: (error: string) => void; onPerformanceEnd?: () => void; onAnalyserNodeCreated?: (node: AnalyserNode) => void; + instanceMode?: InstanceMode; +} + +export interface PerformanceMetrics { + kperiodCount: number; + currentTime: number; + isRunning: boolean; +} + +export interface CompilationResult { + success: boolean; + errorMessage?: string; } export class CsoundEngine { private csound: Csound | null = null; private initialized = false; + private compiled = false; private running = false; private options: CsoundEngineOptions; private scopeNode: AnalyserNode | null = null; private audioNode: AudioNode | null = null; private audioContext: AudioContext | null = null; + private instanceMode: InstanceMode; constructor(options: CsoundEngineOptions = {}) { this.options = options; + this.instanceMode = options.instanceMode ?? 'ephemeral'; } async init(): Promise { @@ -27,7 +44,98 @@ export class CsoundEngine { this.log('Csound ready'); } - async evaluateCode(code: string): Promise { + private async ensureCsoundInstance(): Promise { + if (!this.csound) { + this.log('Creating Csound instance...'); + this.csound = await Csound(); + this.setupCallbacks(); + await this.csound.setOption('-odac'); + } + } + + private setupCallbacks(): void { + if (!this.csound) return; + + this.csound.on('message', (message: string) => { + this.options.onMessage?.(message); + }); + + this.csound.on('onAudioNodeCreated', (node: AudioNode) => { + this.audioNode = node; + this.audioContext = node.context as AudioContext; + this.log('Audio node created and captured'); + }); + + this.csound.on('realtimePerformanceEnded', async () => { + this.running = false; + this.log('Performance complete'); + this.options.onPerformanceEnd?.(); + }); + } + + async compileOrchestra(orchestra: string): Promise { + if (!this.initialized) { + throw new Error('Csound not initialized. Call init() first.'); + } + + try { + await this.ensureCsoundInstance(); + + this.log('Compiling orchestra...'); + const result = await this.csound!.compileOrc(orchestra); + + if (result !== 0) { + const errorMsg = 'Orchestra compilation failed'; + this.error(errorMsg); + return { success: false, errorMessage: errorMsg }; + } + + this.compiled = true; + this.log('Orchestra compiled successfully'); + return { success: true }; + + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Compilation failed'; + this.error(errorMsg); + return { success: false, errorMessage: errorMsg }; + } + } + + async startPerformance(): Promise { + if (!this.compiled) { + throw new Error('No orchestra compiled. Call compileOrchestra() first.'); + } + + if (this.running) { + this.log('Already running'); + return; + } + + this.log('Starting performance...'); + await this.csound!.start(); + this.running = true; + this.setupAnalyser(); + this.log('Performance started'); + } + + async readScore(score: string): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + this.log('Reading score...'); + await this.csound.readScore(score); + } + + async sendScoreEvent(event: string): Promise { + if (!this.running) { + throw new Error('Csound not running. Call startPerformance() first.'); + } + + await this.csound!.readScore(event); + } + + async evaluateCode(code: string, forceNewInstance = false): Promise { if (!this.initialized) { throw new Error('Csound not initialized. Call init() first.'); } @@ -37,32 +145,21 @@ export class CsoundEngine { await this.stop(); } + const needsNewInstance = forceNewInstance || + this.instanceMode === 'ephemeral' || + !this.csound; + + if (needsNewInstance && this.csound) { + this.log('Destroying existing instance...'); + await this.cleanupInstance(); + } else if (this.csound && this.compiled) { + this.log('Resetting Csound for new performance...'); + await this.resetForNewPerformance(); + } + this.scopeNode = null; - this.log('Creating new Csound instance...'); - this.csound = await Csound(); - - this.csound.on('message', (message: string) => { - this.options.onMessage?.(message); - }); - - this.csound.on('onAudioNodeCreated', (node: AudioNode) => { - this.audioNode = node; - this.audioContext = node.context as AudioContext; - this.log('Audio node created and captured'); - }); - - this.csound.on('realtimePerformanceEnded', async () => { - try { - await this.csound?.cleanup(); - } catch {} - this.running = false; - this.log('Performance complete'); - this.options.onPerformanceEnd?.(); - }); - - this.log('Setting audio output...'); - await this.csound.setOption('-odac'); + await this.ensureCsoundInstance(); const orcMatch = code.match(/([\s\S]*?)<\/CsInstruments>/); const scoMatch = code.match(/([\s\S]*?)<\/CsScore>/); @@ -74,42 +171,125 @@ export class CsoundEngine { const orc = orcMatch[1].trim(); const sco = scoMatch[1].trim(); - this.log('Compiling orchestra...'); - const compileResult = await this.csound.compileOrc(orc); - - if (compileResult !== 0) { - throw new Error('Failed to compile orchestra'); + const compileResult = await this.compileOrchestra(orc); + if (!compileResult.success) { + throw new Error(compileResult.errorMessage || 'Compilation failed'); } - this.log('Reading score...'); - await this.csound.readScore(sco); - - this.log('Starting performance...'); - this.running = true; - await this.csound.start(); - - this.setupAnalyser(); + await this.readScore(sco); + await this.startPerformance(); } catch (error) { this.running = false; - if (this.csound) { - try { - await this.csound.cleanup(); - } catch {} - } + this.compiled = false; const errorMsg = error instanceof Error ? error.message : 'Evaluation failed'; this.error(errorMsg); throw error; } } + async setControlChannel(name: string, value: number): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + await this.csound.setControlChannel(name, value); + } + + async getControlChannel(name: string): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + return await this.csound.getControlChannel(name); + } + + async setStringChannel(name: string, value: string): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + await this.csound.setStringChannel(name, value); + } + + async getStringChannel(name: string): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + return await this.csound.getStringChannel(name); + } + + async getTable(tableNumber: number): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + return await this.csound.getTable(tableNumber); + } + + async setTable(tableNumber: number, data: Float32Array | number[]): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + + const tableData = data instanceof Float32Array ? data : new Float32Array(data); + await this.csound.setTable(tableNumber, tableData); + } + + async getPerformanceMetrics(): Promise { + if (!this.csound) { + return { + kperiodCount: 0, + currentTime: 0, + isRunning: this.running + }; + } + + try { + const currentTime = await this.csound.getCurrentTimeSamples(); + + return { + kperiodCount: 0, + currentTime: currentTime / (await this.csound.getSr()), + isRunning: this.running + }; + } catch (error) { + return { + kperiodCount: 0, + currentTime: 0, + isRunning: this.running + }; + } + } + + async getSampleRate(): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + return await this.csound.getSr(); + } + + async getKsmps(): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + return await this.csound.getKsmps(); + } + + async getOutputChannelCount(): Promise { + if (!this.csound) { + throw new Error('No Csound instance available'); + } + return await this.csound.getNchnls(); + } + async stop(): Promise { if (!this.csound || !this.running) return; try { - this.log('Stopping...'); + this.log('Stopping performance...'); await this.csound.stop(); - await this.csound.cleanup(); this.running = false; this.log('Stopped'); } catch (error) { @@ -118,6 +298,49 @@ export class CsoundEngine { } } + async reset(): Promise { + this.log('Resetting Csound...'); + await this.stop(); + await this.cleanupInstance(); + this.compiled = false; + await this.ensureCsoundInstance(); + this.log('Reset complete'); + } + + private async resetForNewPerformance(): Promise { + if (!this.csound) return; + + try { + await this.csound.reset(); + await this.csound.setOption('-odac'); + this.compiled = false; + this.running = false; + this.audioNode = null; + this.audioContext = null; + this.scopeNode = null; + } catch (error) { + this.log('Reset failed, creating new instance...'); + await this.cleanupInstance(); + } + } + + private async cleanupInstance(): Promise { + if (!this.csound) return; + + try { + if (this.running) { + await this.csound.stop(); + } + await this.csound.cleanup(); + } catch (error) { + console.error('Cleanup error:', error); + } finally { + this.csound = null; + this.running = false; + this.compiled = false; + } + } + isRunning(): boolean { return this.running; } @@ -126,6 +349,10 @@ export class CsoundEngine { return this.initialized; } + isCompiled(): boolean { + return this.compiled; + } + getAudioContext(): AudioContext | null { return this.audioContext; } @@ -166,10 +393,7 @@ export class CsoundEngine { } async destroy(): Promise { - if (this.running) { - await this.stop(); - } - this.csound = null; + await this.cleanupInstance(); this.initialized = false; } } diff --git a/src/lib/csound/index.ts b/src/lib/csound/index.ts index 0e676ee..eaae68b 100644 --- a/src/lib/csound/index.ts +++ b/src/lib/csound/index.ts @@ -1,4 +1,9 @@ export { CsoundEngine } from './engine'; -export type { CsoundEngineOptions } from './engine'; +export type { + CsoundEngineOptions, + InstanceMode, + PerformanceMetrics, + CompilationResult +} from './engine'; export { createCsoundStore, createCsoundDerivedStores } from './store'; export type { LogEntry, CsoundStore } from './store'; diff --git a/src/lib/csound/store.ts b/src/lib/csound/store.ts index 2d7dbac..a28bea7 100644 --- a/src/lib/csound/store.ts +++ b/src/lib/csound/store.ts @@ -1,5 +1,5 @@ import { writable, derived, get } from 'svelte/store'; -import { CsoundEngine } from './engine'; +import { CsoundEngine, type InstanceMode, type PerformanceMetrics, type CompilationResult } from './engine'; export interface LogEntry { timestamp: Date; @@ -9,6 +9,7 @@ export interface LogEntry { interface CsoundState { initialized: boolean; + compiled: boolean; running: boolean; logs: LogEntry[]; } @@ -16,8 +17,23 @@ interface CsoundState { export interface CsoundStore { subscribe: (run: (value: CsoundState) => void) => () => void; init: () => Promise; - evaluate: (code: string) => Promise; + compileOrchestra: (orchestra: string) => Promise; + startPerformance: () => Promise; + readScore: (score: string) => Promise; + sendScoreEvent: (event: string) => Promise; + evaluate: (code: string, forceNewInstance?: boolean) => Promise; stop: () => Promise; + reset: () => Promise; + setControlChannel: (name: string, value: number) => Promise; + getControlChannel: (name: string) => Promise; + setStringChannel: (name: string, value: string) => Promise; + getStringChannel: (name: string) => Promise; + getTable: (tableNumber: number) => Promise; + setTable: (tableNumber: number, data: Float32Array | number[]) => Promise; + getPerformanceMetrics: () => Promise; + getSampleRate: () => Promise; + getKsmps: () => Promise; + getOutputChannelCount: () => Promise; clearLogs: () => void; getAudioContext: () => AudioContext | null; getAnalyserNode: () => AnalyserNode | null; @@ -25,9 +41,10 @@ export interface CsoundStore { destroy: () => Promise; } -export function createCsoundStore(): CsoundStore { +export function createCsoundStore(instanceMode?: InstanceMode): CsoundStore { const initialState: CsoundState = { initialized: false, + compiled: false, running: false, logs: [] }; @@ -54,9 +71,13 @@ export function createCsoundStore(): CsoundStore { engine = new CsoundEngine({ onMessage: (msg) => addLog(msg, 'info'), onError: (err) => addLog(err, 'error'), + onPerformanceEnd: () => { + update(state => ({ ...state, running: false })); + }, onAnalyserNodeCreated: (node) => { analyserNodeListeners.forEach(listener => listener(node)); - } + }, + instanceMode }); await engine.init(); @@ -72,20 +93,70 @@ export function createCsoundStore(): CsoundStore { } }, - async evaluate(code: string) { + async compileOrchestra(orchestra: string): Promise { if (!engine) { - throw new Error('CSound engine not initialized'); + throw new Error('Csound engine not initialized'); } try { + const result = await engine.compileOrchestra(orchestra); + update(state => ({ ...state, compiled: result.success })); + return result; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Compilation failed'; + addLog(errorMsg, 'error'); + throw error; + } + }, + + async startPerformance() { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + + try { + await engine.startPerformance(); update(state => ({ ...state, running: true })); - await engine.evaluateCode(code); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Failed to start performance'; + addLog(errorMsg, 'error'); + throw error; + } + }, + + async readScore(score: string) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + + await engine.readScore(score); + }, + + async sendScoreEvent(event: string) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + + await engine.sendScoreEvent(event); + }, + + async evaluate(code: string, forceNewInstance = false) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + + try { + await engine.evaluateCode(code, forceNewInstance); + update(state => ({ + ...state, + compiled: engine!.isCompiled(), + running: engine!.isRunning() + })); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Evaluation failed'; addLog(errorMsg, 'error'); + update(state => ({ ...state, compiled: false, running: false })); throw error; - } finally { - update(state => ({ ...state, running: false })); } }, @@ -101,6 +172,88 @@ export function createCsoundStore(): CsoundStore { } }, + async reset() { + if (!engine) return; + + try { + await engine.reset(); + update(state => ({ ...state, compiled: false, running: false })); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Reset failed'; + addLog(errorMsg, 'error'); + } + }, + + async setControlChannel(name: string, value: number) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + await engine.setControlChannel(name, value); + }, + + async getControlChannel(name: string): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getControlChannel(name); + }, + + async setStringChannel(name: string, value: string) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + await engine.setStringChannel(name, value); + }, + + async getStringChannel(name: string): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getStringChannel(name); + }, + + async getTable(tableNumber: number): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getTable(tableNumber); + }, + + async setTable(tableNumber: number, data: Float32Array | number[]) { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + await engine.setTable(tableNumber, data); + }, + + async getPerformanceMetrics(): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getPerformanceMetrics(); + }, + + async getSampleRate(): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getSampleRate(); + }, + + async getKsmps(): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getKsmps(); + }, + + async getOutputChannelCount(): Promise { + if (!engine) { + throw new Error('Csound engine not initialized'); + } + return await engine.getOutputChannelCount(); + }, + clearLogs() { update(state => ({ ...state, @@ -142,6 +295,7 @@ export function createCsoundDerivedStores(csound: CsoundStore) { return { logs: derived(csound, $csound => $csound.logs), initialized: derived(csound, $csound => $csound.initialized), + compiled: derived(csound, $csound => $csound.compiled), running: derived(csound, $csound => $csound.running) }; }