Як я аднаўляў дадзеныя ў невядомым фармаце з магнітнай стужкі

перадгісторыя

Будучы аматарам рэтра жалеза, набыў я неяк у прадаўца з Вялікабрытаніі 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-цы ў сінхрабайце. Яго атрымалася выявіць значна пазней падчас распрацоўкі праграмы для распазнання дадзеных, калі дадзеныя ў пачатку файла не маглі стабільна лічыцца.

Цяпер паспрабуем апісаць алгарытм, які апрацуе аўдыё файл і загрузіць дадзеныя.

Загрузка дадзеных

Перш разгледзім некалькі дапушчэнняў, каб не ўскладняць алгарытм:

  1. Будзем разглядаць файлы толькі ў фармаце WAV;
  2. Аўдыёфайл павінен пачынацца з пілотнага тону і не павінен утрымоўваць цішыню ў пачатку
  3. Зыходны файл павінен мець частату дыскрэтызацыі 44100 Гц. У такім разе адлегласць паміж апорнымі імпульсамі ў 48 сэмплаў ужо вызначана і нам не трэба праграмна яго разлічваць;
  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. На папярэднім кроку таксама неабходна было б праверыць, што апорны імпульс увогуле знойдзены. Гэта значыць, калі проста шукаць максімум, гэта не гарантуе, што імпульс у дадзеным адрэзку прысутнічае. У сваёй апошняй рэалізацыі праграмы чытання я правяраю розніцу паміж максімальным і мінімальным значэннем амплітуды на адрэзку, і калі яна перавышае некаторую мяжу, залічваю наяўнасць імпульсу. Пытанне таксама, што рабіць, калі апорны імпульс не знойдзены. Тут 2 варыянты: альбо дадзеныя скончыліся і далей варта цішыня, альбо гэта варта разглядаць як памылку чытання. Аднак апусцім гэта для спрашчэння алгарытму;
  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

Дадаць каментар