prapovijest
Pošto sam zaljubljenik u retro hardver, jednom sam kupio ZX Spectrum+ od prodavca u Velikoj Britaniji. Uz sam kompjuter sam dobio nekoliko audio kaseta sa igricama (u originalnom pakovanju sa uputstvom), kao i programe snimljene na kasetama bez posebnih oznaka. Iznenađujuće, podaci sa kaseta starih 40 godina bili su čitljivi i mogao sam da skinem skoro sve igre i programe sa njih.
Međutim, na nekim kasetama sam našao snimke koje očito nije napravio ZX Spectrum kompjuter. Zvučale su potpuno drugačije i, za razliku od snimaka sa pomenutog kompjutera, nisu počinjale kratkim BASIC bootloaderom koji je inače prisutan na snimcima svih programa i igrica.
To me je neko vrijeme proganjalo – zaista sam želio saznati šta se krije u njima. Ako biste mogli pročitati audio signal kao niz bajtova, mogli biste potražiti znakove ili bilo šta što ukazuje na porijeklo signala. Neka vrsta retroarheologije.
Sad kad sam otišao do kraja i pogledao etikete na samim kasetama, smiješim se jer
odgovor mi je cijelo vrijeme bio pred očima
Na etiketi lijeve kasete je naziv računara TRS-80, a odmah ispod imena proizvođača: “Proizvođač Radio Shack u SAD”
(Ako želite da zadržite intrigu do kraja, ne ulazite ispod spojlera)
Poređenje audio signala
Prije svega, digitalizirajmo audio snimke. Možete poslušati kako to zvuči:
I kao i obično zvuči snimak sa računara ZX Spectrum:
U oba slučaja, na početku snimka nalazi se tzv pilot ton - zvuk iste frekvencije (na prvom snimku je vrlo kratak <1 sekunde, ali se razlikuje). Pilot ton signalizira kompjuteru da se pripremi za prijem podataka. Po pravilu, svaki kompjuter prepoznaje samo svoj „vlastiti“ pilot ton po obliku signala i njegovoj frekvenciji.
Potrebno je reći nešto o samom obliku signala. Na primjer, na ZX Spectrumu njegov oblik je pravokutni:
Kada se detektuje pilot ton, ZX Spectrum prikazuje naizmenične crvene i plave trake na ivici ekrana kako bi označio da je signal prepoznat. Pilot ton se završava sinhro puls, koji signalizira računaru da počne da prima podatke. Karakterizira ga kraće trajanje (u poređenju sa pilot tonom i naknadnim podacima) (vidi sliku)
Nakon što je primljen sinhronizacioni impuls, računar beleži svaki porast/pad signala, mereći njegovo trajanje. Ako je trajanje manje od određene granice, bit 1 se upisuje u memoriju, u suprotnom 0. Bitovi se skupljaju u bajtove i proces se ponavlja dok se ne primi N bajtova. Broj N se obično uzima iz zaglavlja preuzete datoteke. Redoslijed učitavanja je sljedeći:
- pilot ton
- zaglavlje (fiksna dužina), sadrži veličinu preuzetih podataka (N), naziv datoteke i tip
- pilot ton
- sami podaci
Kako bi bili sigurni da su podaci ispravno učitani, ZX Spectrum očitava tzv bajt parnosti (paritetni bajt), koji se izračunava prilikom snimanja datoteke XOR-om svih bajtova upisanih podataka. Prilikom čitanja datoteke računar izračunava paritetni bajt iz primljenih podataka i, ako se rezultat razlikuje od sačuvanog, prikazuje poruku o grešci “R Tape loading error”. Strogo govoreći, računar može izdati ovu poruku ranije ako pri čitanju ne može prepoznati puls (propušten ili njegovo trajanje ne odgovara određenim granicama)
Dakle, da vidimo kako izgleda nepoznati signal:
Ovo je pilot ton. Oblik signala je značajno drugačiji, ali je jasno da se signal sastoji od ponavljanja kratkih impulsa određene frekvencije. Na frekvenciji uzorkovanja od 44100 Hz, razmak između "pikova" je približno 48 uzoraka (što odgovara frekvenciji od ~918 Hz).
Pogledajmo sada fragment podataka:
Ako izmjerimo udaljenost između pojedinačnih impulsa, ispada da je udaljenost između “dugih” impulsa i dalje ~48 uzoraka, a između kratkih ~24. Gledajući malo unaprijed, reći ću da se na kraju ispostavilo da “referentni” impulsi frekvencije 918 Hz slijede kontinuirano, od početka do kraja datoteke. Može se pretpostaviti da prilikom prijenosa podataka, ako se naiđe na dodatni impuls između referentnih impulsa, smatramo ga bitom 1, inače 0.
Šta je sa sinhronizacionim pulsom? Pogledajmo početak podataka:
Pilot ton se završava i podaci počinju odmah. Nešto kasnije, nakon analize nekoliko različitih audio zapisa, uspjeli smo otkriti da je prvi bajt podataka uvijek isti (10100101b, A5h). Računar može početi čitati podatke nakon što ih primi.
Također možete obratiti pažnju na pomak prvog referentnog impulsa odmah nakon posljednjeg 1. u bajtu za sinhronizaciju. Otkriveno je mnogo kasnije u procesu razvoja programa za prepoznavanje podataka, kada se podaci na početku datoteke nisu mogli stabilno čitati.
Pokušajmo sada opisati algoritam koji će obraditi audio datoteku i učitati podatke.
Učitavanje podataka
Prvo, pogledajmo nekoliko pretpostavki kako bi algoritam bio jednostavan:
- Mi ćemo uzeti u obzir samo datoteke u WAV formatu;
- Audio datoteka mora početi pilot tonom i ne smije sadržavati tišinu na početku
- Izvorni fajl mora imati brzinu uzorkovanja od 44100 Hz. U ovom slučaju, udaljenost između referentnih impulsa od 48 uzoraka je već određena i ne moramo je programski izračunati;
- Format uzorka može biti bilo koji (8/16 bita/pokretni zarez) - pošto ga prilikom čitanja možemo konvertovati u željeni;
- Pretpostavljamo da je izvorni fajl normalizovan amplitudom, što bi trebalo da stabilizuje rezultat;
Algoritam čitanja će biti sljedeći:
- Čitamo datoteku u memoriju, istovremeno pretvarajući format uzorka u 8 bita;
- Odredite poziciju prvog impulsa u audio podacima. Da biste to učinili, morate izračunati broj uzorka s maksimalnom amplitudom. Radi jednostavnosti, izračunat ćemo ga jednom ručno. Sačuvajmo ga u varijablu prev_pos;
- Dodajte 48 na poziciju posljednjeg pulsa (poz := prev_pos + 48)
- Budući da povećanje pozicije za 48 ne garantuje da ćemo doći do pozicije sljedećeg referentnog impulsa (defekti trake, nestabilan rad mehanizma trake itd.), potrebno je podesiti poziciju impulsa pos. Da biste to učinili, uzmite mali dio podataka (pos-8;pos+8) i na njemu pronađite maksimalnu vrijednost amplitude. Položaj koji odgovara maksimumu bit će pohranjen u poz. Ovdje je 8 = 48/6 eksperimentalno dobijena konstanta, koja garantuje da ćemo odrediti tačan maksimum i neće uticati na druge impulse koji se mogu nalaziti u blizini. U vrlo lošim slučajevima, kada je razmak između impulsa mnogo manji ili veći od 48, možete implementirati prisilno traženje impulsa, ali u okviru članka to neću opisivati u algoritmu;
- U prethodnom koraku također bi bilo potrebno provjeriti da li je referentni puls uopće pronađen. Odnosno, ako jednostavno tražite maksimum, to ne garantuje da je impuls prisutan u ovom segmentu. U svojoj najnovijoj implementaciji programa za čitanje, provjeravam razliku između maksimalne i minimalne vrijednosti amplitude na segmentu, a ako pređe određenu granicu, računam prisustvo impulsa. Pitanje je i šta učiniti ako referentni puls nije pronađen. Postoje 2 opcije: ili su podaci završili i slijedi tišina ili ovo treba smatrati greškom čitanja. Međutim, ovo ćemo izostaviti kako bismo pojednostavili algoritam;
- U sljedećem koraku moramo utvrditi prisustvo impulsa podataka (bit 0 ili 1), za to uzimamo sredinu segmenta (prev_pos;pos) middle_pos jednaku middle_pos := (prev_pos+pos)/2 i u nekom susjedstvu mid_pos na segmentu (middle_pos-8;middle_pos +8) izračunajmo maksimalnu i minimalnu amplitudu. Ako je razlika između njih veća od 10, u rezultat upisujemo bit 1, u suprotnom 0. 10 je konstanta dobijena eksperimentalno;
- Sačuvajte trenutnu poziciju u prev_pos (prev_pos := pos)
- Ponovite počevši od koraka 3 dok ne pročitamo cijeli fajl;
- Rezultirajući niz bitova mora biti sačuvan kao skup bajtova. S obzirom da pri čitanju nismo uzeli u obzir sinhroni bajt, broj bitova možda nije višekratnik 8, a potreban pomak bita je također nepoznat. U prvoj implementaciji algoritma nisam znao za postojanje bajta za sinhronizaciju i stoga sam jednostavno sačuvao 8 fajlova sa različitim brojem pomaka bitova. Jedan od njih je sadržavao tačne podatke. U konačnom algoritmu jednostavno uklanjam sve bitove do A5h, što mi omogućava da odmah dobijem ispravan izlazni fajl
Algoritam u Rubyju, za zainteresovane
Odabrao sam Ruby kao jezik za pisanje programa, jer... Većinu vremena programiram na njemu. Opcija nije visokih performansi, ali zadatak da ubrzate čitanje što je brže moguće se ne isplati.
# Используем 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*")
rezultat
Nakon što sam isprobao nekoliko varijanti algoritma i konstanti, imao sam sreću da sam dobio nešto izuzetno zanimljivo:
Dakle, sudeći po znakovnim nizovima, imamo program za crtanje grafova. Međutim, u tekstu programa nema ključnih riječi. Sve ključne riječi su kodirane kao bajtovi (svaka vrijednost > 80h). Sada treba da saznamo koji računar iz 80-ih je mogao da čuva programe u ovom formatu.
U stvari, veoma je sličan BASIC programu. Računar ZX Spectrum pohranjuje programe u približno istom formatu u memoriju i sprema programe na traku. Za svaki slučaj, provjerio sam ključne riječi
Provjerio sam i BASIC ključne riječi popularnih Atari, Commodore 64 i nekoliko drugih kompjutera tog vremena, za koje sam uspio pronaći dokumentaciju, ali bezuspješno - pokazalo se da moje znanje o vrstama retro kompjutera nije tako široko.
Onda sam odlučio da idem
Computer Tandy/Radio Shack TRS-80
Vrlo je vjerovatno da je dotični audio snimak, koji sam naveo kao primjer na početku članka, napravljen na računaru ovako:
Ispostavilo se da su ovaj računar i njegove varijante (Model I/Model III/Model IV, itd.) u jednom trenutku bili veoma popularni (naravno, ne u Rusiji). Važno je napomenuti da je procesor koji su koristili takođe Z80. Za ovaj računar možete pronaći na internetu
Skinuo sam emulator
I ja sam našao
Nakon što sam shvatio CAS format datoteke (za koji se ispostavilo da je samo bit-po-bit kopija podataka sa trake koju sam već imao pri ruci, osim zaglavlja sa prisustvom bajta za sinhronizaciju), napravio sam nekoliko izmena u mom programu i uspeo sam da izbacim radni CAS fajl koji je radio u emulatoru (TRS-80 Model III):
Najnoviju verziju uslužnog programa za konverziju sa automatskim određivanjem prvog impulsa i udaljenosti između referentnih impulsa dizajnirao sam kao GEM paket, izvorni kod je dostupan na
zaključak
Put koji smo prešli pokazao se kao fascinantno putovanje u prošlost i drago mi je da sam na kraju pronašao odgovor. Između ostalog, ja:
- Shvatio sam format za čuvanje podataka u ZX Spectrumu i proučio ugrađene ROM rutine za čuvanje/čitanje podataka sa audio kaseta
- Upoznao sam računar TRS-80 i njegove varijante, proučavao operativni sistem, pogledao uzorke programa i čak imao priliku da radim otklanjanje grešaka u mašinskim kodovima (na kraju krajeva, svi Z80 mnemonici su mi poznati)
- Napisao je punopravni uslužni program za pretvaranje audio zapisa u CAS format, koji može čitati podatke koje "službeni" uslužni program ne prepoznaje
izvor: www.habr.com