poc: render pattern audio with node script

This commit is contained in:
Felix Roos 2025-06-05 11:04:55 +02:00
parent f0e6c5483c
commit ccec7e725a
No known key found for this signature in database
6 changed files with 81 additions and 12 deletions

1
packages/superdough/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
pattern.wav

View file

@ -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('<pulse saw tri>*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));
});

View file

@ -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 });
}

View file

@ -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"

View file

@ -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) {

8
pnpm-lock.yaml generated
View file

@ -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