Let the riddim wobble drive the liquid drop and bring back its texture

Two new features follow the bass line itself: its envelope against the last moment,
which rises and falls with every wub, and how open its filter is (spectral centroid
of the bass and mids). Each wub swells the drop, and as the filter opens its surface
crinkles and spikes like before. Every layer keeps a floor, so the drop never goes
still and nothing snaps in from nothing; riddim gets its soft terraces back. Kicks
still make it jump.
This commit is contained in:
nvrl committed 2026-11-01 14:54:43 +01:00
1 parent cccdf1d11c
commit b09a7370ca
6 files changed
+101 -38

No files matched your search

+15 -4
View File
@@ -9,7 +9,7 @@ import type { Look } from "./style";
const FOV = (30 * Math.PI) / 180; const FOV = (30 * Math.PI) / 180;
/// The canvas is this much larger than the art slot, so the liquid can spill out. /// The canvas is this much larger than the art slot, so the liquid can spill out.
export const SPILL = 1.8; export const SPILL = 2;
const DIST = SPILL / Math.tan(FOV / 2); const DIST = SPILL / Math.tan(FOV / 2);
const MORPH_IN = 1.4; // s from flat artwork to liquid const MORPH_IN = 1.4; // s from flat artwork to liquid
// The jump: a spring that each prominent kick or bass hit kicks upwards. Hits are // The jump: a spring that each prominent kick or bass hit kicks upwards. Hits are
@@ -21,6 +21,9 @@ const STRETCH = 0.08; // stretch per unit of speed: long while flying, squashed
const PUMP = 0.7; // size kick of a snare hit const PUMP = 0.7; // size kick of a snare hit
const PUMP_STIFF = 160; const PUMP_STIFF = 160;
const PUMP_DAMPING = 12; const PUMP_DAMPING = 12;
const THROB = 0.09; // size swing with the bass line's envelope
const SNARE_ATTACK = 0.03; // s; spikes grow in rather than pop
const SNARE_RELEASE = 0.15; // s
const TEXTURE_SIZE = 512; const TEXTURE_SIZE = 512;
const SLOW_FRAME = 0.02; // s; frames slower than this for SLOW_FOR … const SLOW_FRAME = 0.02; // s; frames slower than this for SLOW_FOR …
const SLOW_FOR = 2; // … seconds step the quality down const SLOW_FOR = 2; // … seconds step the quality down
@@ -28,8 +31,8 @@ const SLOW_FOR = 2; // … seconds step the quality down
const QUALITY: [number, number][] = [[5, 1], [4, 1], [4, 0.6], [3, 0.5]]; const QUALITY: [number, number][] = [[5, 1], [4, 1], [4, 0.6], [3, 0.5]];
const UNIFORMS = [ const UNIFORMS = [
"uProj", "uDist", "uTime", "uMorph", "uLevel", "uWobble", "uKick", "uKickAge", "uDrop", "uProj", "uDist", "uTime", "uMorph", "uLevel", "uWobble", "uKick", "uKickAge", "uSnare", "uDrop", "uBass", "uGrowl",
"uGain", "uTwist", "uRipple", "uShock", "uScale", "uLift", "uJelly", "uPointer", "uGain", "uSpike", "uStep", "uTwist", "uRipple", "uShock", "uScale", "uLift", "uJelly", "uPointer",
"uArt", "uAccent", "uHue", "uIri", "uMetal", "uHeat", "uArt", "uAccent", "uHue", "uIri", "uMetal", "uHeat",
] as const; ] as const;
@@ -56,6 +59,7 @@ export class LiquidBlob {
/// Springs, position and speed: the jump (upwards) and the pump (size). /// Springs, position and speed: the jump (upwards) and the pump (size).
private jump = [0, 0]; private jump = [0, 0];
private pump = [0, 0]; private pump = [0, 0];
private snare = 0;
/// Null where WebGL2 is not available. /// Null where WebGL2 is not available.
static create(touch: boolean): LiquidBlob | null { static create(touch: boolean): LiquidBlob | null {
@@ -189,7 +193,13 @@ export class LiquidBlob {
gl.uniform1f(u.uKick, f.kick); gl.uniform1f(u.uKick, f.kick);
gl.uniform1f(u.uKickAge, f.kickAge); gl.uniform1f(u.uKickAge, f.kickAge);
gl.uniform1f(u.uDrop, f.drop); gl.uniform1f(u.uDrop, f.drop);
gl.uniform1f(u.uBass, f.bass * look.throb);
gl.uniform1f(u.uGrowl, f.growl);
this.snare += (f.snare - this.snare) * (1 - Math.exp(-dt / (f.snare > this.snare ? SNARE_ATTACK : SNARE_RELEASE)));
gl.uniform1f(u.uSnare, this.snare);
gl.uniform1f(u.uGain, look.gain); gl.uniform1f(u.uGain, look.gain);
gl.uniform1f(u.uSpike, look.spike);
gl.uniform1f(u.uStep, look.step);
gl.uniform1f(u.uTwist, look.twist); gl.uniform1f(u.uTwist, look.twist);
gl.uniform1f(u.uRipple, look.ripple); gl.uniform1f(u.uRipple, look.ripple);
gl.uniform1f(u.uShock, look.shock); gl.uniform1f(u.uShock, look.shock);
@@ -224,7 +234,8 @@ export class LiquidBlob {
jump[0] = Math.min(MAX_LIFT, Math.max(-MAX_LIFT / 2, jump[0])); jump[0] = Math.min(MAX_LIFT, Math.max(-MAX_LIFT / 2, jump[0]));
// Long while it flies up, squashed as it comes down and lands. // Long while it flies up, squashed as it comes down and lands.
const stretch = 1 + Math.min(0.18, Math.max(-0.15, jump[1] * STRETCH + Math.min(0, jump[0]) * 1.2)); const stretch = 1 + Math.min(0.18, Math.max(-0.15, jump[1] * STRETCH + Math.min(0, jump[0]) * 1.2));
const size = 1 + pump[0]; // Each "womb" of the bass line swells the whole drop.
const size = 1 + pump[0] + THROB * look.throb * f.bass;
const side = size / Math.sqrt(stretch); const side = size / Math.sqrt(stretch);
const u = this.u; const u = this.u;
this.gl.uniform3f(u.uScale, side, size * stretch, side); this.gl.uniform3f(u.uScale, side, size * stretch, side);
+21 -12
View File
@@ -64,9 +64,9 @@ uniform float uDist; // camera distance
uniform float uTime; // flow time, runs with the tempo uniform float uTime; // flow time, runs with the tempo
uniform float uMorph; // 0 flat disc (the static artwork), 1 liquid uniform float uMorph; // 0 flat disc (the static artwork), 1 liquid
// Music, see features.ts. // Music, see features.ts.
uniform float uLevel, uWobble, uKickAge, uDrop; uniform float uLevel, uWobble, uKickAge, uSnare, uDrop, uBass, uGrowl;
// Character of the track, see style.ts. // Character of the track, see style.ts.
uniform float uGain, uTwist, uRipple, uShock; uniform float uGain, uSpike, uStep, uTwist, uRipple, uShock;
// The jump on prominent hits, from the springs in blob.ts. // The jump on prominent hits, from the springs in blob.ts.
uniform vec3 uScale; // squash and stretch uniform vec3 uScale; // squash and stretch
uniform float uLift; // upwards uniform float uLift; // upwards
@@ -79,20 +79,29 @@ out vec2 vFlat; // front projection of the resting sphere, for the artwork
${NOISE} ${NOISE}
// Only broad, smooth shapes: the drop always wobbles like jelly and never creases. // Every layer keeps a floor, so the drop never goes still and nothing snaps in from
// nothing; the music only turns each layer up and down.
float disp(vec3 p) { float disp(vec3 p) {
vec3 q = p; vec3 q = p;
float a = uTwist * (p.y + 0.3 * sin(uTime)) * (0.5 + uWobble); float a = uTwist * (p.y + 0.3 * sin(uTime)) * (0.5 + uWobble);
q.xz = mat2(cos(a), -sin(a), sin(a), cos(a)) * q.xz; q.xz = mat2(cos(a), -sin(a), sin(a), cos(a)) * q.xz;
float t = uTime; float t = uTime;
float body = snoise(q * 0.9 + vec3(0.0, t * 0.35, t * 0.2)) * (0.11 + 0.05 * uLevel + 0.07 * uDrop + 0.1 * uJelly); // Big lobes: swell on every "womb" of the bass line.
float sway = snoise(q * 1.7 - vec3(t * 0.5, 0.0, t * 0.3)) * (0.035 + 0.03 * uWobble + 0.05 * uJelly); float lobes = snoise(q * 1.1 + vec3(0.0, t * 0.35, t * 0.2))
float ripple = snoise(q * 2.6 + vec3(t * 1.1, -t * 0.8, 0.0)) * 0.02 * uWobble * uRipple; * (0.07 + 0.05 * uLevel + 0.11 * uBass + 0.08 * uDrop + 0.08 * uJelly);
// After each kick a broad wave runs from the front of the drop round to the back. // Terraces with soft edges: stiff, but no torn shards.
float steps = lobes * 10.0;
lobes = mix(lobes, (floor(steps) + smoothstep(0.3, 0.7, fract(steps))) / 10.0, uStep);
float sway = snoise(q * 1.7 - vec3(t * 0.5, 0.0, t * 0.3)) * (0.03 + 0.03 * uWobble);
// Crinkles open up with the wobble's filter: dark "oo" smooth, bright "wah" rough.
float ripples = snoise(q * 3.2 + vec3(t * 1.3, -t, 0.0)) * (0.012 + 0.035 * uWobble + 0.045 * uGrowl * uBass) * uRipple;
float n = snoise(q * 2.4 - vec3(0.0, t * 0.6, t * 0.9));
float spikes = pow(max(n, 0.0), 3.0) * (0.05 + 0.25 * uSnare + 0.15 * uGrowl * uBass + 0.1 * uDrop) * uSpike;
// After each kick a wave runs from the front of the drop round to the back.
float angle = acos(clamp(p.z, -1.0, 1.0)); float angle = acos(clamp(p.z, -1.0, 1.0));
float wave = exp(-pow((angle - uKickAge * 4.5) * 1.3, 2.0)) * exp(-uKickAge * 2.5) * 0.06 * uShock; float wave = exp(-pow((angle - uKickAge * 5.0) * 1.8, 2.0)) * exp(-uKickAge * 2.8) * 0.09 * uShock;
float dent = -0.18 * uPointer.w * exp(-pow(distance(p, uPointer.xyz) * 2.2, 2.0)); float dent = -0.18 * uPointer.w * exp(-pow(distance(p, uPointer.xyz) * 2.2, 2.0));
return clamp((body + sway + ripple + wave) * uGain + dent, -0.3, 0.32); return clamp((lobes + sway + ripples + spikes + wave) * uGain + dent, -0.3, 0.4);
} }
vec3 shape(vec3 p) { vec3 shape(vec3 p) {
@@ -126,7 +135,7 @@ in vec2 vFlat;
uniform sampler2D uArt; uniform sampler2D uArt;
uniform vec3 uAccent; uniform vec3 uAccent;
uniform float uTime, uMorph, uKick, uDrop; uniform float uTime, uMorph, uKick, uDrop, uBass, uGrowl;
uniform float uHue; // rim hue offset, radians uniform float uHue; // rim hue offset, radians
uniform float uIri; // how much the rim hue drifts uniform float uIri; // how much the rim hue drifts
uniform float uMetal; // sharper, brighter highlights uniform float uMetal; // sharper, brighter highlights
@@ -147,7 +156,7 @@ void main() {
// Looking through the surface bends the artwork like water does. // Looking through the surface bends the artwork like water does.
vec2 bend = n.xy * (0.22 + 0.1 * uDrop) * uMorph; vec2 bend = n.xy * (0.22 + 0.1 * uDrop) * uMorph;
vec2 uv = 0.5 + vec2(vFlat.x, -vFlat.y) * 0.48 - vec2(bend.x, -bend.y) * 0.5; vec2 uv = 0.5 + vec2(vFlat.x, -vFlat.y) * 0.48 - vec2(bend.x, -bend.y) * 0.5;
vec2 split = n.xy * (0.004 + 0.02 * uKick) * uMorph; vec2 split = n.xy * (0.004 + 0.02 * uKick + 0.008 * uGrowl * uBass) * uMorph;
vec3 art = vec3(texture(uArt, uv + split).r, texture(uArt, uv).g, texture(uArt, uv - split).b); vec3 art = vec3(texture(uArt, uv + split).r, texture(uArt, uv).g, texture(uArt, uv - split).b);
art = mix(art, smoothstep(0.0, 1.0, art), 0.6 * uDrop); art = mix(art, smoothstep(0.0, 1.0, art), 0.6 * uDrop);
@@ -157,7 +166,7 @@ void main() {
rim = mix(rim, vec3(1.0, 0.35, 0.1), uHeat * 0.6); rim = mix(rim, vec3(1.0, 0.35, 0.1), uHeat * 0.6);
vec3 col = art * mix(1.0, 0.45, fres * uMorph); vec3 col = art * mix(1.0, 0.45, fres * uMorph);
col += rim * fres * (0.6 + 0.7 * uKick + 0.4 * uDrop) * uMorph; col += rim * fres * (0.5 + 0.7 * uKick + 0.4 * uDrop + 0.5 * uBass) * uMorph;
col += spec * uMorph; col += spec * uMorph;
outColor = vec4(col, 1.0); outColor = vec4(col, 1.0);
} }
+12 -6
View File
@@ -8,6 +8,12 @@ export interface Look {
flow: number; flow: number;
/// Overall size of the movement. /// Overall size of the movement.
gain: number; gain: number;
/// Spikes on snares and bright wobbles.
spike: number;
/// 0 smooth, 1 stepped (stiff, quantised) lobes.
step: number;
/// How much the bass line's envelope swells the whole drop.
throb: number;
/// How high prominent hits make the drop jump. /// How high prominent hits make the drop jump.
bounce: number; bounce: number;
/// Spring stiffness of the jump: higher snaps back faster, lower floats. /// Spring stiffness of the jump: higher snaps back faster, lower floats.
@@ -25,14 +31,14 @@ export interface Look {
hue: number; hue: number;
} }
const BASE: Look = { flow: 1, gain: 1, bounce: 1, stiff: 90, twist: 0.15, ripple: 1, shock: 1, iri: 0.1, metal: 0.3, heat: 0, hue: 0 }; const BASE: Look = { flow: 1, gain: 1, spike: 0.5, step: 0, throb: 1, bounce: 1, stiff: 90, twist: 0.15, ripple: 1, shock: 1, iri: 0.1, metal: 0.3, heat: 0, hue: 0 };
const TAGS: Record<string, (l: Look) => void> = { const TAGS: Record<string, (l: Look) => void> = {
riddim: (l) => Object.assign(l, { bounce: 1.3, stiff: 120, metal: 0.9, shock: 1.4, ripple: 0.7 }), riddim: (l) => Object.assign(l, { step: 0.5, spike: 0.6, throb: 1.4, bounce: 1.3, stiff: 120, metal: 0.9, shock: 1.4, ripple: 1.2 }),
tearout: (l) => Object.assign(l, { bounce: 1.4, gain: l.gain * 1.2, heat: 0.7 }), tearout: (l) => Object.assign(l, { spike: 1.3, bounce: 1.4, gain: l.gain * 1.15, heat: 0.7 }),
deathstep: (l) => Object.assign(l, { bounce: 1.4, gain: l.gain * 1.2, heat: 0.9, metal: 0.7 }), deathstep: (l) => Object.assign(l, { spike: 1.4, bounce: 1.4, gain: l.gain * 1.15, heat: 0.9, metal: 0.7 }),
melodic: (l) => Object.assign(l, { iri: 0.8, bounce: 0.6, stiff: 55, gain: l.gain * 0.9, flow: l.flow * 0.8, metal: 0.1 }), melodic: (l) => Object.assign(l, { iri: 0.8, spike: 0.15, throb: 0.7, bounce: 0.6, stiff: 55, gain: l.gain * 0.9, flow: l.flow * 0.8, metal: 0.1 }),
colourbass: (l) => Object.assign(l, { iri: 1, bounce: 0.8, stiff: 70, metal: 0.15 }), colourbass: (l) => Object.assign(l, { iri: 1, spike: 0.25, bounce: 0.8, stiff: 70, metal: 0.15 }),
dnb: (l) => Object.assign(l, { ripple: 1.6 }), dnb: (l) => Object.assign(l, { ripple: 1.6 }),
"170": (l) => Object.assign(l, { ripple: 1.5 }), "170": (l) => Object.assign(l, { ripple: 1.5 }),
halftime: (l) => Object.assign(l, { flow: l.flow * 0.7, gain: l.gain * 1.15, stiff: 70 }), halftime: (l) => Object.assign(l, { flow: l.flow * 0.7, gain: l.gain * 1.15, stiff: 70 }),
+2 -2
View File
@@ -3,7 +3,7 @@
import type { Features } from "./features"; import type { Features } from "./features";
const KEYS: (keyof Features)[] = ["sub", "low", "mid", "high", "kick", "snare", "wobble", "drop"]; const KEYS: (keyof Features)[] = ["sub", "low", "mid", "high", "bass", "growl", "kick", "snare", "wobble", "drop"];
/// Onsets counted since the overlay opened; also on `window.audioDebug` for scripts. /// Onsets counted since the overlay opened; also on `window.audioDebug` for scripts.
const counts = { kicks: 0, snares: 0, frames: 0 }; const counts = { kicks: 0, snares: 0, frames: 0 };
@@ -13,7 +13,7 @@ export function audioDebug(): (f: Features) => void {
let lastAge = 99, lastSnare = 0; let lastAge = 99, lastSnare = 0;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.className = "audio-debug"; canvas.className = "audio-debug";
const w = (canvas.width = 240), h = (canvas.height = 124); const w = (canvas.width = 300), h = (canvas.height = 124);
document.body.append(canvas); document.body.append(canvas);
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
ctx.font = "10px system-ui, sans-serif"; ctx.font = "10px system-ui, sans-serif";
+48 -11
View File
@@ -25,6 +25,12 @@ export interface Features {
snareHit: number; snareHit: number;
/// How much the mids move (wobble bass, growls), 0..1. /// How much the mids move (wobble bass, growls), 0..1.
wobble: number; wobble: number;
/// The bass line's own envelope, 0..1 against the last moment: follows every
/// "womb" of a riddim wobble up and down.
bass: number;
/// How open the wobble's filter is, 0..1: low for a dark "oo", high for a bright
/// "wah" (spectral centroid of the bass and mids against its recent range).
growl: number;
/// 0..1, high while the low end sits well above its recent average. /// 0..1, high while the low end sits well above its recent average.
drop: number; drop: number;
} }
@@ -46,6 +52,15 @@ const SNARE_DECAY = 0.12; // s
const PROMINENCE_MEMORY = 6; // s; a hit is weighed against the strongest ones this recent const PROMINENCE_MEMORY = 6; // s; a hit is weighed against the strongest ones this recent
const WOBBLE_SMOOTH = 0.12; // s const WOBBLE_SMOOTH = 0.12; // s
const BASS_BAND = [40, 400]; // Hz; body of a wobble bass
const BASS_FOLLOW = 14; // dB/s; its range forgets this fast, so each wub spans it
const BASS_MIN_RANGE = 6; // dB
const BASS_ATTACK = 0.015; // s
const BASS_RELEASE = 0.06; // s
const GROWL_BAND = [100, 3000]; // Hz
const GROWL_FOLLOW = 0.6; // octaves/s
const GROWL_MIN_RANGE = 1.2; // octaves
const GROWL_SMOOTH = 0.05; // s
const DROP_SHORT = 1; // s of low-end loudness … const DROP_SHORT = 1; // s of low-end loudness …
const DROP_LONG = 12; // … against this much const DROP_LONG = 12; // … against this much
const DROP_HEARD_DB = -70; // the low end counts as sounding above this const DROP_HEARD_DB = -70; // the low end counts as sounding above this
@@ -57,16 +72,21 @@ const DROP_RELEASE = 2; // s
const ease = (dt: number, tau: number) => 1 - Math.exp(-dt / tau); const ease = (dt: number, tau: number) => 1 - Math.exp(-dt / tau);
const clamp01 = (x: number) => Math.min(1, Math.max(0, x)); const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
/// Maps a band's dB level into its recent range. /// Maps a value (a band's dB level) into its recent range.
class Range { class Range {
private floor = -60; constructor(
private ceil = -30; private floor = -60,
private ceil = -30,
private ceilFall = CEIL_FALL,
private floorRise = FLOOR_RISE,
private minRange = MIN_RANGE,
) {}
update(db: number, dt: number): number { update(x: number, dt: number): number {
this.ceil = db > this.ceil ? db : this.ceil - CEIL_FALL * dt; this.ceil = x > this.ceil ? x : this.ceil - this.ceilFall * dt;
this.floor = db < this.floor ? db : this.floor + FLOOR_RISE * dt; this.floor = x < this.floor ? x : this.floor + this.floorRise * dt;
const span = Math.max(MIN_RANGE, this.ceil - this.floor); const span = Math.max(this.minRange, this.ceil - this.floor);
return clamp01((db - (this.ceil - span)) / span); return clamp01((x - (this.ceil - span)) / span);
} }
} }
@@ -105,12 +125,14 @@ class Onset {
} }
export class FeatureTracker { export class FeatureTracker {
readonly f: Features = { sub: 0, low: 0, mid: 0, high: 0, kick: 0, snare: 0, kickAge: 99, kickHit: 0, snareHit: 0, wobble: 0, drop: 0 }; readonly f: Features = { sub: 0, low: 0, mid: 0, high: 0, kick: 0, snare: 0, kickAge: 99, kickHit: 0, snareHit: 0, wobble: 0, bass: 0, growl: 0, drop: 0 };
private bins: Float32Array<ArrayBuffer> | null = null; private bins: Float32Array<ArrayBuffer> | null = null;
private prev: Float32Array<ArrayBuffer> | null = null; private prev: Float32Array<ArrayBuffer> | null = null;
private ranges = { sub: new Range(), low: new Range(), mid: new Range(), high: new Range() }; private ranges = { sub: new Range(), low: new Range(), mid: new Range(), high: new Range() };
private kick = new Onset(KICK_DECAY); private kick = new Onset(KICK_DECAY);
private snare = new Onset(SNARE_DECAY); private snare = new Onset(SNARE_DECAY);
private bassRange = new Range(-60, -30, BASS_FOLLOW, BASS_FOLLOW, BASS_MIN_RANGE);
private growlRange = new Range(7, 9, GROWL_FOLLOW, GROWL_FOLLOW, GROWL_MIN_RANGE);
private lastMid = 0; private lastMid = 0;
private wobblePeak = 1; private wobblePeak = 1;
private short = SILENCE_DB; private short = SILENCE_DB;
@@ -150,6 +172,20 @@ export class FeatureTracker {
const f = this.f; const f = this.f;
for (const band of Object.keys(BANDS) as Band[]) f[band] = this.ranges[band].update(db[band], dt); for (const band of Object.keys(BANDS) as Band[]) f[band] = this.ranges[band].update(db[band], dt);
// The wobble: loudness of the bass body, and where its energy sits.
let bassPower = 0;
for (let i = bin(BASS_BAND[0]); i <= bin(BASS_BAND[1]); i++) bassPower += 10 ** (Math.max(SILENCE_DB, bins[i]) / 10);
const bassDb = Math.max(SILENCE_DB, 10 * Math.log10(bassPower / (bin(BASS_BAND[1]) - bin(BASS_BAND[0]) + 1)));
const bass = this.bassRange.update(bassDb, dt);
f.bass += (bass - f.bass) * ease(dt, bass > f.bass ? BASS_ATTACK : BASS_RELEASE);
let weight = 0, sum = 0;
for (let i = bin(GROWL_BAND[0]); i <= bin(GROWL_BAND[1]); i++) {
const p = 10 ** (Math.max(SILENCE_DB, bins[i]) / 10);
weight += p;
sum += p * Math.log2(i * hz);
}
if (weight > 0) f.growl += (this.growlRange.update(sum / weight, dt) - f.growl) * ease(dt, GROWL_SMOOTH);
this.kick.update((flux.sub + flux.low) / 2, now, dt); this.kick.update((flux.sub + flux.low) / 2, now, dt);
this.snare.update((flux.mid + flux.high) / 2, now, dt); this.snare.update((flux.mid + flux.high) / 2, now, dt);
f.kick = this.kick.env; f.kick = this.kick.env;
@@ -164,7 +200,8 @@ export class FeatureTracker {
/// One frame where there is no analyser: only the loudness from the waveform. /// One frame where there is no analyser: only the loudness from the waveform.
fromLoudness(level: number, dt: number): Features { fromLoudness(level: number, dt: number): Features {
const f = this.f; const f = this.f;
f.sub = f.low = f.mid = f.high = level; f.sub = f.low = f.mid = f.high = f.bass = level;
f.growl = 0.3;
f.kick = f.snare = f.kickHit = f.snareHit = 0; f.kick = f.snare = f.kickHit = f.snareHit = 0;
f.kickAge = 99; f.kickAge = 99;
this.track(level, level > 0 ? 20 * Math.log10(level) : SILENCE_DB, dt); this.track(level, level > 0 ? 20 * Math.log10(level) : SILENCE_DB, dt);
@@ -176,7 +213,7 @@ export class FeatureTracker {
const f = this.f; const f = this.f;
const k = Math.exp(-dt / 0.35); const k = Math.exp(-dt / 0.35);
f.sub *= k; f.low *= k; f.mid *= k; f.high *= k; f.sub *= k; f.low *= k; f.mid *= k; f.high *= k;
f.kick *= k; f.snare *= k; f.wobble *= k; f.drop *= k; f.kick *= k; f.snare *= k; f.wobble *= k; f.drop *= k; f.bass *= k; f.growl *= k;
f.kickAge += dt; f.kickAge += dt;
f.kickHit = f.snareHit = 0; f.kickHit = f.snareHit = 0;
return f; return f;
+3 -3
View File
@@ -257,9 +257,9 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
/* The liquid drop spills past the art slot; taps go through it to the card. */ /* The liquid drop spills past the art slot; taps go through it to the card. */
.liquid-canvas { .liquid-canvas {
position: absolute; position: absolute;
inset: -40%; inset: -50%;
width: 180%; width: 200%;
height: 180%; height: 200%;
pointer-events: none; pointer-events: none;
} }
.card.liquid .art { background-image: none !important; background-color: transparent; box-shadow: none; scale: 1; } .card.liquid .art { background-image: none !important; background-color: transparent; box-shadow: none; scale: 1; }