From ccec7e725ab6344d8a4e58828f6b1cba1c85f776 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 5 Jun 2025 11:04:55 +0200 Subject: [PATCH] poc: render pattern audio with node script --- packages/superdough/.gitignore | 1 + packages/superdough/dough-export.mjs | 61 ++++++++++++++++++++++++++++ packages/superdough/dough.mjs | 15 +++---- packages/superdough/package.json | 3 +- packages/superdough/worklets.mjs | 5 +-- pnpm-lock.yaml | 8 ++++ 6 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 packages/superdough/.gitignore create mode 100644 packages/superdough/dough-export.mjs diff --git a/packages/superdough/.gitignore b/packages/superdough/.gitignore new file mode 100644 index 00000000..d21cbdf3 --- /dev/null +++ b/packages/superdough/.gitignore @@ -0,0 +1 @@ +pattern.wav diff --git a/packages/superdough/dough-export.mjs b/packages/superdough/dough-export.mjs new file mode 100644 index 00000000..5f8b9a8e --- /dev/null +++ b/packages/superdough/dough-export.mjs @@ -0,0 +1,61 @@ +// this is a poc of how a pattern can be rendered as a wav file using node +// run via: node dough-export.mjs +import fs from 'node:fs'; +import WavEncoder from 'wav-encoder'; +import { evalScope } from '@strudel/core'; +import { miniAllStrings } from '@strudel/mini'; +import { Dough } from './dough.mjs'; + +await evalScope( + import('@strudel/core'), + import('@strudel/mini'), + import('@strudel/tonal'), + // import('@strudel/tonal'), +); + +miniAllStrings(); // allows using single quotes for mini notation / skip transpilation + +let sampleRate = 48000, + cps = 0.5; + +let pat = note('[c e g b]*3') + .add(note(7)) + .lpf(sine.rangex(200, 4000).slow(2)) + .lpq(0.3) + .s('*2') + .att(0.01) + .rel(0.2) + .clip(2) + .delay(0.5) + .jux(rev) + .sometimes(add(note(12))) + .gain(0.25) + .slow(1 / cps); + +let cycles = 4; +let seconds = cycles + 1; // 1s release tail +const haps = pat.queryArc(0, cycles); + +const dough = new Dough(sampleRate); + +console.log('spawn voices...'); +haps.forEach((hap) => { + hap.value._begin = Number(hap.whole.begin); + hap.value._duration = hap.duration / cps; + dough.scheduleSpawn(hap.value); +}); +console.log(`render ${seconds}s long buffer...`); +const buffer = new Float32Array(seconds * sampleRate); +while (dough.t <= buffer.length) { + buffer[dough.t] = dough.update(); +} +console.log('done!'); + +const patternAudio = { + sampleRate, + channelData: [buffer], +}; + +WavEncoder.encode(patternAudio).then((buffer) => { + fs.writeFileSync('pattern.wav', new Float32Array(buffer)); +}); diff --git a/packages/superdough/dough.mjs b/packages/superdough/dough.mjs index 7261c4a5..21918f95 100644 --- a/packages/superdough/dough.mjs +++ b/packages/superdough/dough.mjs @@ -1,12 +1,12 @@ // this is dough, the superdough without dependencies - -const ISR = 1 / sampleRate; +const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; +const ISR = 1 / SAMPLE_RATE; // https://garten.salat.dev/audio-DSP/oscillators.html export class SineOsc { phase = 0; update(freq) { const value = Math.sin(this.phase * 2 * Math.PI); - this.phase = (this.phase + freq / sampleRate) % 1; + this.phase = (this.phase + freq / SAMPLE_RATE) % 1; return value; } } @@ -39,7 +39,7 @@ export class SawOsc { //phase = Math.random(); phase = 0; update(freq) { - const dt = freq / sampleRate; + const dt = freq / SAMPLE_RATE; let p = polyBlep(this.phase, dt); let s = 2 * this.phase - 1 - p; this.phase += dt; @@ -216,12 +216,12 @@ const MAX_DELAY_TIME = 10; export class Delay { writeIdx = 0; readIdx = 0; - buffer = new Float32Array(MAX_DELAY_TIME * sampleRate); // .fill(0) + buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); // .fill(0) write(s, delayTime) { this.writeIdx = (this.writeIdx + 1) % this.buffer.length; this.buffer[this.writeIdx] = s; // Calculate how far in the past to read - let numSamples = Math.min(Math.floor(sampleRate * delayTime), this.buffer.length - 1); + let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); this.readIdx = this.writeIdx - numSamples; // If past the start of the buffer, wrap around if (this.readIdx < 0) this.readIdx += this.buffer.length; @@ -501,7 +501,7 @@ export class Dough { r = 0; // tbd t = 0; // sampleRate: number, currentTime: number (seconds) - constructor(sampleRate, currentTime) { + constructor(sampleRate = 48000, currentTime = 0) { this.sampleRate = sampleRate; this.t = Math.floor(currentTime * sampleRate); // samples // console.log('init dough', this.sampleRate, this.t); @@ -513,6 +513,7 @@ export class Dough { if (value._duration === undefined) { throw new Error('[dough]: scheduleSpawn expected _duration to be set'); } + value.sampleRate = this.sampleRate; const time = value._begin; // set from supradough.mjs this.schedule({ time, type: 'spawn', arg: value }); } diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a835f252..6b45144e 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -33,7 +33,8 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "devDependencies": { "vite": "^6.0.11", - "vite-plugin-bundle-audioworklet": "workspace:*" + "vite-plugin-bundle-audioworklet": "workspace:*", + "wav-encoder": "^1.3.0" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 5e549425..2edcc6f5 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -901,10 +901,7 @@ class DoughProcessor extends AudioWorkletProcessor { constructor() { super(); this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => { - event.data.sampleRate = sampleRate; - this.dough.scheduleSpawn(event.data); - }; + this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data); } process(inputs, outputs, params) { if (this.disconnected) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a23c8092..ed8a7c01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,9 @@ importers: vite-plugin-bundle-audioworklet: specifier: workspace:* version: link:../vite-plugin-bundle-audioworklet + wav-encoder: + specifier: ^1.3.0 + version: 1.3.0 packages/tidal: dependencies: @@ -7464,6 +7467,9 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + wav-encoder@1.3.0: + resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==} + wav@1.0.2: resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==} @@ -15839,6 +15845,8 @@ snapshots: walk-up-path@3.0.1: {} + wav-encoder@1.3.0: {} + wav@1.0.2: dependencies: buffer-alloc: 1.2.0