fixed offset time

This commit is contained in:
Jade (Rose) Rowland 2024-06-12 01:11:58 -04:00
parent 065b24a2ee
commit 9f958abb08
9 changed files with 784 additions and 825 deletions

View file

@ -62,7 +62,6 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"events": "^3.3.0", "events": "^3.3.0",
"fft-js": "^0.0.12",
"jsdoc": "^4.0.2", "jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0", "jsdoc-to-markdown": "^8.0.0",

View file

@ -1390,9 +1390,7 @@ export const { speed } = registerControl('speed');
* @name stretch * @name stretch
* @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards. * @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
* @example * @example
* s("bd*6").speed("1 2 4 1 -2 -4") * s("gm_flute").stretch("1 2 .5")
* @example
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
* *
*/ */
export const { stretch } = registerControl('stretch'); export const { stretch } = registerControl('stretch');
@ -1407,7 +1405,6 @@ export const { stretch } = registerControl('stretch');
* *
*/ */
export const { unit } = registerControl('unit'); export const { unit } = registerControl('unit');
/** /**
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as: * Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:

View file

@ -21,6 +21,7 @@ import {
numeralArgs, numeralArgs,
parseNumeral, parseNumeral,
pairs, pairs,
clamp,
} from './util.mjs'; } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@ -1970,6 +1971,7 @@ export const late = register(
true, true,
); );
/** /**
* Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
* *

View file

@ -18,7 +18,7 @@ export default class FFT {
// NOTE: Use of `var` is intentional for old V8 versions // NOTE: Use of `var` is intentional for old V8 versions
var table = new Array(this.size * 2); var table = new Array(this.size * 2);
for (var i = 0; i < table.length; i += 2) { for (var i = 0; i < table.length; i += 2) {
const angle = Math.PI * i / this.size; const angle = (Math.PI * i) / this.size;
table[i] = Math.cos(angle); table[i] = Math.cos(angle);
table[i + 1] = -Math.sin(angle); table[i + 1] = -Math.sin(angle);
} }
@ -26,8 +26,7 @@ export default class FFT {
// Find size's power of two // Find size's power of two
var power = 0; var power = 0;
for (var t = 1; this.size > t; t <<= 1) for (var t = 1; this.size > t; t <<= 1) power++;
power++;
// Calculate initial step's width: // Calculate initial step's width:
// * If we are full radix-4 - it is 2x smaller to give inital len=8 // * If we are full radix-4 - it is 2x smaller to give inital len=8
@ -50,14 +49,12 @@ export default class FFT {
} }
fromComplexArray(complex, storage) { fromComplexArray(complex, storage) {
var res = storage || new Array(complex.length >>> 1); var res = storage || new Array(complex.length >>> 1);
for (var i = 0; i < complex.length; i += 2) for (var i = 0; i < complex.length; i += 2) res[i >>> 1] = complex[i];
res[i >>> 1] = complex[i];
return res; return res;
} }
createComplexArray() { createComplexArray() {
const res = new Array(this._csize); const res = new Array(this._csize);
for (var i = 0; i < res.length; i++) for (var i = 0; i < res.length; i++) res[i] = 0;
res[i] = 0;
return res; return res;
} }
toComplexArray(input, storage) { toComplexArray(input, storage) {
@ -77,8 +74,7 @@ export default class FFT {
} }
} }
transform(out, data) { transform(out, data) {
if (out === data) if (out === data) throw new Error('Input and output buffers must be different');
throw new Error('Input and output buffers must be different');
this._out = out; this._out = out;
this._data = data; this._data = data;
@ -88,8 +84,7 @@ export default class FFT {
this._data = null; this._data = null;
} }
realTransform(out, data) { realTransform(out, data) {
if (out === data) if (out === data) throw new Error('Input and output buffers must be different');
throw new Error('Input and output buffers must be different');
this._out = out; this._out = out;
this._data = data; this._data = data;
@ -99,15 +94,13 @@ export default class FFT {
this._data = null; this._data = null;
} }
inverseTransform(out, data) { inverseTransform(out, data) {
if (out === data) if (out === data) throw new Error('Input and output buffers must be different');
throw new Error('Input and output buffers must be different');
this._out = out; this._out = out;
this._data = data; this._data = data;
this._inv = 1; this._inv = 1;
this._transform4(); this._transform4();
for (var i = 0; i < out.length; i++) for (var i = 0; i < out.length; i++) out[i] /= this.size;
out[i] /= this.size;
this._out = null; this._out = null;
this._data = null; this._data = null;
} }
@ -224,8 +217,7 @@ export default class FFT {
// radix-2 implementation // radix-2 implementation
// //
// NOTE: Only called for len=4 // NOTE: Only called for len=4
_singleTransform2(outOff, off, _singleTransform2(outOff, off, step) {
step) {
const out = this._out; const out = this._out;
const data = this._data; const data = this._data;
@ -247,8 +239,7 @@ export default class FFT {
// radix-4 // radix-4
// //
// NOTE: Only called for len=8 // NOTE: Only called for len=8
_singleTransform4(outOff, off, _singleTransform4(outOff, off, step) {
step) {
const out = this._out; const out = this._out;
const data = this._data; const data = this._data;
const inv = this._inv ? -1 : 1; const inv = this._inv ? -1 : 1;
@ -401,8 +392,7 @@ export default class FFT {
} }
// Do not overwrite ourselves // Do not overwrite ourselves
if (i === hquarterLen) if (i === hquarterLen) continue;
continue;
// In the flipped case: // In the flipped case:
// MAi = -MAi // MAi = -MAi
@ -438,9 +428,7 @@ export default class FFT {
// radix-2 implementation // radix-2 implementation
// //
// NOTE: Only called for len=4 // NOTE: Only called for len=4
_singleRealTransform2(outOff, _singleRealTransform2(outOff, off, step) {
off,
step) {
const out = this._out; const out = this._out;
const data = this._data; const data = this._data;
@ -458,9 +446,7 @@ export default class FFT {
// radix-4 // radix-4
// //
// NOTE: Only called for len=8 // NOTE: Only called for len=8
_singleRealTransform4(outOff, _singleRealTransform4(outOff, off, step) {
off,
step) {
const out = this._out; const out = this._out;
const data = this._data; const data = this._data;
const inv = this._inv ? -1 : 1; const inv = this._inv ? -1 : 1;
@ -500,17 +486,3 @@ export default class FFT {
out[outOff + 7] = FDi; out[outOff + 7] = FDi;
} }
} }

View file

@ -1,4 +1,4 @@
"use strict"; 'use strict';
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const WEBAUDIO_BLOCK_SIZE = 128; const WEBAUDIO_BLOCK_SIZE = 128;
@ -168,7 +168,7 @@ class OLAProcessor extends AudioWorkletProcessor {
this.readInputs(inputs); this.readInputs(inputs);
this.shiftInputBuffers(); this.shiftInputBuffers();
this.prepareInputBuffersToSend() this.prepareInputBuffersToSend();
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params); this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params);
this.handleOutputBuffersToRetrieve(); this.handleOutputBuffersToRetrieve();
this.writeOutputs(outputs); this.writeOutputs(outputs);
@ -178,7 +178,7 @@ class OLAProcessor extends AudioWorkletProcessor {
} }
processOLA(inputs, outputs, params) { processOLA(inputs, outputs, params) {
console.assert(false, "Not overriden"); console.assert(false, 'Not overriden');
} }
} }

View file

@ -35,7 +35,6 @@
"vite": "^5.0.10" "vite": "^5.0.10"
}, },
"dependencies": { "dependencies": {
"fft.js": "^4.0.4",
"nanostores": "^0.9.5" "nanostores": "^0.9.5"
} }
} }

View file

@ -8,7 +8,7 @@ import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs'; import { clamp, nanFallback, _mod } from './util.mjs';
import workletsUrl from './worklets.mjs?url'; import workletsUrl from './worklets.mjs?worker&url';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@ -309,6 +309,13 @@ export function resetGlobalEffects() {
} }
export const superdough = async (value, t, hapDuration) => { export const superdough = async (value, t, hapDuration) => {
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
if (stretch != null) {
//account for phase vocoder latency
const latency = 0.04;
t = t - latency;
}
const ac = getAudioContext(); const ac = getAudioContext();
if (typeof value !== 'object') { if (typeof value !== 'object') {
throw new Error( throw new Error(
@ -320,7 +327,7 @@ export const superdough = async (value, t, hapDuration) => {
// duration is passed as value too.. // duration is passed as value too..
value.duration = hapDuration; value.duration = hapDuration;
// calculate absolute time // calculate absolute time
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
if (t < ac.currentTime) { if (t < ac.currentTime) {
console.warn( console.warn(
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
@ -371,7 +378,6 @@ export const superdough = async (value, t, hapDuration) => {
// //
coarse, coarse,
crush, crush,
stretch,
shape, shape,
shapevol = getDefaultValue('shapevol'), shapevol = getDefaultValue('shapevol'),
distort, distort,
@ -441,7 +447,6 @@ export const superdough = async (value, t, hapDuration) => {
chain.push(sourceNode); chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
// gain stage // gain stage
chain.push(gainNode(gain)); chain.push(gainNode(gain));
@ -587,4 +592,6 @@ export const superdough = async (value, t, hapDuration) => {
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
}; };
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps); export const superdoughTrigger = (t, hap, ct, cps) => {
superdough(hap, t - ct, hap.duration / cps, cps);
};

View file

@ -2,7 +2,7 @@
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT // TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
import OLAProcessor from "./ola-processor" import OLAProcessor from './ola-processor';
import FFT from './fft.js'; import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
@ -469,26 +469,25 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048; const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) { function genHannWindow(length) {
let win = new Float32Array(length); let win = new Float32Array(length);
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
win[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / length)); win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length));
} }
return win; return win;
} }
class PhaseVocoderProcessor extends OLAProcessor { class PhaseVocoderProcessor extends OLAProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
return [{ return [
{
name: 'pitchFactor', name: 'pitchFactor',
defaultValue: 1.0 defaultValue: 1.0,
}]; },
];
} }
constructor(options) { constructor(options) {
@ -513,7 +512,13 @@ class PhaseVocoderProcessor extends OLAProcessor {
processOLA(inputs, outputs, parameters) { processOLA(inputs, outputs, parameters) {
// no automation, take last value // no automation, take last value
const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
if (pitchFactor < 0) {
pitchFactor = pitchFactor * 0.25;
}
pitchFactor = Math.max(0, pitchFactor + 1);
for (var i = 0; i < this.nbInputs; i++) { for (var i = 0; i < this.nbInputs; i++) {
for (var j = 0; j < inputs[i].length; j++) { for (var j = 0; j < inputs[i].length; j++) {
@ -532,7 +537,6 @@ class PhaseVocoderProcessor extends OLAProcessor {
this.fft.completeSpectrum(this.freqComplexBufferShifted); this.fft.completeSpectrum(this.freqComplexBufferShifted);
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
this.fft.fromComplexArray(this.timeComplexBuffer, output); this.fft.fromComplexArray(this.timeComplexBuffer, output);
this.applyHannWindow(output); this.applyHannWindow(output);
} }
} }
@ -543,13 +547,14 @@ class PhaseVocoderProcessor extends OLAProcessor {
/** Apply Hann window in-place */ /** Apply Hann window in-place */
applyHannWindow(input) { applyHannWindow(input) {
for (var i = 0; i < this.blockSize; i++) { for (var i = 0; i < this.blockSize; i++) {
input[i] = input[i] * this.hannWindow[i]; input[i] = input[i] * this.hannWindow[i] * 1.62;
} }
} }
/** Compute squared magnitudes for peak finding **/ /** Compute squared magnitudes for peak finding **/
computeMagnitudes() { computeMagnitudes() {
var i = 0, j = 0; var i = 0,
j = 0;
while (i < this.magnitudes.length) { while (i < this.magnitudes.length) {
let real = this.freqComplexBuffer[j]; let real = this.freqComplexBuffer[j];
let imag = this.freqComplexBuffer[j + 1]; let imag = this.freqComplexBuffer[j + 1];
@ -621,7 +626,7 @@ class PhaseVocoderProcessor extends OLAProcessor {
} }
// apply phase correction // apply phase correction
let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize; let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize;
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
@ -642,8 +647,4 @@ class PhaseVocoderProcessor extends OLAProcessor {
} }
} }
registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);

18
pnpm-lock.yaml generated
View file

@ -48,9 +48,6 @@ importers:
events: events:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
fft-js:
specifier: ^0.0.12
version: 0.0.12
jsdoc: jsdoc:
specifier: ^4.0.2 specifier: ^4.0.2
version: 4.0.2 version: 4.0.2
@ -433,9 +430,6 @@ importers:
packages/superdough: packages/superdough:
dependencies: dependencies:
fft.js:
specifier: ^4.0.4
version: 4.0.4
nanostores: nanostores:
specifier: ^0.9.5 specifier: ^0.9.5
version: 0.9.5 version: 0.9.5
@ -7700,18 +7694,6 @@ packages:
resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==}
dev: true dev: true
/fft-js@0.0.12:
resolution: {integrity: sha512-nLOa0/SYYnN2NPcLrI81UNSPxyg3q0sGiltfe9G1okg0nxs5CqAwtmaqPQdGcOryeGURaCoQx8Y4AUkhGTh7IQ==}
engines: {node: '>=0.12.0'}
dependencies:
bit-twiddle: 1.0.2
commander: 2.7.1
dev: true
/fft.js@4.0.4:
resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==}
dev: false
/fftjs@0.0.4: /fftjs@0.0.4:
resolution: {integrity: sha512-nIWxQyth1LVD6NH8a+YZUv+McjzbOY6dMe4wv6Pq5cGfP+c8Rd1T8Dsd50DCWlNgzSqA3y9lOkpD6dZD3qHa1A==} resolution: {integrity: sha512-nIWxQyth1LVD6NH8a+YZUv+McjzbOY6dMe4wv6Pq5cGfP+c8Rd1T8Dsd50DCWlNgzSqA3y9lOkpD6dZD3qHa1A==}
dependencies: dependencies: