Rework the interface a bit

This commit is contained in:
2025-10-13 12:41:39 +02:00
parent 51e7c44c93
commit 467558efd2
23 changed files with 218 additions and 224 deletions

View File

@ -0,0 +1,253 @@
import { Csound } from '@csound/browser';
import type { SynthEngine, PitchLock } from './SynthEngine';
export interface CsoundParameter {
channelName: string;
value: number;
}
export abstract class CsoundEngine<T = any> implements SynthEngine<T> {
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();
const leftChannel = new Float32Array(audioBuffer.leftChannel);
const rightChannel = new Float32Array(audioBuffer.rightChannel);
// Apply short fade-in to prevent click at start
this.applyFadeIn(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 `<CsoundSynthesizer>
<CsOptions>
-W -d -m0 -o ${outputFile}
</CsOptions>
<CsInstruments>
sr = ${sampleRate}
ksmps = 64
nchnls = 2
0dbfs = 1.0
${paramInit}
${orchestra}
</CsInstruments>
<CsScore>
i 1 0 ${duration}
e
</CsScore>
</CsoundSynthesizer>`;
}
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 applyFadeIn(
leftChannel: Float32Array,
rightChannel: Float32Array,
sampleRate: number
): void {
const fadeInMs = 5; // 5ms fade-in to prevent clicks
const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate);
const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);
for (let i = 0; i < actualFadeSamples; i++) {
const gain = i / actualFadeSamples;
leftChannel[i] *= gain;
rightChannel[i] *= 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<U>(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));
}
}