CosmoScroll
18. 9. 2026

Až do téhle aktualizace dělal Pixel Wheels Next všechny zvuky přes AY. Výbuch byl dávka šumu, kulomet kratší dávka šumu a odpočet tři velmi slušně vychovaná pípnutí. Fungovalo to. A pokud jste vyrůstali na 128K Spectru, možná vám to dokonce přišlo naprosto v pořádku. Jenže původní Pixel Wheels má v adresáři opravdové WAVy: výbuchy, smyk, motory… a všechny tam celou dobu ležely a koukaly na mě. Takže jsem udělal to, co už u mě pomalu začíná být tradice: místo abych byl spokojený s funkční verzí, přidal jsem si další práci.

Pixel Wheels Next / zvuk pod kapotou

Pixel Wheels – DAC, DMA a trocha mixování

Až do téhle aktualizace dělal Pixel Wheels Next všechny zvuky přes AY. Výbuch byl dávka šumu, kulomet kratší dávka šumu a odpočet tři velmi slušně vychovaná pípnutí. Fungovalo to. A pokud jste vyrůstali na 128K Spectru, možná vám to dokonce přišlo naprosto v pořádku.

Jenže původní Pixel Wheels má v adresáři opravdové WAVy: výbuchy, smyk, motory… a všechny tam celou dobu ležely a koukaly na mě. Takže jsem udělal to, co už u mě pomalu začíná být tradice: místo abych byl spokojený s funkční verzí, přidal jsem si další práci.

Samply už jsem na Nextu přehrávat uměl. Nová část byla jinde: jak namixovat několik zvuků dohromady tak, aby DAC dostal hotový stream a Z80 se při tom tvářil, že se ho to vlastně netýká.

DAC je ta snadná část

ZX Spectrum Next má vestavěné 8bitové DAC výstupy kompatibilní s tradičním způsobem přehrávání vzorků přes SpecDrum/Covox. Z pohledu procesoru je princip krásně primitivní: zapíšu bajt na správný port a na výstupu zůstane odpovídající úroveň, dokud nepřijde další hodnota.

U unsigned 8bitového PCM je střed kolem hodnoty 128, tedy ticho. Hodnota 255 je jeden kraj rozsahu, 0 druhý. Posílám-li postupně bajty WAVu, dostanu na výstupu jeho průběh.

Háček je samozřejmě v tom slově „postupně“. Pro Pixel Wheels jsem zvolil přibližně 12,8 kHz. To je na dnešní poměry směšně málo, ale pro výbuchy, výstřely, smyk a motor to úplně stačí. 12 800 vzorků za sekundu znamená zhruba jeden bajt každých 78 mikrosekund.

Z80N na 28 MHz takovou frekvenci zvládne. Jen ne současně s tím, že má scrollovat trať, řídit čtyři auta, počítat AI, kolize a sprity. Kdyby audio běželo přes přerušení pro každý vzorek, hra by byla v zásadě velmi sofistikovaný přehrávač WAVů s občasným autem.

DMA udělá tu nudnou práci

Tady přichází na řadu DMA. Má jednu vlastnost, bez které by celé řešení bylo podstatně méně příjemné: prescaler. Místo okamžitého přesunu celého bloku může DMA posílat jeden bajt po pravidelných intervalech odvozených od pevného hodinového základu 875 kHz.

Při dělení hodnotou 68 vychází přibližně 12 868 Hz, tedy dost blízko zamýšleným 12,8 kHz. Jeden blok o 256 bajtech tak trvá přibližně 19,9 ms, což je přesně to, co potřebuji pro 50Hz hru: jeden blok audia na jeden snímek.

Záměrně je o chlup kratší než 20ms frame. Kdyby byl delší, nový blok by mohl přijít ve chvíli, kdy předchozí ještě běží, a jeho konec by se usekl. To by vytvořilo pravidelné lupání nebo bzučení. Kratší blok je bezpečnější: DAC několik desítek mikrosekund podrží poslední hodnotu a pak pokračuje dalším.

CPU tak místo 12 800 jednotlivých zápisů za sekundu jednou za snímek jen připraví DMA registraci dalšího 256bajtového bloku. To už je práce, která se vejde mezi auta, rakety a moje další špatné nápady.

Dva buffery a rytmus

Samply samotné žijí v bankované paměti, ale DMA potřebuje číst z oblasti, kterou má v daný okamžik skutečně dostupnou. Proto audio neposílám přímo z bank se samply. Používám dva 256bajtové buffery v pevně namapované paměti a střídám je.

Hned po začátku snímku pošlu DMA buffer, který už je připravený. Druhý se mezitím naplní daty pro následující frame. Na konci se jejich role prohodí. Důležitý detail je, že spuštění DMA jde co nejdřív, aby mezera mezi jednotlivými bloky zůstávala pravidelná bez ohledu na to, kolik práce měla herní smyčka.

Samotné buffery mi připomněly jednu klasickou pravdu osmibitového programování: paměť máte vždycky – až do chvíle, kdy ji opravdu potřebujete. Pevná oblast pod tabulkou přerušení byla volná přesně asi tak na nic: zbylo sedm bajtů.

Přehrávač, tabulky i buffery tedy dostaly vlastní stránku natrvalo namapovanou do spodních 8 KB, kde normálně sídlí ROM. Hra ROM nepotřebuje… tedy skoro. Po několika zamrznutích jsem si připomněl, že ukládání high-score jde přes esxDOS, který ji potřebuje. Při tomhle volání tedy ROM na chvíli vrátím a potom zase odstraním.

Jestli někdy program na Nextu funguje úplně perfektně až na jednu záhadnou funkci, která se ozve jednou za hodinu, je docela slušná šance, že někde nenápadně čeká ROM.

Jeden sample najednou – a kdo má přednost

DMA je pro audio jen jedno, takže běžný efekt může hrát vždy pouze jeden. Každý zvuk proto dostal prioritu. Výbuch a odpočet jsou nahoře, nárazy, bonusy a turbo uprostřed, kulomet a raketa dole.

Nový efekt může přerušit ten starý, pokud má alespoň stejnou prioritu. Některé odmítnuté zvuky si hra může zapamatovat a pustit později; jiné nemá smysl frontovat. Výstřel, který se měl ozvat během exploze a dostal se ke slovu o půl sekundy později, už totiž není zvukový efekt, ale historický dokument.

Pro jednorázové efekty tohle fungovalo krásně. Pak jsem chtěl přidat kvílení pneumatik.

Mixování: buffer jsou pořád jen bajty

Smyk v originálu hraje ve smyčce pod ostatními efekty, dokud auto driftuje. Má plynulý náběh a na ledu je ještě posunutý níž. Nemůže tedy čekat, až se výbuch uráčí skončit.

A pak mi došlo něco až trapně jednoduchého: DMA vůbec nemusí vědět, kolik zvuků hra právě přehrává. Dostane prostě hotových 256 bajtů. Takže připravím základní buffer – buď efekt, nebo hodnotu 128 pro ticho – a drift do něj přimíchám ještě před odesláním.

Protože PCM je unsigned, nemůžu dva bajty prostě sečíst. Druhý sample nejdřív převedu na příspěvek kolem nuly. Se ziskem gain používám trik: spočítám sample × gain, vezmu horní bajt výsledku a odečtu gain / 2. Tím dostanu přibližně symetrický příspěvek se znaménkem bez nutnosti dělat signed 8×8 násobení.

Výsledek přičtu k bajtu, který už je v mixovacím bufferu, a saturovaně oříznu na rozsah 0 až 255. Bez saturace by přetečení obalilo hodnotu na opačný konec rozsahu a hlasité špičky by se změnily v digitální praskání.

Prakticky je to na jeden výstupní bajt načtení, jedno 8×8 násobení, odečtení středu, součet, kontrola přetečení a zápis. Pro 256 vzorků se i s režií pořád vejdu hluboko do jednoho 20ms snímku.

Hlasitost smyku je právě gain, takže náběh a útlum znamenají jen měnit jednu hodnotu každý frame. Ledová varianta je stejný zvuk předem převzorkovaný na PC na nižší rychlost. Převzorkovat jednou při buildu je přece jen elegantnější než to dělat padesátkrát za sekundu na Z80.

Motor: stejný trik, jen o patro výš

Motor je zajímavější. Originál nemá jednu smyčku, ale pět nahrávek od hlubokého bublání po vysoký řev. V určité rychlosti se mixují dvě sousední smyčky a zároveň se mění jejich pitch.

K tomu potřebuju sample číst jinou rychlostí než 1:1. Používám tedy zlomkovou pozici v pevné řádové čárce 8.8. Krok 128 znamená 0,5× rychlost, 256 normální rychlost a 512 dvojnásobnou. Ke každému výstupnímu vzorku přičtu krok a z celé části pozice vezmu index do smyčky.

Příjemný trik je v délce samplů. Konverzní nástroj každou motorovou smyčku upraví na přesně 8192 bajtů. Díky tomu je wrap přes konec smyčky levný – index je modulo 8192 a obejde se bez obecného dělení. Ano, pitch se tím o pár procent změní. Ne, při závodě to nikdo nepozná.

Pro každý výstupní vzorek tedy vezmu dvě motorové smyčky, každou jiným ziskem, sečtu je do mixu a posunu společnou pozici. Rychlost auta určuje, která dvojice samplů se použije, jejich poměr a zároveň rychlost čtení.

A když už ten motor existuje jako opravdový zvuk, nechávám ho běžet na volnoběh i na startovním roštu pod odpočtem. Najednou to opravdu zní jako závod, který se chystá začít. Což byl, pokud se nepletu, od začátku plán.

Rozpočet

V nejhorším případě jeden frame znamená: zkopírovat 256 bajtů hlavního efektu, přimíchat drift a potom ještě dvě motorové smyčky se zlomkovým krokem. Celé plnění vychází přibližně na čtyři milisekundy z dvaceti.

Ověřil jsem to jednoduše a prakticky: testovací build, který tuhle nejhorší kombinaci vynucuje každý frame, jsem nechal běžet proti časování hry. Časomíra zůstala synchronní, takže žádný snímek nevypadával.

Hlasitosti motoru a driftu jsou nastavené tak, aby dohromady běžně nesnědly celý dynamický rozsah. Efekty tak mají rezervu a saturace se používá hlavně pro skutečné špičky, ne jako standardní způsob mixování.

Samply dohromady zabírají přibližně 224 KB z 2MB paměti. To zní jako dost, dokud si člověk nevzpomene, že předtím ty stránky stejně nic nedělaly. Teď aspoň dělají rámus.

A co AY?

Předchozí verze přes AY řešila motor, drift, odpočet, výbuchy, kulomet i hudbu. Po téhle změně dělá AY v zásadě hlavně hudbu.

Třetí čip TurboSoundu tedy momentálně skoro zahálí, což mi po několika hrách, kde jsem bojoval o každý kanál, přijde docela ironické.

Vrtulník a šplouchnutí z původní hry zůstaly doma. V tomhle portu žádný vrtulník není a voda je tady jen pomalá, nikoli mokrá.

Novou verzi Pixel Wheels si můžete stáhnout zde: https://shrek128.itch.io/pixel-wheels-zx-spectrum-next

screenshot_2026_9_5___1_29_36

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *