diff --git a/app/components/Player/Fullscreen/Background/Wash.vue b/app/components/Player/Fullscreen/Background/Wash.vue index 0fc71a3..15972bb 100644 --- a/app/components/Player/Fullscreen/Background/Wash.vue +++ b/app/components/Player/Fullscreen/Background/Wash.vue @@ -5,6 +5,7 @@ import { toOkLab, type AlbumPalette, } from "./palette" +import { shapeBeatPulse } from "~/utils/beat" const props = defineProps<{ src: string; active: boolean }>() const ANIMATION_SPEED = 0.3 @@ -159,7 +160,7 @@ function draw(now: number) { (value, channel) => value * (1 - blend) + accent[channel] * blend, ) as [number, number, number] - const beat = beatEnergy.value + const beat = shapeBeatPulse(beatEnergy.value) lab[0] = Math.min(1, lab[0] + beat * 0.01) lab[1] *= 1 + beat * 0.02 lab[2] *= 1 + beat * 0.02 @@ -171,7 +172,7 @@ function draw(now: number) { image.data[offset + 3] = 255 } } - element.style.transform = `scale(${1.08 + beatEnergy.value * 0.003})` + element.style.transform = `scale(${1.08 + shapeBeatPulse(beatEnergy.value) * 0.003})` context.putImageData(image, 0, 0) if (props.active) frame = requestAnimationFrame(draw) } diff --git a/app/plugins/player.ts b/app/plugins/player.ts index 8099863..7398a8f 100644 --- a/app/plugins/player.ts +++ b/app/plugins/player.ts @@ -1,4 +1,5 @@ import { Howl, Howler } from "howler" +import { shapeBeatPulse } from "~/utils/beat" export default defineNuxtPlugin((nuxtApp) => { const mediaBaseUrl = useRuntimeConfig().public.mediaBaseUrl @@ -145,12 +146,11 @@ export default defineNuxtPlugin((nuxtApp) => { const samples = new Uint8Array(analysisNode.frequencyBinCount) let previousBass: number | null = null + let riseAverage = 0.01 + let riseVariance = 0.0004 let pulse = 0 let lastBeatLogAt = 0 let lastLevelLogAt = 0 - const attackNoiseFloor = 0.05 - const strongThumpThreshold = 0.05 - const thumpCooldown = 800 const sample = () => { if (!analysisNode) return analysisNode.getByteFrequencyData(samples) @@ -164,17 +164,37 @@ export default defineNuxtPlugin((nuxtApp) => { for (let bin = firstBin; bin <= lastBin; bin++) bass += samples[bin] bass /= Math.max(1, lastBin - firstBin + 1) * 255 if (previousBass === null) previousBass = bass - const attack = Math.max(0, bass - previousBass - attackNoiseFloor) + const rise = Math.max(0, bass - previousBass) previousBass = previousBass * 0.985 + bass * 0.015 + const riseDeviation = Math.sqrt(riseVariance) + const strongThumpThreshold = Math.min( + 0.1, + Math.max(0.045, riseAverage + riseDeviation * 2), + ) + const attack = Math.max(0, rise - strongThumpThreshold) + const thumpCooldown = Math.min( + 500, + Math.max(300, 300 + riseDeviation * 4000), + ) const now = performance.now() const isStrongThump = - attack >= strongThumpThreshold && now - lastBeatLogAt > thumpCooldown + rise >= strongThumpThreshold && now - lastBeatLogAt > thumpCooldown + const riseDelta = rise - riseAverage + riseAverage += riseDelta * 0.01 + riseVariance += (riseDelta * riseDelta - riseVariance) * 0.01 if (isStrongThump) { const thumpStrength = Math.min( 0.38, - 0.1 + (attack - strongThumpThreshold) * 2.5, + 0.1 + attack * 2.5, ) pulse = Math.max(pulse, thumpStrength) + if (DEBUG_BEAT_ANALYSIS) { + console.log( + "[fullscreen beat]", + `detected=${thumpStrength.toFixed(2)}`, + `visual=${shapeBeatPulse(thumpStrength).toFixed(2)}`, + ) + } } else pulse *= 0.94 beatEnergy.value = Math.min(1, pulse) if (DEBUG_BEAT_ANALYSIS && now - lastLevelLogAt > 1000) { @@ -183,13 +203,12 @@ export default defineNuxtPlugin((nuxtApp) => { "[beat level]", `bass=${bass.toFixed(3)}`, `attack=${attack.toFixed(3)}`, + `gate=${strongThumpThreshold.toFixed(3)}`, + `cooldown=${Math.round(thumpCooldown)}`, `pulse=${beatEnergy.value.toFixed(2)}`, ) } - if (isStrongThump) { - lastBeatLogAt = now - console.log("[fullscreen beat]", beatEnergy.value.toFixed(2)) - } + if (isStrongThump) lastBeatLogAt = now analysisFrame = requestAnimationFrame(sample) } analysisFrame = requestAnimationFrame(sample) diff --git a/app/utils/beat.ts b/app/utils/beat.ts new file mode 100644 index 0000000..c0cad77 --- /dev/null +++ b/app/utils/beat.ts @@ -0,0 +1,9 @@ +export function shapeBeatPulse(value: number) { + const strength = Math.max(0, Math.min(0.4, value)) + if (strength <= 0.2) return strength * 0.4 + if (strength <= 0.35) { + const progress = (strength - 0.2) / 0.15 + return 0.08 + (1 - (1 - progress) ** 3) * 0.42 + } + return 0.5 + ((strength - 0.35) / 0.05) * 0.05 +}