This commit is contained in:
2025-11-14 01:53:18 +01:00
parent e7ffcda096
commit 8941ee13bc
29 changed files with 3752 additions and 122 deletions

View File

@ -1,10 +1,15 @@
import { Csound } from '@csound/browser';
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<File[]>;
useCsound7?: boolean;
}
export interface PerformanceMetrics {
@ -19,7 +24,7 @@ export interface CompilationResult {
}
export class CsoundEngine {
private csound: Csound | null = null;
private csound: CsoundObj | null = null;
private initialized = false;
private compiled = false;
private running = false;
@ -27,9 +32,11 @@ export class CsoundEngine {
private scopeNode: AnalyserNode | null = null;
private audioNode: AudioNode | null = null;
private audioContext: AudioContext | null = null;
private useCsound7: boolean;
constructor(options: CsoundEngineOptions = {}) {
this.options = options;
this.useCsound7 = options.useCsound7 ?? false;
}
async init(): Promise<void> {
@ -42,7 +49,9 @@ export class CsoundEngine {
private async ensureCsoundInstance(): Promise<void> {
if (!this.csound) {
this.log('Creating Csound instance...');
const version = this.useCsound7 ? '7' : '6';
this.log(`Creating Csound ${version} instance...`);
const Csound = this.useCsound7 ? Csound7 : Csound6;
this.csound = await Csound();
this.setupCallbacks();
}
@ -68,6 +77,7 @@ export class CsoundEngine {
}
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');
@ -82,14 +92,43 @@ export class CsoundEngine {
throw new Error('Failed to compile base header');
}
await this.csound!.start();
this.compiled = true;
this.running = true;
this.setupAnalyser();
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<void> {
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;
@ -174,7 +213,98 @@ export class CsoundEngine {
await this.csound!.readScore(event);
}
async evaluateCode(code: string): Promise<void> {
/**
* Run a .csd file (composition mode)
* Syncs all files to virtual FS, then uses native CSD compilation
*/
async playCSD(filename: string, content?: string): Promise<void> {
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('</CsoundSynthesizer>'));
} 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<void> {
if (!this.initialized) {
throw new Error('Csound not initialized. Call init() first.');
}
@ -185,25 +315,25 @@ export class CsoundEngine {
}
await this.restart();
this.scopeNode = null;
const orcMatch = code.match(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
const scoMatch = code.match(/<CsScore>([\s\S]*?)<\/CsScore>/);
this.log('Compiling orchestra...');
if (!orcMatch || !scoMatch) {
throw new Error('Invalid CSD format. Must contain <CsInstruments> and <CsScore> sections.');
const result = await this.csound!.compileOrc(orchestraCode);
if (result !== 0) {
throw new Error('Failed to compile orchestra');
}
const orc = orcMatch[1].trim();
const sco = scoMatch[1].trim();
await this.csound!.compileOrc(orc);
this.compiled = true;
this.log('Orchestra compiled successfully');
if (sco) {
await this.readScore(sco);
}
// 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;
@ -214,6 +344,28 @@ export class CsoundEngine {
}
}
/**
* Incremental code evaluation (live coding mode)
* Requires Csound to be already running
*/
async evalCode(code: string): Promise<void> {
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<void> {
if (!this.csound) {
throw new Error('No Csound instance available');