Eelalugu
Olles retroarvutite entusiast, ostsin kunagi Ühendkuningriigi müüjalt ZX Spectrum+. Selle arvuti komplektis olid mõned helikassetid mängudega (originaalpakendis koos juhenditega) ning ka mitmed programmid, mis olid salvestatud kassetidele ilma eriliste tähisteta. Üllatuslikult lugesid 40 aastat vanad kassetid hästi ja sain peaaegu kõik mängud ja programmid nendelt laadida.

Kuid mõnel kassetil leidsin ma salvestisi, mis olid selgelt mitte ZX Spectrumi arvutist. Need kõlavad täiesti teisiti ja erinevalt eespool mainitud arvuti salvestustest, ei alga nad lühikese BASIC laadijaga, mis tavaliselt on kõigi programmide ja mängude salvestistes olemas.
Mõnda aega ei andnud see mulle rahu – olin väga uudishimulik teada saada, mis neis varjatud on. Kui õnnestuks lugeda helisignaal nagu järjestikku baite, oleks võimalik otsida sealt märke või midagi, mis näitaks signaali päritolu. Teatud mõttes retro-arheoloogia.
Nüüd, kui olen kogu tee läbinud ja vaatan kassetihindade peale, naeratavad mulle, sest
vastus oli kogu aeg silme ees.
Vasakul olevas kassetil on arvuti TRS-80 nimi ja veidi allpool tootja nimi: „Tootja: Radio Shack USA-s“
(Kui soovite suspense'i kuni lõpuni hoida, ärge avage spoilerit)
Heli signaalide võrdlus
Kõigepealt digitaliseerime helisalvestised. Võite kuulata, kuidas see kõlab:
Ja kuidas tavaliselt kõlab salvestus arvutist ZX Spectrum:
Mõlemal juhul alguses salvestusel on nn pilottoon — ühe sagedusega heli (esimeses salvestuses on see väga lühike <1 sek, kuid äratuntav). Pilottoon toimib signaalina arvutile, et valmistuda andmete vastuvõtmiseks. Üldiselt tunneb iga arvuti ära ainult oma „pilottooni“ signaali kuju ja sageduse järgi.
Tuleb rääkida ka signaali kujust. Näiteks ZX Spectrumil on selle kuju ristkülikuline:

Pilottooni tuvastamisel näitab ZX Spectrum ekraani servas vahelduvaid punaseid ja siniseid ribasid, näidates, et signaal on tuvastatud. Pilottoon lõppeb sünkroimpulsiga, mis signaaliga teatatakse arvutile, et see peaks hakkama andmeid vastu võtma. Selle kestus on väiksem (võrreldes piloottooni ja järgnevate andmetega) (vt joonist)
Pärast sünkro-impulsi saamist fikseerib arvuti iga signaali tõusu/langemist, mõõtes selle kestust. Kui kestus on väiksem kui määratud piir, salvestatakse mällu bitt 1, vastasel juhul 0. Bitid kogutakse baitideks ja protsess kordub, kuni saadakse N baidi. N-number on üldiselt saadud laaditava faili pealkirjast. Laadimise järjestus on järgmine:
- pilottoon
- pealkiri (kindla pikkusega), mis sisaldab laaditavate andmete suurust (N), faili nime ja tüüpi
- pilottoon
- andmed ise
Kinnitamaks, et andmed on õigesti alla laaditud, loeb ZX Spectrum viimasena nii nimetatud pariteedibaiti (pariteedibait), mis arvutatakse faili salvestamisel XOR-operatsiooniga kõigi kirjutatud andmete baitide üle. Faili lugemisel arvutab arvuti pariteedibaiti saadud andmete põhjal, ja kui tulemus erineb salvestatud väärtusest, kuvab see veateate „R Tape loading error“. Täpselt öeldes võib arvuti selle teate anda ka varem, kui lugemisel ei suuda ta impulssi tuvastada (puudub või selle kestus ei vasta määratud piiridele)
Nii, vaatame nüüd, milline tundmatu signaal välja näeb:

See on piloottoon. Signaali kuju erineb oluliselt, kuid on näha, et signaal koosneb korduvatest lühikatkestustest kindla sagedusega. Kui proovide sagedus on 44100 Hz, on „tipude“ vaheline kaugus umbes 48 proovi (see vastab sagedusele ~918 Hz). Salvestame selle numbri meelde.
Vaatame nüüd andmefragmenti:

Kui mõõta kaugust eraldi impulsse, selgub, et "pikkade" impulsside vahel on vahe endiselt ~48 proovi, samas kui lühikeste vahel on see ~24. Veidi ettepoole vaadates, võib öelda, et "toetavad" impulsid sagedusega 918 Hz järgivad ühtlaselt, algusest lõpuni failis. Võib eeldada, et andmete edastamise korral, kui toetavate impulside vahel esineb täiendav impulss, loeme selle bitiks 1, vastasel juhul 0.
Kuidas on asjadega sünkroimpulsi? Vaadakem andmete algusesse:

Piloottoon lõppeb ja kohe algavad andmed. Veidi hiljem, analüüsides mitmeid erinevaid helisalvestisi, õnnestus leida, et esimene andmebaid on alati üks ja sama (10100101b, A5h). Võib-olla hakkab arvuti andmeid lugema, kui see on selle kätte saanud.
Samuti võib tähele panna, et esimese toetava impulsi nihke kohe pärast viimast 1 sünkrobaidis. Selle avastamine õnnestus oluliselt hiljem andmete tuvastamise tarkvara arendamise käigus, kui faili alguses andmeid ei suudetud stabiilselt lugeda.
Nüüd proovime kirjeldada algoritmi, mis töötleb heli faili ja laadib andmed üles.
Andmete laadimine
Esiteks vaatame üle mõned eeldused, et algoritmi mitte keeruliseks teha:
- Käsitleme faile ainult WAV vormingus;
- Helifail peab algama pilottone ja selle alguses ei tohi olla vaikus.
- Algne fail peab olema 44100 Hz proovivõtssagedusega. Sel juhul on 48 näidisevaheline kaugus juba määratud ja me ei pea seda programmiliselt arvutama;
- Proovide formaat võib olla ükskõik milline (8/16 bitti/ujuvpunkt) — kuna lugemise ajal saame selle vajalikuks konverteerida;
- Eeldame, et algne fail on amplituudis normaliseeritud, mis peaks stabiliseerima tulemuse;
Lugemis algoritm on järgmine:
- Lugeda fail mällu, samal ajal konverteerides proovide vormingu 8 bittiseks;
- Määrame esimest impulssi asukoha helidata. Selleks tuleb arvutada maksimaalse amplituudiga näidise number. Lihtsuse huvides arvutame selle üks kord käsitsi. Salvestame muutujasse prev_pos;
- Liidame viimase impulsi positsioonile 48 (pos := prev_pos + 48)
- Kuna positsiooni suurendamine 48 ei garanteeri, et me jõuame järgmise tugimpulsi positsiooni (lintide defektid, lintkonveierimehhanismi ebastabiilne töö jne), tuleb impulsipositsioon pos korrigeerida. Selleks võtame väikese andmeosa (pos-8; pos+8) ja leiame selle pealt maksimaalse amplituudi väärtuse. Positsiooni, mis vastab maksimumile, salvestame pos-sse. Siin 8 = 48/6 — eksperimentaalselt saadud konstant, mis garanteerib, et me määratleme õige maksimumi ja ei sega teisi impulsse, mis võivad läheduses olla. Väga halbadest juhtudest, kui impulside vahemaa on oluliselt väiksem või suurem kui 48, võib rakendada sundotsingu algoritmi, kuid artikli raames ma seda algoritmi ei kirjelda;
- Eelneval sammul tuleb samuti kontrollida, kas tugimpulssi on üldse leitud. Teisisõnu, lihtsalt maksimumi otsimine ei garanteeri, et impulss antud lõigus on olemas. Oma viimases lugemisprogrammis kontrollin amplituudi maksimaalse ja minimaalse väärtuse vahet antud lõigus, ja kui see ületab teatud piiri, arvestan impulsi olemasolu. Küsimus on ka selles, mida teha, kui tugimpulssi ei leitud. Siin on kaks varianti: kas andmed on otsa saanud ja järgneb vaikus, või tuleks seda pidada lugemisveaks. Siiski, lihtsustame algoritmi ja jätame selle kõrvale;
- Järgmises etapis tuleb määrata, kas andmetes on impulss (bit 0 või 1), selleks võtame lõike keskosa (prev_pos; pos) middle_pos, mis on võrdne middle_pos := (prev_pos + pos) / 2 ja mingis ümbruses middle_pos lõigus (middle_pos - 8; middle_pos + 8) arvutame amplituudi maksimaalse ja minimaalse väärtuse. Kui nende vahe on suurem kui 10, salvestame tulemuseks bit 1, vastasel juhul 0. 10 on konstant, mis on saadud eksperimendi käigus;
- Salvestame praeguse positsiooni prev_pos (prev_pos := pos)
- Kordame alates sammust 3, kuni oleme kogu faili läbi lugenud;
- Saadud bitivaru tuleb salvestada baitidekogumina. Kuna me ei arvestanud sünkro-baiti lugemisel, võib bitite arv osutuda 8-le mittepäriseks ning vajalik nihke pikkus bitti osutuda tundmatuks. Algoritmi esimeses elluviimises ei teadnud ma sünkro-baadist ja seega salvestasin lihtsalt 8 faili erineva bitnihkega. Üks neist sisaldas õigeid andmeid. Lõppalgoritmis eemaldan ma lihtsalt kõik bitid kuni A5h, mis võimaldab kohe saada korrektse faili väljundina.
Ruby algoritm, kellele huvi pakub
Programmi kirjutamiseks valisin Ruby, kuna programmeerin selle keelel enamus ajast. Variant ei ole väga efektiivne, kuid ülesanne maksimaalselt kiire lugemise saavutamine ei seisa.
# Используем 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*")
Tulemus
Katsudes läbi mitu algoritmi ja konstandi varianti, õnnestus mul saada midagi erakordselt huvitavat:

Nii et, arvestades sümbolilisi stringe, on meil programm graafikute koostamiseks. Kuid programmi tekstist puuduvad võtmesõnad. Kõik võtmesõnad on kodeeritud baitidena (igaühe väärtus > 80h). Nüüd tuleb välja selgitada, milline 80ndate arvuti võis programme sellises formaadis salvestada.
Tegelikult on see väga sarnane BASIC-keeles kirjutatud programmile. Umbes samas vormingus salvestab ZX Spectrum arvuti mälu ja salvestab programme lindile. Igaks juhuks kontrollisin võtmesõnu vastavuse osas . Kuid tulemus osutus, nagu näha, negatiivseks.
Samuti kontrollisin populaarses toona kasutuses olnud Atari, Commodore 64 ja mitmete teiste arvutite BASIC-võtmesõnu, mille kohta õnnestus leida dokumentatsiooni, kuid edutult — mu teadmised retro-arvutite eri liikidest ei olnud nii laiad.
Siis otsustasin minna edasi , ja siis jäi mu pilk tootja Radio Shack ja arvuti TRS-80 nimele. Just need nimed olid kirjutatud kassettide etiketidele, mis lebasid minu laual! Ma ei teadnud enne neid nimesid ja ei olnud tuttav arvuti TRS-80-ga, seega tundus mulle, et Radio Shack on audiosalvestiste tootja, nagu BASF, Sony või TDK, ja TRS-80 — mängimise kestus. Miks mitte?
Arvuti Tandy/Radio Shack TRS-80
On väga tõenäoline, et arutatud helisalvestis, mille tõin näitena välja artikli alguses, on tehtud sellise arvuti peal:

Selgus, et see arvuti ja selle variatsioonid (Model I/Model III/Model IV jne) olid oma aja jooksul väga populaarsed (loomulikult mitte Venemaal). Kõnealusest protsessorist, mis nendes kasutati, oli samuti Z80. Selle arvuti kohta on Internetis võimalik leida . 80ndatel levis teave arvuti kohta . Praeguseks on olemas mitu arvutist erinevatele platvormidele.
Laadisin alla emulaatori Ja sain esmakordselt võimaluse näha, kuidas see arvuti töötas. Loomulikult ei toetanud arvuti värvi väljundit, ekraani eraldusvõime oli vaid 128x48 punkti, kuid olemas oli palju laiendusi ja modifikatsioone, mis suudaksid suurendada ekraani eraldusvõimet. Samuti oli palju erinevaid operatsioonisüsteemide variante sellele arvutile ning erinevaid BASIC keele realiseerimise võimalusi (mis, erinevalt ZX Spectrum'ist, ei olnud mõnes mudelis isegi "fikseeritud" ROM-is ning iga variant võis laadida disketilt, nagu ka enda OS).
Leidsin ka audiofailide konvertimiseks CAS formaati, mida emulaatorid toetavad, kuid mingil põhjusel ei õnnestunud mul nende abil oma kassettide salvestisi lugeda.
Pärast CAS faili formaadiga tutvumist (mis osutus lihtsalt bitipõhiseks koopiaks lindilt, mis mul juba käes oli, välja arvatud pealkiri koos sünkro- байтиga), tegin oma programmile mõned muudatused ja suutsin saada väljundiks töötava CAS faili, mis töötas emulaatoris (TRS-80 Model III):

Viimase variandi utiliidist automaatse impulsi tuvastamise ja toiteimpulsside vahelise kauguse konverteerimiseks olen vormistanud GEM paketina, lähtekood on saadaval .
Kokkuvõte
Läbitud tee osutus põnevaks teekonnaks minevikku ja mul on hea meel, et leidsin lõpuks lahenduse. Lisaks olen ma:
- Selgitanud andmete salvestamise formaati ZX Spectrumis ja uurinud sisse ehitatud programmide alt salvestamist/üleslugemist kassettide abil
- Tutvunud TRS-80 arvutiga ja selle erinevustega, uurinud operatsioonisüsteemi, vaadanud programmide näiteid ning isegi saanud võimaluse tegeleda masinkoodide tõrkeotsinguga (kuna kõik Z80 mnemoonikad on mulle hästi tuttavad)
- Kirjutanud täisfunktsionaalse utiliidi audiofailide konverteerimiseks CAS-formaati, mis suudab lugeda andmeid, mida "ametlik" utiliit ei tunne
Allikas: habr.com
