Як я відновлював дані у невідомому форматі з магнітної стрічки

Передісторія

Будучи любителем ретро заліза, я придбав якось у продавця з Великобританії ZX Spectrum+. У комплекті з самим комп'ютером мені дісталися кілька аудіокасет з іграми (в оригінальній упаковці з інструкціями), а також програмами, записаними на касети без особливих позначень. Напрочуд дані з касет 40-річної давності добре читалися і мені вдалося завантажити майже всі ігри та програми з них.

Як я відновлював дані у невідомому форматі з магнітної стрічки

Однак, на деяких касетах я виявив записи, зроблені не комп'ютером ZX Spectrum. Звучали вони зовсім інакше і, на відміну записів зі згаданого комп'ютера, не починалися з короткого BASIC завантажувача, який зазвичай є у записах всіх програм та ігор.

Якийсь час мені не давало цього спокою — дуже хотілося дізнатися, що приховано в них. Якби вдалося прочитати аудіо сигнал як послідовність байтів, можна було б пошукати в них символи або щось, що вказує на походження сигналу. Свого роду ретро-археологія.

Зараз, коли я пройшов весь шлях і дивлюся на етикетки самих касет, я посміхаюся, бо

відповідь була прямо перед очима весь цей час
На етикетці лівої касети - назва комп'ютера TRS-80, і трохи нижче назва виробника: "Manufactured by Radio Shack in USA"

(Якщо хочете зберегти інтригу до кінця, не заходьте під спойлер)

Порівняння аудіо сигналів

Насамперед оцифруємо аудіозаписи. Можна послухати, як це звучить:


І як завжди звучить запис із комп'ютера ZX Spectrum:


В обох випадках на початку запису є так званий пілотний тон - звук однієї частоти (на першому записі він дуже короткий <1 сек, проте помітний). Пілотний тон служить сигналом комп'ютера, що потрібно підготуватися для отримання даних. Як правило, кожен комп'ютер розпізнає тільки «свій» пілотний тон за формою сигналу та його частотою.

Треба сказати про саму форму сигналу. Наприклад, на ZX Spectrum його форма прямокутна:

Як я відновлював дані у невідомому форматі з магнітної стрічки

При виявленні пілотного тону ZX Spectrum відображає червоно-блакитні смужки, що чергуються на бордюрній частині екрана, показуючи, що сигнал розпізнаний. Пілотний тон закінчується синхро-імпульсом, який сигналізує комп'ютера про те, що потрібно починати приймати дані. Він характеризується меншою (порівняно з пілотним тоном та наступними даними) тривалістю (див. малюнок)

Після отримання синхро-імпульсу комп'ютер фіксує кожен підйом/спуск сигналу, вимірюючи його тривалість. Якщо тривалість менша за певну межу, в пам'ять записується біт 1, інакше 0. Біти збираються в байти і процес повторюється до тих пір, поки не буде отримано N байт. Число N, як правило, береться із заголовка файлу, що завантажується. Послідовність завантаження наступна:

  1. пілотний тон
  2. заголовок (фіксованої довжини), містить розмір завантажених даних (N), ім'я та тип файлу
  3. пілотний тон
  4. самі дані

Щоб переконатися, що дані завантажені правильно, ZX Spectrum останнім байтом читає так званий байт парності (Parity byte), який обчислюється при збереженні файлу операцією XOR над усіма байтами записаних даних. Під час читання файлу комп'ютер обчислює байт парності з даних і, якщо результат відрізняється від збереженого, виводить повідомлення про помилку «R Tape loading error». Строго кажучи, комп'ютер може видати це повідомлення і раніше, якщо читання не може розпізнати імпульс (пропущений або його тривалість не відповідає певним межам)

Отже, подивимося тепер, як виглядає невідомий сигнал:

Як я відновлював дані у невідомому форматі з магнітної стрічки

Це пілотний тон. Форма сигналу значно відрізняється, але видно, що сигнал складається з коротких імпульсів певної частоти, що повторюються. При частоті дискретизації 44100 Гц, відстань між піками приблизно дорівнює 48 семплів (що відповідає частоті ~918 Гц) Запам'ятаємо цю цифру.

Подивимося тепер на фрагмент із даними:

Як я відновлював дані у невідомому форматі з магнітної стрічки

Якщо виміряти відстань між окремими імпульсами, виявиться, що між «довгими» імпульсами відстань, як і раніше, ~48 семплів, а між короткими — ~24. Небагато забігаючи вперед, скажу, що в результаті з'ясувалося, «опорні» імпульси з частотою 918 Гц йдуть безперервно, від початку і до кінця файлу. Можна припустити, що з передачі даних, якщо між опорними імпульсами зустрічається додатковий імпульс, вважаємо його за біт 1, інакше 0.

Що із синхро-імпульсом? Подивимося на початок даних:

Як я відновлював дані у невідомому форматі з магнітної стрічки

Пілотний тон закінчується і відразу починаються дані. Трохи пізніше, проаналізувавши кілька різних аудіо записів, вдалося виявити, що перший байт даних завжди той самий (10100101b, A5h). Можливо, комп'ютер починає зчитувати дані після того, як отримає його.

Можна також звернути увагу на зрушення першого опорного імпульсу відразу після останньої перші в синхробаті. Його вдалося виявити значно пізніше в процесі розробки програми для розпізнавання даних, коли дані на початку файлу не могли стабільно рахуватися.

Тепер спробуємо описати алгоритм, який обробить аудіо файл та завантажить дані.

завантаження даних

Спочатку розглянемо кілька припущень, щоб не ускладнювати алгоритм:

  1. Розглянемо файли тільки у форматі WAV;
  2. Аудіофайл повинен починатися з пілотного тону і не повинен містити тишу на початку
  3. Вихідний файл повинен мати частоту дискретизації 44100 48 Гц. У такому разі відстань між опорними імпульсами в XNUMX семплів вже визначено і нам не потрібно програмно розраховувати;
  4. Формат семплів може бути будь-який (8/16 біт/с плаваючою точкою) - так як при читанні ми можемо сконвертувати його в потрібний;
  5. Припускаємо, що вихідний файл нормалізовано за амплітудою, що має стабілізувати результат;

Алгоритм читання буде наступним:

  1. Читаємо файл у пам'ять, одночасно конвертуємо формат семплів у 8 біт;
  2. Визначаємо позицію першого імпульсу в аудіодані. Для цього потрібно визначити номер семпла з максимальною амплітудою. Для простоти вважаємо його один раз вручну. Збережемо змінну prev_pos;
  3. Додаємо до позиції останнього імпульсу 48 (pos := prev_pos + 48)
  4. Так як збільшення позиції на 48 не гарантує, що ми потрапимо в позицію наступного опорного імпульсу (дефекти стрічки, нестабільна робота стрічкопротяжного механізму та інше) потрібно відкоригувати позицію імпульсу pos. Для цього візьмемо невеликий відрізок даних (pos-8; pos+8) та знайдемо на ньому максимум значення амплітуди. Позицію, що відповідає максимуму, збережемо в pos. Тут 8 = 48/6 - експериментально отримана константа, яка гарантує, що ми визначимо вірний максимум і не торкнемося інших імпульсів, які можуть йти поруч. У дуже поганих випадках, коли відстань між імпульсами значно менше або більше 48, можна реалізувати примусовий пошук імпульсу, але в рамках статті я не описуватиму це в алгоритмі;
  5. На попередньому кроці також необхідно перевірити, що опорний імпульс взагалі знайдено. Тобто, якщо просто шукати максимум, це не гарантує, що імпульс у даному відрізку присутній. У своїй останній реалізації програми читання я перевіряю різницю між максимальним та мінімальним значенням амплітуди на відрізку, і якщо вона перевищує певний кордон, зараховую наявність імпульсу. Питання також, що робити, якщо опорний імпульс не знайдено. Тут два варіанти: або дані закінчилися і далі слідує тиша, або це слід розглядати як помилку читання. Однак опустимо це спрощення алгоритму;
  6. На наступному кроці потрібно визначити наявність імпульсу даних (біт 0 або 1), для цього візьмемо середину відрізка (prev_pos;pos) middle_pos рівну middle_pos := (prev_pos+pos)/2 і в околиці middle_pos на відрізку (middle_pos-8; +8) порахуємо максимум та мінімум амплітуди. Якщо різниця між ними більше 10, записуємо в результат біт 1 або 0. 10 — константа отримана дослідним шляхом;
  7. Зберігаємо поточну позицію в prev_pos (prev_pos := pos)
  8. Повторюємо починаючи з кроку 3, доки прочитаємо весь файл;
  9. Отриманий бітовий масив необхідно зберегти як набір байт. Оскільки ми не врахували синхробайт при читанні, кількість бітів може виявитися не кратно 8, а також невідомо необхідне зміщення в бітах. У першій реалізації алгоритму я не знав існування синхро-байта і тому просто зберігав 8 файлів з різною кількістю біт зміщення. Один із них містив коректні дані. У фінальному алгоритмі я видаляю всі біти до A5h, що дозволяє відразу отримувати коректний файл на виході

Алгоритм на Ruby, кому цікаво
Як мову для написання програми вибрав Ruby, т.к. Більшу частину часу програмую на ньому. Варіант не є високопродуктивним, проте завдання зробити швидкість читання максимально швидкою не стоїть.

# Используем 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*")

Результат

Перепробувавши кілька варіантів алгоритму та констант, мені пощастило отримати щось надзвичайно цікаве:

Як я відновлював дані у невідомому форматі з магнітної стрічки

Отже, судячи з символьних рядків, ми маємо програму для побудови графіків. Однак у тексті програми немає ключових слів. Усі ключові слова закодовані як байтів (значення кожного > 80h). Тепер потрібно з'ясувати, який комп'ютер із 80-х міг зберігати програми у такому форматі.

Насправді, це дуже схоже на програму на мові BASIC. Приблизно в такому форматі комп'ютер ZX Spectrum зберігає в пам'яті і зберігає програми на стрічку. Про всяк випадок я перевірив ключові слова на відповідність до таблицею. Проте результат, очевидно, виявився негативним.

Також я перевірив ключові слова BASIC популярних на той час комп'ютерів Atari, Commodore 64 та кількох інших, на які вдалося знайти документацію, проте безуспішно — мої знання у різновидах ретро-комп'ютерів виявилися не такими широкими.

Тоді я вирішив піти по список, і тут мій погляд впав на назву виробника Radio Shack та комп'ютера TRS-80. Саме ці назви були написані на етикетках касет, які лежали на столі! Я не знав раніше ці назви і не був знайомий з комп'ютером TRS-80, тому мені здавалося, що Radio Shack це виробник аудіокасет, такий як BASF, Sony або TDK, a TRS-80 - тривалість відтворення. Чому ні?

Комп'ютер Tandy/Radio Shack TRS-80

Дуже ймовірно, що аудіозапис, який я навів як приклад на початку статті, зроблено на такому комп'ютері:

Як я відновлював дані у невідомому форматі з магнітної стрічки

Виявилося, що цей комп'ютер та його різновиди (Model I/Model III/Model IV і т.д.) були дуже популярні свого часу (звичайно, не в Росії). Примітно, що процесор, яких у них використовувався теж Z80. По даному комп'ютеру в Інтернеті можна знайти багато інформації. У 80-х роках інформація про комп'ютер поширювалася в журналах. На даний момент існує декілька емуляторів комп'ютер під різні платформи.

Я завантажив емулятор trs80gp і мені вперше вдалося подивитися, як працював цей комп'ютер. Звичайно, комп'ютер не підтримував виведення кольору, роздільна здатність екрану всього 128х48 пікселів, але існувало безліч розширень і модифікацій які могли збільшувати роздільну здатність екрану. Також існувало безліч варіантів операційних систем для даного комп'ютера та варіантів реалізації мови BASIC (який, на відміну від ZX Spectrum, у деяких моделях навіть не був «прошитий» у ПЗУ і будь-який варіант міг завантажуватися з дискети, так само як і сама ОС)

Також я знайшов утиліту для конвертування аудіозаписів у формат CAS, яких підтримується емуляторами, проте прочитати з їх допомогою записи з моїх касет з якихось причин не вдалося.

Розібравшись із форматом файлу CAS (який виявився просто побітовою копією даних зі стрічки, яка у мене вже була на руках, за винятком заголовка з наявністю синхро-байта), я вніс кілька змін у свою програму та зміг отримати на виході робочий CAS файл, який заробив в емуляторі (TRS-80 Model III):

Як я відновлював дані у невідомому форматі з магнітної стрічки

Останній варіант утиліти для конвертації з автоматичним визначенням першого імпульсу і відстанню між опорними імпульсами я оформив у вигляді пакета GEM, вихідний код доступний на Github.

Висновок

Пройдений шлях виявився захоплюючою подорожжю в минуле, і я радий, що в результаті знайшов розгадку. До того ж я:

  • Розібрався із форматом збереження даних у ZX Spectrum та вивчив вбудовані у ПЗУ підпрограми збереження/читання даних з аудіокасет
  • Познайомився з комп'ютером TRS-80 та його різновидами, вивчив операційну систему, подивився приклади програм і навіть мав можливість зайнятися налагодженням у машинних кодах (все-таки всі мнемоніки Z80 мені добре знайомі)
  • Написав повноцінну утиліту для конвертування аудіо записів у CAS формат, яка може зчитувати дані, які не розпізнаються «офіційною» утилітою

Джерело: habr.com

Додати коментар або відгук