Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Site #14

Merged
merged 5 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading