如何从磁带中恢复未知格式的数据

史前

作为复古硬件的爱好者,我曾经从英国的卖家那里购买过 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. header(固定长度),包含下载数据的大小(N)、文件名和类型
  3. 导频音
  4. 数据本身

为了确保数据正确加载,ZX Spectrum 读取所谓的 奇偶校验字节 (奇偶校验字节),在保存文件时通过对写入数据的所有字节进行异或来计算。 读取文件时,计算机根据接收到的数据计算奇偶校验字节,如果结果与保存的结果不同,则显示错误消息“R 磁带加载错误”。 严格来说,如果在读取时计算机无法识别脉冲(丢失或其持续时间不符合某些限制),则计算机可以提前发出此消息

那么,现在让我们看看未知信号是什么样的:

如何从磁带中恢复未知格式的数据

这是导频音。 信号的形状明显不同,但很明显该信号由特定频率的重复短脉冲组成。 在采样频率为 44100 Hz 时,“峰值”之间的距离约为 48 个样本(对应于约 918 Hz 的频率)。让我们记住这个数字。

现在让我们看一下数据片段:

如何从磁带中恢复未知格式的数据

如果我们测量各个脉冲之间的距离,结果表明“长”脉冲之间的距离仍然约为 48 个样本,短脉冲之间的距离约为 24 个样本。 展望未来,我会说,最终结果是,频率为 918 Hz 的“参考”脉冲从文件的开头到结尾连续不断地跟随。 可以假设,在传输数据时,如果在参考脉冲之间遇到额外的脉冲,我们将其视为位1,否则视为0。

同步脉冲呢? 我们看一下数据的开头:

如何从磁带中恢复未知格式的数据

导频音结束,数据立即开始。 稍后,在分析了几个不同的录音后,我们发现数据的第一个字节总是相同的(10100101b,A5h)。 计算机收到数据后即可开始读取数据。

您还可以注意同步字节中最后一个参考脉冲紧随其后的第一个参考脉冲的移位。 后来在开发数据识别程序的过程中才发现,无法稳定读取文件开头的数据。

现在让我们尝试描述一种处理音频文件和加载数据的算法。

加载数据中

首先,让我们看一些保持算法简单的假设:

  1. 我们只会考虑 WAV 格式的文件;
  2. 音频文件必须以导频音开头,并且开头不得包含静音
  3. 源文件的采样率必须为 44100 Hz。 在这种情况下,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;middle_pos +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 计算机以大致相同的格式将程序存储在内存中并将程序保存到磁带上。 以防万一,我检查了关键字 桌子。 然而,结果显然是否定的。

我还检查了当时流行的 Atari、Commodore 64 和其他几台计算机的 BASIC 关键字,我能够找到这些计算机的文档,但没有成功 - 我对复古计算机类型的了解并不是那么广泛。

然后我决定去 列表,然后我的目光落在了制造商Radio Shack的名字和TRS-80电脑上。 这些是我桌上的磁带标签上写的名字! 以前我不知道这些名字,也不熟悉TRS-80电脑,所以在我看来Radio Shack是巴斯夫、索尼或TDK等录音带制造商,TRS-80是播放时间。 为什么不?

电脑 Tandy/无线电棚 TRS-80

我在文章开头举的例子中所讨论的录音很可能是在这样的计算机上录制的:

如何从磁带中恢复未知格式的数据

事实证明,这款计算机及其变种(Model I/Model III/Model IV等)一度非常流行(当然不是在俄罗斯)。 值得注意的是,他们使用的处理器也是Z80。 对于这台计算机,您可以在互联网上找到 很多信息。 80世纪XNUMX年代,计算机信息广泛传播 杂志。 目前有几个 模拟器 不同平台的计算机。

我下载了模拟器 trs80gp 我第一次能够看到这台计算机是如何工作的。 当然,计算机不支持彩色输出;屏幕分辨率只有128x48像素,但是有许多扩展和修改可以提高屏幕分辨率。 该计算机的操作系统还有许多选项以及用于实现 BASIC 语言的选项(与 ZX Spectrum 不同,在某些型号中甚至没有“闪存”到 ROM 中,任何选项都可以从软盘加载,就像操作系统本身)

我还发现 效用 将音频录音转换为模拟器支持的 CAS 格式,但由于某种原因,无法使用它们从我的磁带中读取录音。

弄清楚 CAS 文件格式(结果只是我手头已有的磁带中数据的逐位复制,除了存在同步字节的标头之外),我制作了一个对我的程序进行了一些更改,并且能够输出在模拟器(TRS-80 Model III)中工作的工作 CAS 文件:

如何从磁带中恢复未知格式的数据

我设计了最新版本的转换实用程序,自动确定第一个脉冲和参考脉冲之间的距离作为 GEM 包,源代码可在 Github上.

结论

我们走过的路原来是一次令人着迷的回溯之旅,我很高兴最终找到了答案。 除其他事项外,我:

  • 我弄清楚了 ZX Spectrum 中保存数据的格式,并研究了用于保存/读取录音带数据的内置 ROM 例程
  • 我熟悉了TRS-80计算机及其品种,研究了操作系统,查看了示例程序,甚至有机会用机器代码进行调试(毕竟Z80的助记符我都很熟悉)
  • 编写了一个成熟的实用程序,用于将录音转换为 CAS 格式,它可以读取“官方”实用程序无法识别的数据

来源: habr.com

添加评论