prehistoria
Sendo un amante do hardware retro, merquei unha vez un ZX Spectrum+ a un vendedor no Reino Unido. Incluído co propio ordenador, recibín varios casetes de audio con xogos (no envase orixinal con instrucións), así como programas gravados en casetes sen marcas especiais. Sorprendentemente, os datos de casetes de 40 anos podían lerse ben e puiden descargar case todos os xogos e programas deles.
Non obstante, nalgúns casetes atopei gravacións que claramente non foron feitas polo ordenador ZX Spectrum. Soaban completamente diferentes e, a diferenza das gravacións do mencionado ordenador, non comezaban cun pequeno cargador de arranque BASIC, que adoita estar presente nas gravacións de todos os programas e xogos.
Durante algún tempo isto perseguíame: quería moito descubrir o que se agochaba neles. Se puideses ler o sinal de audio como unha secuencia de bytes, podes buscar caracteres ou calquera cousa que indique a orixe do sinal. Unha especie de retro-arqueoloxía.
Agora que fun todo o camiño e miro as etiquetas dos propios casetes, sorrí porque
a resposta estivo xusto diante dos meus ollos todo o tempo
Na etiqueta do casete esquerdo está o nome do ordenador TRS-80, e xusto debaixo do nome do fabricante: "Fabricado por Radio Shack en USA"
(Se queres manter a intriga ata o final, non pases por baixo do spoiler)
Comparación de sinais de audio
En primeiro lugar, imos dixitalizar as gravacións de audio. Podes escoitar como soa:
E como é habitual a gravación do ordenador ZX Spectrum soa:
En ambos os casos, ao comezo da gravación hai un chamado ton piloto - un son da mesma frecuencia (na primeira gravación é moi curto <1 segundo, pero é distinguible). O ton piloto indica ao ordenador que se prepare para recibir datos. Como regra xeral, cada ordenador recoñece só o seu "propio" ton piloto pola forma do sinal e a súa frecuencia.
É necesario dicir algo sobre a propia forma do sinal. Por exemplo, no ZX Spectrum a súa forma é rectangular:
Cando se detecta un ton piloto, o ZX Spectrum mostra barras vermellas e azuis alternas no bordo da pantalla para indicar que o sinal foi recoñecido. O ton do piloto remata pulso sincronizado, que indica ao ordenador para comezar a recibir datos. Caracterízase por unha duración máis curta (en comparación co ton piloto e os datos posteriores) (ver figura)
Despois de recibir o pulso de sincronización, o ordenador rexistra cada subida/descenso do sinal, medindo a súa duración. Se a duración é inferior a un determinado límite, o bit 1 escríbese na memoria, noutro caso 0. Os bits recóllense en bytes e o proceso repítese ata que se reciben N bytes. O número N adoita tomarse da cabeceira do ficheiro descargado. A secuencia de carga é a seguinte:
- ton piloto
- cabeceira (longitude fixa), contén o tamaño dos datos descargados (N), o nome e o tipo de ficheiro
- ton piloto
- os propios datos
Para asegurarse de que os datos se cargan correctamente, o ZX Spectrum le o chamado byte de paridade (byte de paridade), que se calcula ao gardar un ficheiro XORing todos os bytes dos datos escritos. Ao ler un ficheiro, o ordenador calcula o byte de paridade a partir dos datos recibidos e, se o resultado é diferente do gardado, mostra a mensaxe de erro "Erro de carga de cinta R". En rigor, o ordenador pode emitir esta mensaxe antes se, ao ler, non pode recoñecer un pulso (perdida ou a súa duración non corresponde a determinados límites)
Entón, vexamos agora como é un sinal descoñecido:
Este é o ton piloto. A forma do sinal é significativamente diferente, pero está claro que o sinal consiste en repetir pulsos curtos dunha determinada frecuencia. A unha frecuencia de mostraxe de 44100 Hz, a distancia entre os "picos" é de aproximadamente 48 mostras (o que corresponde a unha frecuencia de ~918 Hz). Lembremos esta figura.
Vexamos agora o fragmento de datos:
Se medimos a distancia entre os pulsos individuais, resulta que a distancia entre os pulsos "longos" aínda é de ~ 48 mostras, e entre as curtas - ~ 24. Mirando un pouco cara adiante, direi que ao final resultou que os pulsos de “referencia” cunha frecuencia de 918 Hz seguen continuamente, dende o principio ata o final do ficheiro. Pódese supoñer que ao transmitir datos, se se atopa un pulso adicional entre os pulsos de referencia, considerámolo como o bit 1, noutro caso 0.
E o pulso de sincronización? Vexamos o inicio dos datos:
O ton piloto remata e os datos comezan inmediatamente. Un pouco máis tarde, tras analizar varias gravacións de audio diferentes, puidemos descubrir que o primeiro byte de datos é sempre o mesmo (10100101b, A5h). O ordenador pode comezar a ler datos despois de que os reciba.
Tamén pode prestar atención ao desprazamento do primeiro pulso de referencia inmediatamente despois do último 1 do byte de sincronización. Descubriuse moito máis tarde no proceso de desenvolvemento dun programa de recoñecemento de datos, cando os datos ao comezo do ficheiro non se podían ler de forma estable.
Agora imos tentar describir un algoritmo que procesará un ficheiro de audio e cargará datos.
Cargando datos
Primeiro, vexamos algunhas suposicións para manter o algoritmo sinxelo:
- Só teremos en conta os ficheiros en formato WAV;
- O ficheiro de audio debe comezar cun ton piloto e non debe conter silencio ao principio
- O ficheiro de orixe debe ter unha frecuencia de mostraxe de 44100 Hz. Neste caso, a distancia entre os pulsos de referencia de 48 mostras xa está determinada e non necesitamos calculala programadamente;
- O formato de mostra pode ser calquera (8/16 bits/coma flotante) -xa que ao ler podemos convertelo ao desexado;
- Supoñemos que o ficheiro fonte está normalizado pola amplitude, o que debería estabilizar o resultado;
O algoritmo de lectura será o seguinte:
- Lemos o ficheiro na memoria, ao mesmo tempo convertendo o formato de mostra a 8 bits;
- Determine a posición do primeiro pulso nos datos de audio. Para iso, cómpre calcular o número de mostra coa amplitude máxima. Para simplificar, calcularémolo unha vez manualmente. Gardémolo na variable prev_pos;
- Engade 48 á posición do último pulso (pos := prev_pos + 48)
- Dado que aumentar a posición en 48 non garante que imos chegar á posición do seguinte pulso de referencia (defectos na cinta, funcionamento inestable do mecanismo de unidade de cinta, etc.), necesitamos axustar a posición do pulso pos. Para iso, colle un pequeno anaco de datos (pos-8;pos+8) e atopa o valor máximo de amplitude nel. A posición correspondente ao máximo almacenarase en pos. Aquí 8 = 48/6 é unha constante obtida experimentalmente, o que garante que determinaremos o máximo correcto e non afectará a outros impulsos que poidan estar preto. En casos moi malos, cando a distancia entre os pulsos é moito menor ou maior que 48, pódese implementar unha busca forzada dun pulso, pero no ámbito do artigo non o describirei no algoritmo;
- No paso anterior, tamén sería necesario comprobar que se atopou o pulso de referencia. É dicir, se simplemente busca o máximo, isto non garante que o impulso estea presente neste segmento. Na miña última implementación do programa de lectura, comprobo a diferenza entre os valores de amplitude máximo e mínimo nun segmento e, se supera un determinado límite, conto a presenza dun impulso. A pregunta tamén é que facer se non se atopa o pulso de referencia. Hai 2 opcións: ou os datos remataron e segue o silencio, ou isto debería considerarse un erro de lectura. Non obstante, omitiremos isto para simplificar o algoritmo;
- No seguinte paso, necesitamos determinar a presenza dun pulso de datos (bit 0 ou 1), para iso tomamos o medio do segmento (prev_pos;pos) middle_pos igual a middle_pos := (prev_pos+pos)/2 e nalgún barrio de middle_pos no segmento (middle_pos-8; middle_pos +8) calculemos a amplitude máxima e mínima. Se a diferenza entre eles é superior a 10, escribimos o bit 1 no resultado, senón 0. 10 é unha constante obtida experimentalmente;
- Garda a posición actual en prev_pos (prev_pos := pos)
- Repita a partir do paso 3 ata que lemos todo o ficheiro;
- A matriz de bits resultante debe gardarse como un conxunto de bytes. Dado que non tivemos en conta o byte de sincronización ao ler, é posible que o número de bits non sexa un múltiplo de 8 e tamén se descoñece a compensación de bits necesaria. Na primeira implementación do algoritmo, non sabía sobre a existencia do byte de sincronización e, polo tanto, simplemente gardaba 8 ficheiros con diferentes números de bits de compensación. Un deles contiña datos correctos. No algoritmo final, simplemente quito todos os bits ata A5h, o que me permite obter inmediatamente o ficheiro de saída correcto
Algoritmo en Ruby, para os interesados
Escollín Ruby como linguaxe para escribir o programa, porque... Programo nela a maior parte do tempo. A opción non é de altas prestacións, pero a tarefa de facer que a velocidade de lectura sexa o máis rápida posible non paga a pena.
# Используем 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*")
Resultado
Despois de probar varias variantes do algoritmo e das constantes, tiven a sorte de conseguir algo moi interesante:
Entón, a xulgar polas cadeas de caracteres, temos un programa para trazar gráficos. Non obstante, non hai palabras clave no texto do programa. Todas as palabras clave están codificadas como bytes (cada valor > 80 h). Agora temos que descubrir que ordenador dos 80 podería gardar programas neste formato.
De feito, é moi semellante a un programa BASIC. O ordenador ZX Spectrum almacena programas aproximadamente no mesmo formato na memoria e garda os programas en cinta. Por se acaso, comprobei as palabras clave
Tamén comprobei as palabras clave BÁSICAS dos populares Atari, Commodore 64 e varios outros ordenadores daquela época, para os que puiden atopar documentación, pero sen éxito: os meus coñecementos sobre os tipos de ordenadores retro resultaron non ser tan amplos.
Entón decidín ir
Ordenador Tandy/Radio Shack TRS-80
É moi probable que a gravación de audio en cuestión, que puxen como exemplo ao comezo do artigo, se fixera nun ordenador coma este:
Resultou que este ordenador e as súas variedades (Modelo I/Modelo III/Modelo IV, etc.) foron moi populares nun momento (por suposto, non en Rusia). Cabe destacar que o procesador que empregaron tamén era o Z80. Para este ordenador podes atopar en Internet
Descarguei o emulador
Eu tamén atopei
Despois de descubrir o formato do ficheiro CAS (que resultou ser só unha copia pouco a pouco dos datos da cinta que xa tiña a man, excepto a cabeceira coa presenza dun byte de sincronización), fixen un algúns cambios no meu programa e puiden sacar un ficheiro CAS que funcionou no emulador (TRS-80 Model III):
Deseñei a última versión da utilidade de conversión coa determinación automática do primeiro pulso e da distancia entre os pulsos de referencia como paquete GEM, o código fonte está dispoñible en
Conclusión
O camiño que percorremos resultou ser unha viaxe fascinante ao pasado, e alégrome de que ao final atopei a resposta. Entre outras cousas, eu:
- Descubrín o formato para gardar datos no ZX Spectrum e estudei as rutinas ROM integradas para gardar/ler datos de casetes de audio
- Coñecín a computadora TRS-80 e as súas variedades, estudei o sistema operativo, mirei programas de mostra e mesmo tiven a oportunidade de facer depuración en códigos de máquina (ao final, todos os mnemotécnicos Z80 son familiares para min)
- Escribiu unha utilidade completa para converter gravacións de audio ao formato CAS, que pode ler datos que non son recoñecidos pola utilidade "oficial"
Fonte: www.habr.com