import { Csound } from '@csound/browser'; import type { SynthEngine, PitchLock } from './SynthEngine'; export interface CsoundParameter { channelName: string; value: number; } export abstract class CsoundEngine implements SynthEngine { abstract getName(): string; abstract getDescription(): string; abstract getType(): 'generative' | 'sample' | 'input'; protected abstract getOrchestra(): string; protected abstract getParametersForCsound(params: T): CsoundParameter[]; abstract randomParams(pitchLock?: PitchLock): T; abstract mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T; async generate( params: T, sampleRate: number, duration: number, pitchLock?: PitchLock ): Promise<[Float32Array, Float32Array]> { const orchestra = this.getOrchestra(); const csoundParams = this.getParametersForCsound(params); const outputFile = '/output.wav'; const csd = this.buildCSD(orchestra, duration, sampleRate, csoundParams, outputFile); try { const csound = await Csound(); if (!csound) { throw new Error('Failed to initialize Csound'); } await csound.compileCSD(csd); await csound.start(); await csound.perform(); await csound.cleanup(); const wavData = await csound.fs.readFile(outputFile); const audioBuffer = await this.parseWavManually(wavData, sampleRate); await csound.terminateInstance(); let leftChannel = new Float32Array(audioBuffer.leftChannel); let rightChannel = new Float32Array(audioBuffer.rightChannel); this.removeDCOffset(leftChannel, rightChannel); const trimmed = this.trimToZeroCrossing(leftChannel, rightChannel, sampleRate); leftChannel = trimmed.left; rightChannel = trimmed.right; this.applyFadeIn(leftChannel, rightChannel, sampleRate); this.applyFadeOut(leftChannel, rightChannel, sampleRate); const peak = this.findPeak(leftChannel, rightChannel); if (peak > 0.001) { const normalizeGain = 0.85 / peak; this.applyGain(leftChannel, rightChannel, normalizeGain); } return [leftChannel, rightChannel]; } catch (error) { console.error('Csound generation failed:', error); const numSamples = Math.floor(sampleRate * duration); return [new Float32Array(numSamples), new Float32Array(numSamples)]; } } private parseWavManually( wavData: Uint8Array, expectedSampleRate: number ): { leftChannel: Float32Array; rightChannel: Float32Array } { const view = new DataView(wavData.buffer); // Check RIFF header const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); if (riff !== 'RIFF') { throw new Error('Invalid WAV file: no RIFF header'); } // Check WAVE format const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11)); if (wave !== 'WAVE') { throw new Error('Invalid WAV file: no WAVE format'); } // Find fmt chunk let offset = 12; while (offset < wavData.length) { const chunkId = String.fromCharCode( view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3) ); const chunkSize = view.getUint32(offset + 4, true); if (chunkId === 'fmt ') { const audioFormat = view.getUint16(offset + 8, true); const numChannels = view.getUint16(offset + 10, true); const sampleRate = view.getUint32(offset + 12, true); const bitsPerSample = view.getUint16(offset + 22, true); // Find data chunk let dataOffset = offset + 8 + chunkSize; while (dataOffset < wavData.length) { const dataChunkId = String.fromCharCode( view.getUint8(dataOffset), view.getUint8(dataOffset + 1), view.getUint8(dataOffset + 2), view.getUint8(dataOffset + 3) ); const dataChunkSize = view.getUint32(dataOffset + 4, true); if (dataChunkId === 'data') { const bytesPerSample = bitsPerSample / 8; const numSamples = Math.floor(dataChunkSize / bytesPerSample / numChannels); const leftChannel = new Float32Array(numSamples); const rightChannel = new Float32Array(numSamples); let audioDataOffset = dataOffset + 8; if (bitsPerSample === 16) { // 16-bit PCM for (let i = 0; i < numSamples; i++) { const leftSample = view.getInt16(audioDataOffset, true); leftChannel[i] = leftSample / 32768.0; audioDataOffset += 2; if (numChannels > 1) { const rightSample = view.getInt16(audioDataOffset, true); rightChannel[i] = rightSample / 32768.0; audioDataOffset += 2; } else { rightChannel[i] = leftChannel[i]; } } } else if (bitsPerSample === 32 && audioFormat === 3) { // 32-bit float for (let i = 0; i < numSamples; i++) { leftChannel[i] = view.getFloat32(audioDataOffset, true); audioDataOffset += 4; if (numChannels > 1) { rightChannel[i] = view.getFloat32(audioDataOffset, true); audioDataOffset += 4; } else { rightChannel[i] = leftChannel[i]; } } } else { throw new Error(`Unsupported WAV format: ${bitsPerSample}-bit, format ${audioFormat}`); } return { leftChannel, rightChannel }; } dataOffset += 8 + dataChunkSize; } throw new Error('No data chunk found in WAV file'); } offset += 8 + chunkSize; } throw new Error('No fmt chunk found in WAV file'); } private buildCSD( orchestra: string, duration: number, sampleRate: number, parameters: CsoundParameter[], outputFile: string ): string { const paramInit = parameters .map(p => `chnset ${p.value}, "${p.channelName}"`) .join('\n'); return ` -W -d -m0 -o ${outputFile} sr = ${sampleRate} ksmps = 64 nchnls = 2 0dbfs = 1.0 ${paramInit} ${orchestra} i 1 0 ${duration} e `; } private findPeak(leftChannel: Float32Array, rightChannel: Float32Array): number { let peak = 0; for (let i = 0; i < leftChannel.length; i++) { peak = Math.max(peak, Math.abs(leftChannel[i]), Math.abs(rightChannel[i])); } return peak; } private applyGain( leftChannel: Float32Array, rightChannel: Float32Array, gain: number ): void { for (let i = 0; i < leftChannel.length; i++) { leftChannel[i] *= gain; rightChannel[i] *= gain; } } private removeDCOffset(leftChannel: Float32Array, rightChannel: Float32Array): void { let leftSum = 0; let rightSum = 0; const length = leftChannel.length; for (let i = 0; i < length; i++) { leftSum += leftChannel[i]; rightSum += rightChannel[i]; } const leftDC = leftSum / length; const rightDC = rightSum / length; for (let i = 0; i < length; i++) { leftChannel[i] -= leftDC; rightChannel[i] -= rightDC; } } private trimToZeroCrossing( leftChannel: Float32Array, rightChannel: Float32Array, sampleRate: number ): { left: Float32Array; right: Float32Array } { const maxSearchSamples = Math.min(Math.floor(sampleRate * 0.01), leftChannel.length); let trimIndex = 0; for (let i = 1; i < maxSearchSamples; i++) { const prevL = leftChannel[i - 1]; const currL = leftChannel[i]; const prevR = rightChannel[i - 1]; const currR = rightChannel[i]; if ( (prevL <= 0 && currL >= 0) || (prevL >= 0 && currL <= 0) || (prevR <= 0 && currR >= 0) || (prevR >= 0 && currR <= 0) ) { trimIndex = i; break; } } if (trimIndex > 0) { const newLeft = new Float32Array(leftChannel.length - trimIndex); const newRight = new Float32Array(rightChannel.length - trimIndex); newLeft.set(leftChannel.subarray(trimIndex)); newRight.set(rightChannel.subarray(trimIndex)); return { left: newLeft, right: newRight }; } return { left: leftChannel, right: rightChannel }; } private applyFadeIn( leftChannel: Float32Array, rightChannel: Float32Array, sampleRate: number ): void { const fadeInMs = 5; const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate); const actualFadeSamples = Math.min(fadeSamples, leftChannel.length); for (let i = 0; i < actualFadeSamples; i++) { const phase = i / actualFadeSamples; const gain = 0.5 - 0.5 * Math.cos(phase * Math.PI); leftChannel[i] *= gain; rightChannel[i] *= gain; } } private applyFadeOut( leftChannel: Float32Array, rightChannel: Float32Array, sampleRate: number ): void { const fadeOutMs = 5; const fadeSamples = Math.floor((fadeOutMs / 1000) * sampleRate); const actualFadeSamples = Math.min(fadeSamples, leftChannel.length); const startSample = leftChannel.length - actualFadeSamples; for (let i = 0; i < actualFadeSamples; i++) { const sampleIndex = startSample + i; const phase = i / actualFadeSamples; const gain = 0.5 + 0.5 * Math.cos(phase * Math.PI); leftChannel[sampleIndex] *= gain; rightChannel[sampleIndex] *= gain; } } protected randomRange(min: number, max: number): number { return min + Math.random() * (max - min); } protected randomInt(min: number, max: number): number { return Math.floor(this.randomRange(min, max + 1)); } protected randomChoice(choices: readonly U[]): U { return choices[Math.floor(Math.random() * choices.length)]; } protected mutateValue(value: number, amount: number, min: number, max: number): number { const variation = value * amount * (Math.random() * 2 - 1); return Math.max(min, Math.min(max, value + variation)); } }