Abgespalten aus #325 (Frage 6, Antwort: eigenes Issue).
Problem
Jeder Sound wird beim ersten Abspielen ganz geladen und in ein AudioBuffer entpackt (SoundLibrary.load, decodeAudioData). Bei kurzen Effekten ist das richtig. Bei langer Musik kostet es viel Speicher: 5 Minuten Stereo bei 48 kHz sind als Float32 etwa 115 MB, bevor der erste Ton zu hören ist.
Idee
Lange Sounds über ein <audio>-Element mit MediaElementAudioSourceNode spielen. Der Browser streamt und dekodiert dann nur, was gerade gebraucht wird.
Der Weg danach bleibt gleich: Sound FX, Bus und Master aus #325.
Offene Fragen
Wann wird gestreamt: ab einer Länge, ab einer Dateigröße oder per Schalter am Sound-Asset?
Was geht beim Streamen nicht, und wie zeigt der Editor das? Kandidaten:
sample-genaues Timing im Song (at auf der Audio-Uhr)
Wie verhält sich ein gestreamter Sound bei Pause und Stopp?
Fertig, wenn
eine 5-Minuten-Datei spielt, ohne dass der Speicher um die Größe der entpackten Datei wächst (gemessen)
Bus und Master aus #325 auch für gestreamte Sounds gelten
Abgespalten aus #325 (Frage 6, Antwort: eigenes Issue).
## Problem
Jeder Sound wird beim ersten Abspielen ganz geladen und in ein `AudioBuffer` entpackt (`SoundLibrary.load`, `decodeAudioData`). Bei kurzen Effekten ist das richtig. Bei langer Musik kostet es viel Speicher: 5 Minuten Stereo bei 48 kHz sind als Float32 etwa 115 MB, bevor der erste Ton zu hören ist.
## Idee
- Lange Sounds über ein `<audio>`-Element mit `MediaElementAudioSourceNode` spielen. Der Browser streamt und dekodiert dann nur, was gerade gebraucht wird.
- Der Weg danach bleibt gleich: Sound FX, Bus und Master aus #325.
## Offene Fragen
- Wann wird gestreamt: ab einer Länge, ab einer Dateigröße oder per Schalter am Sound-Asset?
- Was geht beim Streamen nicht, und wie zeigt der Editor das? Kandidaten:
- Pitch über den Worklet (braucht die Samples)
- Schnitt mit Fades und *Reverse* (#343, #345, #321)
- sample-genaues Timing im Song (`at` auf der Audio-Uhr)
- Wie verhält sich ein gestreamter Sound bei Pause und Stopp?
## Fertig, wenn
- eine 5-Minuten-Datei spielt, ohne dass der Speicher um die Größe der entpackten Datei wächst (gemessen)
- Bus und Master aus #325 auch für gestreamte Sounds gelten
Überarbeitet am 09.10. nach deinen Ergänzungen im Chat:
Ziel ist Chrome. Damit fällt Frage 5 (Dateitypen in nginx) weg. Der neue Player holt die Datei selbst, der Typ am Server ist ihm egal.
Statt <audio> ein eigener Stream-Player über WebCodecs. Er ist sample-genau, deshalb haben sich die Empfehlungen zu den Fragen 2 und 4 geändert.
Gestreamt wird Opus in Ogg, wie bei den Bakes in #340. Neu ist Frage 6, ob dafür eine Kopie angelegt wird.
Clips lassen sich baken und dann ebenfalls streamen (Frage 2).
Was ich im Code gefunden habe
Es ist schlimmer als im Issue beschrieben: Nicht erst das erste Abspielen entpackt einen Sound. preloadProjectFiles dekodiert beim Öffnen des Projekts jeden Sound, im Editor (App.ts) wie im veröffentlichten Spiel (play.ts). Entpackte Puffer werden nie freigegeben. Eine 5-Minuten-Datei kostet also ab dem Laden rund 115 MB, solange die Seite offen ist.
Diese Teile brauchen die Samples:
Pitch über den Worklet: pitch-player bekommt die Kanäle geschickt.
Clips (#357) mit Schnitt, Fades und Reverse: cutChannels schneidet aus dem ganzen Puffer.
Sounds auf einer Song-Spur (playClip): Start sample-genau auf der Audio-Uhr, mit Offset und Ausgleich der Latenz (#326, #346).
Die Wellenform im Sound-Tab und im Song (peaks, duration).
Sample im Instrument (Loop-Punkte) und Reverb mit einem Sound als Impuls.
Ohne Samples geht: alles nach der Quelle. Dazu gehören FX-Stufen, Bus, Master und die Abgriffe fürs Spektrum (#374). Pitch geht auch, und zwar über den Shifter der Busse (#346). Der verarbeitet einen laufenden Strom und ist dabei um einen Frame später, bei 48 kHz sind das 2048 Samples = 42,7 ms.
Kompression allein hilft nicht: Ein Opus- oder Ogg-Sound, der per decodeAudioData dekodiert wird, liegt danach genauso groß im Speicher wie ein WAV. Spart Speicher erst zusammen mit Streamen.
Server:Range funktioniert (HTTP 206).
Im Projekt gibt es zurzeit keine lange Datei. Die größte ist ein WAV mit 1,3 MB.
Der Stream-Player
Er holt die Datei stückweise per Range, liest die Ogg-Seiten und lässt AudioDecoder ein paar Sekunden im Voraus dekodieren.
Die dekodierten Samples gehen in einen Ringpuffer in einem Worklet. Der Worklet spielt daraus und gibt den Speicher laufend wieder frei.
Sample-genau: Der Start lässt sich wie heute auf die Audio-Uhr planen, mit Offset und Ausgleich der Latenz. Der Loop springt im Ringpuffer ohne Lücke, der Anfang der Schleife bleibt dafür vorrätig.
Kein Warten aufs Netz: Beim Laden des Projekts werden die ersten 2 s jedes gestreamten Sounds geholt und dekodiert. Der erste Ton kommt so sofort.
Pause: Hält der Kontext an, liest auch der Worklet nicht weiter. Fortgesetzt wird sample-genau an derselben Stelle.
Eine gestreamte Stimme bekommt denselben Source-Vertrag wie heute (start, stop, ended, fade, leave). Dadurch greifen Volume, Stop, let ring, Sound FX, Bus und Master ohne Sonderweg.
Bakes aus #340 streamen immer, sobald es den Player gibt. Sie sind genau der Fall langer Dateien.
Vorgehen
Erst eine Probe, dann bauen. Ein Wegwerfskript im Test-Chrome auf melektaus-atx spielt eine 5-Minuten-Datei einmal dekodiert und einmal gestreamt. Gemessen werden:
der Speicher (RSS) des Renderer-Prozesses
ob AudioDecoder und AudioEncoder dort Opus können, und wie schnell
ob der Worklet unter --mute-audio Pegel an den Analyser liefert, denn sonst lässt sich nichts davon in e2e prüfen
ob der gestreamte Klang sample-genau auf dem dekodierten liegt, am Start und am Loop-Punkt
was context.suspend() mit dem Nachladen macht
Fällt dabei etwas durch, melde ich mich, bevor ich baue.
Bau: zuerst die Opus-Kodierung (gemeinsam mit #340), dann der Player, dann der Schalter am Sound und die Clips.
Tests:
Unit-Tests für die reinen Regeln: welcher Sound auf welchem Weg spielt, Ogg-Seiten lesen, Ringpuffer und Loop-Sprung.
Ein e2e-Check in Modul 34 mit einer 5-Minuten-Datei, die der Seed erzeugt (WAV, Stereo, 8 kHz, 9,6 MB). Entpackt auf 48 kHz wären das wieder rund 115 MB.
Der Check misst den Zuwachs des RSS beim gestreamten Abspielen. Die Gegenprobe ist derselbe Sound dekodiert, dort muss der Zuwachs etwa so groß sein wie die entpackte Datei.
Dazu Pegel am Bus und am Master, Pause, Stop, Loop und der Start auf einer Song-Spur.
Fragen
1. Wann wird gestreamt?
a) Schalter Stream am Sound-Asset, Standard aus. Ab 60 s Länge zeigt der Sound-Tab einen Hinweis darauf. Was beim Streamen anders ist, steht direkt am Schalter. (empfohlen)
a) Der Clip wird gebakt: Schnitt, Fades und Reverse werden vorab zu einer eigenen Opus-Datei gerechnet, mit demselben Mechanismus wie in #340. Der Clip streamt dann auch, sample-genau und auch rückwärts. Ändert sich der Clip, wird neu gebakt. (empfohlen)
b) Sie spielen wie heute dekodiert. Dafür wird die Datei ganz geladen, sobald ein Clip spielt.
c) Ein gestreamter Sound hat keine Clips.
3. Pitch bei einem gestreamten Sound?
a) Über den Shifter der Busse (#346). Die Länge bleibt gleich, der Ton kommt 42,7 ms später. (empfohlen)
b) Pitch wirkt beim Streamen nicht, und die Pitch-Stufe zeigt einen Hinweis.
4. Ein gestreamter Sound auf einer Song-Spur?
a) Er streamt auch im Song. Der Player startet ihn sample-genau im Takt, wie heute. Der Speicher wächst nicht. (empfohlen)
b) Im Song wird er dekodiert gespielt. Der Speicher wächst dann wie heute.
5. Dateitypen am Server? Entfällt, weil das Ziel Chrome ist und der Player die Datei selbst holt. Die nginx.conf bleibt unberührt.
6. Was wird gestreamt?
a) Eine Opus-Kopie, die beim Einschalten von Stream im Browser kodiert und hochgeladen wird (160 kbit/s Stereo, 96 kbit/s Mono, wie in #340). Das Original bleibt für die Wellenform und das Bearbeiten. Ein 5-Minuten-Sound sind dann rund 6 MB Download statt 58 MB als WAV (48 kHz, 16 bit). (empfohlen)
b) Das Original direkt. Dann gehen nur Formate, für die ich einen Leser baue (WAV und Ogg), und WAV bleibt groß im Download.
Das entscheide ich selbst, außer es soll anders sein
Stop sound blendet wie heute aus, danach wird die Stimme freigegeben.
Das Sample im Instrument und der Reverb-Impuls bleiben immer dekodiert.
Für die Wellenform im Editor dekodiere ich nur das Bild, mit 8 kHz. Danach bleiben nur Spitzen und Länge im Speicher, der Puffer wird freigegeben.
Die Opus-Kopie gehört wie jede Datei deinem Ordner (#344). Schaltet man Stream aus, wird sie gelöscht, außer ein Snapshot hält sie noch.
In einem Browser ohne Opus in WebCodecs spielt der Sound dekodiert wie heute. Geprüft wird nur Chrome.
Fertig, wenn
(aus dem Issue) eine 5-Minuten-Datei spielt, ohne dass der Speicher um die Größe der entpackten Datei wächst, gemessen und mit Gegenprobe
(aus dem Issue) Bus und Master aus #325 auch für gestreamte Sounds gelten
Pause, Stop, Loop und der Start auf einer Song-Spur im e2e geprüft sind, Loop und Start sample-genau
das Wiki unter Sounds den Schalter und seine Grenzen beschreibt
## Plan
> **Überarbeitet am 09.10.** nach deinen Ergänzungen im Chat:
> - **Ziel ist Chrome.** Damit fällt Frage 5 (Dateitypen in nginx) weg. Der neue Player holt die Datei selbst, der Typ am Server ist ihm egal.
> - **Statt `<audio>` ein eigener Stream-Player** über WebCodecs. Er ist sample-genau, deshalb haben sich die Empfehlungen zu den Fragen 2 und 4 geändert.
> - **Gestreamt wird Opus in Ogg**, wie bei den Bakes in #340. Neu ist Frage 6, ob dafür eine Kopie angelegt wird.
> - **Clips lassen sich baken** und dann ebenfalls streamen (Frage 2).
### Was ich im Code gefunden habe
- **Es ist schlimmer als im Issue beschrieben:** Nicht erst das erste Abspielen entpackt einen Sound. `preloadProjectFiles` dekodiert beim Öffnen des Projekts **jeden** Sound, im Editor (`App.ts`) wie im veröffentlichten Spiel (`play.ts`). Entpackte Puffer werden nie freigegeben. Eine 5-Minuten-Datei kostet also ab dem Laden rund 115 MB, solange die Seite offen ist.
- **Diese Teile brauchen die Samples:**
- Pitch über den Worklet: `pitch-player` bekommt die Kanäle geschickt.
- Clips (#357) mit Schnitt, Fades und *Reverse*: `cutChannels` schneidet aus dem ganzen Puffer.
- Sounds auf einer Song-Spur (`playClip`): Start sample-genau auf der Audio-Uhr, mit Offset und Ausgleich der Latenz (#326, #346).
- Die Wellenform im Sound-Tab und im Song (`peaks`, `duration`).
- *Sample* im Instrument (Loop-Punkte) und Reverb mit einem Sound als Impuls.
- **Ohne Samples geht:** alles nach der Quelle. Dazu gehören FX-Stufen, Bus, Master und die Abgriffe fürs Spektrum (#374). Pitch geht auch, und zwar über den Shifter der Busse (#346). Der verarbeitet einen laufenden Strom und ist dabei um einen Frame später, bei 48 kHz sind das 2048 Samples = 42,7 ms.
- **Kompression allein hilft nicht:** Ein Opus- oder Ogg-Sound, der per `decodeAudioData` dekodiert wird, liegt danach genauso groß im Speicher wie ein WAV. Spart Speicher erst zusammen mit Streamen.
- **Server:** `Range` funktioniert (HTTP 206).
- Im Projekt gibt es zurzeit keine lange Datei. Die größte ist ein WAV mit 1,3 MB.
### Der Stream-Player
- Er holt die Datei stückweise per `Range`, liest die Ogg-Seiten und lässt `AudioDecoder` ein paar Sekunden im Voraus dekodieren.
- Die dekodierten Samples gehen in einen Ringpuffer in einem Worklet. Der Worklet spielt daraus und gibt den Speicher laufend wieder frei.
- **Sample-genau:** Der Start lässt sich wie heute auf die Audio-Uhr planen, mit Offset und Ausgleich der Latenz. Der Loop springt im Ringpuffer ohne Lücke, der Anfang der Schleife bleibt dafür vorrätig.
- **Kein Warten aufs Netz:** Beim Laden des Projekts werden die ersten 2 s jedes gestreamten Sounds geholt und dekodiert. Der erste Ton kommt so sofort.
- **Pause:** Hält der Kontext an, liest auch der Worklet nicht weiter. Fortgesetzt wird sample-genau an derselben Stelle.
- Eine gestreamte Stimme bekommt denselben `Source`-Vertrag wie heute (start, stop, ended, fade, leave). Dadurch greifen Volume, Stop, *let ring*, Sound FX, Bus und Master ohne Sonderweg.
- **Bakes aus #340 streamen immer**, sobald es den Player gibt. Sie sind genau der Fall langer Dateien.
### Vorgehen
1. **Erst eine Probe, dann bauen.** Ein Wegwerfskript im Test-Chrome auf `melektaus-atx` spielt eine 5-Minuten-Datei einmal dekodiert und einmal gestreamt. Gemessen werden:
- der Speicher (RSS) des Renderer-Prozesses
- ob `AudioDecoder` und `AudioEncoder` dort Opus können, und wie schnell
- ob der Worklet unter `--mute-audio` Pegel an den Analyser liefert, denn sonst lässt sich nichts davon in e2e prüfen
- ob der gestreamte Klang sample-genau auf dem dekodierten liegt, am Start und am Loop-Punkt
- was `context.suspend()` mit dem Nachladen macht
Fällt dabei etwas durch, melde ich mich, bevor ich baue.
2. **Bau:** zuerst die Opus-Kodierung (gemeinsam mit #340), dann der Player, dann der Schalter am Sound und die Clips.
3. **Tests:**
- Unit-Tests für die reinen Regeln: welcher Sound auf welchem Weg spielt, Ogg-Seiten lesen, Ringpuffer und Loop-Sprung.
- Ein e2e-Check in Modul 34 mit einer 5-Minuten-Datei, die der Seed erzeugt (WAV, Stereo, 8 kHz, 9,6 MB). Entpackt auf 48 kHz wären das wieder rund 115 MB.
- Der Check misst den Zuwachs des RSS beim gestreamten Abspielen. Die Gegenprobe ist derselbe Sound dekodiert, dort muss der Zuwachs etwa so groß sein wie die entpackte Datei.
- Dazu Pegel am Bus und am Master, Pause, Stop, Loop und der Start auf einer Song-Spur.
### Fragen
**1. Wann wird gestreamt?**
- **a) Schalter *Stream* am Sound-Asset, Standard aus. Ab 60 s Länge zeigt der Sound-Tab einen Hinweis darauf. Was beim Streamen anders ist, steht direkt am Schalter. (empfohlen)**
- b) Automatisch ab 60 s Länge.
- c) Automatisch ab einer Dateigröße, z. B. 5 MB.
**2. Clips (#357) eines gestreamten Sounds?**
- **a) Der Clip wird gebakt: Schnitt, Fades und *Reverse* werden vorab zu einer eigenen Opus-Datei gerechnet, mit demselben Mechanismus wie in #340. Der Clip streamt dann auch, sample-genau und auch rückwärts. Ändert sich der Clip, wird neu gebakt. (empfohlen)**
- b) Sie spielen wie heute dekodiert. Dafür wird die Datei ganz geladen, sobald ein Clip spielt.
- c) Ein gestreamter Sound hat keine Clips.
**3. Pitch bei einem gestreamten Sound?**
- **a) Über den Shifter der Busse (#346). Die Länge bleibt gleich, der Ton kommt 42,7 ms später. (empfohlen)**
- b) Pitch wirkt beim Streamen nicht, und die Pitch-Stufe zeigt einen Hinweis.
**4. Ein gestreamter Sound auf einer Song-Spur?**
- **a) Er streamt auch im Song. Der Player startet ihn sample-genau im Takt, wie heute. Der Speicher wächst nicht. (empfohlen)**
- b) Im Song wird er dekodiert gespielt. Der Speicher wächst dann wie heute.
**5. ~~Dateitypen am Server?~~** Entfällt, weil das Ziel Chrome ist und der Player die Datei selbst holt. Die `nginx.conf` bleibt unberührt.
**6. Was wird gestreamt?**
- **a) Eine Opus-Kopie, die beim Einschalten von *Stream* im Browser kodiert und hochgeladen wird (160 kbit/s Stereo, 96 kbit/s Mono, wie in #340). Das Original bleibt für die Wellenform und das Bearbeiten. Ein 5-Minuten-Sound sind dann rund 6 MB Download statt 58 MB als WAV (48 kHz, 16 bit). (empfohlen)**
- b) Das Original direkt. Dann gehen nur Formate, für die ich einen Leser baue (WAV und Ogg), und WAV bleibt groß im Download.
### Das entscheide ich selbst, außer es soll anders sein
- *Stop sound* blendet wie heute aus, danach wird die Stimme freigegeben.
- Das *Sample* im Instrument und der Reverb-Impuls bleiben immer dekodiert.
- Für die Wellenform im Editor dekodiere ich nur das Bild, mit 8 kHz. Danach bleiben nur Spitzen und Länge im Speicher, der Puffer wird freigegeben.
- Die Opus-Kopie gehört wie jede Datei deinem Ordner (#344). Schaltet man *Stream* aus, wird sie gelöscht, außer ein Snapshot hält sie noch.
- In einem Browser ohne Opus in WebCodecs spielt der Sound dekodiert wie heute. Geprüft wird nur Chrome.
### Fertig, wenn
- (aus dem Issue) eine 5-Minuten-Datei spielt, ohne dass der Speicher um die Größe der entpackten Datei wächst, gemessen und mit Gegenprobe
- (aus dem Issue) Bus und Master aus #325 auch für gestreamte Sounds gelten
- Pause, Stop, Loop und der Start auf einer Song-Spur im e2e geprüft sind, Loop und Start sample-genau
- das Wiki unter *Sounds* den Schalter und seine Grenzen beschreibt
Opus-Kodierung, Ogg-Seiten und die Ablage als versteckte Datei gibt es schon (src/core/music/ogg.ts, src/viewport/bakeRender.ts). #349 nutzt sie mit.
Ogg wird mit 48 kHz dekodiert, bei 44,1 kHz fehlte pro Loop-Runde ein Sample. Der Stream-Player läuft deshalb auch mit fester Rate 48 kHz.
Ich fange mit der Probe an. Erster Punkt: ob AudioDecoder im Test-Chrome Opus Paket für Paket dekodiert.
Entscheidungen (per Rückfrage am 09.10., überall die Empfehlung):
| # | Frage | Entscheidung |
|---|---|---|
| 1 | Wann wird gestreamt? | Schalter *Stream* am Sound, Standard aus. Ab 60 s Länge zeigt der Sound-Tab einen Hinweis. |
| 2 | Clips eines gestreamten Sounds | Der Clip wird mit dem Mechanismus aus #340 gebakt und streamt dann auch, sample-genau, auch rückwärts. Ändert sich der Clip, wird er neu gebakt. |
| 3 | Pitch | Über den Shifter der Busse (#346), der Ton kommt 42,7 ms später. |
| 4 | Song-Spur | Streamt auch im Song, sample-genau im Takt. |
| 5 | nginx | entfällt (Ziel ist Chrome) |
| 6 | Was wird gestreamt? | Eine Opus-Kopie (160 bzw. 96 kbit/s), das Original bleibt fürs Bearbeiten. |
Seit dem Plan durch #340 geklärt:
- Opus-Kodierung, Ogg-Seiten und die Ablage als versteckte Datei gibt es schon (`src/core/music/ogg.ts`, `src/viewport/bakeRender.ts`). #349 nutzt sie mit.
- Ogg wird mit 48 kHz dekodiert, bei 44,1 kHz fehlte pro Loop-Runde ein Sample. Der Stream-Player läuft deshalb auch mit fester Rate 48 kHz.
Ich fange mit der Probe an. Erster Punkt: ob `AudioDecoder` im Test-Chrome Opus Paket für Paket dekodiert.
Commits: 2764919 (Streamen) und 82e96e6 (Clip-Kopien auch ohne Stream). Wiki: 5515c67, 8a4dde0 (Sounds, Songs, Architecture, Development).
Probe vorab (Test-Chrome auf melektaus-atx)
AudioDecoder kann Opus in Stereo und Mono, ungefähr 100-mal schneller als Echtzeit.
Pre-skip: Ohne description zieht Chrome ihn nicht ab, der Player schneidet ihn selbst.
Mitten in der Datei einsteigen: Mit 32 Paketen (640 ms) Vorlauf ist das Ergebnis bitgleich mit dem Dekodieren von Anfang an. Mit 4 Paketen lag es noch 0,06 daneben.
Der CI-Server ignoriert Range und schickt die ganze Datei. Der Player kommt mit beidem klar.
Der Test-Chrome läuft mit 44,1 kHz. Der Player rechnet von 48 kHz um.
So funktioniert es
Schalter Stream am Sound, standardmäßig aus. Der Sound-Tab sagt ab 60 s „long, try Stream“, danach „copy 40%“ bzw. „streamed“.
Kopie: Der Editor kodiert sie nach jeder Änderung im Browser als Opus (160/96 kbit/s) und lädt sie als „[stream] Name“ hoch. Der Sound-Picker blendet sie aus. Die alte Datei wird gelöscht, sobald das Projekt ohne sie gespeichert ist.
Abspielen: Ein Spiel holt die Kopie stückweise und dekodiert sie etwa 4 s im Voraus. Bus, Master, Sound FX, Pause und Stop gelten wie bei jedem anderen Sound. Auf einer Song-Spur startet er sample-genau im Takt.
Pitch läuft über den Shifter der Busse (#346), mit 42,7 ms Verzögerung.
Was dekodiert bleibt:Sample, der Reverb-Impuls und das Original im Sound-Tab, solange man es bearbeitet.
Nachtrag: Clip-Kopien auch ohne Stream
Entschieden per Rückfrage am 09.10., überall die Empfehlung:
#
Frage
Entscheidung
1
Wann bekommt ein Clip eine Kopie?
Automatisch, jeder Clip jedes Sounds, ohne Schalter
2
Wann wird das volle File noch vorab dekodiert?
Nur wenn das Projekt den Sound selbst nennt, sonst beim ersten Abspielen mit Verzögerung
3
Format
Opus in Ogg, dieselbe Kopie wie beim Streamen
Jeder Clip hat eine Opus-Kopie seines Schnitts. Fades und Reverse sind eingerechnet, Lautstärke und FX des Clips bleiben live. Gültig ist sie, solange der Schnittname passt (Datei, Start, Ende, Fades, Kurven, Reverse).
Stream entscheidet nur noch, ob der Sound selbst eine Kopie bekommt und ob Clips gestreamt oder dekodiert werden.
Ein Clip wird aus seiner Kopie dekodiert. Ist das volle File ohnehin schon dekodiert, wird er wie früher daraus geschnitten. Ist die Kopie nicht zu bekommen, nimmt er das volle File.
Vorab dekodiert wird das volle File nur noch, wenn der Sound keine Clips hat, ein Clip noch keine passende Kopie hat oder das ganze File umfasst, oder die Id des Sounds irgendwo in den Daten eines Assets steht. Diese Suche geht über die Daten, nicht über einzelne Felder, damit keine Stelle fehlt.
Selbst entschieden: Der Bake rechnet weiter aus den Originalen, damit sein Fingerabdruck stimmt.
Zu beachten: Beim Öffnen bestehender Projekte bekommen alle Clips nach und nach eine Kopie, die auch hochgeladen wird („Making a copy of …“ in der Werkzeugleiste).
Abweichung vom Plan
Mono statt Stereo für die 5-Minuten-Datei im e2e: 4,8 MB statt 9,6 MB. Vermutet ist eine Upload-Grenze am PHP-Testserver (Standard 8 MB), gemessen nicht.
Tests
Gate (melektaus-atx) → CHECK EXIT 0, 280 Dateien, 4319 Tests (38 neu in #349)
e2e 34 (melektaus-atx) → ALL PASSED, 108 Checks (9 neu)
e2e 65 (melektaus-atx) → alles grün
Renderer-Zuwachs (e2e 34)
5 min gestreamt
+5,8 MB
derselbe Sound dekodiert (Gegenprobe)
+69,6 MB
Sample-genau offline gegen die ganz dekodierte Kopie: Start bei 0,25 s, Loop-Naht und umgedrehter Clip weichen um 0 ab. Kein Sample kam zu spät. Im Song startet der Clip genau zu seiner Zeit.
Clip ohne Stream: Eine frische Bibliothek dekodiert nur die Kopie des 10-s-Clips, nicht das 5-Minuten-File. Gegen den Schnitt aus dem Original weicht sie höchstens um 0,0091 pro Sample und 0,013 dB je 20 ms ab. Spielt ein Node den Sound selbst, wird das volle File wieder vorab dekodiert.
Gegenproben, alle greifen: ohne Vorlauf, ohne Nachholen am Ende, ohne erstes Stück beim Anlegen, Stream-Sound vorab dekodiert, Bibliothek ignoriert die Clip-Kopien, volles File immer vorab, Clip-Kopien nur mit Stream.
Unterwegs behoben: Der erste Lauf verlor beim Start 557 Samples, weil der Anfang per Nachricht zu spät beim Worklet ankam. Jetzt geht das erste Stück beim Anlegen mit. Außerdem blieb der Sound-Tab auf „copy 100%“ stehen.
Nicht geprüft
wie es klingt (CI ist stumm)
Range am echten nginx im Browser (den Weg in Stücken prüft nur der Unit-Test)
das veröffentlichte Spiel (play.html)
viele Streams gleichzeitig, ein langsames Netz, ein Projekt mit vielen Clips
das Löschen alter Kopien nach dem Speichern im Browser
Firefox und Safari
der CI-Lauf zu 82e96e6 (läuft, nicht abgewartet)
## Umgesetzt
Commits: `2764919` (Streamen) und `82e96e6` (Clip-Kopien auch ohne *Stream*). Wiki: `5515c67`, `8a4dde0` (*Sounds*, *Songs*, *Architecture*, *Development*).
### Probe vorab (Test-Chrome auf `melektaus-atx`)
- **`AudioDecoder` kann Opus** in Stereo und Mono, ungefähr 100-mal schneller als Echtzeit.
- **Pre-skip:** Ohne `description` zieht Chrome ihn nicht ab, der Player schneidet ihn selbst.
- **Mitten in der Datei einsteigen:** Mit 32 Paketen (640 ms) Vorlauf ist das Ergebnis bitgleich mit dem Dekodieren von Anfang an. Mit 4 Paketen lag es noch 0,06 daneben.
- **Der CI-Server ignoriert `Range`** und schickt die ganze Datei. Der Player kommt mit beidem klar.
- **Der Test-Chrome läuft mit 44,1 kHz.** Der Player rechnet von 48 kHz um.
### So funktioniert es
- **Schalter *Stream*** am Sound, standardmäßig aus. Der Sound-Tab sagt ab 60 s „long, try Stream“, danach „copy 40%“ bzw. „streamed“.
- **Kopie:** Der Editor kodiert sie nach jeder Änderung im Browser als Opus (160/96 kbit/s) und lädt sie als „[stream] Name“ hoch. Der Sound-Picker blendet sie aus. Die alte Datei wird gelöscht, sobald das Projekt ohne sie gespeichert ist.
- **Abspielen:** Ein Spiel holt die Kopie stückweise und dekodiert sie etwa 4 s im Voraus. Bus, Master, Sound FX, Pause und Stop gelten wie bei jedem anderen Sound. Auf einer Song-Spur startet er sample-genau im Takt.
- **Pitch** läuft über den Shifter der Busse (#346), mit 42,7 ms Verzögerung.
- **Bakes aus #340 streamen jetzt immer.**
- **Was dekodiert bleibt:** *Sample*, der Reverb-Impuls und das Original im Sound-Tab, solange man es bearbeitet.
### Nachtrag: Clip-Kopien auch ohne *Stream*
Entschieden per Rückfrage am 09.10., überall die Empfehlung:
| # | Frage | Entscheidung |
|---|---|---|
| 1 | Wann bekommt ein Clip eine Kopie? | Automatisch, jeder Clip jedes Sounds, ohne Schalter |
| 2 | Wann wird das volle File noch vorab dekodiert? | Nur wenn das Projekt den Sound selbst nennt, sonst beim ersten Abspielen mit Verzögerung |
| 3 | Format | Opus in Ogg, dieselbe Kopie wie beim Streamen |
- Jeder Clip hat eine Opus-Kopie seines Schnitts. Fades und *Reverse* sind eingerechnet, Lautstärke und FX des Clips bleiben live. Gültig ist sie, solange der Schnittname passt (Datei, Start, Ende, Fades, Kurven, *Reverse*).
- *Stream* entscheidet nur noch, ob der Sound selbst eine Kopie bekommt und ob Clips gestreamt oder dekodiert werden.
- Ein Clip wird aus seiner Kopie dekodiert. Ist das volle File ohnehin schon dekodiert, wird er wie früher daraus geschnitten. Ist die Kopie nicht zu bekommen, nimmt er das volle File.
- **Vorab dekodiert** wird das volle File nur noch, wenn der Sound keine Clips hat, ein Clip noch keine passende Kopie hat oder das ganze File umfasst, oder die Id des Sounds irgendwo in den Daten eines Assets steht. Diese Suche geht über die Daten, nicht über einzelne Felder, damit keine Stelle fehlt.
- **Selbst entschieden:** Der Bake rechnet weiter aus den Originalen, damit sein Fingerabdruck stimmt.
- **Zu beachten:** Beim Öffnen bestehender Projekte bekommen alle Clips nach und nach eine Kopie, die auch hochgeladen wird („Making a copy of …“ in der Werkzeugleiste).
### Abweichung vom Plan
- **Mono statt Stereo** für die 5-Minuten-Datei im e2e: 4,8 MB statt 9,6 MB. Vermutet ist eine Upload-Grenze am PHP-Testserver (Standard 8 MB), gemessen nicht.
### Tests
```
Gate (melektaus-atx) → CHECK EXIT 0, 280 Dateien, 4319 Tests (38 neu in #349)
e2e 34 (melektaus-atx) → ALL PASSED, 108 Checks (9 neu)
e2e 65 (melektaus-atx) → alles grün
```
| Renderer-Zuwachs (e2e 34) | |
|---|---|
| 5 min gestreamt | **+5,8 MB** |
| derselbe Sound dekodiert (Gegenprobe) | **+69,6 MB** |
- **Sample-genau** offline gegen die ganz dekodierte Kopie: Start bei 0,25 s, Loop-Naht und umgedrehter Clip weichen um **0** ab. Kein Sample kam zu spät. Im Song startet der Clip genau zu seiner Zeit.
- **Clip ohne *Stream*:** Eine frische Bibliothek dekodiert nur die Kopie des 10-s-Clips, nicht das 5-Minuten-File. Gegen den Schnitt aus dem Original weicht sie höchstens um 0,0091 pro Sample und 0,013 dB je 20 ms ab. Spielt ein Node den Sound selbst, wird das volle File wieder vorab dekodiert.
- **Gegenproben, alle greifen:** ohne Vorlauf, ohne Nachholen am Ende, ohne erstes Stück beim Anlegen, Stream-Sound vorab dekodiert, Bibliothek ignoriert die Clip-Kopien, volles File immer vorab, Clip-Kopien nur mit *Stream*.
- **Unterwegs behoben:** Der erste Lauf verlor beim Start 557 Samples, weil der Anfang per Nachricht zu spät beim Worklet ankam. Jetzt geht das erste Stück beim Anlegen mit. Außerdem blieb der Sound-Tab auf „copy 100%“ stehen.
### Nicht geprüft
- wie es klingt (CI ist stumm)
- `Range` am echten nginx im Browser (den Weg in Stücken prüft nur der Unit-Test)
- das veröffentlichte Spiel (`play.html`)
- viele Streams gleichzeitig, ein langsames Netz, ein Projekt mit vielen Clips
- das Löschen alter Kopien nach dem Speichern im Browser
- Firefox und Safari
- der CI-Lauf zu `82e96e6` (läuft, nicht abgewartet)
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.
Abgespalten aus #325 (Frage 6, Antwort: eigenes Issue).
Problem
Jeder Sound wird beim ersten Abspielen ganz geladen und in ein
AudioBufferentpackt (SoundLibrary.load,decodeAudioData). Bei kurzen Effekten ist das richtig. Bei langer Musik kostet es viel Speicher: 5 Minuten Stereo bei 48 kHz sind als Float32 etwa 115 MB, bevor der erste Ton zu hören ist.Idee
<audio>-Element mitMediaElementAudioSourceNodespielen. Der Browser streamt und dekodiert dann nur, was gerade gebraucht wird.Offene Fragen
atauf der Audio-Uhr)Fertig, wenn
Plan
Was ich im Code gefunden habe
preloadProjectFilesdekodiert beim Öffnen des Projekts jeden Sound, im Editor (App.ts) wie im veröffentlichten Spiel (play.ts). Entpackte Puffer werden nie freigegeben. Eine 5-Minuten-Datei kostet also ab dem Laden rund 115 MB, solange die Seite offen ist.pitch-playerbekommt die Kanäle geschickt.cutChannelsschneidet aus dem ganzen Puffer.playClip): Start sample-genau auf der Audio-Uhr, mit Offset und Ausgleich der Latenz (#326, #346).peaks,duration).decodeAudioDatadekodiert wird, liegt danach genauso groß im Speicher wie ein WAV. Spart Speicher erst zusammen mit Streamen.Rangefunktioniert (HTTP 206).Der Stream-Player
Range, liest die Ogg-Seiten und lässtAudioDecoderein paar Sekunden im Voraus dekodieren.Source-Vertrag wie heute (start, stop, ended, fade, leave). Dadurch greifen Volume, Stop, let ring, Sound FX, Bus und Master ohne Sonderweg.Vorgehen
Erst eine Probe, dann bauen. Ein Wegwerfskript im Test-Chrome auf
melektaus-atxspielt eine 5-Minuten-Datei einmal dekodiert und einmal gestreamt. Gemessen werden:AudioDecoderundAudioEncoderdort Opus können, und wie schnell--mute-audioPegel an den Analyser liefert, denn sonst lässt sich nichts davon in e2e prüfencontext.suspend()mit dem Nachladen machtFällt dabei etwas durch, melde ich mich, bevor ich baue.
Bau: zuerst die Opus-Kodierung (gemeinsam mit #340), dann der Player, dann der Schalter am Sound und die Clips.
Tests:
Fragen
1. Wann wird gestreamt?
2. Clips (#357) eines gestreamten Sounds?
3. Pitch bei einem gestreamten Sound?
4. Ein gestreamter Sound auf einer Song-Spur?
5.
Dateitypen am Server?Entfällt, weil das Ziel Chrome ist und der Player die Datei selbst holt. Dienginx.confbleibt unberührt.6. Was wird gestreamt?
Das entscheide ich selbst, außer es soll anders sein
Fertig, wenn
Entscheidungen (per Rückfrage am 09.10., überall die Empfehlung):
Seit dem Plan durch #340 geklärt:
src/core/music/ogg.ts,src/viewport/bakeRender.ts). #349 nutzt sie mit.Ich fange mit der Probe an. Erster Punkt: ob
AudioDecoderim Test-Chrome Opus Paket für Paket dekodiert.Umgesetzt
Commits:
2764919(Streamen) und82e96e6(Clip-Kopien auch ohne Stream). Wiki:5515c67,8a4dde0(Sounds, Songs, Architecture, Development).Probe vorab (Test-Chrome auf
melektaus-atx)AudioDecoderkann Opus in Stereo und Mono, ungefähr 100-mal schneller als Echtzeit.descriptionzieht Chrome ihn nicht ab, der Player schneidet ihn selbst.Rangeund schickt die ganze Datei. Der Player kommt mit beidem klar.So funktioniert es
Nachtrag: Clip-Kopien auch ohne Stream
Entschieden per Rückfrage am 09.10., überall die Empfehlung:
Abweichung vom Plan
Tests
Nicht geprüft
Rangeam echten nginx im Browser (den Weg in Stücken prüft nur der Unit-Test)play.html)82e96e6(läuft, nicht abgewartet)