Hintergrund
Als Liebhaber von Retro-Hardware erwarb ich einmal einen ZX Spectrum+ von einem Verkäufer aus Großbritannien. Zusammen mit dem Computer erhielt ich mehrere Audiokassetten mit Spielen (in der origianalen Verpackung mit Anleitungen) sowie Programmen, die auf Kassetten ohne besondere Bezeichnungen aufgezeichnet waren. Überraschenderweise ließen sich die Daten von den 40 Jahre alten Kassetten gut lesen und ich konnte fast alle Spiele und Programme von ihnen laden.

Allerdings entdeckte ich auf einigen Kassetten Aufnahmen, die eindeutig nicht mit dem ZX Spectrum gemacht worden waren. Sie klangen ganz anders und begannen im Gegensatz zu den Aufnahmen des genannten Computers nicht mit dem kurzen BASIC-Lader, der normalerweise in allen Programm- und Spielaufnahmen enthalten ist.
Eine Zeit lang ließ mich das nicht in Ruhe – ich wollte unbedingt herausfinden, was sich darin verbarg. Wenn ich das Audiosignal als Bytefolge lesen könnte, könnte ich nach Zeichen oder etwas suchen, das auf die Herkunft des Signals hinweist. Eine Art Retro-Archäologie.
Jetzt, wo ich den ganzen Weg gegangen bin und auf die Etiketten der Kassetten schaue, lächle ich, denn
die Antwort war die ganze Zeit direkt vor meinen Augen.
Auf dem Etikett der linken Kassette steht der Name des Computers TRS-80, und etwas darunter der Hersteller: „Hergestellt von Radio Shack in den USA“.
(Wenn Sie die Spannung bis zum Ende aufrechterhalten möchten, gehen Sie nicht zum Spoiler.)
Vergleich der Audiosignale
Zuerst digitalisieren wir die Audiomitschnitte. Hier können Sie hören, wie es klingt:
Und so klingt die Aufnahme vom Computer ZX Spectrum:
In beiden Fällen beginnt die Aufnahme mit dem sogenannten Pilotton, ein Signal einer Frequenz (in der ersten Aufnahme ist es sehr kurz <1 Sekunde, aber er ist wahrnehmbar). Der Pilotton dient dem Computer als Signal, sich auf den Empfang von Daten vorzubereiten. In der Regel erkennt jeder Computer nur seinen eigenen Pilotton anhand der Signalform und -frequenz.
Ich muss auch die Signalform erwähnen. Zum Beispiel hat sie beim ZX Spectrum eine rechteckige Form:

Wenn der Pilotton erkannt wird, zeigt der ZX Spectrum abwechselnd rote und blaue Streifen auf dem Rand des Bildschirms an, um zu zeigen, dass das Signal erkannt wurde. Der Pilotton endet mit einem Synchronisationsimpuls., der dem Computer signalisiert, dass er mit der Datenverarbeitung beginnen soll. Er hat eine kürzere Dauer (im Vergleich zum Pilotton und den folgenden Daten; siehe Abbildung).
Nachdem der Synchronimpuls empfangen wurde, erfasst der Computer jeden Anstieg/Abfall des Signals und misst dessen Dauer. Liegt die Dauer unter einem bestimmten Grenzwert, wird ein Bit 1 in den Speicher geschrieben, andernfalls 0. Die Bits werden zu Bytes gruppiert, und dieser Prozess wiederholt sich, bis N Bytes empfangen sind. Die Zahl N stammt in der Regel aus dem Header der heruntergeladenen Datei. Der Ablauf ist wie folgt:
- Pilotton,
- Header (fester Länge), der die Größe der heruntergeladenen Daten (N), den Namen und den Typ der Datei enthält.
- Pilotton,
- Die eigentlichen Daten.
Um sicherzustellen, dass die Daten korrekt geladen wurden, liest der ZX Spectrum im letzten Byte das sogenannte Paritätsbyte. (Parity-Bit), der beim Speichern einer Datei durch die XOR-Operation über alle Bytes der gespeicherten Daten berechnet wird. Beim Lesen der Datei berechnet der Computer das Paritätsbyte aus den empfangenen Daten und gibt, falls das Ergebnis von dem gespeicherten abweicht, die Fehlermeldung „R Tape loading error“ aus. Streng genommen kann der Computer diese Nachricht auch schon vorher ausgeben, wenn er beim Lesen den Impuls nicht erkennen kann (entweder fehlt er oder seine Dauer entspricht nicht bestimmten Grenzen).
Schauen wir uns jetzt an, wie ein unbekanntes Signal aussieht:

Dies ist ein Pilotton. Die Form des Signals unterscheidet sich erheblich, aber es ist zu erkennen, dass das Signal aus sich wiederholenden kurzen Impulsen einer bestimmten Frequenz besteht. Bei einer Abtastrate von 44100 Hz beträgt der Abstand zwischen den „Spitzen“ ungefähr 48 Samples (was einer Frequenz von ~918 Hz entspricht). Merken wir uns diese Zahl.
Lassen Sie uns nun einen Datenabschnitt ansehen:

Wenn man den Abstand zwischen den einzelnen Impulsen misst, stellt man fest, dass der Abstand zwischen den "langen" Impulsen nach wie vor etwa 48 Samples beträgt, während er zwischen den kurzen etwa 24 ist. Um vorauszunehmen, stellte sich heraus, dass die "Referenz"-Impulse mit einer Frequenz von 918 Hz kontinuierlich vom Anfang bis zum Ende der Datei folgen. Man könnte annehmen, dass bei der Datenübertragung, wenn zwischen den Referenzimpulsen ein zusätzlicher Impuls auftritt, dieser als Bit 1 gezählt wird, andernfalls als 0.
Was ist mit dem Synchronimpuls? Schauen wir uns den Beginn der Daten an:

Der Pilotton endet und sofort beginnen die Daten. Etwas später, als ich mehrere verschiedene Audioaufnahmen analysierte, stellte ich fest, dass das erste Datenbyte immer gleich ist (10100101b, A5h). Möglicherweise beginnt der Computer mit dem Auslesen der Daten, nachdem er dies erhalten hat.
Es ist auch bemerkenswert, dass der erste Referenzimpuls sofort nach der letzten 1 im Synchronbyte verschoben ist. Dies wurde viel später entdeckt, als ich ein Programm zur Datenerkennung entwickelte, da die Daten zu Beginn der Datei nicht stabil ausgelesen werden konnten.
Jetzt versuchen wir, den Algorithmus zu beschreiben, der die Audiodatei verarbeitet und die Daten hochlädt.
Daten hochladen
Zunächst betrachten wir einige Annahmen, um den Algorithmus nicht unnötig zu komplizieren:
- Wir betrachten nur Dateien im WAV-Format;
- Die Audiodatei muss mit einem Pilotton beginnen und darf zu Beginn keine Stille enthalten.
- Die Quelldatei muss eine Abtastrate von 44100 Hz haben. In diesem Fall ist der Abstand zwischen den Referenzimpulsen von 48 Samples bereits festgelegt, sodass wir ihn nicht programmatisch berechnen müssen;
- Das Format der Samples kann beliebig sein (8/16 Bit/Gleitkomma) – da wir beim Lesen in das benötigte Format konvertieren können;
- Wir gehen davon aus, dass die Quelldatei hinsichtlich der Amplitude normalisiert ist, was das Ergebnis stabilisieren sollte;
Der Lesealgorithmus wird folgendermaßen aussehen:
- Wir lesen die Datei in den Speicher und konvertieren gleichzeitig das Sample-Format in 8 Bit;
- Wir bestimmen die Position des ersten Impulses in den Audiodaten. Dazu müssen wir die Nummer des Samples mit der maximalen Amplitude berechnen. Zur Vereinfachung berechnen wir dies einmal manuell. Wir speichern es in der Variablen prev_pos;
- Wir addieren 48 zur Position des letzten Impulses (pos := prev_pos + 48)
- Da die Erhöhung der Position um 48 nicht garantiert, dass wir in die Position des nächsten Bezugsspielraums gelangen (Bandfehler, instabile Funktion des Fördermechanismus usw.), muss die Impulsposition pos angepasst werden. Dazu nehmen wir einen kleinen Datenabschnitt (pos-8; pos+8) und finden das Maximum des Amplitudenwerts darin. Die Position, die dem Maximum entspricht, wird in pos gespeichert. Hierbei ist 8 = 48/6 — eine experimentell ermittelte Konstante, die sicherstellt, dass wir das korrekte Maximum bestimmen und andere benachbarte Impulse nicht stören. In sehr schlechten Fällen, in denen der Abstand zwischen den Impulsen deutlich weniger oder mehr als 48 beträgt, kann eine erzwungene Impulsersuche implementiert werden, aber im Rahmen dieses Artikels werde ich das im Algorithmus nicht beschreiben.
- Im vorherigen Schritt sollte ebenfalls überprüft werden, ob der Referenzimpuls tatsächlich gefunden wurde. Das bloße Suchen nach einem Maximum garantiert nicht, dass im gegebenen Abschnitt ein Impuls vorhanden ist. In meiner letzten Implementierung des Leseprogramms prüfe ich die Differenz zwischen dem maximalen und minimalen Amplitudenwert im Abschnitt. Wenn diese einen bestimmten Grenzwert überschreitet, bestätigen wir das Vorhandensein eines Impulses. Eine weitere Frage ist, was zu tun ist, wenn der Referenzimpuls nicht gefunden wird. Hier gibt es zwei Möglichkeiten: entweder sind die Daten zu Ende und es folgt Ruhe, oder dies sollte als Lesefehler betrachtet werden. Lassen wir dies jedoch zur Vereinfachung des Algorithmus außen vor.
- Im nächsten Schritt muss das Vorhandensein eines Datenimpulses (Bit 0 oder 1) bestimmt werden. Dazu nehmen wir die Mitte des Abschnitts (prev_pos; pos), wobei middle_pos := (prev_pos + pos) / 2 und in einer bestimmten Nachbarschaft von middle_pos im Abschnitt (middle_pos - 8; middle_pos + 8) das Maximum und Minimum der Amplitude berechnen. Wenn die Differenz zwischen ihnen größer als 10 ist, notieren wir im Ergebnis Bit 1, andernfalls 0. 10 ist eine konstatierte Konstante, die experimentell ermittelt wurde.
- Wir speichern die aktuelle Position in prev_pos (prev_pos := pos).
- Wir wiederholen ab Schritt 3, bis wir die gesamte Datei gelesen haben.
- Das erhaltene Bit-Array muss als Byte-Set gespeichert werden. Da wir das Synchronisationsbyte beim Lesen nicht berücksichtigt haben, kann die Anzahl der Bits nicht durch 8 teilbar sein und die erforderliche Bit-Versetzung ist unbekannt. In der ersten Implementierung des Algorithmus war mir die Existenz eines Synchronisationsbytes nicht bekannt, weshalb ich einfach 8 Dateien mit unterschiedlichen Bitverschiebungen gespeichert habe. Eine davon enthielt die korrekten Daten. Im endgültigen Algorithmus entferne ich einfach alle Bits bis A5h, was es ermöglicht, sofort eine korrekte Datei zu erhalten.
Algorithmus in Ruby, für Interessierte
Für die Programmiersprache habe ich Ruby gewählt, da ich die meiste Zeit damit programmiere. Die Variante ist nicht hochgradig leistungsfähig, jedoch steht die Aufgabe, die Lesezeit maximal zu beschleunigen, nicht im Vordergrund.
# Используем 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*")
Ergebnis
Nachdem ich mehrere Varianten des Algorithmus und Konstanten ausprobiert habe, hatte ich das Glück, etwas extrem Interessantes zu erzielen:

Anhand der Zeichenfolgen scheint es, dass wir ein Programm zur Erstellung von Diagrammen haben. Allerdings fehlen im Programmtext die Schlüsselwörter. Alle Schlüsselwörter sind in Form von Bytes codiert (jedes Wert > 80h). Jetzt muss herausgefunden werden, welcher Computer aus den 80er-Jahren Programme in einem solchen Format speichern konnte.
Tatsächlich ähnelt es sehr einem Programm in BASIC. In einem ähnlichen Format speichert der Computer ZX Spectrum Programme im Speicher und sichert diese auf Band. Vorsichtshalber habe ich die Schlüsselwörter mit den vorhandenen verglichen. . Das Ergebnis war jedoch offensichtlich negativ.
Ich habe auch die BASIC-Schlüsselwörter der damals beliebten Computer wie Atari, Commodore 64 und einiger anderer, für die ich Dokumentationen finden konnte, überprüft, jedoch ohne Erfolg – meine Kenntnisse über die verschiedenen Retro-Computer waren nicht so umfassend.
Da entschloss ich mich, einen anderen Weg zu gehen. , und dabei fiel mein Blick auf den Herstellernamen Radio Shack und den Computer TRS-80. Genau diese Namen standen auf den Etiketten der Kassetten, die auf meinem Tisch lagen! Ich kannte diese Namen vorher nicht und war mit dem Computer TRS-80 nicht vertraut, deshalb dachte ich, dass Radio Shack ein Hersteller von Audiokassetten wie BASF, Sony oder TDK sei, und TRS-80 die Wiedergabedauer bedeutet. Warum nicht?
Computer Tandy/Radio Shack TRS-80
Es ist sehr wahrscheinlich, dass die betreffende Audioaufnahme, die ich als Beispiel zu Beginn des Artikels angeführt habe, auf einem solchen Computer gemacht wurde:

Es stellte sich heraus, dass dieser Computer und seine Varianten (Model I/Model III/Model IV usw.) in ihrer Zeit sehr beliebt waren (natürlich nicht in Russland). Bemerkenswert ist, dass der Prozessor, der darin verwendet wurde, ebenfalls Z80 war. Man kann im Internet viel Informationen zu diesem Computer finden In den 80er Jahren verbreitete sich das Wissen über Computer in Derzeit gibt es mehrere des Computers für verschiedene Plattformen.
Ich habe den Emulator Ich hatte zum ersten Mal die Gelegenheit zu sehen, wie dieser Computer funktionierte. Natürlich unterstützte der Computer keine Farbausgabe, die Bildschirmauflösung betrug nur 128x48 Pixel, aber es gab viele Erweiterungen und Modifikationen, die die Bildschirmauflösung erhöhen konnten. Es standen auch zahlreiche Betriebssysteme zur Verfügung sowie verschiedene Implementierungen der BASIC-Sprache (die, anders als beim ZX Spectrum, in einigen Modellen nicht einmal im ROM „eingebrannt“ war und jede Variante von Diskette geladen werden konnte, genau wie das Betriebssystem selbst).
Außerdem fand ich ein Tool zum Konvertieren von Audiodateien in das CAS-Format, das von Emulatoren unterstützt wird. Allerdings konnte ich aus irgendeinem Grund Aufnahmen von meinen Kassetten mit deren Hilfe nicht lesen.
Nachdem ich das CAS-Dateiformat verstanden hatte (das sich als einfache bitweise Kopie der Daten von dem Band herausstellte, das ich bereits in der Hand hatte, abgesehen vom Header mit dem Synchronisationsbyte), nahm ich einige Änderungen an meinem Programm vor und konnte eine funktionierende CAS-Datei erstellen, die im Emulator (TRS-80 Model III) funktionierte.

Die letzte Version des Tools zur Konvertierung mit automatischer Erkennung des ersten Impulses und dem Abstand zwischen den Referenzimpulsen habe ich als GEM-Paket gestaltet, der Quellcode ist verfügbar unter .
Fazit
Der zurückgelegte Weg war eine faszinierende Reise in die Vergangenheit, und ich bin froh, dass ich schließlich das Rätsel gelöst habe. Unter anderem habe ich:
- Den Datenformat in ZX Spectrum untersucht und die im ROM integrierten Unterprogramme zum Speichern/Lesen von Daten von Audiokassetten studiert.
- Mich mit dem Computer TRS-80 und seinen Varianten vertraut gemacht, das Betriebssystem studiert, Programmbeispiele angesehen und sogar die Möglichkeit gehabt, mit dem Debuggen in Maschinencode zu experimentieren (schließlich sind mir alle Z80-Mnemonics gut bekannt).
- Eine vollständige Anwendung zur Konvertierung von Audioaufnahmen in das CAS-Format geschrieben, die Daten lesen kann, die von dem "offiziellen" Tool nicht erkannt werden.
Quelle: habr.com
