Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Binary file modified examples/assets/demo/textures/ship-a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/assets/image/ship-a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/audio-basics/audio-buses.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class AudioBusesScene extends Scene {
this.panel.addButton({
label: 'Play sound effect',
onClick: () => {
audio.play(this.sfx, { bus: audio.sound });
audio.play(this.sfx, { bus: audio.sound, volume: 0.5 });
},
});
this.musicVoice = audio.play(this.music, { bus: audio.music, loop: true, volume: 0.6 });
Expand Down
2 changes: 1 addition & 1 deletion examples/audio-basics/audio-buses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class AudioBusesScene extends Scene {
this.panel.addButton({
label: 'Play sound effect',
onClick: () => {
audio.play(this.sfx, { bus: audio.sound });
audio.play(this.sfx, { bus: audio.sound, volume: 0.5 });
},
});

Expand Down
58 changes: 44 additions & 14 deletions examples/audio-fx/convolution-rooms.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Auto-generated from convolution-rooms.ts - edit the .ts source, not this file.
import { Application, Color, FixedResolutionCanvasSizing, Graphics, Scene, Text } from '@codexo/exojs';
import { ConvolutionEffect } from '@codexo/exojs-audio-fx';
import { mountControls } from '@examples/runtime';
import { mountControlPanel, mountControls, trackPressButtons } from '@examples/runtime';
// Every impulse response in the AK-SROOMS set, shortest tail first. The
// duration is what actually decides the character: a few milliseconds only
// colours the sound, ~30-150 ms reads as a cupboard or a shaft, and past
Expand Down Expand Up @@ -78,6 +78,9 @@ class ConvolutionRoomsScene extends Scene {
detail;
tapPrompt;
hud;
panel;
pressButtons = null;
roomControl;
init() {
const { width, height } = this.app;
this.listener = { x: width / 2, y: height / 2 };
Expand Down Expand Up @@ -112,32 +115,49 @@ class ConvolutionRoomsScene extends Scene {
new Text(pad.label, { fillColor: Color.white, fontSize: 20 }).setAnchor(0.5, 0.5).setPosition(pad.x + pad.w / 2, pad.y + pad.h + 26),
);
this.root.addChild(this.gfx, this.label, this.detail, this.tapPrompt, ...materialLabels);
this.app.input.onPointerDown.add(pointer => {
if (!(pointer.buttons & 1)) {
return;
}
for (const pad of this.pads) {
if (pointer.x >= pad.x && pointer.x <= pad.x + pad.w && pointer.y >= pad.y && pointer.y <= pad.y + pad.h) {
this.strike(pad);
return;
}
}
});
this.app.input.onContextMenu.add(() => this.select(this.index + 1));
if (this.app.canvas instanceof HTMLCanvasElement) {
this.pressButtons = trackPressButtons(this.app.canvas);
}
this.app.input.onPointerDown.add(this.onPointerDown);
this.app.input.onContextMenu.add(this.onContextMenu);
this.hud = mountControls({
title: 'Convolution rooms',
controls: [
{ keys: 'Click', action: 'strike a pad, panned to its position' },
{ keys: 'Right-click', action: 'next room' },
{ keys: 'Right-click', action: 'next room (or use the panel)' },
],
status: 'Click or press any key to start…',
hint: 'Same four materials every time — only the room around them changes.',
});
this.panel = mountControlPanel({ title: 'Room' });
this.roomControl = this.panel.addCycle({
label: 'Impulse',
options: ROOMS.map((room, i) => `${i + 1} · ${room.ms} ms`),
onChange: index => this.select(index),
});
this.select(0);
}
// Application input outlives the scene, so both handlers are removed in destroy().
onPointerDown = (pointer, x, y) => {
// Presses are delivered at the next frame, when `pointer.buttons` already
// reads the live state, so a right click released before then looks like a
// main click. The button recorded at press time decides: right-click
// switches rooms instead of striking.
if ((this.pressButtons?.take(pointer.id) ?? 0) !== 0) {
return;
}
for (const pad of this.pads) {
if (x >= pad.x && x <= pad.x + pad.w && y >= pad.y && y <= pad.y + pad.h) {
this.strike(pad);
return;
}
}
};
onContextMenu = () => this.select(this.index + 1);
/** Swap the impulse response. The handle heals in place, so the effect picks it up once decoded. */
select(next) {
this.index = (next + ROOMS.length) % ROOMS.length;
this.roomControl.set(this.index);
const room = ROOMS[this.index];
const ir = this.loader.get(`audio/ir/${room.file}.wav`);
this.label.text = `${this.index + 1} / ${ROOMS.length} — ${character(room.ms)}`;
Expand Down Expand Up @@ -180,6 +200,16 @@ class ConvolutionRoomsScene extends Scene {
}
context.render(this.root);
}
destroy() {
this.app.input.onPointerDown.remove(this.onPointerDown);
this.app.input.onContextMenu.remove(this.onContextMenu);
this.pressButtons?.dispose();
this.app.audio.sound.removeEffect(this.convolution);
this.convolution.destroy();
this.panel.dispose();
this.hud.dispose();
super.destroy();
}
}
const application = new Application({
canvas: {
Expand Down
66 changes: 51 additions & 15 deletions examples/audio-fx/convolution-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import {
Color,
FixedResolutionCanvasSizing,
Graphics,
type Pointer,
type RenderingContext,
Scene,
type Seconds,
type Sound,
Text,
} from '@codexo/exojs';
import { ConvolutionEffect } from '@codexo/exojs-audio-fx';
import { mountControls } from '@examples/runtime';
import { type ControlBinding, mountControlPanel, mountControls, type PressButtonTracker, trackPressButtons } from '@examples/runtime';

// Every impulse response in the AK-SROOMS set, shortest tail first. The
// duration is what actually decides the character: a few milliseconds only
Expand Down Expand Up @@ -108,6 +109,9 @@ class ConvolutionRoomsScene extends Scene {
private detail!: Text;
private tapPrompt!: Text;
private hud!: ReturnType<typeof mountControls>;
private panel!: ReturnType<typeof mountControlPanel>;
private pressButtons: PressButtonTracker | null = null;
private roomControl!: ControlBinding<number>;

override init(): void {
const { width, height } = this.app;
Expand Down Expand Up @@ -151,37 +155,58 @@ class ConvolutionRoomsScene extends Scene {

this.root.addChild(this.gfx, this.label, this.detail, this.tapPrompt, ...materialLabels);

this.app.input.onPointerDown.add(pointer => {
if (!(pointer.buttons & 1)) {
return;
}

for (const pad of this.pads) {
if (pointer.x >= pad.x && pointer.x <= pad.x + pad.w && pointer.y >= pad.y && pointer.y <= pad.y + pad.h) {
this.strike(pad);
if (this.app.canvas instanceof HTMLCanvasElement) {
this.pressButtons = trackPressButtons(this.app.canvas);
}

return;
}
}
});
this.app.input.onContextMenu.add(() => this.select(this.index + 1));
this.app.input.onPointerDown.add(this.onPointerDown);
this.app.input.onContextMenu.add(this.onContextMenu);

this.hud = mountControls({
title: 'Convolution rooms',
controls: [
{ keys: 'Click', action: 'strike a pad, panned to its position' },
{ keys: 'Right-click', action: 'next room' },
{ keys: 'Right-click', action: 'next room (or use the panel)' },
],
status: 'Click or press any key to start…',
hint: 'Same four materials every time — only the room around them changes.',
});

this.panel = mountControlPanel({ title: 'Room' });
this.roomControl = this.panel.addCycle({
label: 'Impulse',
options: ROOMS.map((room, i) => `${i + 1} · ${room.ms} ms`),
onChange: index => this.select(index),
});

this.select(0);
}

// Application input outlives the scene, so both handlers are removed in destroy().
private readonly onPointerDown = (pointer: Pointer, x: number, y: number): void => {
// Presses are delivered at the next frame, when `pointer.buttons` already
// reads the live state, so a right click released before then looks like a
// main click. The button recorded at press time decides: right-click
// switches rooms instead of striking.
if ((this.pressButtons?.take(pointer.id) ?? 0) !== 0) {
return;
}

for (const pad of this.pads) {
if (x >= pad.x && x <= pad.x + pad.w && y >= pad.y && y <= pad.y + pad.h) {
this.strike(pad);

return;
}
}
};

private readonly onContextMenu = (): void => this.select(this.index + 1);

/** Swap the impulse response. The handle heals in place, so the effect picks it up once decoded. */
private select(next: number): void {
this.index = (next + ROOMS.length) % ROOMS.length;
this.roomControl.set(this.index);

const room = ROOMS[this.index]!;
const ir = this.loader.get(`audio/ir/${room.file}.wav`);
Expand Down Expand Up @@ -235,6 +260,17 @@ class ConvolutionRoomsScene extends Scene {

context.render(this.root);
}

override destroy(): void {
this.app.input.onPointerDown.remove(this.onPointerDown);
this.app.input.onContextMenu.remove(this.onContextMenu);
this.pressButtons?.dispose();
this.app.audio.sound.removeEffect(this.convolution);
this.convolution.destroy();
this.panel.dispose();
this.hud.dispose();
super.destroy();
}
}

const application = new Application({
Expand Down
2 changes: 1 addition & 1 deletion examples/audio-fx/ducking.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class DuckingScene extends Scene {
if (app.audio.locked) {
return;
}
app.audio.play(this.voice, { bus: this.voiceBus });
app.audio.play(this.voice, { bus: this.voiceBus, volume: 0.4 });
this.hud.setStatus('Voice playing — music ducked');
});
// Core defers playback until the AudioContext unlocks on the first
Expand Down
2 changes: 1 addition & 1 deletion examples/audio-fx/ducking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class DuckingScene extends Scene {
return;
}

app.audio.play(this.voice, { bus: this.voiceBus });
app.audio.play(this.voice, { bus: this.voiceBus, volume: 0.4 });
this.hud.setStatus('Voice playing — music ducked');
});

Expand Down
8 changes: 7 additions & 1 deletion examples/audio-fx/effect-chains.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const CHAINS = [
{
name: 'Telephone',
note: 'Band-limit the sound before adding saturation.',
trim: 0.14,
stages: [
{ name: 'Highpass', create: () => new HighpassFilter({ frequency: 800 }) },
{ name: 'Lowpass', create: () => new LowpassFilter({ frequency: 2500 }) },
Expand All @@ -25,6 +26,7 @@ const CHAINS = [
{
name: 'Underwater',
note: 'Remove highs, then add a slow chorus and room tail.',
trim: 2,
stages: [
{ name: 'Lowpass', create: () => new LowpassFilter({ frequency: 420, resonance: 2 }) },
{ name: 'Chorus', create: () => new ChorusEffect({ delayMs: 25, depthMs: 8, rateHz: 0.6, wet: 0.6 }) },
Expand All @@ -34,6 +36,7 @@ const CHAINS = [
{
name: 'Arcade',
note: 'Quantize the samples, then warm the edges with distortion.',
trim: 0.35,
stages: [
{ name: 'BitCrusher', create: () => new BitCrusherEffect({ bits: 5, frequencyReduction: 0.35 }) },
{ name: 'Distortion', create: () => new DistortionEffect({ drive: 0.25, wet: 0.6 }) },
Expand All @@ -42,6 +45,7 @@ const CHAINS = [
{
name: 'Space Echo',
note: 'Stereo repeats follow a short room reflection.',
trim: 0.35,
stages: [
{ name: 'Reverb', create: () => new ReverbEffect({ decay: 1.5, wet: 0.25 }) },
{ name: 'PingPongDelay', create: () => new PingPongDelayEffect({ delayTime: 0.32, feedback: 0.5, wet: 0.45 }) },
Expand Down Expand Up @@ -93,7 +97,7 @@ class EffectChainsScene extends Scene {
label: 'Play impact',
onClick: () => {
if (!audio.locked) {
audio.play(this.impact);
audio.play(this.impact, { volume: 0.4 });
}
},
});
Expand Down Expand Up @@ -126,6 +130,7 @@ class EffectChainsScene extends Scene {
}
});
this.hud.setHint(chain.note);
master.volume = this.bypass ? 1 : chain.trim;
if (this.bypass) {
this.hud.setStatus(`${chain.name} bypassed; dry signal.`);
return;
Expand Down Expand Up @@ -178,6 +183,7 @@ class EffectChainsScene extends Scene {
effect.destroy();
}
this.active = [];
this.app.audio.master.volume = 1;
this.analyser?.destroy();
this.panel?.dispose();
this.hud?.dispose();
Expand Down
13 changes: 12 additions & 1 deletion examples/audio-fx/effect-chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import { mountControlPanel, mountControls } from '@examples/runtime';
interface Chain {
name: string;
note: string;
// Output gain that brings the chain back to roughly the dry loudness.
// Without it the louder chain always sounds "better", so an A/B comparison
// judges level rather than the effect.
trim: number;
stages: Array<{ name: string; create(): AudioEffect }>;
}

const CHAINS: Chain[] = [
{
name: 'Telephone',
note: 'Band-limit the sound before adding saturation.',
trim: 0.14,
stages: [
{ name: 'Highpass', create: () => new HighpassFilter({ frequency: 800 }) },
{ name: 'Lowpass', create: () => new LowpassFilter({ frequency: 2500 }) },
Expand All @@ -36,6 +41,7 @@ const CHAINS: Chain[] = [
{
name: 'Underwater',
note: 'Remove highs, then add a slow chorus and room tail.',
trim: 2,
stages: [
{ name: 'Lowpass', create: () => new LowpassFilter({ frequency: 420, resonance: 2 }) },
{ name: 'Chorus', create: () => new ChorusEffect({ delayMs: 25, depthMs: 8, rateHz: 0.6, wet: 0.6 }) },
Expand All @@ -45,6 +51,7 @@ const CHAINS: Chain[] = [
{
name: 'Arcade',
note: 'Quantize the samples, then warm the edges with distortion.',
trim: 0.35,
stages: [
{ name: 'BitCrusher', create: () => new BitCrusherEffect({ bits: 5, frequencyReduction: 0.35 }) },
{ name: 'Distortion', create: () => new DistortionEffect({ drive: 0.25, wet: 0.6 }) },
Expand All @@ -53,6 +60,7 @@ const CHAINS: Chain[] = [
{
name: 'Space Echo',
note: 'Stereo repeats follow a short room reflection.',
trim: 0.35,
stages: [
{ name: 'Reverb', create: () => new ReverbEffect({ decay: 1.5, wet: 0.25 }) },
{ name: 'PingPongDelay', create: () => new PingPongDelayEffect({ delayTime: 0.32, feedback: 0.5, wet: 0.45 }) },
Expand Down Expand Up @@ -107,7 +115,7 @@ class EffectChainsScene extends Scene {
label: 'Play impact',
onClick: () => {
if (!audio.locked) {
audio.play(this.impact);
audio.play(this.impact, { volume: 0.4 });
}
},
});
Expand Down Expand Up @@ -147,6 +155,8 @@ class EffectChainsScene extends Scene {
});
this.hud.setHint(chain.note);

master.volume = this.bypass ? 1 : chain.trim;

if (this.bypass) {
this.hud.setStatus(`${chain.name} bypassed; dry signal.`);

Expand Down Expand Up @@ -209,6 +219,7 @@ class EffectChainsScene extends Scene {
}

this.active = [];
this.app.audio.master.volume = 1;
this.analyser?.destroy();
this.panel?.dispose();
this.hud?.dispose();
Expand Down
2 changes: 1 addition & 1 deletion examples/audio-fx/reverb-and-delay.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ReverbAndDelayScene extends Scene {
label: 'Play impact',
onClick: () => {
if (!audio.locked) {
audio.play(this.sound, { bus: audio.sound });
audio.play(this.sound, { bus: audio.sound, volume: 0.4 });
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion examples/audio-fx/reverb-and-delay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ReverbAndDelayScene extends Scene {
label: 'Play impact',
onClick: () => {
if (!audio.locked) {
audio.play(this.sound, { bus: audio.sound });
audio.play(this.sound, { bus: audio.sound, volume: 0.4 });
}
},
});
Expand Down
Loading
Loading