Skip to content

Commit

Permalink
Add multiple random table generation to OneCycleWaveform
Browse files Browse the repository at this point in the history
Don't forget to set `randomAmount` to non 0 value.
  • Loading branch information
ryukau committed Oct 30, 2023
1 parent 45a9ef3 commit 86ae178
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 30 deletions.
13 changes: 13 additions & 0 deletions OneCycleWaveform/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ function randomize() {
// selectRandom.value === "Default"
for (const key in param) {
if (key === "renderSamples") continue;
if (key === "nTable") continue;
if (key === "randomAmount") continue;
if (key === "highpass") continue;
if (key === "lowpass") continue;
if (Array.isArray(param[key])) {
Expand Down Expand Up @@ -42,6 +44,8 @@ const scales = {
bipolarScale: new parameter.LinearScale(-1, 1),

renderSamples: new parameter.IntScale(1, 2 ** 16),
nTable: new parameter.IntScale(1, 1024),
seed: new parameter.IntScale(0, 2 ** 32),

waveform: new parameter.LinearScale(0, 3),
powerOf: new parameter.DecibelScale(-40, 40, false),
Expand All @@ -57,6 +61,9 @@ const scales = {

const param = {
renderSamples: new parameter.Parameter(2048, scales.renderSamples),
nTable: new parameter.Parameter(1, scales.nTable),
seed: new parameter.Parameter(0, scales.seed),
randomAmount: new parameter.Parameter(0, scales.defaultScale),

waveform: new parameter.Parameter(0, scales.waveform, true),
powerOf: new parameter.Parameter(1, scales.powerOf, true),
Expand Down Expand Up @@ -123,6 +130,7 @@ const createDetailInBlock = (name) => {
};

const detailRender = widget.details(divLeft, "Render");
const detailMultiTable = widget.details(divLeft, "Multiple Tables");
const detailShape = widget.details(divRight, "Shape");
const detailSpectral = widget.details(divRight, "Spectral");
const detailFilter = widget.details(divRight, "Filter");
Expand All @@ -131,6 +139,11 @@ const ui = {
renderSamples: new widget.NumberInput(
detailRender, "Duration [sample]", param.renderSamples, render),

nTable: new widget.NumberInput(detailMultiTable, "nTable", param.nTable, render),
seed: new widget.NumberInput(detailMultiTable, "Seed", param.seed, render),
randomAmount:
new widget.NumberInput(detailMultiTable, "Random Amount", param.randomAmount, render),

waveform: new widget.NumberInput(detailShape, "Sine-Saw-Pulse", param.waveform, render),
powerOf: new widget.NumberInput(detailShape, "Power", param.powerOf, render),
skew: new widget.NumberInput(detailShape, "Skew", param.skew, render),
Expand Down
130 changes: 100 additions & 30 deletions OneCycleWaveform/renderer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright 2022 Takamitsu Endo
// SPDX-License-Identifier: Apache-2.0

import * as util from "../common/util.js";
import {clamp, exponentialMap, lerp, uniformDistributionMap} from "../common/util.js";
import {PcgRandom} from "../lib/pcgrandom/pcgrandom.js";
import PocketFFT from "../lib/pocketfft/pocketfft.js";

// import {PcgRandom} from "../lib/pcgrandom/pcgrandom.js";
Expand All @@ -16,19 +17,38 @@ function alignToOdd(x) {
return x + x % 2;
}

// Linear regression. `x` and `y` are arrays of the same length.
function linregress(x, y) {
if (x.length !== y.length) {
console.warn("Size mismatch between x and y.");
return 0;
}

const sumX = x.reduce((p, c) => p + c);
const sumY = y.reduce((p, c) => p + c);
const dotXX = x.reduce((p, c) => p + c * c);
const dotXY = x.reduce((p, c, i) => p + c * y[i]);
const N = x.length;

const slope = (N * dotXY - sumX * sumY) / (N * dotXX - sumX * sumX);
// const intercept = (sumY - slope * sumX) / N;

return slope;
}

// `phase` is in [0, 1).
function generateWave(phase, waveform) {
if (waveform < 1) {
// Sin-Tri
const sin = Math.sin(Math.PI * phase);
const tri = 2 * phase - 1;
return util.lerp(sin, tri, waveform);
return lerp(sin, tri, waveform);
} else if (waveform < 2) {
// Tri-Pulse
const frac = waveform - Math.floor(waveform);
const tri = 2 * phase - 1;
const pulse = phase < 0.5 ? 1.0 : -1.0;
return util.lerp(tri, pulse, frac);
return lerp(tri, pulse, frac);
} else if (waveform < 3) {
// Pulse.
const frac = waveform - Math.floor(waveform);
Expand All @@ -38,60 +58,84 @@ function generateWave(phase, waveform) {
return phase == 0 ? 1.0 : -1.0;
}

onmessage = async (event) => {
const pv = event.data; // Parameter values.

let sound = new Array(alignToOdd(pv.renderSamples)).fill(0);

// const rng = new PcgRandom(BigInt(pv.seed));

let dsp = {};
function generateTable(renderSamples, freqIdx, pv, rng, fft) {
let sound = new Array(renderSamples).fill(0);

const tiltLin = (rng, base, lower, upper, range) => {
const value = base + uniformDistributionMap(rng.number(), -range, range);
return clamp(value, lower, upper);
};
const tiltExp = (rng, base, lower, upper, range) => {
const logRange = range * Math.log(upper / lower);
const low = Math.max(lower, base * Math.exp(-logRange));
const high = Math.min(upper, base * Math.exp(logRange));
return base * exponentialMap(rng.number(), low, high);
};

const waveform = tiltLin(rng, pv.waveform, 0, 1, pv.randomAmount);
const powerOf = tiltExp(rng, pv.powerOf, 0.01, 100, pv.randomAmount);
const skew = tiltExp(rng, pv.skew, 0.01, 100, pv.randomAmount);
const sineShaper = tiltLin(rng, pv.sineShaper, 0, 1, pv.randomAmount);
const sineRatio = Math.floor(tiltExp(rng, pv.sineRatio, 1, 1024, pv.randomAmount));
const hardSync = tiltExp(rng, pv.hardSync, 0.1, 10, pv.randomAmount);
const mirrorRange = tiltLin(rng, pv.mirrorRange, 0, 1, pv.randomAmount);
const mirrorRepeat = tiltLin(rng, pv.mirrorRepeat, 0, 1, pv.randomAmount);
const flip = tiltLin(rng, pv.flip, -1, 1, pv.randomAmount);

const spectralSpread = tiltExp(rng, pv.spectralSpread, 0.01, 100, pv.randomAmount);
const phaseSlope = tiltExp(rng, pv.phaseSlope, 0.001, 1000, pv.randomAmount);

const highpass = tiltExp(rng, pv.highpass, 0.001, 1, pv.randomAmount);
const lowpass = tiltExp(rng, pv.lowpass, 0.001, 1, pv.randomAmount);
const notchStart = tiltExp(rng, pv.notchStart, 0.001, 1, pv.randomAmount);
const notchRange = tiltExp(rng, pv.notchRange, 0.001, 1, pv.randomAmount);
const lowshelfEnd = tiltExp(rng, pv.lowshelfEnd, 0.001, 1, pv.randomAmount);
const lowshelfGain = tiltExp(rng, pv.lowshelfGain, 0.01, 100, pv.randomAmount);

// Base waveform.
const mid = Math.floor(sound.length * (1 - pv.mirrorRange / 2));
const mid = Math.floor(sound.length * (1 - mirrorRange / 2));
let idx = 0;
for (; idx < mid; ++idx) {
let phase = pv.hardSync * idx / mid;
phase = Math.pow(phase, pv.skew);
let phase = hardSync * idx / mid;
phase = Math.pow(phase, skew);

let sinePhase = 2 * Math.sin(Math.PI * phase * pv.sineRatio);
let sinePhase = 2 * Math.sin(Math.PI * phase * sineRatio);

let sig = generateWave(phase + util.lerp(0, sinePhase, pv.sineShaper), pv.waveform);
let sig = generateWave(phase + lerp(0, sinePhase, sineShaper), waveform);

sig = signedPower(sig, pv.powerOf);
sig = signedPower(sig, powerOf);

sound[idx] += sig;
}
for (; idx < sound.length; ++idx) {
const mirror = sound[sound.length - 1 - idx];
const repeat = sound[idx - mid];
sound[idx] = pv.flip * util.lerp(mirror, repeat, pv.mirrorRepeat);
sound[idx] = flip * lerp(mirror, repeat, mirrorRepeat);
}

// Spectral processing. See `lib/pocketfft/build/test.html` for `fft` usage.
const fft = await PocketFFT();

let inVec = new fft.vector_f64();
inVec.resize(sound.length, 0);
for (let i = 0; i < sound.length; ++i) inVec.set(i, sound[i]);

let inSpc = fft.r2c(inVec);
inVec.delete();

let powerSpc = new Array(inSpc.size()).fill(0);
let outSpc = new fft.vector_complex128();
outSpc.resize(inSpc.size());

const lengthWithoutDC = inSpc.size() - 1;
const start = Math.floor(pv.highpass * lengthWithoutDC);
const end = Math.floor(pv.lowpass * lengthWithoutDC);
const notchStart = Math.floor(pv.notchStart * lengthWithoutDC);
const start = Math.floor(highpass * lengthWithoutDC);
const end = Math.floor(lowpass * lengthWithoutDC);
const notchStartIndex = Math.floor(notchStart * lengthWithoutDC);

for (let i = 0; i < outSpc.size(); ++i) outSpc.setValue(i, 0, 0);

for (let idx = start; idx < end; ++idx) {
if (idx == notchStart) idx += Math.floor(pv.notchRange * lengthWithoutDC);
if (idx == notchStartIndex) idx += Math.floor(notchRange * lengthWithoutDC);

const target = pv.spectralSpread * idx + 1;
const target = spectralSpread * idx + 1;
const index = Math.floor(target);
if (index >= inSpc.size()) break;
const frac = target - index;
Expand All @@ -104,15 +148,16 @@ onmessage = async (event) => {
const len = Math.sqrt(reIn * reIn + imIn * imIn);
const arg = Math.atan2(imIn, reIn);

const reVal = len * Math.cos(arg + pv.phaseSlope * len);
const imVal = len * Math.sin(arg + pv.phaseSlope * len);
const reVal = len * Math.cos(arg + phaseSlope * len);
const imVal = len * Math.sin(arg + phaseSlope * len);

powerSpc[idx] = gain * Math.sqrt(reVal * reVal + imVal * imVal);
outSpc.setValue(idx + 1, gain * reVal, gain * imVal);
}
const lowshelfEnd = Math.floor(pv.lowshelfEnd * lengthWithoutDC);
for (let idx = 1; idx < lowshelfEnd + 1; ++idx) {
const lowshelfEndIndex = Math.floor(lowshelfEnd * lengthWithoutDC);
for (let idx = 1; idx < lowshelfEndIndex + 1; ++idx) {
outSpc.setValue(
idx, pv.lowshelfGain * outSpc.getReal(idx), pv.lowshelfGain * outSpc.getImag(idx));
idx, lowshelfGain * outSpc.getReal(idx), lowshelfGain * outSpc.getImag(idx));
}
inSpc.delete();

Expand All @@ -122,5 +167,30 @@ onmessage = async (event) => {
for (let i = 0; i < outVec.size(); ++i) sound[i] = outVec.get(i);
outVec.delete();

// Normalize amplitude.
const maxSample = sound.reduce((p, c) => Math.max(p, Math.abs(c)), 0);
if (maxSample > Number.EPSILON) {
for (let idx = 0; idx < sound.length; ++idx) sound[idx] /= maxSample;
}

return {data: sound, slope: -linregress(freqIdx, powerSpc) / maxSample};
}

onmessage = async (event) => {
const pv = event.data; // Parameter values.
const fft = await PocketFFT();
const rng = new PcgRandom(BigInt(pv.seed));
const renderSamples = alignToOdd(pv.renderSamples);

const spcLength = Math.floor(renderSamples / 2 + 1);
const freqIdx = new Array(spcLength).fill(0).map((_, i) => i);

let tables = [];
for (let i = 0; i < pv.nTable; ++i) {
tables.push(generateTable(renderSamples, freqIdx, pv, rng, fft));
}
tables.sort((a, b) => a.slope < b.slope ? -1 : a.slope > b.slope ? 1 : 0);
let sound = tables.flatMap(v => v.data);

postMessage({sound: sound});
}

0 comments on commit 86ae178

Please sign in to comment.