Skip to content

Commit

Permalink
fix: ad enveloper
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb committed Sep 4, 2024
1 parent 32335cc commit 16904de
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 56 deletions.
41 changes: 41 additions & 0 deletions packages/ad/src/__snapshots__/ad.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AdWorkletNode has parameter descriptors 1`] = `
[
{
"automationRate": "k-rate",
"defaultValue": 0,
"maxValue": 1,
"minValue": 0,
"name": "trigger",
},
{
"automationRate": "k-rate",
"defaultValue": 0.01,
"maxValue": 10,
"minValue": 0,
"name": "attack",
},
{
"automationRate": "k-rate",
"defaultValue": 0.1,
"maxValue": 10,
"minValue": 0,
"name": "decay",
},
{
"automationRate": "k-rate",
"defaultValue": 0,
"maxValue": 20000,
"minValue": 0,
"name": "offset",
},
{
"automationRate": "k-rate",
"defaultValue": 1,
"maxValue": 10000,
"minValue": 0,
"name": "gain",
},
]
`;
1 change: 1 addition & 0 deletions packages/ad/src/_worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function connectAll(
const param = node[paramName];
const input = inputs[paramName];
if (typeof input === "number") {
console.log("SET", paramName, input);
param.value = input;
} else if (input instanceof AudioNode) {
param.value = 0;
Expand Down
107 changes: 107 additions & 0 deletions packages/ad/src/ad.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
describe("AdWorkletNode", () => {
let AdWorklet: any;

beforeAll(async () => {
createWorkletTestContext();
AdWorklet = (await import("./worklet")).AdProcessor;
});

it("registers processor", () => {
expect(global.registerProcessor).toHaveBeenCalledWith(
"AdProcessor",
AdWorklet
);
});

it("is generates offset when not triggered", () => {
const node = new AdWorklet();
const params = {
trigger: [0],
attack: [0.01],
decay: [0.1],
offset: [100],
gain: [0.5],
};
let output = runProcessMono(node, 10, params);
expect(output).toEqual(new Float32Array(10).fill(100));
});

it("generates an envelope with gain", () => {
const node = new AdWorklet();
const params = {
trigger: [1],
attack: [0.2],
decay: [0.2],
offset: [100],
gain: [50],
};
let output = runProcessMono(node, 10, params);
expect(Array.from(output)).toEqual([
119.67346954345703, 131.6060333251953, 138.84349060058594,
143.2332305908203, 145.895751953125, 147.51065063476562,
148.49012756347656, 149.08421325683594, 149.44454956054688,
149.66310119628906,
]);
});

it("has parameter descriptors", () => {
expect(AdWorklet.parameterDescriptors).toMatchSnapshot();
});
});

function createWorkletTestContext(sampleRate = 10) {
// @ts-ignore
global.sampleRate = sampleRate;
// @ts-ignore
global.AudioWorkletProcessor = class AudioWorkletNodeStub {
port: {
postMessage: jest.Mock<any, any, any>;
onmessage: jest.Mock<any, any, any>;
};

constructor() {
this.port = {
postMessage: jest.fn(),
onmessage: jest.fn(),
};
}
};
// @ts-ignore
global.registerProcessor = jest.fn(); // Mock registerProcessor
}

function createInputsOutputs(
options: { ins?: number; outs?: number; length?: number } = {}
) {
const inCount = options.ins ?? 1;
const outCount = options.outs ?? 1;
const length = options.length ?? 10;
const inputs: Float32Array[][] = [];
const outputs: Float32Array[][] = [];

for (let i = 0; i < inCount; i++) {
inputs.push([new Float32Array(length)]);
}
for (let i = 0; i < outCount; i++) {
outputs.push([new Float32Array(length)]);
}
return { inputs, outputs };
}

type Worklet = {
process: (
inputs: Float32Array[][],
outputs: Float32Array[][],
parameters: any
) => boolean;
};

export function runProcessMono(
worklet: Worklet,
size: number,
params: any = {}
) {
const { inputs, outputs } = createInputsOutputs({ length: size });
worklet.process(inputs, outputs, params);
return outputs[0][0];
}
2 changes: 1 addition & 1 deletion packages/ad/src/processor.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PROCESSOR = `"use strict";(()=>{var D=class extends AudioWorkletProcessor{r;d;constructor(){super(),this.r=!0,this.d=b(sampleRate,.01,.1),this.port.onmessage=r=>{switch(r.data.type){case"DISPOSE":this.r=!1;break}}}process(r,s,e){return this.d.update(e.trigger[0],e.attack[0],e.decay[0]),this.d.gen(s[0][0],e.offset[0],e.gain[0]),this.r}static get parameterDescriptors(){return[["trigger",0,0,1],["attack",.01,0,10],["decay",.1,0,10],["offset",0,0,2e4],["gain",1,0,1e4]].map(([r,s,e,O])=>({name:r,defaultValue:s,minValue:e,maxValue:O,automationRate:"k-rate"}))}};registerProcessor("AdProcessor",D);function b(a,r,s){let f=!1,o=r,c=s,h=Math.exp(-1/(o*a)),d=Math.exp(-1/(c*a)),i=0,n=0;return{update(E,u,l){E===1?f||(f=!0,i=1):f=!1,u!==o&&(o=u,h=Math.exp(-1/(Math.max(o,.001)*a))),l!==c&&(c=l,d=Math.exp(-1/(Math.max(c,.001)*a)))},gen(E,u,l){let t=0;for(let M=0;M<E.length;M++){switch(i){case 1:t=h*n+(1-h),t-n<=5e-8&&(i=2),n=t;break;case 2:t=d*n,n=t,t<=5e-8&&(i=0);break;case 0:default:t=0;break}E[M]=u+t*l}}}}})();`;
export const PROCESSOR = `"use strict";(()=>{var M=class extends AudioWorkletProcessor{r;d;constructor(){super(),this.r=!0,this.d=O(sampleRate),this.port.onmessage=a=>{switch(a.data.type){case"DISPOSE":this.r=!1;break}}}process(a,n,t){return this.d.update(t.trigger[0],t.attack[0],t.decay[0]),this.d.gen(n[0][0],t.offset[0],t.gain[0]),this.r}static get parameterDescriptors(){return[["trigger",0,0,1],["attack",.01,0,10],["decay",.1,0,10],["offset",0,0,2e4],["gain",1,0,1e4]].map(([a,n,t,d])=>({name:a,defaultValue:n,minValue:t,maxValue:d,automationRate:"k-rate"}))}};registerProcessor("AdProcessor",M);function O(l){let D=l*.05,g=l*.1,u=!1,E=.1,f=.1,h=Math.exp(-1/(.1*D)),A=Math.exp(-1/(.1*g)),r=0,s=0;return{update(o,c,i){if(o===1?u||(u=!0,r=1):u=!1,c!==E){E=c;let e=Math.max(E*D,.001);h=Math.exp(-1/e)}if(i!==f){f=i;let e=Math.max(f*g,.001);A=Math.exp(-1/e)}},gen(o,c,i){let e=0;for(let p=0;p<o.length;p++)r===1?(e=h*s+(1-h),e-s<=5e-8&&(r=2),s=e):r===2?(e=A*s,s=e,e<=5e-8&&(r=0)):e=0,o[p]=c+e*i}}}})();`;
64 changes: 28 additions & 36 deletions packages/ad/src/worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class AdProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.r = true;
this.d = createEnvelope(sampleRate, 0.01, 0.1);
this.d = createEnvelope(sampleRate);
this.port.onmessage = (event) => {
switch (event.data.type) {
case "DISPOSE":
Expand Down Expand Up @@ -42,22 +42,22 @@ export class AdProcessor extends AudioWorkletProcessor {
registerProcessor("AdProcessor", AdProcessor);

// Attack-Decay envelope based on https://paulbatchelor.github.io/sndkit/env/
function createEnvelope(
sampleRate: number,
attackTime: number,
decayTime: number
) {
function createEnvelope(sampleRate: number) {
const MODE_ZERO = 0;
const MODE_ATTACK = 1;
const MODE_DECAY = 2;
const EPS = 5e-8;

// This time constants are obtained empirically
const attackTime2Tau = sampleRate * 0.05;
const decayTime2Tau = sampleRate * 0.1;

// Convert seconds to time constants
let gate = false;
let attack = attackTime;
let decay = decayTime;
let attackEnv = Math.exp(-1.0 / (attack * sampleRate));
let decayEnv = Math.exp(-1.0 / (decay * sampleRate));
let attack = 0.1;
let decay = 0.1;
let attackEnv = Math.exp(-1.0 / (0.1 * attackTime2Tau));
let decayEnv = Math.exp(-1.0 / (0.1 * decayTime2Tau));

let mode = MODE_ZERO;
let prev = 0;
Expand All @@ -74,40 +74,32 @@ function createEnvelope(
}
if (attackTime !== attack) {
attack = attackTime;
attackEnv = Math.exp(-1.0 / (Math.max(attack, 0.001) * sampleRate));
const tau = Math.max(attack * attackTime2Tau, 0.001);
attackEnv = Math.exp(-1.0 / tau);
}
if (decayTime !== decay) {
decay = decayTime;
decayEnv = Math.exp(-1.0 / (Math.max(decay, 0.001) * sampleRate));
const tau = Math.max(decay * decayTime2Tau, 0.001);
decayEnv = Math.exp(-1.0 / tau);
}
},
gen(output: Float32Array, offset: number, gain: number) {
let out = 0;
for (let i = 0; i < output.length; i++) {
switch (mode) {
case MODE_ATTACK:
out = attackEnv * prev + (1.0 - attackEnv);

if (out - prev <= EPS) {
mode = MODE_DECAY;
}

prev = out;
break;

case MODE_DECAY:
out = decayEnv * prev;
prev = out;

if (out <= EPS) {
mode = MODE_ZERO;
}
break;

case MODE_ZERO:
default:
out = 0;
break;
if (mode === MODE_ATTACK) {
out = attackEnv * prev + (1.0 - attackEnv);
if (out - prev <= EPS) {
mode = MODE_DECAY;
}
prev = out;
} else if (mode === MODE_DECAY) {
out = decayEnv * prev;
prev = out;
if (out <= EPS) {
mode = MODE_ZERO;
}
} else {
out = 0;
}

output[i] = offset + out * gain;
Expand Down
1 change: 1 addition & 0 deletions packages/polyblep-oscillator/src/_worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function connectAll(
if (typeof input === "number") {
param.value = input;
} else if (input instanceof AudioNode) {
console.log("POLY CNNECT", paramName, input);
param.value = 0;
input.connect(param);
connected.push(input);
Expand Down
22 changes: 13 additions & 9 deletions packages/synthlet/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createClipAmpNode } from "@synthlet/clip-amp";
import { createImpulseNode } from "@synthlet/impulse";
import { createNoiseNode } from "@synthlet/noise";
import { createParamNode, ParamType, ParamWorkletNode } from "@synthlet/param";
import { createPolyblepOscillatorNode } from "@synthlet/polyblep-oscillator";
import { DisposableAudioNode, ParamInput } from "./src/_worklet";
import {
createConstantNode,
Expand All @@ -17,15 +18,13 @@ export function createOperators(context: AudioContext) {
const set = new Set<DisposableAudioNode>();

const add = <T extends DisposableAudioNode>(node: T): T => {
if (!set.has(node)) console.log("ADD", node);
set.add(node);
return node;
};
const disposer = (node: AudioNode) => {
const _dispose = (node as any).dispose;
const disposables = Array.from(set).filter((d) => d !== node);
set.clear();
console.log("DISPOSER", node, disposables);
return () => {
_dispose?.();
disposables.forEach((d) => d.dispose());
Expand All @@ -45,7 +44,6 @@ export function createOperators(context: AudioContext) {
return destination;
},
// Parameters
trigger: () => add(createParamNode(context)),
param: (value?: ParamInput) =>
add(createParamNode(context, { input: value })),
dbToGain: (value?: ParamInput) =>
Expand All @@ -66,18 +64,24 @@ export function createOperators(context: AudioContext) {

// Oscillators
sine: (frequency?: ParamInput, detune?: ParamInput) =>
add(createOscillator(context, { type: "sine", frequency, detune })),
add(createOscillator(context, { type: "sine", frequency })),
tri: (frequency?: ParamInput, detune?: ParamInput) =>
add(createOscillator(context, { type: "triangle", frequency, detune })),
add(
createPolyblepOscillatorNode(context, { type: "triangle", frequency })
),

white: () => add(createNoiseNode(context)),

pulse: (trigger: ParamInput) =>
add(createImpulseNode(context, { trigger })),

// Envelope generators
genAd: (trigger: ParamInput, attack?: ParamInput, decay?: ParamInput) =>
add(createAdNode(context, { trigger, attack, decay })),
ad: (
trigger: ParamInput,
attack?: ParamInput,
decay?: ParamInput,
params?: Partial<AdInputParams>
) => add(createAdNode(context, { trigger, attack, decay, ...params })),

// Amplifiers
amp: (gain?: ParamInput) =>
Expand All @@ -91,8 +95,8 @@ export function createOperators(context: AudioContext) {
})
),

// Attack-Decay
ad: (
// Attack-Decay (percussive) envelope
perc: (
trigger: ParamInput,
attack?: ParamInput,
decay?: ParamInput,
Expand Down
6 changes: 3 additions & 3 deletions packages/synthlet/src/synths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type KickDrumNode = DrumNode & {};
export function KickDrum(context: AudioContext): KickDrumNode {
const op = createOperators(context);

const trigger = op.trigger();
const trigger = op.param();
const volume = op.volume();
const tone = op.linear(30, 100, 0.2);

Expand All @@ -36,7 +36,7 @@ export type SnareDrumNode = DrumNode & {};
export function SnareDrum(context: AudioContext): SnareDrumNode {
const op = createOperators(context);

const trigger = op.trigger();
const trigger = op.param();
const volume = op.volume();
const tone = op.param();

Expand All @@ -60,7 +60,7 @@ export function ClaveDrum(context: AudioContext): DrumNode {
const op = createOperators(context);

const volume = op.volume();
const trigger = op.trigger();
const trigger = op.param();
const tone = op.linear(1200, 1800, 0.6);

const out = op.serial(
Expand Down
8 changes: 4 additions & 4 deletions site/components/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function Slider({
step = 1,
transform = (x) => x,
inputClassName,
labelClassName = "text-right",
labelClassName,
valueClassName,
units,
initialize,
Expand All @@ -36,7 +36,7 @@ export function Slider({

return (
<>
<p className={labelClassName}>{label}</p>
<div className={labelClassName}>{label}</div>
<input
className={inputClassName}
type="range"
Expand All @@ -50,10 +50,10 @@ export function Slider({
onChange(transform(value));
}}
/>
<p className={valueClassName}>
<div className={valueClassName}>
{transform(value).toFixed(2)}
{units}
</p>
</div>
</>
);
}
Loading

0 comments on commit 16904de

Please sign in to comment.