Skip to content

Commit

Permalink
Site (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb authored Jul 27, 2024
1 parent 5a0a49c commit c6383c7
Show file tree
Hide file tree
Showing 40 changed files with 5,890 additions and 65 deletions.
23 changes: 13 additions & 10 deletions packages/adsr/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PROCESSOR } from "./processor";

export type ProcessorOptions = {
export type AdsrProcessorOptions = {
mode?: "generator" | "modulator";
};

Expand All @@ -14,6 +14,9 @@ export type AdsrParams = {
gain: number;
};

/**
* An ADSR WorkletNode
*/
export type AdsrWorkletNode = AudioWorkletNode & {
gate: AudioParam;
attack: AudioParam;
Expand All @@ -36,11 +39,11 @@ const PARAM_NAMES = [
"gain",
] as const;

export function getProcessorName() {
export function getAdsrProcessorName() {
return "AdsrWorkletProcessor"; // Can't import from worklet because globals
}

export function getWorkletUrl() {
function getWorkletUrl() {
const blob = new Blob([PROCESSOR], { type: "application/javascript" });
return URL.createObjectURL(blob);
}
Expand Down Expand Up @@ -82,7 +85,7 @@ export function registerAdsrWorkletOnce(
* It can be used to modulate amplitude of another signal.
*
* @param audioContext - The AudioContext
* @returns AudioWorkletNode
* @returns AdsrWorkletNode
* @example
* ```
* const osc = audioContext.createOscillator();
Expand All @@ -97,29 +100,29 @@ export function registerAdsrWorkletOnce(
export function createVca(
audioContext: AudioContext,
params?: Partial<AdsrParams>
): AudioWorkletNode {
): AdsrWorkletNode {
return createWorkletNode(audioContext, { mode: "modulator" });
}

/**
* Create a ADSR AudioWorkletNode.
* It can be used to modulate other parameters
* @param audioContext
* @returns
* @returns AdsrWorkletNode
*/
export function createAdsr(
audioContext: AudioContext,
params?: Partial<AdsrParams>
): AudioWorkletNode {
): AdsrWorkletNode {
return createWorkletNode(audioContext, { mode: "generator" }, params);
}

function createWorkletNode(
audioContext: AudioContext,
processorOptions: ProcessorOptions,
processorOptions: AdsrProcessorOptions,
params: Partial<AdsrParams> = {}
): AudioWorkletNode {
const node = new AudioWorkletNode(audioContext, getProcessorName(), {
): AdsrWorkletNode {
const node = new AudioWorkletNode(audioContext, getAdsrProcessorName(), {
numberOfInputs: 1,
numberOfOutputs: 1,
processorOptions,
Expand Down
4 changes: 2 additions & 2 deletions packages/adsr/src/worklet.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getProcessorName } from "./index";
import { getAdsrProcessorName } from "./index";

describe("AdsrWorkletNode", () => {
let Worklet: any;
Expand All @@ -10,7 +10,7 @@ describe("AdsrWorkletNode", () => {

it("registers processor", () => {
expect(global.registerProcessor).toHaveBeenCalledWith(
getProcessorName(),
getAdsrProcessorName(),
Worklet
);
});
Expand Down
22 changes: 13 additions & 9 deletions packages/polyblep-oscillator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type PolyblepOscillatorParams = {
frequency: number;
};

export function getProcessorName() {
export function getPolyblepOscillatorProcessorName() {
return "PolyBLEPWorkletProcessor"; // Can't import from worklet because globals
}

Expand All @@ -22,13 +22,13 @@ const PARAM_NAMES = ["waveform", "frequency"] as const;
/**
* A PolyBLEP Oscillator AudioWorkletNode
*/
export type PolyblepWorkletNode = AudioWorkletNode & {
export type PolyblepOscillatorWorkletNode = AudioWorkletNode & {
type: PolyblepWaveformType;
waveform: AudioParam;
frequency: AudioParam;
};

export function getWorkletUrl() {
function getWorkletUrl() {
const blob = new Blob([PROCESSOR], { type: "application/javascript" });
return URL.createObjectURL(blob);
}
Expand All @@ -37,17 +37,21 @@ export function getWorkletUrl() {
* Create a PolyblepOscillator worklet node
* @param audioContext
* @param options
* @returns PolyblepWorkletNode
* @returns PolyblepOscillatorWorkletNode
*/
export function createPolyblepOscillator(
audioContext: AudioContext,
options: Partial<PolyblepOscillatorOptions> = {}
): PolyblepWorkletNode {
): PolyblepOscillatorWorkletNode {
const params = optionsToParams(options);
const node = new AudioWorkletNode(audioContext, getProcessorName(), {
numberOfInputs: 1,
numberOfOutputs: 1,
}) as PolyblepWorkletNode;
const node = new AudioWorkletNode(
audioContext,
getPolyblepOscillatorProcessorName(),
{
numberOfInputs: 1,
numberOfOutputs: 1,
}
) as PolyblepOscillatorWorkletNode;

for (const paramName of PARAM_NAMES) {
const param = node.parameters.get(paramName)!;
Expand Down
4 changes: 2 additions & 2 deletions packages/polyblep-oscillator/src/worklet.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getProcessorName } from "./index";
import { getPolyblepOscillatorProcessorName } from "./index";

describe("ProcessorNode", () => {
let Processor: any;
Expand All @@ -11,7 +11,7 @@ describe("ProcessorNode", () => {

it("registers processor", () => {
expect(global.registerProcessor).toHaveBeenCalledWith(
getProcessorName(),
getPolyblepOscillatorProcessorName(),
Processor
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

exports[`ProcessorNode has parameter descriptors 1`] = `
[
{
"automationRate": "k-rate",
"defaultValue": 1,
"maxValue": 3,
"minValue": 0,
"name": "filterType",
},
{
"automationRate": "k-rate",
"defaultValue": 1000,
Expand Down
15 changes: 10 additions & 5 deletions packages/state-variable-filter/src/filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { SVFilter, SVFilterType } from "./filter";
import { SVFilter } from "./filter";

describe("SVFilter", () => {
it("filters the signal", () => {
const filter = SVFilter(20, SVFilterType.LowPass);
const filter = SVFilter(20);
const input = new Float32Array(20);
const output = new Float32Array(20);

for (let i = 0; i < input.length; i++) {
input[i] = i % 2 ? 1 : -1;
}
expect(input).toMatchSnapshot();
const output = new Float32Array(20);
filter.fill(input, output, {
filter.update({
filterType: [1],
frequency: [10],
resonance: [0.5],
});
filter.fill(input, output);
expect(output).toMatchSnapshot();
filter.fill(input, output, {
filter.update({
filterType: [1],
frequency: [5],
resonance: [0.5],
});
filter.fill(input, output);
expect(output).toMatchSnapshot();
});
});
22 changes: 13 additions & 9 deletions packages/state-variable-filter/src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ export function clamp(value: number, min: number, max: number): number {
}

export enum SVFilterType {
ByPass = 0,
LowPass = 1,
BandPass = 2,
HighPass = 3,
}

export type Inputs = {
filterType: number[];
frequency: number[];
resonance: number[];
};
Expand All @@ -27,14 +29,14 @@ export type Inputs = {
* https://github.com/Ardour/ardour/blob/71e049202c017c0a546f39b455bdc9e4be182f06/libs/plugins/a-eq.lv2/a-eq.c
* https://github.com/SoundStacks/cmajor/blob/main/standard_library/std_library_filters.cmajor#L157
*/
export function SVFilter(sampleRate: number, type = SVFilterType.LowPass) {
export function SVFilter(sampleRate: number) {
// Params
let $frequency = 1000;
let $resonance = 0.5;
let $type = SVFilterType.LowPass;

const period = 0.5 / sampleRate;
const pi2 = 2 * Math.PI;
const freqCoef = 2.0 * sampleRate * period;

let d = 0;
let a = 0;
Expand All @@ -45,7 +47,8 @@ export function SVFilter(sampleRate: number, type = SVFilterType.LowPass) {
let band = 0;
let low = 0;

function read(inputs: Inputs) {
function update(inputs: Inputs) {
$type = inputs.filterType[0];
if (
inputs.frequency[0] !== $frequency ||
inputs.resonance[0] !== $resonance
Expand All @@ -62,8 +65,7 @@ export function SVFilter(sampleRate: number, type = SVFilterType.LowPass) {
}
}

function fill(input: Float32Array, output: Float32Array, inputs: Inputs) {
read(inputs);
function fill(input: Float32Array, output: Float32Array) {
for (let i = 0; i < input.length; i++) {
const x = input[i];
high = (x - g1 * z0 - z1) * d;
Expand All @@ -72,12 +74,14 @@ export function SVFilter(sampleRate: number, type = SVFilterType.LowPass) {
z0 = a * high + band;
z1 = a * band + low;
output[i] =
type === SVFilterType.LowPass
$type === SVFilterType.LowPass
? low
: type === SVFilterType.HighPass
: $type === SVFilterType.HighPass
? high
: band;
: $type === SVFilterType.BandPass
? band
: x;
}
}
return { fill };
return { update, fill };
}
89 changes: 89 additions & 0 deletions packages/state-variable-filter/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
createStateVariableFilter,
registerStateVariableFilterWorkletOnce,
} from "./index";

describe("PolyblepOscillator", () => {
it("registers only once", () => {
const context = new AudioContextMock();

registerStateVariableFilterWorkletOnce(context.asAudioContext());
expect(context.audioWorklet?.addModule).toHaveBeenCalledTimes(1);
registerStateVariableFilterWorkletOnce(context.asAudioContext());
expect(context.audioWorklet?.addModule).toHaveBeenCalledTimes(1);
});

it("creates the worklet node with default parameters", () => {
// @ts-ignore
global.AudioWorkletNode = AudioWorkletNodeMock;
const node = createStateVariableFilter(
new AudioContextMock().asAudioContext()
);
expect(node.frequency.value).toBe(4000);
expect(node.filterType.value).toBe(1);
expect(node.type).toEqual("lowpass");
});

it("changes the waveform using type property", () => {
// @ts-ignore
global.AudioWorkletNode = AudioWorkletNodeMock;
const node = createStateVariableFilter(
new AudioContextMock().asAudioContext()
);
expect(node.filterType.value).toBe(1);
node.type = "highpass";
expect(node.filterType.value).toBe(3);
});
});

class AudioContextMock {
audioWorklet?: { addModule: jest.Mock<any, any, any> };

constructor(worklets = true) {
if (worklets) {
this.audioWorklet = {
addModule: jest.fn(),
};
}
}

asAudioContext(): AudioContext {
return this as unknown as AudioContext;
}
}

class AudioWorkletNodeMock {
params: Record<string, ParamMock>;
parameters: { get(name: string): void };
disconnect = jest.fn();

constructor(
public context: any,
public processorName: any,
public options: any
) {
this.params = {
frequency: new ParamMock(),
resonance: new ParamMock(),
filterType: new ParamMock(),
};

const get = (name: string): ParamMock => {
return this.params[name];
};

this.parameters = {
get,
};
}
}

class ParamMock {
value = 0;
values: { value: number; time: number }[] = [];

setValueAtTime(value: number, time: number) {
this.values.push({ value, time });
if (time === 0) this.value = value;
}
}
Loading

0 comments on commit c6383c7

Please sign in to comment.