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
|
||||
|
||||
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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
8
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue