őstörténet
Mivel a retro hardverek szerelmese, egyszer vásároltam egy ZX Spectrum+-t egy eladótól az Egyesült Királyságban. Magához a számítógéphez mellékelve kaptam több hangkazettát játékokkal (eredeti csomagolásban, használati utasítással), valamint kazettára rögzített műsorokat külön jelölés nélkül. Meglepő módon a 40 éves kazetták adatai jól olvashatóak voltak, és szinte az összes játékot, programot le tudtam tölteni róluk.
Néhány kazettán azonban olyan felvételeket találtam, amelyek egyértelműen nem a ZX Spectrum számítógéppel készültek. Teljesen más a hangzásuk, és az említett számítógépről készült felvételekkel ellentétben nem egy rövid BASIC bootloaderrel indultak, ami általában minden program és játék felvételén megtalálható.
Egy ideig ez kísértett – nagyon szerettem volna megtudni, mi rejtőzik bennük. Ha az audiojelet bájtok sorozataként tudná olvasni, kereshetne karaktereket vagy bármit, ami a jel eredetét jelzi. Egyfajta retro-régészet.
Most, hogy végigmentem és megnéztem magukat a kazetták címkéit, elmosolyodom, mert
a válasz végig a szemem előtt volt
A bal oldali kazetta címkéjén a TRS-80 számítógép neve, közvetlenül alatta pedig a gyártó neve: „A Radio Shack az USA-ban gyártotta”
(Ha a végéig meg akarod őrizni az intrikát, ne menj a spoiler alá)
Hangjelek összehasonlítása
Először is digitalizáljuk a hangfelvételeket. Meghallgathatod, hogyan hangzik:
És szokás szerint a ZX Spectrum számítógépről készült felvétel hangzik:
Mindkét esetben a felvétel elején van egy ún pilótahang - azonos frekvenciájú hang (az első felvételen nagyon rövid <1 másodperc, de jól megkülönböztethető). A pilótahang jelzi a számítógépnek, hogy készüljön fel az adatok fogadására. Általában minden számítógép csak a „saját” pilothangját ismeri fel a jel alakja és frekvenciája alapján.
Mondani kell valamit magáról a jelformáról. Például a ZX Spectrumon az alakja téglalap alakú:
Pilótahang észlelésekor a ZX Spectrum váltakozó piros és kék sávokat jelenít meg a képernyő szélén, jelezve, hogy a jel felismerése megtörtént. Pilot hang véget ér szinkron impulzus, amely jelzi a számítógépnek, hogy kezdje meg az adatok fogadását. Rövidebb időtartam jellemzi (a pilótahanghoz és az azt követő adatokhoz képest) (lásd az ábrát)
A szinkronimpulzus vétele után a számítógép rögzíti a jel minden egyes emelkedését/esését, és méri annak időtartamát. Ha az időtartam egy bizonyos határnál kisebb, az 1. bit a memóriába kerül, ellenkező esetben a 0. A biteket bájtokba gyűjtik, és a folyamatot addig ismétlik, amíg N bájt meg nem érkezik. Az N számot általában a letöltött fájl fejlécéből veszik. A betöltési sorrend a következő:
- pilótahang
- fejléc (fix hosszúságú), tartalmazza a letöltött adatok méretét (N), a fájl nevét és típusát
- pilótahang
- magát az adatot
Az adatok helyes betöltésére a ZX Spectrum beolvassa az ún paritás bájt (paritás bájt), amely egy fájl mentésekor az írott adatok összes bájtjának XOR-ával kerül kiszámításra. Fájl beolvasásakor a számítógép a kapott adatokból kiszámítja a paritásbájtot, és ha az eredmény eltér a mentetttől, megjelenik az „R Tape loading error” hibaüzenet. Szigorúan véve a számítógép korábban kiadhatja ezt az üzenetet, ha olvasás közben nem tud felismerni egy impulzust (kimaradt vagy időtartama nem felel meg bizonyos határoknak)
Lássuk tehát, hogyan néz ki egy ismeretlen jel:
Ez a pilótahang. A jel alakja jelentősen eltér, de egyértelmű, hogy a jel bizonyos frekvenciájú rövid impulzusok ismétlődéséből áll. 44100 Hz-es mintavételezési frekvenciánál a „csúcsok” közötti távolság hozzávetőlegesen 48 minta (ami ~918 Hz-es frekvenciának felel meg) Emlékezzünk erre az ábrára.
Nézzük most az adatrészletet:
Ha megmérjük az egyes impulzusok közötti távolságot, akkor kiderül, hogy a „hosszú” impulzusok közötti távolság még mindig ~48 minta, a rövidek között pedig ~24. Kicsit előretekintve elmondom, hogy a végén kiderült, hogy a fájl elejétől a végéig folyamatosan 918 Hz-es „referencia” impulzusok következnek. Feltételezhető, hogy adatátvitelkor, ha a referenciaimpulzusok között további impulzus is előfordul, azt 1-es bitnek, egyébként 0-nak tekintjük.
Mi a helyzet a szinkronimpulzussal? Nézzük az adatok elejét:
A pilótahang véget ér, és az adatok azonnal elkezdődnek. Kicsit később, több különböző hangfelvétel elemzése után felfedezhettük, hogy az adatok első bájtja mindig ugyanaz (10100101b, A5h). A számítógép elkezdheti olvasni az adatokat, miután megkapta azokat.
Arra is figyelhet, hogy a szinkronizálási bájtban közvetlenül az utolsó 1. utáni első referenciaimpulzus eltolódik. Jóval később, egy adatfelismerő program fejlesztése során fedezték fel, amikor a fájl elején lévő adatokat nem lehetett stabilan kiolvasni.
Most próbáljunk meg leírni egy algoritmust, amely feldolgozza a hangfájlt és betölti az adatokat.
adatok betöltése
Először is nézzünk meg néhány feltételezést, hogy az algoritmus egyszerű legyen:
- Csak a WAV formátumú fájlokat vesszük figyelembe;
- Az audiofájlnak pilothanggal kell kezdődnie, és nem tartalmazhat csendet az elején
- A forrásfájl mintavételi frekvenciájának 44100 Hz-nek kell lennie. Ebben az esetben 48 minta referenciaimpulzusai közötti távolság már meg van határozva, és ezt nem kell programozottan kiszámítanunk;
- A minta formátuma tetszőleges lehet (8/16 bit/lebegőpont) - hiszen olvasáskor tudjuk konvertálni a kívántra;
- Feltételezzük, hogy a forrásfájlt az amplitúdó normalizálja, ami stabilizálja az eredményt;
Az olvasási algoritmus a következő lesz:
- A fájlt beolvassuk a memóriába, egyúttal a mintaformátumot 8 bitesre konvertáljuk;
- Határozza meg az első impulzus helyét az audioadatokban. Ehhez ki kell számítani a maximális amplitúdójú minta számát. Az egyszerűség kedvéért egyszer manuálisan számoljuk ki. Mentsük el a prev_pos változóba;
- Adjon hozzá 48-at az utolsó impulzus helyéhez (poz := prev_pos + 48)
- Mivel a pozíció 48-cal történő növelése nem garantálja, hogy a következő referenciaimpulzus pozíciójába kerülünk (szalaghibák, a szalagos meghajtó mechanizmusának instabil működése stb.), ezért a poziciós impulzus helyzetét módosítanunk kell. Ehhez vegyünk egy kis adatot (pos-8;pos+8), és keressük meg rajta a maximális amplitúdóértéket. A maximumnak megfelelő pozíció a pozícióban lesz tárolva. Itt a 8 = 48/6 egy kísérletileg kapott állandó, amely garantálja, hogy a helyes maximumot határozzuk meg, és nem befolyásolja a közelben esetlegesen előforduló egyéb impulzusokat. Nagyon rossz esetekben, amikor az impulzusok közötti távolság sokkal kisebb vagy nagyobb, mint 48, végrehajthat kényszerített impulzuskeresést, de a cikk keretein belül ezt nem írom le az algoritmusban;
- Az előző lépésnél azt is ellenőrizni kell, hogy a referenciaimpulzus egyáltalán megtalálható-e. Vagyis ha egyszerűen a maximumot keresed, az nem garantálja, hogy az impulzus jelen van ebben a szegmensben. A leolvasó program legújabb implementációjában egy szegmensen ellenőrzöm a maximális és minimális amplitúdóértékek közötti különbséget, és ha túllép egy bizonyos határt, akkor számolom az impulzus jelenlétét. A kérdés az is, hogy mi a teendő, ha nem található a referenciaimpulzus. 2 lehetőség van: vagy az adatok véget értek, és csend következik, vagy ez olvasási hibának tekintendő. Ezt azonban az algoritmus egyszerűsítése érdekében elhagyjuk;
- A következő lépésben meg kell határoznunk egy adatimpulzus jelenlétét (bit 0 vagy 1), ehhez vesszük a szegmens közepét (prev_pos;pos) middle_pos egyenlő middle_pos := (prev_pos+pos)/2 és a szegmens middle_pos valamelyik szomszédságában (middle_pos-8;middle_pos +8) számítsuk ki a maximális és minimális amplitúdót. Ha a különbség nagyobb, mint 10, akkor az 1-es bitet írjuk az eredménybe, ellenkező esetben 0. 10 egy kísérletileg kapott állandó;
- Az aktuális pozíció mentése a prev_pos fájlba (prev_pos := pos)
- Ismételje meg a 3. lépéstől kezdve, amíg el nem olvassuk a teljes fájlt;
- Az eredményül kapott bittömböt bájtok halmazaként kell elmenteni. Mivel olvasáskor nem vettük figyelembe a szinkron bájtot, előfordulhat, hogy a bitek száma nem lehet 8 többszöröse, és a szükséges biteltolás sem ismert. Az algoritmus első implementációjában nem tudtam a szinkronizálási bájt létezéséről, ezért egyszerűen elmentettem 8 fájlt különböző számú offset bittel. Az egyik helyes adatokat tartalmazott. Az utolsó algoritmusban egyszerűen eltávolítom az összes bitet A5h-ig, ami lehetővé teszi, hogy azonnal megkapjam a megfelelő kimeneti fájlt
Ruby algoritmus, az érdeklődők számára
A program írásához a Rubyt választottam, mert... Legtöbbször programozom rá. Az opció nem nagy teljesítményű, de az olvasási sebesség minél gyorsabbá tétele nem éri meg.
# Используем gem 'wavefile'
require 'wavefile'
reader = WaveFile::Reader.new('input.wav')
samples = []
format = WaveFile::Format.new(:mono, :pcm_8, 44100)
# Читаем WAV файл, конвертируем в формат Mono, 8 bit
# Массив samples будет состоять из байт со значениями 0-255
reader.each_buffer(10000) do |buffer|
samples += buffer.convert(format).samples
end
# Позиция первого импульса (вместо 0)
prev_pos = 0
# Расстояние между импульсами
distance = 48
# Значение расстояния для окрестности поиска локального максимума
delta = (distance / 6).floor
# Биты будем сохранять в виде строки из "0" и "1"
bits = ""
loop do
# Рассчитываем позицию следующего импульса
pos = prev_pos + distance
# Выходим из цикла если данные закончились
break if pos + delta >= samples.size
# Корректируем позицию pos обнаружением максимума на отрезке [pos - delta;pos + delta]
(pos - delta..pos + delta).each { |p| pos = p if samples[p] > samples[pos] }
# Находим середину отрезка [prev_pos;pos]
middle_pos = ((prev_pos + pos) / 2).floor
# Берем окрестность в середине
sample = samples[middle_pos - delta..middle_pos + delta]
# Определяем бит как "1" если разница между максимальным и минимальным значением на отрезке превышает 10
bit = sample.max - sample.min > 10
bits += bit ? "1" : "0"
end
# Определяем синхро-байт и заменяем все предшествующие биты на 256 бит нулей (согласно спецификации формата)
bits.gsub! /^[01]*?10100101/, ("0" * 256) + "10100101"
# Сохраняем выходной файл, упаковывая биты в байты
File.write "output.cas", [bits].pack("B*")
Eredmény
Az algoritmus és a konstansok több változatát kipróbálva szerencsém volt valami rendkívül érdekes dologgal:
Tehát a karakterláncokból ítélve van egy programunk a grafikonok ábrázolására. A program szövegében azonban nincsenek kulcsszavak. Minden kulcsszó bájtban van kódolva (minden érték > 80 óra). Most meg kell találnunk, hogy a 80-as évek melyik számítógépe menthetett programokat ebben a formátumban.
Valójában nagyon hasonlít egy BASIC programhoz. A ZX Spectrum számítógép megközelítőleg azonos formátumban tárolja a programokat a memóriában, és szalagra menti a programokat. Minden esetre leellenőriztem a kulcsszavakat
Az akkori népszerű Atari, Commodore 64 és több más számítógép BASIC kulcsszavait is leellenőriztem, amelyekhez sikerült dokumentációt találni, de sikertelenül - a retro számítógépek típusaira vonatkozó ismereteim nem bizonyultak olyan szélesnek.
Aztán úgy döntöttem, hogy megyek
Számítógép Tandy/Radio Shack TRS-80
Nagyon valószínű, hogy a szóban forgó hangfelvétel, amelyet a cikk elején példaként hoztam fel, egy ilyen számítógépen készült:
Kiderült, hogy ez a számítógép és fajtái (I. modell/III. modell/IV. modell stb.) egy időben nagyon népszerűek voltak (persze nem Oroszországban). Figyelemre méltó, hogy az általuk használt processzor is Z80 volt. Ehhez a számítógéphez megtalálható az interneten
Letöltöttem az emulátort
én is találtam
Miután kitaláltam a CAS fájlformátumot (ami csak egy bitenkénti másolata a nálam lévő szalagról, kivéve a fejlécet a szinkronizálási bájt jelenlétével), elkészítettem egy néhány módosítást végeztem a programon, és egy működő CAS-fájlt tudtam kiadni, amely működött az emulátorban (TRS-80 Model III):
A konvertáló segédprogram legújabb verzióját az első impulzus és a referenciaimpulzusok távolságának automatikus meghatározásával GEM-csomagként terveztem, a forráskód elérhető a címen.
Következtetés
Az út, amelyet bejártunk, lenyűgöző utazásnak bizonyult a múltba, és örülök, hogy végül megtaláltam a választ. Többek között én:
- Kitaláltam a ZX Spectrum adatmentési formátumát, és tanulmányoztam a beépített ROM-rutinokat a hangkazetták adatainak mentéséhez/olvasásához.
- Megismerkedtem a TRS-80 számítógéppel és fajtáival, tanulmányoztam az operációs rendszert, megnéztem a mintaprogramokat, és még gépi kódokban is volt lehetőségem hibakeresésre (végül is az összes Z80-as mnemonika ismerős számomra)
- Készített egy teljes értékű segédprogramot a hangfelvételek CAS formátumba konvertálására, amely képes olyan adatokat olvasni, amelyeket a „hivatalos” segédprogram nem ismer fel
Forrás: will.com