From 86ae178bc4c0daf67c7551f66eccb10c37912516 Mon Sep 17 00:00:00 2001 From: Takamitsu Endo Date: Mon, 30 Oct 2023 09:43:22 +0900 Subject: [PATCH] Add multiple random table generation to OneCycleWaveform Don't forget to set `randomAmount` to non 0 value. --- OneCycleWaveform/main.js | 13 ++++ OneCycleWaveform/renderer.js | 130 +++++++++++++++++++++++++++-------- 2 files changed, 113 insertions(+), 30 deletions(-) diff --git a/OneCycleWaveform/main.js b/OneCycleWaveform/main.js index 98b1daf..06d13cc 100644 --- a/OneCycleWaveform/main.js +++ b/OneCycleWaveform/main.js @@ -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])) { @@ -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), @@ -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), @@ -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"); @@ -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), diff --git a/OneCycleWaveform/renderer.js b/OneCycleWaveform/renderer.js index 5932ac4..4cc1dde 100644 --- a/OneCycleWaveform/renderer.js +++ b/OneCycleWaveform/renderer.js @@ -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"; @@ -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); @@ -38,39 +58,62 @@ 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]); @@ -78,20 +121,21 @@ onmessage = async (event) => { 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; @@ -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(); @@ -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}); }