Tło
Jako miłośnik retro sprzętu, pewnego razu zakupiłem od sprzedawcy z Wielkiej Brytanii ZX Spectrum+. Wraz z samym komputerem otrzymałem kilka kaset audio z grami (w oryginalnym opakowaniu z instrukcjami), a także programy nagrane na kasetach bez specjalnych oznaczeń. Ku mojemu zdziwieniu dane z kaset sprzed 40 lat dobrze się odtwarzały i udało mi się załadować prawie wszystkie gry i programy z nich.

Jednak na niektórych kasetach znalazłem nagrania, które ewidentnie nie pochodziły z komputera ZX Spectrum. Brzmiały one zupełnie inaczej i, w przeciwieństwie do nagrań z wspomnianego komputera, nie zaczynały się od krótkiego ładowania BASIC, które zazwyczaj jest obecne w nagraniach wszystkich programów i gier.
Przez jakiś czas nie dawało mi to spokoju – chciałem dowiedzieć się, co się w nich kryje. Gdyby udało się odczytać sygnał audio jako ciąg bajtów, można by poszukać w nich symboli lub czegoś, co wskazuje na pochodzenie sygnału. Coś w rodzaju retro-archaeologii.
Teraz, kiedy przeszedłem całą tę drogę i patrzę na etykiety samych kaset, uśmiecham się, bo
odpowiedź była na wyciągnięcie ręki przez cały czas
Na etykiecie lewej kasety widnieje nazwa komputera TRS-80, a nieco niżej nazwa producenta: „Manufactured by Radio Shack in USA”
(Jeśli chcesz zachować napięcie do końca, nie wchodź pod spoiler)
Porównanie sygnałów audio
Najpierw zdigitalizujmy nagrania audio. Można odsłuchać, jak to brzmi:
A jak zwykle brzmi nagranie z komputera ZX Spectrum:
W obu przypadkach na początku nagrania obecny jest tzw. pilot-ton — dźwięk o jednej częstotliwości (w pierwszym nagraniu jest bardzo krótki <1 sek, jednak rozpoznawalny). Pilot-ton służy jako sygnał dla komputera, że należy przygotować się do odbioru danych. Zazwyczaj każdy komputer rozpoznaje tylko „swój” pilot-ton po kształcie sygnału i jego częstotliwości.
Warto wspomnieć o samej formie sygnału. Na przykład, w przypadku ZX Spectrum jego kształt jest prostokątny:

Po wykryciu pilot-tonu ZX Spectrum wyświetla naprzemienne czerwono-niebieskie paski w części krawędziowej ekranu, pokazując, że sygnał został rozpoznany. Pilot-ton kończy się impulsem synchronizacyjnym, który sygnalizuje komputerowi, że należy rozpocząć odbiór danych. Charakteryzuje się krótszą (w porównaniu do tonacji pilotowej i następnych danych) długością (patrz rysunek)
Po otrzymaniu impulsu synchronizującego komputer rejestruje każde wzniesienie / opadanie sygnału, mierząc jego długość. Jeśli długość jest krótsza od ustalonego limitu, do pamięci zapisywany jest bit 1, w przeciwnym razie 0. Bity są zbierane w bajty, a proces powtarza się, aż zostanie odebranych N bajtów. Liczba N zwykle jest pobierana z nagłówka pobieranego pliku. Sekwencja ładowania wygląda następująco:
- pilot-ton
- nagłówek (o stałej długości), zawiera rozmiar ładowanych danych (N), nazwę i typ pliku
- pilot-ton
- same dane
Aby upewnić się, że dane zostały poprawnie załadowane, ZX Spectrum odczytuje ostatni bajt, zwany bajtem parzystości (parity byte), który jest obliczany podczas zapisywania pliku operacją XOR na wszystkich bajtach zapisanych danych. Podczas odczytu pliku komputer oblicza bajt parzystości z otrzymanych danych i, jeśli wynik różni się od zapisanego, wyświetla komunikat o błędzie „Błąd ładowania taśmy R”. Mówiąc ściśle, komputer może wydać ten komunikat wcześniej, jeśli podczas odczytu nie może rozpoznać impulsu (został pominięty lub jego długość nie odpowiada ustalonym granicom)
Zatem przyjrzyjmy się teraz, jak wygląda nieznany sygnał:

To jest ton pilotowy. Kształt sygnału znacznie się różni, ale widać, że sygnał składa się z powtarzających się krótkich impulsów o określonej częstotliwości. Przy częstotliwości próbkowania 44100 Hz odległość między „szczytami” wynosi około 48 próbek (co odpowiada częstotliwości ~918 Hz). Zapamiętajmy tę liczbę.
Przyjrzyjmy się teraz fragmentowi z danymi:

Jeśli zmierzymy odległość między poszczególnymi impulsami, okaże się, że między „długimi” impulsami odległość wciąż wynosi ~48 próbek, a między krótkimi — ~24. Trochę wyprzedzając fakty, powiem, że w końcu okazało się, że „oporne” impulsy o częstotliwości 918 Hz następują nieprzerwanie, od początku do końca pliku. Można przypuszczać, że podczas przesyłania danych, jeśli między opornymi impulsami pojawia się dodatkowy impuls, traktujemy go jako bit 1, w przeciwnym razie 0.
Co z impulsem synchronizującym? Przyjrzyjmy się początkowi danych:

Ton pilotowy się kończy, a dane zaczynają się od razu. Nieco później, analizując różne nagrania audio, zauważono, że pierwszy bajt danych zawsze jest taki sam (10100101b, A5h). Możliwe, że komputer zaczyna odczytywać dane, gdy je otrzyma.
Można również zwrócić uwagę na przesunięcie pierwszego impulsu referencyjnego tuż po ostatniej jedynce w synchronizowanym bajcie. Udało się to zauważyć znacznie później w trakcie opracowywania programu do rozpoznawania danych, kiedy dane na początku pliku nie mogły być stabilnie odczytywane.
Teraz spróbujemy opisać algorytm, który przetworzy plik audio i załaduje dane.
Ładowanie danych
Najpierw rozważymy kilka założeń, aby nie komplikować algorytmu:
- Będziemy rozważać pliki tylko w formacie WAV;
- Plik audio musi zaczynać się od tonu pilotowego i nie może zawierać ciszy na początku.
- Plik źródłowy musi mieć częstotliwość próbkowania 44100 Hz. W takim przypadku odległość między impulsami referencyjnymi w 48 próbkach jest już ustalona i nie musimy jej obliczać programowo;
- Format próbek może być dowolny (8/16 bitów/z zmiennoprzecinkową) — ponieważ podczas odczytu możemy skonwertować go na odpowiedni;
- Zakładamy, że plik źródłowy jest znormalizowany pod względem amplitudy, co powinno ustabilizować wynik;
Algorytm odczytu będzie wyglądał następująco:
- Odczytujemy plik do pamięci, jednocześnie konwertując format próbek na 8 bitów;
- Określamy pozycję pierwszego impulsu w danych audio. Aby to zrobić, musimy obliczyć numer próbki o maksymalnej amplitudzie. Dla uproszczenia obliczmy to raz ręcznie. Zachowamy w zmiennej prev_pos;
- Dodajemy do pozycji ostatniego impulsu 48 (pos := prev_pos + 48)
- Ponieważ zwiększenie pozycji o 48 nie gwarantuje, że trafimy na pozycję następnego impulsu odniesienia (wady taśmy, niestabilna praca mechanizmu przesuwu taśmy itp.), konieczne jest skorygowanie pozycji impulsu pos. W tym celu weźmiemy mały odcinek danych (pos-8; pos+8) i znajdziemy na nim maksimum wartości amplitudy. Pozycję odpowiadającą maksimum zachowamy w pos. Tutaj 8 = 48/6 — eksperymentalnie uzyskana stała, która gwarantuje, że określimy poprawne maksimum i nie dotkniemy innych impulsów, które mogą być blisko. W bardzo złych przypadkach, gdy odległość między impulsami jest znacznie mniejsza lub większa niż 48, można zrealizować wymuszony poszukiwacz impulsu, ale w ramach artykułu nie będę tego opisywał w algorytmie.
- Na poprzednim kroku należy również sprawdzić, czy impuls odniesienia w ogóle został znaleziony. To znaczy, że samo szukanie maksimum nie gwarantuje, że impuls w danym odcinku jest obecny. W swojej ostatniej realizacji programu odczytu sprawdzam różnicę między maksymalną a minimalną wartością amplitudy na odcinku, a jeśli przekracza pewną granicę, uznaję obecność impulsu. Istnieje również pytanie, co zrobić, jeśli impuls odniesienia nie został znaleziony. Są dwa warianty: albo dane się skończyły i następnie następuje cisza, albo należy to traktować jako błąd odczytu. Jednak dla uproszczenia algorytmu pominę to.
- Na następnym kroku należy określić obecność impulsu danych (bit 0 lub 1), w tym celu weźmiemy środek odcinka (prev_pos; pos) middle_pos równy middle_pos := (prev_pos+pos)/2 i w pewnym sąsiedztwie middle_pos na odcinku (middle_pos-8; middle_pos+8) policzymy maksimum i minimum amplitudy. Jeśli różnica między nimi jest większa niż 10, zapisujemy w wyniku bit 1, w przeciwnym razie 0. 10 — stała uzyskana empirycznie.
- Zachowujemy bieżącą pozycję w prev_pos (prev_pos := pos)
- Powtarzamy od kroku 3, aż odczytamy cały plik.
- Otrzymaną tablicę bitów należy zapisać jako zbiór bajtów. Ponieważ nie wzięliśmy pod uwagę bajta synchronizacyjnego podczas odczytywania, liczba bitów może okazać się niepodzielna przez 8, a także nie jest znane wymagane przesunięcie w bitach. W pierwszej wersji algorytmu nie wiedziałem o istnieniu bajta synchronizacyjnego, więc zapisywałem po 8 plików z różną liczbą przesunięć bitowych. Jeden z nich zawierał poprawne dane. W ostatecznym algorytmie po prostu usuwam wszystkie bity do A5h, co pozwala od razu uzyskać poprawny plik na wyjściu.
Algorytm w Ruby, dla zainteresowanych.
Jako język do napisania programu wybrałem Ruby, ponieważ większość czasu programuję w tym języku. Opcja nie jest wysoko wydajna, jednak zadanie uzyskania jak najszybszego odczytu nie jest priorytetem.
# Используем 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*")
Wynik
Po wypróbowaniu kilku wersji algorytmu i stałych, udało mi się uzyskać coś skrajnie interesującego:

Zatem, sądząc po ciągach znaków, mamy program do tworzenia wykresów. Jednak w tekście programu brakuje kluczowych słów. Wszystkie kluczowe słowa są zakodowane w postaci bajtów (wartość każdego > 80h). Teraz należy ustalić, jaki komputer z lat 80. mógł zapisywać programy w takim formacie.
W rzeczywistości to bardzo przypomina program w języku BASIC. W podobnym formacie komputer ZX Spectrum przechowuje w pamięci i zapisuje programy na taśmę. Na wszelki wypadek sprawdziłem kluczowe słowa pod kątem zgodności z . Jednak wynik, oczywiście, okazał się negatywny.
Sprawdziłem również kluczowe słowa BASIC popularnych komputerów tamtego czasu, Atari, Commodore 64 i kilku innych, do których udało mi się znaleźć dokumentację, ale bezskutecznie — moje znajomości w zakresie różnych komputerów retro nie były zbyt szerokie.
Wtedy postanowiłem przejść przez , i tu mój wzrok padł na nazwę producenta Radio Shack i komputera TRS-80. To właśnie te nazwy były napisane na etykietach kaset, które leżały na moim biurku! Nie wiedziałem wcześniej o tych nazwach i nie byłem zaznajomiony z komputerem TRS-80, dlatego wydawało mi się, że Radio Shack to producent kaset audio, taki jak BASF, Sony czy TDK, a TRS-80 — długość odtwarzania. Dlaczego nie?
Komputer Tandy/Radio Shack TRS-80
Bardzo prawdopodobne, że omawiana nagranie audio, które podaję jako przykład na początku artykułu, zostało nagrane na takim komputerze:

Okazało się, że ten komputer i jego odmiany (Model I/Model III/Model IV itp.) były w swoim czasie bardzo popularne (oczywiście nie w Rosji). Ciekawe, że procesor, który w nich używano, to również Z80. W Internecie można znaleźć wiele informacji na temat tego komputera. czasopismach. emulatorów Pobrałem emulator
trs80gp Znalazłem również
narzędzie Po zrozumieniu formatu pliku CAS (który okazał się prostą kopią bitową danych z taśmy, którą już miałem w rękach, z wyjątkiem nagłówka zawierającego bajt synchronizacyjny), wprowadziłem kilka zmian w swoim programie i udało mi się uzyskać działający plik CAS, który zadziałał w emulatorze (TRS-80 Model III):
Ostatnia wersja narzędzia do konwersji z automatycznym określaniem pierwszego impulsu i odległości między impulsami odniesienia została stworzona jako pakiet GEM, a kod źródłowy jest dostępny na

Githubie. .
Podsumowanie
Zrozumiałem format zapisu danych w ZX Spectrum i zbadałem wbudowane w ROM subprogramy do zapisywania/odczytywania danych z kaset audio.
- Poznałem komputer TRS-80 i jego odmiany, badałem system operacyjny, oglądałem przykłady programów i nawet miałem możliwość pracować nad debugowaniem w kodzie maszynowym (wszystkie mnemoniki Z80 są mi doskonale znane).
- Poznałem komputer TRS-80 i jego odmiany, badałem system operacyjny, oglądałem przykłady programów i nawet miałem możliwość zajmować się odtwarzaniem w kodzie maszynowym (wszystkie mnemoniki Z80 są mi dobrze znane).
- Stworzyłem kompleksowe narzędzie do konwersji nagrań audio do formatu CAS, które potrafi odczytywać dane, które nie są rozpoznawane przez "oficjalne" narzędzie.
Źródło: habr.com
