This commit is contained in:
parent
39b0160064
commit
2347ecd201
71 changed files with 3779 additions and 1586 deletions
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue