poc: render pattern audio with node script
This commit is contained in:
parent
f0e6c5483c
commit
ccec7e725a
6 changed files with 81 additions and 12 deletions
1
packages/superdough/.gitignore
vendored
Normal file
1
packages/superdough/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pattern.wav
|
||||||
61
packages/superdough/dough-export.mjs
Normal file
61
packages/superdough/dough-export.mjs
Normal 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));
|
||||||
|
});
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// this is dough, the superdough without dependencies
|
// this is dough, the superdough without dependencies
|
||||||
|
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||||
const ISR = 1 / sampleRate;
|
const ISR = 1 / SAMPLE_RATE;
|
||||||
// https://garten.salat.dev/audio-DSP/oscillators.html
|
// https://garten.salat.dev/audio-DSP/oscillators.html
|
||||||
export class SineOsc {
|
export class SineOsc {
|
||||||
phase = 0;
|
phase = 0;
|
||||||
update(freq) {
|
update(freq) {
|
||||||
const value = Math.sin(this.phase * 2 * Math.PI);
|
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;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ export class SawOsc {
|
||||||
//phase = Math.random();
|
//phase = Math.random();
|
||||||
phase = 0;
|
phase = 0;
|
||||||
update(freq) {
|
update(freq) {
|
||||||
const dt = freq / sampleRate;
|
const dt = freq / SAMPLE_RATE;
|
||||||
let p = polyBlep(this.phase, dt);
|
let p = polyBlep(this.phase, dt);
|
||||||
let s = 2 * this.phase - 1 - p;
|
let s = 2 * this.phase - 1 - p;
|
||||||
this.phase += dt;
|
this.phase += dt;
|
||||||
|
|
@ -216,12 +216,12 @@ const MAX_DELAY_TIME = 10;
|
||||||
export class Delay {
|
export class Delay {
|
||||||
writeIdx = 0;
|
writeIdx = 0;
|
||||||
readIdx = 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) {
|
write(s, delayTime) {
|
||||||
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
|
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
|
||||||
this.buffer[this.writeIdx] = s;
|
this.buffer[this.writeIdx] = s;
|
||||||
// Calculate how far in the past to read
|
// 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;
|
this.readIdx = this.writeIdx - numSamples;
|
||||||
// If past the start of the buffer, wrap around
|
// If past the start of the buffer, wrap around
|
||||||
if (this.readIdx < 0) this.readIdx += this.buffer.length;
|
if (this.readIdx < 0) this.readIdx += this.buffer.length;
|
||||||
|
|
@ -501,7 +501,7 @@ export class Dough {
|
||||||
r = 0; // tbd
|
r = 0; // tbd
|
||||||
t = 0;
|
t = 0;
|
||||||
// sampleRate: number, currentTime: number (seconds)
|
// sampleRate: number, currentTime: number (seconds)
|
||||||
constructor(sampleRate, currentTime) {
|
constructor(sampleRate = 48000, currentTime = 0) {
|
||||||
this.sampleRate = sampleRate;
|
this.sampleRate = sampleRate;
|
||||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||||
// console.log('init dough', this.sampleRate, this.t);
|
// console.log('init dough', this.sampleRate, this.t);
|
||||||
|
|
@ -513,6 +513,7 @@ export class Dough {
|
||||||
if (value._duration === undefined) {
|
if (value._duration === undefined) {
|
||||||
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
|
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
|
||||||
}
|
}
|
||||||
|
value.sampleRate = this.sampleRate;
|
||||||
const time = value._begin; // set from supradough.mjs
|
const time = value._begin; // set from supradough.mjs
|
||||||
this.schedule({ time, type: 'spawn', arg: value });
|
this.schedule({ time, type: 'spawn', arg: value });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11",
|
"vite": "^6.0.11",
|
||||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||||
|
"wav-encoder": "^1.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanostores": "^0.11.3"
|
"nanostores": "^0.11.3"
|
||||||
|
|
|
||||||
|
|
@ -901,10 +901,7 @@ class DoughProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.dough = new Dough(sampleRate, currentTime);
|
this.dough = new Dough(sampleRate, currentTime);
|
||||||
this.port.onmessage = (event) => {
|
this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data);
|
||||||
event.data.sampleRate = sampleRate;
|
|
||||||
this.dough.scheduleSpawn(event.data);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
process(inputs, outputs, params) {
|
process(inputs, outputs, params) {
|
||||||
if (this.disconnected) {
|
if (this.disconnected) {
|
||||||
|
|
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -485,6 +485,9 @@ importers:
|
||||||
vite-plugin-bundle-audioworklet:
|
vite-plugin-bundle-audioworklet:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../vite-plugin-bundle-audioworklet
|
version: link:../vite-plugin-bundle-audioworklet
|
||||||
|
wav-encoder:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
|
|
||||||
packages/tidal:
|
packages/tidal:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -7464,6 +7467,9 @@ packages:
|
||||||
walk-up-path@3.0.1:
|
walk-up-path@3.0.1:
|
||||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||||
|
|
||||||
|
|
@ -15839,6 +15845,8 @@ snapshots:
|
||||||
|
|
||||||
walk-up-path@3.0.1: {}
|
walk-up-path@3.0.1: {}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0: {}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-alloc: 1.2.0
|
buffer-alloc: 1.2.0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue