Flashback na ZX Spectrum Next / 4. díl
Flashback na ZX Spectrum Next – 4. Zvuk
Na konci třetího dílu jsem slíbil zvuk. Je to jediná část portu, kde nejde provést testy automaticky. Místnost se dá porovnat se snímkem pixel po pixelu a entita s referencí pole po poli. Zvuk vzniká tak, že se do DAC dvanáct tisíckrát za sekundu zapíše bajt, a co z toho vyjde, ví jen ten, kdo sedí u reproduktorů. Kontroly umí porovnat bajty, které by DAC dostal. Jestli to hraje, neumí říct žádná.
O tom, jak se na Nextu krmí DAC, jak se hraje přes DMA a jak se míchá v osmi bitech, jsem už psal v článku Pixel Wheels – DAC, DMA a trocha mixování, takže to tu opakovat nebudu. Tenhle díl je o tom, čím je Flashback jiný: o efektech, které začaly hrát obsah zásobníku, o hudbě, která zabrala DMA, o patnácti minutách hudby přehraných už při sestavení, o cutscénách, které měří čas podle hudby, a o namluvených textech z CD verze. A na konci je video, kde je to poprvé slyšet.
Efekty, které hrály zásobník
Zvukové efekty Flashbacku jsou v souboru GLOBAL.FIB. Je jich 66, šedesát z nich má data a dohromady dávají kolem dvou set tisíc vzorků na 6 kHz. Formát je fibonacciovská delta: první bajt je vzorek sám a každý další nese dva čtyřbitové kroky do tabulky šestnácti hodnot – −34, −21, −13, −8, −5, −3, −2, −1, 0, 1, 2, 3, 5, 8, 13, 21. Velké kroky jsou řídké a malé husté, takže se do čtyř bitů vejde jak prudký náraz, tak pozvolné doznívání. Nejdelší efekt má 9 271 vzorků, tedy půldruhé sekundy, a nevejde se do jedné 8KB stránky. Převodník proto efekty skládá těsně za sebe a přehrávač jde po stránkách.
První verze hrála efekty přes DMA, stejně jako Pixel Wheels: blok za snímek a tempo podle předděličky DMA. Znělo to ošklivě a basově. Napřed jsem přehrávání přesunul pod přerušení snímku, aby blok nepřicházel pozdě, když se tik nevejde do snímku – a nezměnilo se nic. Pak jsem našel chybu v konstantě. Dokumentovaný vzorec říká, že DMA přenáší 875 000 / předdělička bajtů za sekundu. Pod CSpectem jsem to změřil proti rastru a pořád to vycházelo blíž 797 000. Při předděličce spočítané podle vzorce se tak do snímku vešlo jen 109 bajtů ze 120 a zbylých jedenáct zahodil další blok. To je padesát zlomů za sekundu přes všechno, co hraje. Oprava nespočívá v jiné konstantě. Přehrávač se teď při startu stroje zeptá: pošle přenos známé délky, změří ho ve snímcích a předděličku z toho vypočítá. Na emulátoru i na skutečném stroji pak platí to, co se změřilo.
Obě opravy byly správné a ani jedna na zvuku nezměnila vůbec nic. Právě to bylo vodítko: když dvě nezávislé opravy časování nemají žádný účinek, chyba není v časování, ale v datech. Převodník zapisoval do indexu stránku efektu relativně k první zvukové stránce a přehrávač ji mapoval jako absolutní číslo. Nejdelší efekt tím připadl na stránku 7, tedy na zásobník. DAC hrál obsah zásobníku správnou rychlostí, ve správné délce a na správném přerušení. Našel jsem to až buildem, který pustí skutečné přehrávací rutiny bez DMA a bez přerušení a všechno, co by šlo do DAC, zapíše do souboru. Vyšlo 9 271 bajtů, přesně správný počet, a všechny špatně od nultého. Pythonový model téhož přitom prošel všech 66 efektů. Modeloval totiž index proti jednomu souvislému bloku dat, a ten žádný základ stránky nepotřebuje. Model i Z80 se na indexu shodly a oba vynechaly totéž.
Patnáct minut hudby přehraných při sestavení
Hudba Flashbacku je 21 modulů ve formátu ProTracker. Modul ale ještě není zvuk: jsou to čtyři stopy not a jednatřicet krátkých vzorků, kterými se ty noty hrají. Na PC je přehrává mixer, který padesátkrát za sekundu spočítá další kus zvuku. Na Nextu na to není čas, protože cutscéna se do svého snímku nevejde už teď. Next má ale kartu. Patnáct minut hudby přehraných jednou, při sestavení, a uložených jako hotové vzorky je pár megabajtů souboru, který Z80 jen čte.
tools/build_music.py je upstreamový mod_player.cpp přepsaný řádek po řádku do Pythonu: tatáž tabulka period, tytéž efekty a tatáž celočíselná aritmetika ve stejném pořadí. Míchá rovnou na 12 867 Hz, takže se nic nepřevzorkovává. Kde modul končí a kam se vrací, se nepředpokládá, ale zjistí: smyčka začíná na řádku, na který se modul vrátí, ať tam dojde koncem tabulky pořadí, nebo skokem. Výsledný music.bin leží na kartě vedle hry a má 14,1 MB. Patnáct minut v něm zabírají moduly a zbytek do devatenácti minut je hudba hry samotné a namluvené texty, ke kterým se ještě dostanu.
- Při sestavení21 modulů projde upstreamovým přehrávačem v Pythonu a do
music.binse zapíšou hotové vzorky na 12 867 Hz. - KartaPo sektorech, 512 bajtů naráz, do kruhu 64 slotů po 256 vzorcích. Dvě stránky paměti, sekunda a čtvrt hudby do zásoby.
- MícháníSlot hudby se zkopíruje do volného bufferu jedním
LDIRa navrch se přičtou až čtyři efekty. Efekty mají poloviční frekvenci, takže se každý jejich vzorek přičte dvakrát. - PřerušeníCTC 12 867krát za sekundu pošle jeden bajt do DAC. Na konci bloku spustí další míchání a čtení.
Hudba se čte jako jediná věc v celém portu s povolenými přerušeními, jinak by každé čtení byla díra v tom, co se právě čte. Jedno odpoledne jsem kvůli tomu ztratil. Hra se zasekávala zhruba při každém třetím startu a záznam zaseknuté hry ukazoval namapovanou ROM, rozdělaný blok a ukazatel zásobníku v ROM. Přesně tak by vypadalo přerušení, které přišlo cestou do RST $08. Čtení jsem tedy obalil zakázanými přerušeními – a skutečná příčina byla úplně jinde. Proměnná úrovně stmívání palety ležela mezi dvěma rutinami a procesor ji občas vykonal jako instrukci. Po její opravě proběhlo čtyřiadvacet startů po sobě čistě i s časovačem běžícím přes čtení. Záznam o tom, kde se program zastavil, není záznam o tom, proč se zastavil. Později jsem stejnou cestou pustil i čtení místností, levelů a scén: hudba při nich hraje z kruhu dál a doplní se, až esxDOS skončí, protože do esxDOS se nesmí vstoupit podruhé z přerušení, zatímco v něm hlavní program je.
Hudba intra pak praskala, a ne v přehrávači. Pro tenhle případ mám build, který počítá dva způsoby, jak může zvuk zaškobrtnout: blok, který se začal hrát dřív, než byl namíchaný, a blok ticha, protože kruhu došla data z karty. Za celé INTRO1 ukázal nulu u obou. Praskání tedy bylo už v datech. Upstream sčítá čtyři stopy se saturací na šestnácti bitech, každá z nich umí sama zaplnit celý rozsah a 3,6 % vzorků sedí na dorazu dřív, než se přimíchá jediný efekt. Já jsem stopy sečetl celé a výsledek vydělil dvěma, abych nechal místo pro efekty. Ani to nestačilo: špičky intra šly na 219 ze 127 a ořez na osmi bitech je slyšet jako praskot. Převodník teď místo ořezu používá omezovač. Blok 64 vzorků, tedy pět milisekund, dostane takové zesílení, jaké dovolí jeho nejhlasitější vzorek a vzorky sousedních bloků. Zesílení klesne o blok dřív, než přijde špička, a během osmdesáti milisekund se vrátí. Co se vejde, toho se omezovač nedotkne.
A pak je tu hudba, která moduly vůbec nejsou. Když si Conrada všimne stráž nebo povolí dveře, skript „přehraje zvuk“ 68 až 75 a upstream ho předá druhému, mnohem menšímu přehrávači: tři kanály, pět nástrojů a partitura po dvou bajtech na kanál a řádek. Všechno to jsou tabulky přímo ve zdrojácích upstreamu, ne v datech hry. Převádějí se stejně a v adresáři mají místo smyčky „hraje jednou“. Právě tady se ukázalo, že fork REminiscence, ze kterého vycházím, má v staticres.cpp na stovkách míst 0xFF přepsané na 0x27, zřejmě nějakým hromadným nahrazováním. Jeden nástroj měl hlavičku mimo o deset tisíc, 134 bajtů vzorků mělo +39 místo −1 a každý z nich cvakal. Znělka 73 se po opravě liší v 93 % vzorků. Reference i Z80 četly tatáž poškozená data, takže to žádná kontrola vidět nemohla. Našlo se to náhodou, při čtení kódu, a tools/upstream_fixes.py teď opravy vrací zpátky a selže na každé, kde v datech nenajde 0x27.
Hodiny, které jdou podle hudby
Když jsem si intro poprvé pustil celé, hudba neseděla na zásah vznášedla. Výbuch přišel dlouho poté, co ho hudba zahrála. A jak se ukázalo, byly to tři chyby, každá skrytá pod tou předchozí.
První byla rychlost kreslení. Next kreslí honičku v intru dvakrát až třikrát pomaleji, než upstream předepisuje: INTRO1 a INTRO2 trvaly 24,4 a 40,8 s místo 20,2 a 28,3. Hodiny scény teď nic neodpouštějí. Když je scéna o půl snímku nebo víc pozadu, další snímek vynechá – opkódy se přečtou všechny, pozadí se postaví, jen se nic nezobrazí – a to nejvýš třikrát za sebou. Obě scény pak končily do tří snímků od upstreamového času.
A tím se ukázalo, že obraz je napřed. Upstream počítá šedesátinu sekundy v celých milisekundách, 1000 / 60 = 16, takže každá scéna u něj běží o čtyři procenta rychleji. A moje hodiny počítaly blok zvuku za dvacet milisekund, i když 256 vzorků na 12 867 Hz je 19,896 ms. Po opravě obou běžely scény přesně na šedesátiny a obraz byl pořád napřed.
Třetí oprava proto nevychází z upstreamu, ale z hudby. Při tiku 52/3 ms, tedy 17,33, se potkají tři nezávislá místa. Záblesk zásahu – snímek 300 INTRO2, 2 713 tiků od začátku intra – padne na náraz v hudbě ve 47,0 s. Intro skončí v 52,5 s, když jeho hudba dozní. A loga skončí ve 14,35 s, zatímco jejich hudba ve 14,6. Při šedesátinách přijdou všechna tři o sekundy dřív a při žádném rychlejším tiku se nepotkají. Upstream je pro mě reference ve všem, ale jeho časová konstanta z originálu není, a kontrola proti referenci by správnou hodnotu nenašla nikdy. Musela ji najít hudba.
Aby to vydrželo, počítá cutscéna čas v blocích zvuku, ne ve snímcích. Každý blok se do hodin připíše jako 20 ms, chybějících 0,104 ms se střádá v 65 536tinách milisekundy a odečte se, jakmile se nasbírá celá. Obraz se tak od hudby nemůže rozejít, ať časovač na daném stroji jde jakkoli – i kdyby kalibrace minula o ty dvě procenta, posunou se obraz i hudba spolu. Mezi INTRO1 a INTRO2 obraz zhasne, hudba hraje dál, druhá scéna se načte potmě a její hodiny navážou tam, kde skončily první. Ztracený čas dožene vynecháním několika snímků na začátku. Celé intro končí v 52,55 s proti 52,50 s v hudbě.
Hlasy z CD
CD verze Flashbacku namlouvá příběhové texty: Jack se ptá, jestli Conrad shání falešné papíry, a u jádra planety se ozve vnitřní hlas. Soubor VOICE.VCE má 32 MB a obsahuje nahrávku každé stránky 45 textů. Je to pět prokládaných proudů v blocích po 10 240 bajtech. Z každého bloku je hlas jen posledních 2 048 bajtů a nic jiného upstream nečte. Bajt hlasu je znaménko a velikost: bit 7 znamená záporné číslo a 0x80 je nula.
Upstream hlasy přehrává na 32 000 Hz. Číslo v kódu ale ještě nic nedokazuje, a co ho mělo potvrdit, si odporovalo. Tempo řeči vycházelo při 32 kHz na rozumných 16,7 znaku za sekundu. Výška hlasu, medián 187 Hz u skoro čistě mužského obsazení, ukazovala spíš na 22 kHz. Rozhodl brum sítě. V tichých místech nahrávek je slyšet bzučení 52 Hz a jeho liché násobky 156 a 260 Hz. Je to brum sítě ve studiu, a jestli se nahrávalo tam, kde Delphine sídlila, tedy ve Francii s 50Hz sítí, je skutečná frekvence kousek pod 32 000, kolem 31 kHz – blízko číslu, na kterém hlasy hraje upstream i port. Při 22 050 Hz by brum vyšel na 73 Hz. Výška hlasu i tempo řeči závisejí na hercích. Brum je fyzikální konstanta místnosti, ve které se nahrávalo.
Hlas nejde vlastní cestou, ale stejným kruhem jako hudba. Převodník každou stránku převede na frekvenci DAC (poměr 875/2176 je mezi 32 000 a 12 867 Hz přesný) a připíše ji do music.bin jako kus, který hraje jednou. Je to 95 stránek, 201,6 s řeči. Pod textem příběhu ve hře jinak nic nehraje, kromě pár sekund znělky, když si Conrada všimne stráž, a tu hlas utne. Tři stránky žádnou nahrávku nemají – dva texty z jediné mezery a první stránka „An inner voice interrupts your thoughts.“ – a ty čekají na klávesu. Upstream je otočí po 80 ms, ale to je jen důsledek malloc(0), ne záměr originálu. Kontrola převádí každou stránku zpátky na 32 kHz a porovnává ji s úsekem VOICE.VCE, který si vyřízne vlastní aritmetikou. Nejmenší shoda je 0,997, zatímco dvojkový doplněk místo znaménka a velikosti, vedlejší proud nebo špatná frekvence neprojdou.
Poprvé slyšet
K tomuhle dílu patří video intra se zvukem a to jsem musel nejdřív natočit. CSpect video nahrávat neumí. Pustil jsem ho tedy Pustil jsem ho tedy na skutečném Nextu, nahrál na mobil a zkusil na zkoušku projít první level - protože jak víte z předchozích dílů tohoto seriálu, tak vykreslení herních obrazovek, včetně logiky chování jednotlivých entit máme hotové. Teda nevím, jestli jste to věděli, ale teď to víte.
Největší zádrhel byl jak už asi tušíte sesynchronizovat hudbu a efekty s obrazem. Cut scény mám hotové, trochu jsem se pral s optimalizací, ale nakonec se to povedlo - přesto jsou v těchto scénach situace, kdy prostě Next nestíhá (možná je to způsobeno mým kódem), ale udělal jsem maximum proto, aby cutscény odpovídali originálu - přesto se mi to myslím povedlo a ani si nevšimnete, že v náročných scénách zámerně zahodím některý snímek, abych se dostal se zvukem do bodu, kdy při zaznění výbuchu z reproduktorů se v animaci trefí kanón do Conradovi vesmírné motorky (nebo co to je :) ).
To měření neříká, že 17,33 ms je ten správný tik. To rozhodla hudba a tohle číslo z ní bylo vybrané. Říká jen, že stroj umí čas, který jsem mu zadal, udržet i ve chvíli, kdy kreslí dvakrát až třikrát pomaleji, než by měl. Na to je potřeba jiná kontrola než na správnost samotného čísla, a obě se snadno zamění.
A co teď?
Zvuk je hotový, cut scény také (byť tam si nechávám pořád otevřené vrátka na případnou optimalizaci), vykreslení levelů, chování entit a podobné srandy jsou hotové taky... teď to jen vše pěkně pospojovat a zařídit, aby vše bylo pěkně pohromadě funkční. Na videu, vidíte jen část prvního levelu (je to jen ukázka, více jsem nestihl) - hru jsem do tohoto stavu dostal doslova před pár desítkami minut - ráno jsem šel spát ve 4 hodiny, abych vstal do práce v 5... Těžký život má programátor ZX Spectra Next.


Komentáře
Zatím tu nikdo nic nenapsal. Buď první!