Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dist
examples/dist
rust/target
coverage
pnpm-lock.yaml

# tests
__tests__/integration/**/*-diff.png
Expand Down
162 changes: 162 additions & 0 deletions __tests__/api-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Audio } from '../src/Audio';
import { Effect } from '../src/effects';

globalThis.AudioContext = class {
destination = {};
createMediaElementSource() {
return { connect: vi.fn() };
}
createAnalyser() {
return {
connect: vi.fn(),
fftSize: 0,
frequencyBinCount: 256,
getByteFrequencyData: vi.fn()
};
}
decodeAudioData() {
return Promise.resolve({
numberOfChannels: 2,
sampleRate: 44100,
length: 1000,
getChannelData: () => new Float32Array(1000).fill(0.1)
});
}
} as any;

globalThis.Worker = class {
postMessage() {}
terminate() {}
onmessage = null;
onerror = null;
addEventListener() {}
removeEventListener() {}
dispatchEvent() { return true; }
} as any;

describe('Audio API Integration Test', () => {
let canvas: HTMLCanvasElement;
let audioElement: HTMLMediaElement;
let mockEffect: Effect;

beforeEach(() => {
canvas = document.createElement('canvas');
audioElement = document.createElement('audio');

mockEffect = {
init: vi.fn().mockResolvedValue(undefined),
resize: vi.fn(),
frame: vi.fn(),
update: vi.fn(),
destroy: vi.fn()
};
});

it('should create an instance of Audio', () => {
const audio = new Audio({ canvas });
expect(audio).toBeInstanceOf(Audio);

const audioWithData = new Audio({ canvas, data: audioElement });
expect(audioWithData['options'].data).toBe(audioElement);

const audioWithEffect = new Audio({ canvas, effect: mockEffect });
expect(audioWithEffect['options'].effect).toBe(mockEffect);
expect(mockEffect.init).toHaveBeenCalledWith(canvas);
});

it('should set the audio data', () => {
const audio = new Audio({ canvas });
const result = audio.data(audioElement);

expect(result).toBe(audio);
expect(audio['options'].data).toBe(audioElement);
expect(audio['analyser']).toBeDefined();
});

it('should set the effect', async () => {
const audio = new Audio({ canvas });
const result = audio.effect(mockEffect);

expect(result).toBe(audio);
expect(audio['options'].effect).toBe(mockEffect);
expect(mockEffect.init).toHaveBeenCalledWith(canvas);

const newEffect: Effect = {
init: vi.fn().mockResolvedValue(undefined),
resize: vi.fn(),
frame: vi.fn(),
update: vi.fn(),
destroy: vi.fn()
};

audio.effect(newEffect);
expect(mockEffect.destroy).toHaveBeenCalled();
expect(audio['options'].effect).toBe(newEffect);
});

it('should update the effect style', () => {
const audio = new Audio({ canvas, effect: mockEffect });
const options = { exposure: 0.3 };
const result = audio.style(options);

expect(result).toBe(audio);
expect(mockEffect.update).toHaveBeenCalledWith(options);
});

it('should play the audio and animation', async () => {
const audio = new Audio({ canvas, effect: mockEffect });

let frameCallCount = 0;
audio.onframe = () => { frameCallCount++; };

await audio.play();
await new Promise(resolve => setTimeout(resolve, 50));

expect(audio['timer']).toBeDefined();
expect(frameCallCount).toBeGreaterThan(0);
expect(mockEffect.frame).toHaveBeenCalled();
});

it('should resize the canvas', () => {
const audio = new Audio({ canvas, effect: mockEffect });
audio.resize(800, 600);

expect(canvas.width).toBe(800 * window.devicePixelRatio);
expect(canvas.height).toBe(600 * window.devicePixelRatio);

expect(mockEffect.resize).toHaveBeenCalledWith(
canvas.width,
canvas.height
);
});

it('should clean up resources', () => {
const mockDisconnect = vi.fn();
const audio = new Audio({ canvas, effect: mockEffect });
audio['analyser'] = { disconnect: mockDisconnect } as any;
audio['timer'] = 111;

audio.destroy();
expect(mockEffect.destroy).toHaveBeenCalled();
expect(mockDisconnect).toHaveBeenCalled();
});

it('should classify the genre of the audio', async () => {
const audio = new Audio({ canvas });
audio.classifyGenre = () => Promise.resolve({
classifyLabel: 'rock',
classifyTime: 123,
classifyOutput: 4
});

const file = new File(['dummy'], 'test.mp3', { type: 'audio/mp3' });
const fileList = { 0: file, length: 1 } as unknown as FileList;
const result = await audio.classifyGenre(fileList);
expect(result).toEqual({
classifyLabel: 'rock',
classifyTime: 123,
classifyOutput: 4
});
});
});
25 changes: 25 additions & 0 deletions examples/demos/alienorb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as lil from 'lil-gui';
import { Audio, AlienOrb } from '../../src';

export function render(audio: Audio, gui: lil.GUI) {
const shaderCompilerPath = new URL(
'/public/glsl_wgsl_compiler_bg.wasm',
import.meta.url,
).href;
const effect = new AlienOrb(shaderCompilerPath);

const folder = gui.addFolder('style');
const config = {
fft: 1.00,
exposure: 0.20,
};

folder.add(config, 'fft', 0, 1).onChange((fft: number) => {
audio.style({ fft });
});
folder.add(config, 'exposure', 0, 1).onChange((exposure: number) => {
audio.style({ exposure });
});

return [effect, folder];
}
69 changes: 69 additions & 0 deletions examples/demos/bifurcation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as lil from 'lil-gui';
import { Audio, Bifurcation } from '../../src';

export function render(audio: Audio, gui: lil.GUI) {
const shaderCompilerPath = new URL(
'/public/glsl_wgsl_compiler_bg.wasm',
import.meta.url,
).href;
const effect = new Bifurcation(shaderCompilerPath);

const folder = gui.addFolder('style');
const config = {
radius: 0.306,
samples: 0.226,
accumulation: 0.890,
noiseAnimation: 1,
exposure: 0.238,
beta: 0.425,
alpha: 0.301,
betaAnim: 0.019,
betaS: 0.430,
gamma: 1,
epsilon: 0.224,
betaA: 0.325,
betaB: 0.301,
};

folder.add(config, 'radius', 0, 1).onChange((radius: number) => {
audio.style({ radius });
});
folder.add(config, 'samples', 0, 1).onChange((samples: number) => {
audio.style({ samples });
});
folder.add(config, 'accumulation', 0, 1).onChange((accumulation: number) => {
audio.style({ accumulation });
});
folder.add(config, 'noiseAnimation', 0, 1).onChange((noiseAnimation: number) => {
audio.style({ noiseAnimation });
});
folder.add(config, 'exposure', 0, 1).onChange((exposure: number) => {
audio.style({ exposure });
});
folder.add(config, 'beta', 0, 1).onChange((beta: number) => {
audio.style({ beta });
});
folder.add(config, 'alpha', 0, 1).onChange((alpha: number) => {
audio.style({ alpha });
});
folder.add(config, 'betaAnim', 0, 1).onChange((betaAnim: number) => {
audio.style({ betaAnim });
});
folder.add(config, 'betaS', 0, 1).onChange((betaS: number) => {
audio.style({ betaS });
});
folder.add(config, 'gamma', 0, 1).onChange((gamma: number) => {
audio.style({ gamma });
});
folder.add(config, 'epsilon', 0, 1).onChange((epsilon: number) => {
audio.style({ epsilon });
});
folder.add(config, 'betaA', 0, 1).onChange((betaA: number) => {
audio.style({ betaA });
});
folder.add(config, 'betaB', 0, 1).onChange((betaB: number) => {
audio.style({ betaB });
});

return [effect, folder];
}
2 changes: 1 addition & 1 deletion examples/demos/blackhole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function render(audio: Audio, gui: lil.GUI) {
).href;
const effect = new BlackHole(shaderCompilerPath);

const folder = gui.addFolder('effect');
const folder = gui.addFolder('style');
const config = {
radius: 1,
timeStep: 0.039,
Expand Down
65 changes: 65 additions & 0 deletions examples/demos/flame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as lil from 'lil-gui';
import { Audio, Flame } from '../../src';

export function render(audio: Audio, gui: lil.GUI) {
const shaderCompilerPath = new URL(
'/public/glsl_wgsl_compiler_bg.wasm',
import.meta.url,
).href;
const effect = new Flame(shaderCompilerPath);

const folder = gui.addFolder('style');
const config = {
radius: 0.220,
samples: 2.198,
accumulation: 0.791,
noiseAnimation: 1,
exposure: 1.532,
pa: 0,
pb: 0.55,
pc: 0.1,
pd: 4.718,
pe: 0.761,
pf: 0.945,
dt: 0.170,
};

folder.add(config, 'radius', 0, 1).onChange((radius: number) => {
audio.style({ radius });
});
folder.add(config, 'samples', 0, 32).onChange((samples: number) => {
audio.style({ samples });
});
folder.add(config, 'accumulation', 0, 1).onChange((accumulation: number) => {
audio.style({ accumulation });
});
folder.add(config, 'noiseAnimation', 0, 1).onChange((noiseAnimation: number) => {
audio.style({ noiseAnimation });
});
folder.add(config, 'exposure', 0, 2).onChange((exposure: number) => {
audio.style({ exposure });
});
folder.add(config, 'pa', 0, 1).onChange((pa: number) => {
audio.style({ pa });
});
folder.add(config, 'pb', 0, 1).onChange((pb: number) => {
audio.style({ pb });
});
folder.add(config, 'pc', 0, 1).onChange((pc: number) => {
audio.style({ pc });
});
folder.add(config, 'pd', 0, 6).onChange((pd: number) => {
audio.style({ pd });
});
folder.add(config, 'pe', 0, 5).onChange((pe: number) => {
audio.style({ pe });
});
folder.add(config, 'pf', 0, 1).onChange((pf: number) => {
audio.style({ pf });
});
folder.add(config, 'dt', 0, 1).onChange((dt: number) => {
audio.style({ dt });
});

return [effect, folder];
}
49 changes: 49 additions & 0 deletions examples/demos/galaxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as lil from 'lil-gui';
import { Audio, Galaxy } from '../../src';

export function render(audio: Audio, gui: lil.GUI) {
const shaderCompilerPath = new URL(
'/public/glsl_wgsl_compiler_bg.wasm',
import.meta.url,
).href;
const effect = new Galaxy(shaderCompilerPath);

const folder = gui.addFolder('style');
const config = {
radius: 0.45,
samples: 0,
noiseAnimation: 0.627,
bulbPower: 0.503,
exposure: 0.182,
powerDelta: 0.9,
gamma: 0.9,
animationSpeed: 1,
};

folder.add(config, 'radius', 0, 1).onChange((radius: number) => {
audio.style({ radius });
});
folder.add(config, 'samples', 0, 1).onChange((samples: number) => {
audio.style({ samples });
});
folder.add(config, 'noiseAnimation', 0, 1).onChange((noiseAnimation: number) => {
audio.style({ noiseAnimation });
});
folder.add(config, 'bulbPower', 0, 1).onChange((bulbPower: number) => {
audio.style({ bulbPower });
});
folder.add(config, 'exposure', 0, 1).onChange((exposure: number) => {
audio.style({ exposure });
});
folder.add(config, 'powerDelta', 0, 1).onChange((powerDelta: number) => {
audio.style({ powerDelta });
});
folder.add(config, 'gamma', 0, 1).onChange((gamma: number) => {
audio.style({ gamma });
});
folder.add(config, 'animationSpeed', 0, 1).onChange((animationSpeed: number) => {
audio.style({ animationSpeed });
});

return [effect, folder];
}
Loading