Add WIP sound generation
This commit is contained in:
parent
cb1df6f29e
commit
34ac200437
10 changed files with 1542 additions and 0 deletions
176
src/audio/garden-audio-energy.ts
Normal file
176
src/audio/garden-audio-energy.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { clamp01 } from '../utils/clamp';
|
||||
import { GardenAudioConfig } from './garden-audio-config';
|
||||
|
||||
interface GardenGestureState {
|
||||
startedAt: number;
|
||||
lastAt: number;
|
||||
distancePixels: number;
|
||||
peakEnergy: number;
|
||||
isErasing: boolean;
|
||||
}
|
||||
|
||||
interface GestureTail {
|
||||
startedAt: number;
|
||||
durationSeconds: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
type GardenAudioRhythmConfig = GardenAudioConfig['rhythm'];
|
||||
|
||||
export class GardenAudioEnergy {
|
||||
private isGestureActive = false;
|
||||
private energy = 0;
|
||||
private targetEnergy = 0;
|
||||
private lastEnergyUpdateAt = 0;
|
||||
private currentGesture: GardenGestureState | null = null;
|
||||
private gestureTails: Array<GestureTail> = [];
|
||||
|
||||
public constructor(private readonly rhythm: GardenAudioRhythmConfig) {}
|
||||
|
||||
public beginGesture(now: number): void {
|
||||
this.isGestureActive = true;
|
||||
this.currentGesture = createGesture(now, false);
|
||||
}
|
||||
|
||||
public endGesture(now: number): void {
|
||||
if (this.currentGesture && !this.currentGesture.isErasing) {
|
||||
this.addGestureTail(this.currentGesture, now);
|
||||
}
|
||||
|
||||
this.isGestureActive = false;
|
||||
this.currentGesture = null;
|
||||
}
|
||||
|
||||
public recordStroke(distancePixels: number, strokeEnergy: number, now: number): void {
|
||||
if (!this.currentGesture || this.currentGesture.isErasing) {
|
||||
this.currentGesture = createGesture(now, false);
|
||||
}
|
||||
|
||||
this.currentGesture.lastAt = now;
|
||||
this.currentGesture.distancePixels += distancePixels;
|
||||
this.currentGesture.peakEnergy = Math.max(
|
||||
this.currentGesture.peakEnergy,
|
||||
strokeEnergy
|
||||
);
|
||||
}
|
||||
|
||||
public recordEraserStroke(now: number): void {
|
||||
this.currentGesture = this.currentGesture ?? createGesture(now, true);
|
||||
this.currentGesture.isErasing = true;
|
||||
}
|
||||
|
||||
public raiseTarget(strokeEnergy: number): void {
|
||||
this.targetEnergy = Math.max(this.targetEnergy, strokeEnergy);
|
||||
}
|
||||
|
||||
public silence(): void {
|
||||
this.targetEnergy = 0;
|
||||
this.gestureTails = [];
|
||||
}
|
||||
|
||||
public update(now: number): void {
|
||||
if (this.lastEnergyUpdateAt <= 0) {
|
||||
this.lastEnergyUpdateAt = now;
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.max(0, now - this.lastEnergyUpdateAt);
|
||||
this.lastEnergyUpdateAt = now;
|
||||
this.targetEnergy *= Math.exp(-elapsedSeconds / 0.75);
|
||||
this.trimGestureTails(now);
|
||||
|
||||
const activeGestureFloor =
|
||||
this.isGestureActive && this.currentGesture && !this.currentGesture.isErasing
|
||||
? 0.04 + this.getGestureAmount(this.currentGesture, now) * 0.3
|
||||
: 0;
|
||||
const target = Math.max(activeGestureFloor, this.targetEnergy);
|
||||
const timeConstant = target > this.energy ? 0.08 : 0.55;
|
||||
const amount = 1 - Math.exp(-elapsedSeconds / timeConstant);
|
||||
this.energy += (target - this.energy) * amount;
|
||||
}
|
||||
|
||||
public getActivityAt(time: number): number {
|
||||
const activeGesture =
|
||||
this.isGestureActive && this.currentGesture && !this.currentGesture.isErasing
|
||||
? 0.08 + this.getGestureAmount(this.currentGesture, time) * 0.34
|
||||
: 0;
|
||||
|
||||
return clamp01(
|
||||
this.energy * 0.58 + this.getTailActivityAt(time) * 0.72 + activeGesture
|
||||
);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.isGestureActive = false;
|
||||
this.energy = 0;
|
||||
this.targetEnergy = 0;
|
||||
this.lastEnergyUpdateAt = 0;
|
||||
this.currentGesture = null;
|
||||
this.gestureTails = [];
|
||||
}
|
||||
|
||||
private addGestureTail(gesture: GardenGestureState, now: number): void {
|
||||
const durationAmount = clamp01(
|
||||
(gesture.lastAt - gesture.startedAt) / this.rhythm.tailDurationForMaxSeconds
|
||||
);
|
||||
const distanceAmount = clamp01(
|
||||
gesture.distancePixels / this.rhythm.tailDistanceForMaxPixels
|
||||
);
|
||||
const gestureAmount = Math.max(distanceAmount, durationAmount * 0.9);
|
||||
const tailAmount = clamp01(gestureAmount * (0.74 + gesture.peakEnergy * 0.26));
|
||||
|
||||
if (tailAmount <= 0.015) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gestureTails.push({
|
||||
startedAt: now,
|
||||
durationSeconds:
|
||||
this.rhythm.minTailSeconds +
|
||||
(this.rhythm.maxTailSeconds - this.rhythm.minTailSeconds) * tailAmount,
|
||||
level: (0.05 + tailAmount * 0.5) * (0.72 + gesture.peakEnergy * 0.28),
|
||||
});
|
||||
this.trimGestureTails(now);
|
||||
}
|
||||
|
||||
private getGestureAmount(gesture: GardenGestureState, now: number): number {
|
||||
const durationAmount = clamp01(
|
||||
(now - gesture.startedAt) / this.rhythm.tailDurationForMaxSeconds
|
||||
);
|
||||
const distanceAmount = clamp01(
|
||||
gesture.distancePixels / this.rhythm.tailDistanceForMaxPixels
|
||||
);
|
||||
|
||||
return clamp01(
|
||||
distanceAmount * 0.62 + durationAmount * 0.24 + gesture.peakEnergy * 0.14
|
||||
);
|
||||
}
|
||||
|
||||
private getTailActivityAt(time: number): number {
|
||||
const decayPower = Math.max(0.2, this.rhythm.tailDecayPower);
|
||||
|
||||
return this.gestureTails.reduce((activity, tail) => {
|
||||
const elapsedSeconds = time - tail.startedAt;
|
||||
if (elapsedSeconds < 0 || elapsedSeconds >= tail.durationSeconds) {
|
||||
return activity;
|
||||
}
|
||||
|
||||
const remaining = clamp01(1 - elapsedSeconds / tail.durationSeconds);
|
||||
return Math.max(activity, tail.level * Math.pow(remaining, decayPower));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private trimGestureTails(now: number): void {
|
||||
this.gestureTails = this.gestureTails.filter(
|
||||
(tail) => now - tail.startedAt < tail.durationSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const createGesture = (now: number, isErasing: boolean): GardenGestureState => ({
|
||||
startedAt: now,
|
||||
lastAt: now,
|
||||
distancePixels: 0,
|
||||
peakEnergy: 0,
|
||||
isErasing,
|
||||
});
|
||||
Loading…
Reference in a new issue