import { Csound as Csound6 } from '@csound/browser'; import { Csound as Csound7 } from 'csound7'; import type { File } from '../project-system/types'; import { compileCSD, type CsoundObj } from './types'; export interface CsoundEngineOptions { onMessage?: (message: string) => void; onError?: (error: string) => void; onPerformanceEnd?: () => void; onAnalyserNodeCreated?: (node: AnalyserNode) => void; getProjectFiles?: () => Promise; useCsound7?: boolean; } export interface PerformanceMetrics { kperiodCount: number; currentTime: number; isRunning: boolean; } export interface CompilationResult { success: boolean; errorMessage?: string; } export class CsoundEngine { private csound: CsoundObj | 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 gainNode: GainNode | null = null; private useCsound7: boolean; constructor(options: CsoundEngineOptions = {}) { this.options = options; this.useCsound7 = options.useCsound7 ?? false; } async init(): Promise { if (this.initialized) return; await this.ensureCsoundInstance(); this.initialized = true; this.log('Csound ready'); } private async ensureCsoundInstance(): Promise { if (!this.csound) { const version = this.useCsound7 ? '7' : '6'; this.log(`Creating Csound ${version} instance...`); const Csound = this.useCsound7 ? Csound7 : Csound6; this.csound = await Csound(); this.setupCallbacks(); } } async restart(): Promise { this.log('Restarting Csound...'); if (this.running) { await this.stop(); } if (this.csound) { try { await this.csound.reset(); } catch (error) { this.log('Reset failed, creating new instance...'); await this.cleanupInstance(); await this.ensureCsoundInstance(); } } else { await this.ensureCsoundInstance(); } await this.csound!.setOption('-m0'); await this.csound!.setOption('-d'); await this.csound!.setOption('-odac'); await this.csound!.setOption('-+msg_color=false'); await this.csound!.setOption('--daemon'); const ac = await this.csound!.getAudioContext(); const sampleRate = ac?.sampleRate || 48000; const header = `sr = ${sampleRate}\nksmps = 32\n0dbfs = 1\nnchnls = 2\nnchnls_i = 1`; const compileResult = await this.csound!.compileOrc(header); if (compileResult !== 0) { throw new Error('Failed to compile base header'); } this.compiled = true; this.log('Csound restarted and ready'); } /** * Sync all project files to Csound's virtual filesystem * This enables #include directives to work properly */ private async syncProjectFilesToFS(): Promise { if (!this.csound) { this.log('Warning: Cannot sync files - no Csound instance'); return; } if (!this.options.getProjectFiles) { this.log('Warning: Cannot sync files - no getProjectFiles callback'); return; } const files = await this.options.getProjectFiles(); this.log(`Syncing ${files.length} files to virtual FS...`); const encoder = new TextEncoder(); for (const file of files) { try { const content = encoder.encode(file.content); await this.csound.fs.writeFile(file.title, content); this.log(` - ${file.title}`); } catch (error) { this.log(`Warning: Failed to write ${file.title}: ${error}`); } } this.log('File sync complete'); } 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.setupGainNode(); 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.'); } if (!this.csound) { throw new Error('No Csound instance available'); } try { 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); } /** * Run a .csd file (composition mode) * Syncs all files to virtual FS, then uses native CSD compilation */ async playCSD(filename: string, content?: string): Promise { if (!this.initialized) { throw new Error('Csound not initialized. Call init() first.'); } try { console.log('[Engine] playCSD() called with filename:', filename); console.log('[Engine] Content provided:', !!content); if (content) { console.log('[Engine] Content preview:', content.substring(0, 100)); console.log('[Engine] Content length:', content.length); } if (this.running) { await this.stop(); } await this.restart(); this.scopeNode = null; // Sync all files to virtual FS so #includes work await this.syncProjectFilesToFS(); // If content is provided, write it to the filename if (content) { const encoder = new TextEncoder(); const encoded = encoder.encode(content); console.log('[Engine] Encoded content length:', encoded.length); await this.csound!.fs.writeFile(filename, encoded); this.log(`Wrote ${filename} to virtual FS (${encoded.length} bytes)`); console.log('[Engine] Successfully wrote to virtual FS'); // Verify the file was written correctly try { const readBack = await this.csound!.fs.readFile(filename); const decoder = new TextDecoder(); const readContent = decoder.decode(readBack); console.log('[Engine] Read back from FS:', readContent.substring(0, 100)); console.log('[Engine] Read back length:', readContent.length); console.log('[Engine] Contains closing tag:', readContent.includes('')); } catch (e) { console.error('[Engine] Failed to read back file:', e); } } this.log(`Compiling ${filename}...`); // Debug: List files in virtual FS root to verify file exists try { const files = await this.csound!.fs.readdir('/'); console.log('[Engine] Files in virtual FS root:', files); } catch (e) { console.error('[Engine] Failed to list FS:', e); } // Use native CSD compilation (Csound handles #includes) // Mode 0 = path (read from virtual FS), Mode 1 = text (parse as CSD content) console.log('[Engine] Calling compileCSD with filename:', filename, 'mode: 0 (path)'); const result = await compileCSD(this.csound!, filename, 0); console.log('[Engine] compileCSD returned:', result); if (result !== 0) { throw new Error(`Failed to compile ${filename}`); } this.compiled = true; this.log('CSD compiled successfully'); // Start performance after successful compilation this.log('Starting performance...'); await this.csound!.start(); this.running = true; this.setupAnalyser(); this.log('Performance started'); } catch (error) { this.running = false; this.compiled = false; const errorMsg = error instanceof Error ? error.message : 'Evaluation failed'; this.error(errorMsg); throw error; } } /** * Run a .orc file (orchestra only, no score) */ async playORC(orchestraCode: string): Promise { if (!this.initialized) { throw new Error('Csound not initialized. Call init() first.'); } try { if (this.running) { await this.stop(); } await this.restart(); this.scopeNode = null; this.log('Compiling orchestra...'); const result = await this.csound!.compileOrc(orchestraCode); if (result !== 0) { throw new Error('Failed to compile orchestra'); } this.compiled = true; this.log('Orchestra compiled successfully'); // Start performance after successful compilation this.log('Starting performance...'); await this.csound!.start(); this.running = true; this.setupAnalyser(); this.log('Performance started'); } catch (error) { this.running = false; this.compiled = false; const errorMsg = error instanceof Error ? error.message : 'Evaluation failed'; this.error(errorMsg); throw error; } } /** * Incremental code evaluation (live coding mode) * Requires Csound to be already running */ async evalCode(code: string): Promise { if (!this.csound) { throw new Error('No Csound instance available'); } if (!this.running) { throw new Error('Csound not running. Use playCSD() or playORC() first.'); } this.log('Evaluating code...'); const result = await this.csound.evalCode(code); if (result !== 0) { this.error('Code evaluation failed'); throw new Error('Code evaluation failed'); } } 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 performance...'); await this.csound.stop(); this.running = false; this.log('Stopped'); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to stop'; this.error(errorMsg); } } 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; } isInitialized(): boolean { return this.initialized; } isCompiled(): boolean { return this.compiled; } getAudioContext(): AudioContext | null { return this.audioContext; } private setupGainNode(): void { if (!this.audioNode || !this.audioContext || this.gainNode) { return; } try { this.gainNode = this.audioContext.createGain(); this.gainNode.gain.value = 1.0; this.audioNode.disconnect(); this.audioNode.connect(this.gainNode); this.gainNode.connect(this.audioContext.destination); this.log('Gain node created and connected'); } catch (error) { console.error('Failed to setup gain node:', error); this.log('Error setting up gain node: ' + error); } } private setupAnalyser(): void { if (!this.audioNode || !this.audioContext) { this.log('Warning: Audio node not available yet'); return; } try { if (!this.gainNode) { this.setupGainNode(); } this.scopeNode = this.audioContext.createAnalyser(); this.scopeNode.fftSize = 2048; this.scopeNode.smoothingTimeConstant = 0.3; if (this.gainNode) { this.gainNode.disconnect(); this.gainNode.connect(this.scopeNode); this.scopeNode.connect(this.audioContext.destination); } this.log('Analyser node created and connected'); this.options.onAnalyserNodeCreated?.(this.scopeNode); } catch (error) { console.error('Failed to setup analyser:', error); this.log('Error setting up analyser: ' + error); } } getAnalyserNode(): AnalyserNode | null { return this.scopeNode; } setVolume(value: number): void { if (!this.gainNode) { return; } this.gainNode.gain.value = Math.max(0, Math.min(1, value)); } getVolume(): number { if (!this.gainNode) { return 1.0; } return this.gainNode.gain.value; } private log(message: string): void { this.options.onMessage?.(message); } private error(message: string): void { this.options.onError?.(message); } async destroy(): Promise { await this.cleanupInstance(); this.initialized = false; } }