Teil von #330. Patterns und Songs müssen sample-genau im Takt spielen, unabhängig von der Bildrate und von Hängern im Spiel. Das hat der Pitch-Sweep nach #315 gezeigt: Was in Spielzeit gezählt wird, läuft gegen den Ton weg.
Vorschlag
Zeit in Ticks: 96 Ticks pro Viertel. Das teilt sich durch 2, 3 und 4, Zweiunddreißigstel und Triolen gehen also ohne Rundung. Sekunden ergeben sich aus dem Tempo.
Eine reine Funktion für die Ereignisse: Aus Song oder Pattern, Tempo und einem Zeitfenster entsteht die Liste der Ereignisse darin. Das sind Note an und aus mit Instrument, Nummer und Velocity, und Starts von Sound-Clips mit Versatz. Die Funktion braucht keinen Browser und ist in Node testbar, wie fixedSteps in src/core/time.ts.
Lookahead: Etwa alle 25 ms werden die Ereignisse der nächsten ~100 ms mit start(when) und Rampen auf der Audio-Uhr eingeplant. Der Takt dafür kommt aus einem Timer, der auch in einem Hintergrund-Tab weiterläuft. requestAnimationFrame steht dort still.
Stimmen: Jede Note wird eine Stimme ihres Instruments (#331) mit dem Plan dieser Note. Die Note-aus wird am Notenende eingeplant. Die Polyphonie ist unbegrenzt.
Steuerung: Start ab einer Position, Stopp mit kurzem Ausblenden, Springen und ein Loop-Bereich. Dazu eine Abfrage, wo die Musik gerade steht (Takt, Schlag, Tick), gerechnet aus der Audio-Uhr. Die braucht der Playhead im Editor und später der Game-Sync (#338).
Ein Tempo pro Song. Tempowechsel kämen mit der Automation (#337).
Haken
Ausgabelatenz: Was eingeplant ist, hört man erst nach outputLatency. Der Playhead im Editor muss das abziehen, sonst läuft er dem Ton voraus.
Ein Pattern im Loop muss an der Nahtstelle ohne Lücke weiterlaufen. Das Fenster muss also über das Ende hinaus in den nächsten Durchlauf schauen.
Änderungen beim Abspielen: Wird eine Note gezeichnet, während der Loop läuft, soll sie im nächsten Durchlauf erklingen. Was für die nächsten 100 ms schon eingeplant ist, bleibt.
Tests: Im e2e-Browser ist der Ton stumm. Messen lässt sich, welche Ereignisse zu welcher Audio-Zeit eingeplant wurden, und mit einem OfflineAudioContext auch der Klang selbst.
Rückfragen
Reichen 96 Ticks pro Viertel? Für Swing und feines Verschieben von Hand wären 480 üblicher.
Taktarten wie 3/4 und 7/8 gleich von Anfang an, oder reicht zuerst 4/4?
Swing (jede zweite Achtel oder Sechzehntel etwas später): gleich mit einplanen oder später?
Teil von #330. Patterns und Songs müssen sample-genau im Takt spielen, unabhängig von der Bildrate und von Hängern im Spiel. Das hat der Pitch-Sweep nach #315 gezeigt: Was in Spielzeit gezählt wird, läuft gegen den Ton weg.
## Vorschlag
- **Zeit in Ticks:** 96 Ticks pro Viertel. Das teilt sich durch 2, 3 und 4, Zweiunddreißigstel und Triolen gehen also ohne Rundung. Sekunden ergeben sich aus dem Tempo.
- **Eine reine Funktion für die Ereignisse:** Aus Song oder Pattern, Tempo und einem Zeitfenster entsteht die Liste der Ereignisse darin. Das sind Note an und aus mit Instrument, Nummer und Velocity, und Starts von Sound-Clips mit Versatz. Die Funktion braucht keinen Browser und ist in Node testbar, wie `fixedSteps` in `src/core/time.ts`.
- **Lookahead:** Etwa alle 25 ms werden die Ereignisse der nächsten ~100 ms mit `start(when)` und Rampen auf der Audio-Uhr eingeplant. Der Takt dafür kommt aus einem Timer, der auch in einem Hintergrund-Tab weiterläuft. `requestAnimationFrame` steht dort still.
- **Stimmen:** Jede Note wird eine Stimme ihres Instruments (#331) mit dem Plan dieser Note. Die Note-aus wird am Notenende eingeplant. Die Polyphonie ist unbegrenzt.
- **Steuerung:** Start ab einer Position, Stopp mit kurzem Ausblenden, Springen und ein Loop-Bereich. Dazu eine Abfrage, wo die Musik gerade steht (Takt, Schlag, Tick), gerechnet aus der Audio-Uhr. Die braucht der Playhead im Editor und später der Game-Sync (#338).
- Ein Tempo pro Song. Tempowechsel kämen mit der Automation (#337).
## Haken
- **Ausgabelatenz:** Was eingeplant ist, hört man erst nach `outputLatency`. Der Playhead im Editor muss das abziehen, sonst läuft er dem Ton voraus.
- **Ein Pattern im Loop** muss an der Nahtstelle ohne Lücke weiterlaufen. Das Fenster muss also über das Ende hinaus in den nächsten Durchlauf schauen.
- **Änderungen beim Abspielen:** Wird eine Note gezeichnet, während der Loop läuft, soll sie im nächsten Durchlauf erklingen. Was für die nächsten 100 ms schon eingeplant ist, bleibt.
- **Tests:** Im e2e-Browser ist der Ton stumm. Messen lässt sich, welche Ereignisse zu welcher Audio-Zeit eingeplant wurden, und mit einem `OfflineAudioContext` auch der Klang selbst.
## Rückfragen
- Reichen 96 Ticks pro Viertel? Für Swing und feines Verschieben von Hand wären 480 üblicher.
- Taktarten wie 3/4 und 7/8 gleich von Anfang an, oder reicht zuerst 4/4?
- Swing (jede zweite Achtel oder Sechzehntel etwas später): gleich mit einplanen oder später?
meleki
added the new label 2026-10-06 07:02:12 +02:00
Vermerk aus #327: Der Delay-Node in Sound FX kennt seine Time vorerst nur in Sekunden (0,01 bis 2 s). Sobald es mit dem Scheduler ein Tempo gibt, soll Time auch in Schlägen gehen (1/4, 1/8, punktiert …). Bewusst nicht schon mit einem eigenen Feld Tempo am Delay gebaut, damit es später nicht zwei Quellen für das Tempo gibt.
Offen dazu: Woher kennt ein Delay in einer Stimme, die von Play sound gestartet wurde (nicht vom Scheduler), das Tempo? Vorschlag: Schläge gelten nur, wenn die Stimme aus einem Song/Pattern kommt, sonst fällt der Delay auf Sekunden zurück.
Vermerk aus #327: Der Delay-Node in Sound FX kennt seine *Time* vorerst nur in Sekunden (0,01 bis 2 s). Sobald es mit dem Scheduler ein Tempo gibt, soll *Time* auch in Schlägen gehen (1/4, 1/8, punktiert …). Bewusst nicht schon mit einem eigenen Feld *Tempo* am Delay gebaut, damit es später nicht zwei Quellen für das Tempo gibt.
Offen dazu: Woher kennt ein Delay in einer Stimme, die von *Play sound* gestartet wurde (nicht vom Scheduler), das Tempo? Vorschlag: Schläge gelten nur, wenn die Stimme aus einem Song/Pattern kommt, sonst fällt der Delay auf Sekunden zurück.
core/music/sequence.ts: die Noten, die in einem Fenster der Spielzeit starten. Über das Loop-Ende hinaus schaut es in die nächste Runde, die Naht hat keine Lücke. Eine Note über das Loop-Ende hinaus endet dort.
core/music/Scheduler.ts plant alle 25 ms die nächsten 100 ms auf der Audio-Uhr ein. Die erste Note klingt 50 ms nach dem Start. Änderungen an der Sequenz wirken ab dem nächsten Fenster. Stopp und Springen blenden aus, was er gestartet hat. Ein neues Tempo oder ein neuer Loop zählt ab dem schon Eingeplanten.
Position = Audio-Uhr minus outputLatency, als Takt, Schlag und Tick.
Der Takt kommt aus einem Web Worker (viewport/musicTimer.ts), der auch in einem Hintergrund-Tab sein Tempo hält.
Player: Eine Note startet auf das Sample genau (Buffer-Source per start(when), das Pitch-Worklet zählt im Render-Block herunter). Das Ende wird beim Start auf der Audio-Uhr gesetzt, auch für Play note mit duration. Nur Loop, then release wartet beim Verlassen der Schleife auf die Seite.
Delay: neue Einstellung Sync (Seconds / Beats). Bei Beats ist Time in Vierteln. Eine eingeplante Note trägt das Tempo ihrer Sequenz.
Gesteuert wird der Scheduler bisher nur über den Test-Hook __gm.music.
Grenzen
Die Delay-Leitung hat höchstens 2 s. Das Feld Time reicht jetzt bis 8, damit es in Schlägen passt. Bei Seconds steht dann z. B. 5 im Feld, gespielt werden 2 s. Steht so im Wiki.
Kein Swing, keine Bedienung im Editor (#333/#334).
Fund in CI
Task 526 (Suite zu 6f1f315) hatte drei rote Checks in e2e 34 (Quad, Step, Play note). Sie haben mit der Uhr der Seite gewartet, gemessen wird aber auf der Audio-Uhr. In der CI war die Stimme beim ersten Hinsehen schon 0,7 s alt. stats().last meldet jetzt at (Startzeit auf der Audio-Uhr), die drei Checks prüfen gegen diese Zeit. Der Fix ist im selben Commit.
Gegenproben: vier Stellen einzeln verbogen, es fallen 1, 6, 1 und 2 Tests, Dateien per cmp zurück
e2e 34 auf melektaus-atx: ALL PASSED, 47 Checks (+2). Neun Noten eines geloopten Takts bei 150 BPM liegen exakt 0,4 s auseinander, auch über die Naht, jede 89 bis 91 ms vorher eingeplant. Die Position stimmt mit der Audio-Uhr minus 32 ms Latenz auf den Tick. Offline setzt das Worklet bei den Samples 590, 12000 und 15200 ein.
Gate auf melektaus-atx: CHECK EXIT 0, 244 Testdateien, 3786 Tests
Nicht geprüft: Gehört hat niemand etwas, Firefox lief nicht. Für die drei umgebauten CI-Checks gab es keine Gegenprobe. Der CI-Lauf zu 23bdc3f war beim Schließen nicht gelesen.
Umgesetzt in 23bdc3f, Doku im Wiki auf [Instruments](https://git.nockal.com/meleki/game-maker/wiki/Instruments) (Abschnitt *Playing in time*) und [Sounds](https://git.nockal.com/meleki/game-maker/wiki/Sounds) (Wiki-Commit fd1bb5b).
## Deine Entscheidungen
| # | Frage | Antwort |
|---|---|---|
| 1 | Zeitraster | **480 Ticks pro Viertel** |
| 2 | Taktarten | gleich frei (3/4, 6/8, 7/8 …), das Tempo zählt immer Viertel pro Minute |
| 3 | Swing | später, mit #333/#334 |
| 4 | *Time* am Delay in Schlägen | jetzt, mit #332 |
| 4a | Tempo einer Stimme außerhalb eines Songs | Eingang `tempo` an *Play sound* und *Play note*, unverbunden 120 BPM |
| 4b | Eingabe in Schlägen | als Zahl in Vierteln (0,75 = punktierte Achtel) |
| 5 | Was man danach hört | nur Grundlage und Tests, hörbar ab #333 |
## Was jetzt gilt
- **`core/music/sequence.ts`**: die Noten, die in einem Fenster der Spielzeit starten. Über das Loop-Ende hinaus schaut es in die nächste Runde, die Naht hat keine Lücke. Eine Note über das Loop-Ende hinaus endet dort.
- **`core/music/Scheduler.ts`** plant alle 25 ms die nächsten 100 ms auf der Audio-Uhr ein. Die erste Note klingt 50 ms nach dem Start. Änderungen an der Sequenz wirken ab dem nächsten Fenster. Stopp und Springen blenden aus, was er gestartet hat. Ein neues Tempo oder ein neuer Loop zählt ab dem schon Eingeplanten.
- **Position** = Audio-Uhr minus `outputLatency`, als Takt, Schlag und Tick.
- **Der Takt kommt aus einem Web Worker** (`viewport/musicTimer.ts`), der auch in einem Hintergrund-Tab sein Tempo hält.
- **Player**: Eine Note startet auf das Sample genau (Buffer-Source per `start(when)`, das Pitch-Worklet zählt im Render-Block herunter). Das Ende wird beim Start auf der Audio-Uhr gesetzt, auch für *Play note* mit `duration`. Nur *Loop, then release* wartet beim Verlassen der Schleife auf die Seite.
- **Delay**: neue Einstellung *Sync* (*Seconds* / *Beats*). Bei *Beats* ist *Time* in Vierteln. Eine eingeplante Note trägt das Tempo ihrer Sequenz.
- Gesteuert wird der Scheduler bisher nur über den Test-Hook `__gm.music`.
## Grenzen
- Die Delay-Leitung hat höchstens 2 s. Das Feld *Time* reicht jetzt bis 8, damit es in Schlägen passt. Bei *Seconds* steht dann z. B. 5 im Feld, gespielt werden 2 s. Steht so im Wiki.
- Kein Swing, keine Bedienung im Editor (#333/#334).
## Fund in CI
Task 526 (Suite zu `6f1f315`) hatte drei rote Checks in e2e 34 (Quad, Step, *Play note*). Sie haben mit der Uhr der Seite gewartet, gemessen wird aber auf der Audio-Uhr. In der CI war die Stimme beim ersten Hinsehen schon 0,7 s alt. `stats().last` meldet jetzt `at` (Startzeit auf der Audio-Uhr), die drei Checks prüfen gegen diese Zeit. Der Fix ist im selben Commit.
## Tests
- `music.test.ts` (neu): 17 Tests, Zeitrechnung, Fenster, Loop-Naht, Scheduler mit zappelndem Timer, Stopp, Springen, Tempowechsel
- sound-library +4, soundfx +1, node-catalog +2
- Gegenproben: vier Stellen einzeln verbogen, es fallen 1, 6, 1 und 2 Tests, Dateien per `cmp` zurück
- e2e 34 auf `melektaus-atx`: ALL PASSED, 47 Checks (+2). Neun Noten eines geloopten Takts bei 150 BPM liegen exakt 0,4 s auseinander, auch über die Naht, jede 89 bis 91 ms vorher eingeplant. Die Position stimmt mit der Audio-Uhr minus 32 ms Latenz auf den Tick. Offline setzt das Worklet bei den Samples 590, 12000 und 15200 ein.
- Gate auf `melektaus-atx`: CHECK EXIT 0, 244 Testdateien, 3786 Tests
**Nicht geprüft:** Gehört hat niemand etwas, Firefox lief nicht. Für die drei umgebauten CI-Checks gab es keine Gegenprobe. Der CI-Lauf zu 23bdc3f war beim Schließen nicht gelesen.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Teil von #330. Patterns und Songs müssen sample-genau im Takt spielen, unabhängig von der Bildrate und von Hängern im Spiel. Das hat der Pitch-Sweep nach #315 gezeigt: Was in Spielzeit gezählt wird, läuft gegen den Ton weg.
Vorschlag
fixedStepsinsrc/core/time.ts.start(when)und Rampen auf der Audio-Uhr eingeplant. Der Takt dafür kommt aus einem Timer, der auch in einem Hintergrund-Tab weiterläuft.requestAnimationFramesteht dort still.Haken
outputLatency. Der Playhead im Editor muss das abziehen, sonst läuft er dem Ton voraus.OfflineAudioContextauch der Klang selbst.Rückfragen
Vermerk aus #327: Der Delay-Node in Sound FX kennt seine Time vorerst nur in Sekunden (0,01 bis 2 s). Sobald es mit dem Scheduler ein Tempo gibt, soll Time auch in Schlägen gehen (1/4, 1/8, punktiert …). Bewusst nicht schon mit einem eigenen Feld Tempo am Delay gebaut, damit es später nicht zwei Quellen für das Tempo gibt.
Offen dazu: Woher kennt ein Delay in einer Stimme, die von Play sound gestartet wurde (nicht vom Scheduler), das Tempo? Vorschlag: Schläge gelten nur, wenn die Stimme aus einem Song/Pattern kommt, sonst fällt der Delay auf Sekunden zurück.
Umgesetzt in
23bdc3f, Doku im Wiki auf Instruments (Abschnitt Playing in time) und Sounds (Wiki-Commit fd1bb5b).Deine Entscheidungen
tempoan Play sound und Play note, unverbunden 120 BPMWas jetzt gilt
core/music/sequence.ts: die Noten, die in einem Fenster der Spielzeit starten. Über das Loop-Ende hinaus schaut es in die nächste Runde, die Naht hat keine Lücke. Eine Note über das Loop-Ende hinaus endet dort.core/music/Scheduler.tsplant alle 25 ms die nächsten 100 ms auf der Audio-Uhr ein. Die erste Note klingt 50 ms nach dem Start. Änderungen an der Sequenz wirken ab dem nächsten Fenster. Stopp und Springen blenden aus, was er gestartet hat. Ein neues Tempo oder ein neuer Loop zählt ab dem schon Eingeplanten.outputLatency, als Takt, Schlag und Tick.viewport/musicTimer.ts), der auch in einem Hintergrund-Tab sein Tempo hält.start(when), das Pitch-Worklet zählt im Render-Block herunter). Das Ende wird beim Start auf der Audio-Uhr gesetzt, auch für Play note mitduration. Nur Loop, then release wartet beim Verlassen der Schleife auf die Seite.__gm.music.Grenzen
Fund in CI
Task 526 (Suite zu
6f1f315) hatte drei rote Checks in e2e 34 (Quad, Step, Play note). Sie haben mit der Uhr der Seite gewartet, gemessen wird aber auf der Audio-Uhr. In der CI war die Stimme beim ersten Hinsehen schon 0,7 s alt.stats().lastmeldet jetztat(Startzeit auf der Audio-Uhr), die drei Checks prüfen gegen diese Zeit. Der Fix ist im selben Commit.Tests
music.test.ts(neu): 17 Tests, Zeitrechnung, Fenster, Loop-Naht, Scheduler mit zappelndem Timer, Stopp, Springen, Tempowechselcmpzurückmelektaus-atx: ALL PASSED, 47 Checks (+2). Neun Noten eines geloopten Takts bei 150 BPM liegen exakt 0,4 s auseinander, auch über die Naht, jede 89 bis 91 ms vorher eingeplant. Die Position stimmt mit der Audio-Uhr minus 32 ms Latenz auf den Tick. Offline setzt das Worklet bei den Samples 590, 12000 und 15200 ein.melektaus-atx: CHECK EXIT 0, 244 Testdateien, 3786 TestsNicht geprüft: Gehört hat niemand etwas, Firefox lief nicht. Für die drei umgebauten CI-Checks gab es keine Gegenprobe. Der CI-Lauf zu
23bdc3fwar beim Schließen nicht gelesen.