import { appConfig } from '../config'; import { clamp01 } from '../utils/clamp'; const STROKE_IMMEDIATE_ACTIVITY_SCALE = 0.85; export class GardenAudioEnergy { private isGestureActive = false; private energy = 0; private targetEnergy = 0; private lastEnergyUpdateAt = 0; public beginGesture(now: number): void { this.isGestureActive = true; this.lastEnergyUpdateAt = now; } public endGesture(): void { this.isGestureActive = false; this.targetEnergy = 0; } public recordStroke(strokeEnergy: number, now: number): void { const energy = clamp01(strokeEnergy); this.targetEnergy = Math.max(this.targetEnergy, energy); if (this.isGestureActive) { this.energy = Math.max(this.energy, energy * STROKE_IMMEDIATE_ACTIVITY_SCALE); } this.lastEnergyUpdateAt ||= now; } public recordEraserStroke(): void { this.targetEnergy = 0; } public silence(): void { this.targetEnergy = 0; this.energy = 0; } 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 / appConfig.audioEngine.energy.strokeDecaySeconds ); const target = this.isGestureActive ? this.targetEnergy : 0; let timeConstant = appConfig.audioEngine.energy.decaySeconds; if (!this.isGestureActive) { timeConstant = appConfig.audioEngine.energy.releaseSeconds; } else if (target > this.energy) { timeConstant = appConfig.audioEngine.energy.attackSeconds; } const amount = 1 - Math.exp(-elapsedSeconds / timeConstant); this.energy += (target - this.energy) * amount; } public getActivity(): number { if (!this.isGestureActive) { return 0; } return this.getLevel(); } public getLevel(): number { return clamp01(this.energy); } public reset(): void { this.isGestureActive = false; this.energy = 0; this.targetEnergy = 0; this.lastEnergyUpdateAt = 0; } }