sligthly better

This commit is contained in:
2025-10-15 02:09:30 +02:00
parent d43d16c73c
commit a4432fa3d9
3 changed files with 442 additions and 59 deletions

View File

@ -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<void> {
@ -27,7 +44,98 @@ export class CsoundEngine {
this.log('Csound ready');
}
async evaluateCode(code: string): Promise<void> {
private async ensureCsoundInstance(): Promise<void> {
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<CompilationResult> {
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<void> {
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<void> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
this.log('Reading score...');
await this.csound.readScore(score);
}
async sendScoreEvent(event: string): Promise<void> {
if (!this.running) {
throw new Error('Csound not running. Call startPerformance() first.');
}
await this.csound!.readScore(event);
}
async evaluateCode(code: string, forceNewInstance = false): Promise<void> {
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(/<CsInstruments>([\s\S]*?)<\/CsInstruments>/);
const scoMatch = code.match(/<CsScore>([\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<void> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
await this.csound.setControlChannel(name, value);
}
async getControlChannel(name: string): Promise<number> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getControlChannel(name);
}
async setStringChannel(name: string, value: string): Promise<void> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
await this.csound.setStringChannel(name, value);
}
async getStringChannel(name: string): Promise<string> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getStringChannel(name);
}
async getTable(tableNumber: number): Promise<Float32Array> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getTable(tableNumber);
}
async setTable(tableNumber: number, data: Float32Array | number[]): Promise<void> {
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<PerformanceMetrics> {
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<number> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getSr();
}
async getKsmps(): Promise<number> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getKsmps();
}
async getOutputChannelCount(): Promise<number> {
if (!this.csound) {
throw new Error('No Csound instance available');
}
return await this.csound.getNchnls();
}
async stop(): Promise<void> {
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<void> {
this.log('Resetting Csound...');
await this.stop();
await this.cleanupInstance();
this.compiled = false;
await this.ensureCsoundInstance();
this.log('Reset complete');
}
private async resetForNewPerformance(): Promise<void> {
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<void> {
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<void> {
if (this.running) {
await this.stop();
}
this.csound = null;
await this.cleanupInstance();
this.initialized = false;
}
}