3. Februar 2026
R.P.M. Rhythm Per Motion: Wenn Klang zu Fluid wird
Ein tiefer Einblick in die Audioanalyse, Beat-Erkennung und Partikelphysik hinter R.P.M., einem Rhythmusspiel, in dem Musik die Welt buchstäblich formt.
Sascha Becker
Author14 Min. Lesezeit

R.P.M.: Wenn Klang zu Fluid wird
Die meisten Rhythmusspiele verlangen, dass man Tasten drückt, wenn Pfeile vorbeiscrollen. R.P.M. stellt eine andere Frage: Was, wenn die Musik selbst die Physik-Engine wäre?
Das Konzept nennt sich Hydrodynamic Aural Shaping. Man steuert zwei gegenüberliegende Magnetfelder, eins pro Daumen, und lenkt einen Strom aus "Audio-Materie" in Kollektoren. Jedes Partikel in diesem Strom wird aus Frequenzdaten geboren. Jede Bewegung wird vom Beat bestimmt. Die Musik begleitet das Gameplay nicht. Die Musik ist das Gameplay.
Dieser Artikel ist ein technischer Deep Dive in die Funktionsweise. Wir zerlegen den Beat-Detection-Algorithmus, verfolgen wie FFT-Daten in Partikelphysik fließen und nerden über die Signalverarbeitung, die eine Kick-Drum den Flüssigkeitsstrom verbreitern und eine Hi-Hat ihn zum Laserstrahl verengen lässt.
Architektur
Das System hat zwei parallele Audio-Verarbeitungs-Pipelines, die eine einzelne Partikelsimulation speisen:
- Echtzeit-Analyse: Live-FFT-Daten bei 60fps treiben Visuals und Physik an
- Offline-Beat-Map-Generierung: Ein vollständiger Pre-Scan des Tracks für deterministische Gameplay-Events
Beide Pipelines teilen sich die gleiche Analyse-Logik. Der Echtzeit-Pfad hält die Visuals ehrlich. Der Offline-Pfad hält die Bewertung fair.
DIAGRAM
Beide Pfade laufen in einem einzigen Methodenaufruf zusammen: applyAudio(). Dort werden Frequenzdaten zu Gravitation, Farbe, Turbulenz und Spawn-Events.
Das Frequenzspektrum
Bevor irgendetwas Visuelles passiert, muss das rohe Audiosignal zerlegt werden. Das System nutzt den AnalyserNode der Web Audio API mit einer FFT-Größe von 2048, was bei einer typischen Abtastrate von 44,1 kHz 1024 Frequenz-Bins ergibt.
Diese Bins werden in vier musikalisch sinnvolle Bänder gruppiert:
| Band | Bereich | Was hier lebt |
|---|---|---|
| Sub-Bass | 20–60 Hz | Physisches Grollen, Sub-Frequenzen |
| Bass | 60–250 Hz | Kick-Drums, Bassline-Körper |
| Mitten | 250–4000 Hz | Vocals, Snare, Gitarren |
| Höhen | 4000 Hz+ | Becken, Hi-Hats, Luft |
Die Energie jedes Bands wird als RMS (Root Mean Square) über seinen Bin-Bereich berechnet:
typescriptconst calculateRMS = (start: number, end: number) => {let sumSq = 0;const n = end - start;for (let i = start; i < end; i++) {const v = dataArray[i] / 255;sumSq += v * v;}return Math.sqrt(sumSq / n);};
Das ergibt vier normalisierte 0–1 Energiewerte pro Frame. Aber Energie allein reicht nicht aus, um Beats zu erkennen. Eine gehaltene Bassnote hat hohe Energie. Eine Kick-Drum hat eine hohe Veränderung der Energie. Diese Unterscheidung ist entscheidend.
Beat-Erkennung
Das ist das Herzstück des Systems. Der Ansatz ist Spectral Flux mit adaptivem Schwellenwert: eine bekannte Technik in der Music Information Retrieval, aber die Implementierung hier hat einige clevere Verfeinerungen.
Schritt 1: Spectral Flux
Spectral Flux misst Frame-zu-Frame-Änderungen im Frequenzspektrum. Nur positive Änderungen werden gezählt: uns interessieren Onsets (Anschläge), keine Abklingvorgänge:
typescriptfor (let i = 0; i < binCount; i++) {const v = dataArray[i] / 255;const diff = v - previousFrame[i];if (diff > 0) fluxSum += diff;previousFrame[i] = v;}const flux = clamp((fluxSum / binCount) * 6, 0, 1);
Eine Kick-Drum verursacht eine scharfe spektrale Änderung in den tiefen Bins. Eine Snare trifft die Mitten. Eine Hi-Hat lässt die Höhen aufleuchten. Spectral Flux erfasst all das als plötzliche Spitzen, während gehaltene Töne minimalen Flux erzeugen. Das unterscheidet sich grundlegend von volumenbasierter Beat-Erkennung, die bei komprimierten Masters versagt, wo alles ständig laut ist.
Schritt 2: Adaptiver Schwellenwert
Ein fester Schwellenwert würde manuelles Tuning pro Track erfordern. Stattdessen pflegt das System einen rollierenden Verlauf von Flux-Werten, etwa eine Sekunde (40 Frames), und leitet einen dynamischen Schwellenwert aus der Signalstatistik ab:
typescriptconst mean = energyHistory.reduce((a, b) => a + b) / energyHistory.length;const variance =energyHistory.map((f) => (f - mean) ** 2).reduce((a, b) => a + b) /energyHistory.length;const stdDev = Math.sqrt(variance);const threshold = mean + stdDev * 1.5;
Ein Beat ist ein Flux-Wert, der den lokalen Mittelwert um 1,5 Standardabweichungen überschreitet. Das passt sich automatisch an: Eine ruhige akustische Passage hat einen niedrigen Schwellenwert, eine Wall-of-Sound-Drop hat einen hohen. Die registrierten Beats sind immer die relativen Spitzen, nicht die absoluten.
Schritt 3: Das Low-End Gate
Hier wird es interessant. Roher Spectral Flux erfasst jede Transiente, einschließlich Hi-Hats und Shaker. In den meisten Musikstücken sollen diese keine Beat-Events auslösen, sie würden 8 oder 16 Mal pro Takt feuern statt 4.
Die Lösung ist ein Low-End Gate:
typescriptconst lowGate = subBass * 0.8 + bass * 0.6;const brightness = 0.65 * centroid + 0.35 * treble;const lowThreshold = 0.28 - brightness * 0.16;const hasLowEnd = lowGate > lowThreshold;
Ein Beat wird nur registriert, wenn ausreichend Bass-Präsenz neben dem spektralen Spike vorhanden ist. Die Feinheit: Der Schwellenwert sinkt, je heller der Track ist. Ein dunkler, basslastiger Track braucht starken Bass zum Triggern. Ein heller, höhenbetonter Track bekommt mehr Spielraum, weil in solchen Tracks selbst moderater Bass musikalisch bedeutsam ist.
Schritt 4: Die High-Transient-Ausnahme
Manche rhythmisch wichtigen Hits haben keinen Bass. Ein Snare Ghost Note. Ein Cross-Stick. Ein Rimshot in einem Jazz-Quartett. Das System berücksichtigt das mit einem separaten Gate:
typescriptconst hasHighTransient =centroid > 0.55 && treble > 0.18 && fluxHighRaw > threshold * 0.55;
Wenn eine Transiente hell genug und stark genug relativ zum aktuellen Schwellenwert ist, wird sie auch ohne Bass-Bestätigung durchgelassen. Das verhindert, dass das System bei höhenlastigen Passagen verstummt, während es trotzdem zufälliges Rauschen filtert.
Schritt 5: Cooldown
Ein 200ms-Cooldown zwischen Beats verhindert Doppel-Triggering bei Hall-Ausklängen oder mehrschichtigen Drum-Hits. Bei 120 BPM liegen Achtel bei 250ms Abstand, der Cooldown ist also knapp genug, um sie zu erfassen, und filtert gleichzeitig Artefakte heraus.
Die finale Beat-Erkennung kombiniert all das:
typescriptconst isBeat =flux > threshold &&(hasLowEnd || hasHighTransient) &&timeSinceLastBeat > 200; // ms Cooldown
Hier der komplette Entscheidungsbaum auf einen Blick:
DIAGRAM
BPM-Erkennung
Neben der Frame-für-Frame Beat-Erkennung schätzt das System auch die BPM des Tracks für tempoabhängiges Spawning. Der Algorithmus ist elegant simpel:
- Tiefpassfilter bei 150 Hz, um Kick-Drums zu isolieren
- Peak-Erkennung in 0,5-Sekunden-Fenstern zur Findung lokaler Maxima
- Intervallanalyse zwischen aufeinanderfolgenden Peaks, umgerechnet in BPM
- Histogramm-Voting: Intervalle auf die nächste BPM runden und Vorkommen zählen
- Oktav-Normalisierung: Beschränkung auf 116–230 BPM, um Half-Time/Double-Time-Verwechslung zu vermeiden
Der letzte Schritt ist entscheidend. Ein 70-BPM-Hip-Hop-Track wird auf 140 BPM verdoppelt. Ein 240-BPM-Drum-&-Bass-Track wird auf 120 halbiert. Das Spiel braucht ein "spielbares" Tempo, kein musikwissenschaftlich korrektes.
Die Beat Map
Für deterministisches, faires Gameplay wird der gesamte Track vor dem Spielstart analysiert, mittels OfflineAudioContext. Das rendert das Audio mit maximaler Geschwindigkeit (nicht in Echtzeit) und erzeugt ein BeatMapEvent[]: eine Timeline jedes erkannten Beats mit vollständigen spektralen Metadaten:
typescriptinterface BeatMapEvent {time: number; // SekundenisBeat: boolean;intensity: number; // 0–1subBass: number;bass: number;mid: number;treble: number;centroid: number; // spektrale Helligkeitlane: number; // 0–3}
Die Lane-Zuweisung verdient einen eigenen Abschnitt.
Lane-Zuweisung
Das Spiel hat vier Lanes, die auf zwei Hände abgebildet werden (links: Lanes 0–1, rechts: Lanes 2–3). Der Zuweisungsalgorithmus optimiert auf Spielbarkeit:
Hand-Auswahl nutzt eine Dualstrategie:
- Schnelle Sequenzen (< 350ms zwischen Noten): Erzwungener Handwechsel, um Ermüdung zu vermeiden
- Langsame Sequenzen: Spektraler Centroid, helle Hits nach rechts, dunkle nach links
Lane-Auswahl innerhalb jeder Hand nutzt die Frequenzbalance:
- Linke Hand: Basslastig → Lane 0, mittenlastig → Lane 1
- Rechte Hand: Höhenlastig → Lane 3, mittenlastig → Lane 2
Anti-Wiederholung: Wenn die gewählte Lane der vorherigen entspricht, gibt es eine 60%ige Chance, zur anderen Lane derselben Hand zu wechseln.
DIAGRAM
Das Ergebnis ist eine Beat Map, die sich musikalisch anfühlt. Bass-Hits landen links. Becken landen rechts. Schnelle Passagen wechseln die Hände. Es spiegelt wider, wie die Gliedmaßen eines Drummers tatsächlich arbeiten.
Audio zu Physik
Jetzt erreichen wir das Partikelsystem, eine 1600+ Zeilen Canvas-2D-Engine, in der jeder Parameter an die Audioanalyse verdrahtet ist. Die applyAudio()-Methode wird jeden Frame aufgerufen und übersetzt Frequenzdaten in physikalische Eigenschaften.
Das Mapping ist many-to-many, aber es verdrahtet rohe Bänder nicht direkt mit Visuals. Der Code blendet sie zuerst zu Composite-Signalen, die dann das Rendering steuern:
| Composite-Signal | Audio-Inputs | Steuert |
|---|---|---|
| Heaviness | Sub-Bass + Bass | Gravitation, Streambreite |
| Busyness | Spectral Flux + Höhen | Geschwindigkeit, Turbulenz |
| Sparkle | Centroid + Höhen | Farbton, Helligkeit |
| Temperature | Lautstärke + Bass + Mitten | Farbton |
| Beat Pulse | Beat-Intensität (abklingend) | Gravitation, Spawn-Rate |
| Volume | (direkt) | Sättigung, Streambreite, Spawn-Rate |
Streambreite
Die Breite des Partikelstroms wird als Fluiddruck behandelt:
typescriptconst baseWidth = 0.35 - volume * 0.2;const kickExpansion = bass * 0.15;const rumbleExpansion = subBass * 0.15;streamWidth = clamp(0.1, 0.5, baseWidth + kickExpansion + rumbleExpansion);
- Ruhige Passagen: Breiter, träger Strom (35% der Canvas)
- Laute Höhen: Schmaler, fokussierter Strahl (15%)
- Kick-Drum-Hit: Plötzliche Expansion, der Strom "schlägt" nach außen
- Sub-Bass-Grollen: Anhaltende Verbreiterung, wie eine Druckwelle
Das erzeugt eine visuelle Metapher, die sofort lesbar ist. Man kann die Kick-Drum im Verhalten des Stroms sehen, bevor man sie bewusst hört.
Geschwindigkeit und Gravitation
Partikelgeschwindigkeit und Gravitation werden von zwei Composite-Signalen gesteuert:
typescriptconst speedDriver = 0.55 * busyness + 0.25 * relativeLoudness + 0.2 * treble;const gravityDriver =0.55 * heaviness + 0.35 * beatPulse + 0.1 * relativeLoudness;baseSpeed = (0.5 + speedDriver * 5.0) * motionScale;gravity = (0.05 + gravityDriver * 0.1) * motionScale;
Wobei busyness aus Spectral Flux + Höhen abgeleitet wird und heaviness aus Sub-Bass + Bass. Der Effekt: Basslastige Drops fühlen sich schwer an. Partikel beschleunigen nach unten. Geschäftige Höhenpassagen fühlen sich hektisch an. Partikel bewegen sich schneller, aber mit weniger Gravitation, fast schwebend.
Der beatPulse-Wert verdient Aufmerksamkeit. Er ist eine abklingende Hüllkurve, die bei jedem Beat ihren Höchstwert erreicht:
typescriptbeatPulse = Math.max(beatPulse * 0.85, beatIntensity);
Schneller Attack (sofortiger Sprung auf Beat-Intensität), langsamer exponentieller Abfall (15% pro Frame). Das erzeugt ein rhythmisches "Atmen" in der Gravitation, das man eher fühlt als sieht.
Farbtemperatur
Der Partikel-Farbton folgt einem Temperaturmodell:
typescriptconst temperature = volume * 0.4 + bass * 0.4 + mid * 0.2 - subBass * 0.2;const targetHue = 180 + temperature * 180;baseHue += (targetHue - baseHue) * 0.1; // sanfter Übergang
- Kalt (Cyan, ~180°): Ruhige, sub-bassdominierte Passagen
- Warm (Rot, ~360°): Laute, bass-und-mittendominierte Passagen
Der sanfte Übergang (EMA mit Alpha 0,1) verhindert abrupte Farbsprünge. Ein Drop baut Wärme über mehrere Frames auf, statt sofort rot zu werden.
Partikel-Helligkeit reagiert auf Höhen (helle Sounds → helle Partikel), und die Sättigung folgt der Lautstärke (laut → Neon, leise → Pastell). Die Kombination bedeutet: Man könnte die Augen schließen und den Mix fast allein aus dem visuellen Output rekonstruieren.
Kraftfelder und Kollision
Die zwei Kraftfelder des Spielers nutzen exponentielle Falloff-Repulsion:
typescriptconst normalizedDist = dist / activeRadius;const falloff = Math.exp(-normalizedDist * 2);const force = baseStrength * falloff;particle.vx += (dx / dist) * force * 0.5;particle.vy += (dy / dist) * force * 0.3;
Aktive Felder sind 5x stärker als passive und erweitern ihren Radius um 50%. Die asymmetrische Dämpfung (horizontal 0,5 vs. vertikal 0,3) erhält den Abwärtsstrom, während sie seitliche Ablenkung ermöglicht. Partikel umkreisen die Felder nicht, sie werden an ihnen vorbeigeleitet.
Kollisionserkennung zwischen Partikeln und Kollektoren nutzt einfache AABB-Checks (Axis-Aligned Bounding Box). Jedes aufgefangene Partikel erhöht den Füllstand des Kollektors um 1,2%. Wenn der Füllstand den Zielwert (60%) erreicht, wird der Kollektor abgeschlossen und löst einen Feier-Burst aus.
Der Feier-Burst ist selbst audioreaktiv: Die Beat-Intensität bestimmt Partikelanzahl (0–130 Partikel), Geschwindigkeit (10–30 px/Frame) und Größe (2–6 px). Einen Kollektor während eines Drops abzuschließen erzeugt eine dramatisch größere Explosion als während einer ruhigen Passage.
Kollektor-Spawning
Kollektoren erscheinen nicht zufällig. Ihre Platzierung wird vom spektralen Inhalt der Beat Map gesteuert:
Horizontale Positionierung: Basslastige Beats spawnen Kollektoren auf der linken Seite, höhenlastige auf der rechten. Das spiegelt natürliches Stereo-Imaging wider, man fängt Kick-Drums mit dem linken Daumen und Hi-Hats mit dem rechten.
Vertikaler Abstand: Ein "Leiter"-Algorithmus verhindert Überlappungen. Spawns auf der gleichen Seite werden vertikal um 15–25% der Canvas-Höhe versetzt. Spawns auf der anderen Seite werden auf die Mitte zurückgesetzt.
Die "Drop"-Mechanik: Wenn Bass 0,8 überschreitet und Mitten 0,6 gleichzeitig (ein Vollfrequenz-Hit), spawnt das System zwei Kollektoren gespiegelt über die Mitte. Beide Daumen werden gebraucht. Das ist das Spieläquivalent eines Drum Fills.
Hintergrund-Balken
Hinter dem Gameplay liefern 64 Frequenzbalken eine persistente FFT-Visualisierung. Das Mapping nutzt eine Potenzskala (Exponent 2,5), um den logarithmischen Frequenzbereich in linearen Bildschirmraum zu komprimieren. Sub-Bass und Bass bekommen proportional mehr visuelle Fläche als bei einem linearen Mapping.
Jeder Balken hat unabhängige Attack- und Decay-Physik. Hochfrequente Balken springen sofort auf neue Werte (Attack-Speed 1,0), während tieffrequente Balken absichtlich träge sind (Attack-Speed 0,8), das imitiert, wie Bass physikalisch länger resoniert als Höhen.
Ein Sub-Bass-Hover-Effekt reduziert die Gravitation der tiefsten Balken um bis zu 80%, sodass sie bei anhaltendem Grollen auf ihren Spitzenwerten schweben. Visuell "atmen" die Low-End-Balken, während die Höhenbalken flackern.
Adaptive Qualität
Das System skaliert sich selbst an das Gerät. Ein motionScale-Faktor normalisiert die Physik auf eine Referenzhöhe von 850px. Auf einem kleinen Handy im Querformat (~350px) laufen die Physik mit 55% Intensität, weniger Partikel, schwächere Kräfte, engere Ströme. Auf einem Tablet im Hochformat (~900px) läuft alles auf voller Stufe.
Die FX-Qualität sinkt eine Stufe auf kleinen, hochauflösenden Bildschirmen (wo die GPU härter pro Pixel arbeitet). Glow-Effekte, sekundäre Ring-Animationen und zusätzliche Partikel-Layer sind die ersten, die wegfallen.
Was das Ganze zusammenhält
Das Geheimnis ist kein einzelner Algorithmus. Es ist das Layering. Beat-Erkennung speist Kollektor-Spawning. Frequenzbänder speisen Partikelphysik. Spektraler Centroid speist Farbtemperatur. Lautstärke speist Spawn-Rate. Jedes Audio-Feature mappt auf mehrere visuelle Parameter, und jeder visuelle Parameter blendet mehrere Audio-Features.
Das Ergebnis ist ein System, in dem man die Musik nicht nur hört, man sieht ihre Struktur. Die Kick-Drum verbreitert den Strom. Die Hi-Hat verengt ihn. Der Drop spawnt Kollektoren auf beiden Seiten. Der Breakdown lässt die Partikel zu geisterhaften Pastelltönen verblassen. Der Build-up beschleunigt alles.
Man drückt keine Tasten im Takt der Musik. Man formt ein Fluid, das die Musik ist.
Und das ist eine fundamental andere Art von Rhythmusspiel.
R.P.M. selbst spielen auf rpm.saschb2b.com - Kopfhörer empfohlen.
