This commit is contained in:
parent
2347ecd201
commit
10a81ba474
45 changed files with 1974 additions and 490 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { appConfig } from '../config';
|
||||
import type { GardenAudioEngineConfig } from '../config';
|
||||
import { clamp, clamp01 } from '../utils/clamp';
|
||||
import { VibePreset } from '../vibes';
|
||||
import { GardenAudioConfig } from './garden-audio-config';
|
||||
import { GardenAudioEnergy } from './garden-audio-energy';
|
||||
import { GardenAudioGestureState } from './garden-audio-gesture-state';
|
||||
import { GardenAudioGraph } from './garden-audio-graph';
|
||||
import { GardenAudioStrokeMetrics, getStrokeMetrics } from './garden-audio-input';
|
||||
import { getStrokeMetrics } from './garden-audio-input';
|
||||
import { getVibeProfile, normalizeColorIndex } from './garden-audio-music';
|
||||
import type {
|
||||
GardenAudioColorIndex,
|
||||
|
|
@ -29,6 +30,7 @@ export class GardenAudio {
|
|||
private readonly piano: PianoSampler;
|
||||
private readonly noise: NoiseBurstPlayer;
|
||||
private readonly energy: GardenAudioEnergy;
|
||||
private readonly gestureState: GardenAudioGestureState;
|
||||
private readonly pianoEngine: GenerativePianoEngine;
|
||||
|
||||
private currentVibeId: string | null = null;
|
||||
|
|
@ -41,12 +43,22 @@ export class GardenAudio {
|
|||
private lastEraserAt = Number.NEGATIVE_INFINITY;
|
||||
private lastVibeStingerAt = Number.NEGATIVE_INFINITY;
|
||||
|
||||
public constructor(private readonly config: GardenAudioConfig) {
|
||||
this.graph = new GardenAudioGraph(config);
|
||||
this.piano = new PianoSampler(config, this.graph);
|
||||
this.noise = new NoiseBurstPlayer(this.graph);
|
||||
this.energy = new GardenAudioEnergy();
|
||||
this.pianoEngine = new GenerativePianoEngine(config, (note) => this.piano.play(note));
|
||||
public constructor(
|
||||
private readonly config: GardenAudioConfig,
|
||||
private readonly engineConfig: GardenAudioEngineConfig,
|
||||
private readonly maxMirrorSegmentCount: number
|
||||
) {
|
||||
this.graph = new GardenAudioGraph(config, engineConfig);
|
||||
this.piano = new PianoSampler(config, engineConfig, this.graph);
|
||||
this.noise = new NoiseBurstPlayer(engineConfig, this.graph);
|
||||
this.energy = new GardenAudioEnergy(engineConfig);
|
||||
this.gestureState = new GardenAudioGestureState(
|
||||
config.rhythm.speedForFullEnergyPixelsPerSecond,
|
||||
engineConfig.input
|
||||
);
|
||||
this.pianoEngine = new GenerativePianoEngine(config, engineConfig, (note) =>
|
||||
this.piano.play(note)
|
||||
);
|
||||
}
|
||||
|
||||
public start(vibe: VibePreset, options: GardenAudioStartOptions = {}): void {
|
||||
|
|
@ -76,7 +88,7 @@ export class GardenAudio {
|
|||
this.graph.setMasterGain(
|
||||
this.config.masterVolume,
|
||||
options.userGesture === true
|
||||
? appConfig.audioEngine.muteRampSeconds
|
||||
? this.engineConfig.muteRampSeconds
|
||||
: this.config.fadeInSeconds
|
||||
);
|
||||
|
||||
|
|
@ -110,8 +122,8 @@ export class GardenAudio {
|
|||
public setMuted(isMuted: boolean): void {
|
||||
this.isMuted = isMuted;
|
||||
this.graph.setMasterGain(
|
||||
isMuted ? appConfig.audioEngine.muteGain : this.config.masterVolume,
|
||||
isMuted ? appConfig.audioEngine.muteRampSeconds : this.config.fadeInSeconds
|
||||
isMuted ? this.engineConfig.muteGain : this.config.masterVolume,
|
||||
isMuted ? this.engineConfig.muteRampSeconds : this.config.fadeInSeconds
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -122,11 +134,13 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
this.isGestureActive = true;
|
||||
this.gestureState.beginGesture();
|
||||
this.energy.beginGesture(context.currentTime);
|
||||
this.pianoEngine.beginGesture();
|
||||
}
|
||||
|
||||
public endGesture(): void {
|
||||
this.gestureState.endGesture();
|
||||
this.isGestureActive = false;
|
||||
this.energy.endGesture();
|
||||
this.pianoEngine.endGesture();
|
||||
|
|
@ -146,6 +160,13 @@ export class GardenAudio {
|
|||
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);
|
||||
const frame = this.gestureState.recordTouchDown({
|
||||
touch,
|
||||
colorIndex: this.selectedColorIndex,
|
||||
mirrorAmount,
|
||||
pressure,
|
||||
strength,
|
||||
});
|
||||
|
||||
this.energy.recordStroke(strength, context.currentTime);
|
||||
this.pianoEngine.recordTouchDown({
|
||||
|
|
@ -154,6 +175,13 @@ export class GardenAudio {
|
|||
strength,
|
||||
selectedColorIndex: this.selectedColorIndex,
|
||||
mirrorAmount,
|
||||
panBias: frame.panBias,
|
||||
registerBias: frame.registerBias,
|
||||
brightnessBias: frame.brightnessBias,
|
||||
contour: frame.contour,
|
||||
pressureAmount: frame.pressure,
|
||||
pressureDelta: frame.pressureDelta,
|
||||
maniaAmount: frame.maniaAmount,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +225,8 @@ export class GardenAudio {
|
|||
const metrics = getStrokeMetrics(
|
||||
stroke,
|
||||
this.config.rhythm.speedForFullEnergyPixelsPerSecond,
|
||||
this.config.input.pressureFallback
|
||||
this.config.input.pressureFallback,
|
||||
this.engineConfig.input
|
||||
);
|
||||
const now = context.currentTime;
|
||||
|
||||
|
|
@ -210,7 +239,8 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
const mirrorAmount = this.getMirrorAmount(stroke.mirrorSegmentCount ?? 1);
|
||||
const strokeEnergy = this.getStrokeMusicActivity(stroke, metrics, mirrorAmount);
|
||||
const frame = this.gestureState.recordStroke({ stroke, metrics, mirrorAmount });
|
||||
const strokeEnergy = frame.activity;
|
||||
this.energy.recordStroke(strokeEnergy, now);
|
||||
this.pianoEngine.recordStroke({
|
||||
vibe: stroke.vibe,
|
||||
|
|
@ -218,6 +248,13 @@ export class GardenAudio {
|
|||
activity: strokeEnergy,
|
||||
selectedColorIndex: this.selectedColorIndex,
|
||||
mirrorAmount,
|
||||
panBias: frame.panBias,
|
||||
registerBias: frame.registerBias,
|
||||
brightnessBias: frame.brightnessBias,
|
||||
contour: frame.contour,
|
||||
pressureAmount: frame.pressure,
|
||||
pressureDelta: frame.pressureDelta,
|
||||
maniaAmount: frame.maniaAmount,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +264,7 @@ export class GardenAudio {
|
|||
|
||||
this.piano.reset();
|
||||
this.energy.reset();
|
||||
this.gestureState.reset();
|
||||
this.pianoEngine.reset();
|
||||
this.currentVibeId = null;
|
||||
this.hasStarted = false;
|
||||
|
|
@ -246,7 +284,7 @@ export class GardenAudio {
|
|||
const now = context.currentTime;
|
||||
if (
|
||||
now - this.lastVibeStingerAt <
|
||||
appConfig.audioEngine.vibeChangeStingerMinIntervalSeconds
|
||||
this.engineConfig.vibeChangeStingerMinIntervalSeconds
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -266,10 +304,10 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
const sizeAmount = clamp01(
|
||||
(stroke.eraserSizePixels ?? appConfig.audioEngine.eraser.defaultSizePixels) /
|
||||
(stroke.eraserSizePixels ?? this.engineConfig.eraser.defaultSizePixels) /
|
||||
Math.max(
|
||||
1,
|
||||
stroke.canvasSize[0] * appConfig.audioEngine.eraser.canvasWidthRatioForFullSize
|
||||
stroke.canvasSize[0] * this.engineConfig.eraser.canvasWidthRatioForFullSize
|
||||
)
|
||||
);
|
||||
const x = clamp01(stroke.to[0] / Math.max(1, stroke.canvasSize[0]));
|
||||
|
|
@ -277,22 +315,22 @@ export class GardenAudio {
|
|||
this.config.eraser.filterMinHz +
|
||||
(this.config.eraser.filterMaxHz - this.config.eraser.filterMinHz) *
|
||||
clamp01(
|
||||
speedAmount * appConfig.audioEngine.eraser.filterSpeedWeight +
|
||||
pressure * appConfig.audioEngine.eraser.filterPressureWeight +
|
||||
sizeAmount * appConfig.audioEngine.eraser.filterSizeWeight
|
||||
speedAmount * this.engineConfig.eraser.filterSpeedWeight +
|
||||
pressure * this.engineConfig.eraser.filterPressureWeight +
|
||||
sizeAmount * this.engineConfig.eraser.filterSizeWeight
|
||||
);
|
||||
|
||||
if (now - this.lastEraserAt >= this.config.eraser.minIntervalSeconds) {
|
||||
this.lastEraserAt = now;
|
||||
this.noise.play({
|
||||
startTime: now,
|
||||
durationSeconds: appConfig.audioEngine.eraser.durationSeconds,
|
||||
durationSeconds: this.engineConfig.eraser.durationSeconds,
|
||||
gain:
|
||||
this.config.eraser.noiseGain *
|
||||
(appConfig.audioEngine.eraser.gainBase +
|
||||
speedAmount * appConfig.audioEngine.eraser.gainSpeedWeight +
|
||||
pressure * appConfig.audioEngine.eraser.gainPressureWeight +
|
||||
sizeAmount * appConfig.audioEngine.eraser.gainSizeWeight),
|
||||
(this.engineConfig.eraser.gainBase +
|
||||
speedAmount * this.engineConfig.eraser.gainSpeedWeight +
|
||||
pressure * this.engineConfig.eraser.gainPressureWeight +
|
||||
sizeAmount * this.engineConfig.eraser.gainSizeWeight),
|
||||
filterHz,
|
||||
pan: clamp(x * 2 - 1, -1, 1),
|
||||
});
|
||||
|
|
@ -307,7 +345,7 @@ export class GardenAudio {
|
|||
|
||||
const profile = getVibeProfile(this.config, snapshot.vibe);
|
||||
const activity = snapshot.isErasing
|
||||
? appConfig.audioEngine.delay.erasingActivity
|
||||
? this.engineConfig.delay.erasingActivity
|
||||
: this.energy.getLevel();
|
||||
this.graph.updateDelay(profile, activity);
|
||||
}
|
||||
|
|
@ -323,7 +361,7 @@ export class GardenAudio {
|
|||
}
|
||||
|
||||
private getMirrorAmount(mirrorSegmentCount: number): number {
|
||||
const maxMirrorSegmentCount = Math.max(1, appConfig.simulation.maxMirrorSegmentCount);
|
||||
const maxMirrorSegmentCount = Math.max(1, this.maxMirrorSegmentCount);
|
||||
const segmentCount = clamp(
|
||||
Number.isFinite(mirrorSegmentCount) ? mirrorSegmentCount : 1,
|
||||
1,
|
||||
|
|
@ -337,44 +375,16 @@ export class GardenAudio {
|
|||
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)
|
||||
? Math.max(
|
||||
this.engineConfig.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