.
Some checks failed
Deploy to Pages / build (pull_request) Failing after 3m15s

This commit is contained in:
Andras Schmelczer 2026-05-13 22:13:15 +01:00
commit 2347ecd201
71 changed files with 3779 additions and 1586 deletions

View file

@ -4,13 +4,14 @@ import { VibePreset } from '../vibes';
import { GardenAudioConfig } from './garden-audio-config';
import { GardenAudioEnergy } from './garden-audio-energy';
import { GardenAudioGraph } from './garden-audio-graph';
import { getStrokeMetrics } from './garden-audio-input';
import { GardenAudioStrokeMetrics, getStrokeMetrics } from './garden-audio-input';
import { getVibeProfile, normalizeColorIndex } from './garden-audio-music';
import type {
GardenAudioColorIndex,
GardenAudioSnapshot,
GardenAudioStartOptions,
GardenAudioStroke,
GardenAudioTouchDown,
} from './garden-audio-types';
import { GenerativePianoEngine } from './generative-piano';
import { NoiseBurstPlayer } from './noise-burst-player';
@ -20,6 +21,7 @@ export type {
GardenAudioSnapshot,
GardenAudioStartOptions,
GardenAudioStroke,
GardenAudioTouchDown,
} from './garden-audio-types';
export class GardenAudio {
@ -35,6 +37,7 @@ export class GardenAudio {
private isMuted = false;
private isGestureActive = false;
private selectedColorIndex: GardenAudioColorIndex = 0;
private hasQueuedPianoLoad = false;
private lastEraserAt = Number.NEGATIVE_INFINITY;
private lastVibeStingerAt = Number.NEGATIVE_INFINITY;
@ -70,9 +73,21 @@ export class GardenAudio {
this.hasStarted = true;
this.applyVibe(vibe);
this.pianoEngine.prime(context.currentTime);
this.graph.setMasterGain(this.config.masterVolume, this.config.fadeInSeconds);
this.graph.setMasterGain(
this.config.masterVolume,
options.userGesture === true
? appConfig.audioEngine.muteRampSeconds
: this.config.fadeInSeconds
);
void this.piano.load(context);
if (!this.hasQueuedPianoLoad) {
this.hasQueuedPianoLoad = true;
void this.piano.load(context).then(() => {
if (this.graph.context === context && !this.isDestroyed) {
this.pianoEngine.cue(context.currentTime);
}
});
}
}
public changeVibe(vibe: VibePreset, options: GardenAudioStartOptions = {}): void {
@ -108,19 +123,38 @@ export class GardenAudio {
this.isGestureActive = true;
this.energy.beginGesture(context.currentTime);
this.pianoEngine.beginGesture(context.currentTime);
this.pianoEngine.beginGesture();
}
public endGesture(): void {
const context = this.graph.context;
this.isGestureActive = false;
this.energy.endGesture();
this.pianoEngine.endGesture();
if (!context) {
}
public touchDown(touch: GardenAudioTouchDown): void {
if (this.isDestroyed || this.isMuted) {
return;
}
this.piano.fadeActive(context.currentTime, appConfig.audioEngine.gestureFadeSeconds);
const context = this.graph.context;
if (!context || !this.isGestureActive) {
return;
}
this.selectedColorIndex = normalizeColorIndex(touch.colorIndex);
const mirrorAmount = this.getMirrorAmount(touch.mirrorSegmentCount ?? 1);
const pressure = this.getTouchPressure(touch.pressure, touch.pointerType);
const strength = clamp01(0.36 + pressure * 0.34 + mirrorAmount * 0.22);
this.energy.recordStroke(strength, context.currentTime);
this.pianoEngine.recordTouchDown({
vibe: touch.vibe,
now: context.currentTime,
strength,
selectedColorIndex: this.selectedColorIndex,
mirrorAmount,
});
}
public update(snapshot: GardenAudioSnapshot): void {
@ -135,22 +169,14 @@ export class GardenAudio {
if (snapshot.isErasing) {
this.energy.silence();
this.piano.fadeActive(
context.currentTime,
appConfig.audioEngine.gestureFadeSeconds
);
this.updateDelay(snapshot);
return;
}
if (this.isGestureActive) {
this.pianoEngine.renderLookahead({
vibe: snapshot.vibe,
now: context.currentTime,
activity: this.energy.getActivity(),
selectedColorIndex: this.selectedColorIndex,
});
}
this.pianoEngine.renderLookahead({
vibe: snapshot.vibe,
now: context.currentTime,
activity: snapshot.isErasing ? 0 : this.energy.getLevel(),
selectedColorIndex: this.selectedColorIndex,
});
this.updateDelay(snapshot);
}
@ -183,9 +209,16 @@ export class GardenAudio {
return;
}
const strokeEnergy = metrics.effectiveEnergy;
const mirrorAmount = this.getMirrorAmount(stroke.mirrorSegmentCount ?? 1);
const strokeEnergy = this.getStrokeMusicActivity(stroke, metrics, mirrorAmount);
this.energy.recordStroke(strokeEnergy, now);
this.pianoEngine.wake(now);
this.pianoEngine.recordStroke({
vibe: stroke.vibe,
now,
activity: strokeEnergy,
selectedColorIndex: this.selectedColorIndex,
mirrorAmount,
});
}
public async destroy(): Promise<void> {
@ -199,6 +232,7 @@ export class GardenAudio {
this.hasStarted = false;
this.isGestureActive = false;
this.selectedColorIndex = 0;
this.hasQueuedPianoLoad = false;
this.lastEraserAt = Number.NEGATIVE_INFINITY;
this.lastVibeStingerAt = Number.NEGATIVE_INFINITY;
}
@ -285,5 +319,62 @@ export class GardenAudio {
this.currentVibeId = vibe.id;
this.graph.applyDelayProfile(getVibeProfile(this.config, vibe));
this.pianoEngine.cue(this.graph.context.currentTime);
}
private getMirrorAmount(mirrorSegmentCount: number): number {
const maxMirrorSegmentCount = Math.max(1, appConfig.simulation.maxMirrorSegmentCount);
const segmentCount = clamp(
Number.isFinite(mirrorSegmentCount) ? mirrorSegmentCount : 1,
1,
maxMirrorSegmentCount
);
if (maxMirrorSegmentCount <= 1) {
return 0;
}
return clamp01((segmentCount - 1) / (maxMirrorSegmentCount - 1));
}
private getStrokeMusicActivity(
stroke: GardenAudioStroke,
metrics: GardenAudioStrokeMetrics,
mirrorAmount: number
): number {
const speedRatio =
(stroke.velocityPixelsPerSecond ?? 0) /
Math.max(1, this.config.rhythm.speedForFullEnergyPixelsPerSecond);
const speedDrive = smoothstep(0.35, 1.1, speedRatio);
const speedOverdrive = smoothstep(1.15, 1.8, speedRatio);
const distanceDrive = smoothstep(10, 90, metrics.distancePixels);
const baseStroke = clamp01(
0.08 + speedDrive * 0.5 + metrics.pressure * 0.2 + distanceDrive * 0.22
);
const mirrorWild = smoothstep(0.45, 0.9, mirrorAmount);
const maniaDrive = speedOverdrive * smoothstep(0.62, 0.82, baseStroke);
const maniaBoost = maniaDrive * (0.18 + mirrorWild * 0.62);
return clamp01(
baseStroke * (0.68 + mirrorAmount * 0.3) +
0.025 +
mirrorAmount * 0.045 +
maniaBoost
);
}
private getTouchPressure(pressure: number | undefined, pointerType?: string): number {
if (pressure !== undefined && Number.isFinite(pressure) && pressure > 0) {
return clamp01(pressure);
}
return pointerType === 'pen'
? Math.max(appConfig.audioEngine.input.penMinPressure, this.config.input.pressureFallback)
: this.config.input.pressureFallback;
}
}
const smoothstep = (edge0: number, edge1: number, value: number): number => {
const amount = clamp01((value - edge0) / (edge1 - edge0));
return amount * amount * (3 - 2 * amount);
};