ประวัติศาสตร์
ในฐานะผู้ชื่นชอบฮาร์ดแวร์ย้อนยุค ฉันเคยซื้อ 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 จะนำมาจากส่วนหัวของไฟล์ที่ดาวน์โหลด ลำดับการโหลดมีดังนี้:
- โทนเสียงนำร่อง
- ส่วนหัว (ความยาวคงที่) ประกอบด้วยขนาดของข้อมูลที่ดาวน์โหลด (N) ชื่อไฟล์และประเภท
- โทนเสียงนำร่อง
- ข้อมูลนั้นเอง
เพื่อให้แน่ใจว่าข้อมูลถูกโหลดอย่างถูกต้อง ZX Spectrum จะอ่านสิ่งที่เรียกว่า พาริตีไบต์ (พาริตีไบต์) ซึ่งคำนวณเมื่อบันทึกไฟล์โดย XORing ไบต์ทั้งหมดของข้อมูลที่เขียน เมื่ออ่านไฟล์ คอมพิวเตอร์จะคำนวณพาริตีไบต์จากข้อมูลที่ได้รับ และหากผลลัพธ์แตกต่างจากข้อมูลที่บันทึกไว้ จะแสดงข้อความแสดงข้อผิดพลาด “R ข้อผิดพลาดในการโหลดเทป” พูดอย่างเคร่งครัด คอมพิวเตอร์สามารถส่งข้อความนี้เร็วขึ้นหากไม่สามารถจดจำชีพจรได้เมื่ออ่าน (พลาดหรือระยะเวลาไม่สอดคล้องกับขีดจำกัดที่แน่นอน)
ตอนนี้เรามาดูกันว่าสัญญาณที่ไม่รู้จักมีลักษณะอย่างไร:
นี่คือโทนเสียงนำร่อง รูปร่างของสัญญาณมีความแตกต่างกันอย่างมีนัยสำคัญ แต่เป็นที่ชัดเจนว่าสัญญาณประกอบด้วยพัลส์สั้น ๆ ซ้ำ ๆ ในความถี่ที่แน่นอน ที่ความถี่สุ่มตัวอย่าง 44100 Hz ระยะห่างระหว่าง "จุดสูงสุด" จะอยู่ที่ประมาณ 48 ตัวอย่าง (ซึ่งสอดคล้องกับความถี่ ~ 918 Hz) มาจำตัวเลขนี้กัน
ตอนนี้เรามาดูส่วนของข้อมูลกัน:
หากเราวัดระยะห่างระหว่างพัลส์แต่ละตัว ปรากฎว่าระยะห่างระหว่างพัลส์ "ยาว" ยังคงอยู่ ~48 ตัวอย่าง และระหว่างพัลส์สั้น – ~24 เมื่อมองไปข้างหน้าเล็กน้อยฉันจะบอกว่าในท้ายที่สุดปรากฎว่าพัลส์ "อ้างอิง" ที่มีความถี่ 918 Hz ติดตามอย่างต่อเนื่องตั้งแต่ต้นจนจบไฟล์ สันนิษฐานได้ว่าเมื่อส่งข้อมูล หากพบพัลส์เพิ่มเติมระหว่างพัลส์อ้างอิง เราจะพิจารณาว่าเป็นบิต 1 มิฉะนั้นจะเป็น 0
แล้วซิงค์พัลส์ล่ะ? ลองดูที่จุดเริ่มต้นของข้อมูล:
โทนเสียงนำสิ้นสุดและข้อมูลจะเริ่มต้นทันที หลังจากนั้นไม่นาน หลังจากวิเคราะห์การบันทึกเสียงต่างๆ หลายๆ รายการ เราก็สามารถค้นพบว่าไบต์แรกของข้อมูลจะเหมือนกันเสมอ (10100101b, A5h) คอมพิวเตอร์อาจเริ่มอ่านข้อมูลหลังจากได้รับข้อมูลแล้ว
คุณยังสามารถให้ความสนใจกับการเปลี่ยนแปลงของพัลส์อ้างอิงแรกทันทีหลังจากพัลส์ที่ 1 สุดท้ายในซิงค์ไบต์ มันถูกค้นพบในภายหลังในกระบวนการพัฒนาโปรแกรมจดจำข้อมูลเมื่อข้อมูลที่จุดเริ่มต้นของไฟล์ไม่สามารถอ่านได้อย่างเสถียร
ตอนนี้เรามาลองอธิบายอัลกอริทึมที่จะประมวลผลไฟล์เสียงและโหลดข้อมูล
กำลังโหลดข้อมูล
ขั้นแรก มาดูสมมติฐานบางประการเพื่อทำให้อัลกอริทึมเรียบง่าย:
- เราจะพิจารณาเฉพาะไฟล์ในรูปแบบ WAV เท่านั้น
- ไฟล์เสียงต้องขึ้นต้นด้วยเสียงนำร่องและต้องไม่มีความเงียบที่จุดเริ่มต้น
- ไฟล์ต้นฉบับต้องมีอัตราการสุ่มตัวอย่าง 44100 Hz ในกรณีนี้ ระยะห่างระหว่างพัลส์อ้างอิงของตัวอย่าง 48 ตัวอย่างถูกกำหนดไว้แล้ว และเราไม่จำเป็นต้องคำนวณโดยทางโปรแกรม
- รูปแบบตัวอย่างอาจเป็นรูปแบบใดก็ได้ (8/16 บิต/จุดลอยตัว) เนื่องจากเมื่ออ่านเราสามารถแปลงเป็นรูปแบบที่ต้องการได้
- เราถือว่าไฟล์ต้นฉบับได้รับการทำให้เป็นมาตรฐานตามแอมพลิจูด ซึ่งน่าจะทำให้ผลลัพธ์มีความเสถียร
อัลกอริธึมการอ่านจะเป็นดังนี้:
- เราอ่านไฟล์ลงในหน่วยความจำในขณะเดียวกันก็แปลงรูปแบบตัวอย่างเป็น 8 บิต
- กำหนดตำแหน่งของพัลส์แรกในข้อมูลเสียง ในการดำเนินการนี้ คุณจะต้องคำนวณจำนวนตัวอย่างด้วยแอมพลิจูดสูงสุด เพื่อความง่าย เราจะคำนวณด้วยตนเองหนึ่งครั้ง มาบันทึกลงในตัวแปร prev_pos;
- เพิ่ม 48 ไปที่ตำแหน่งของพัลส์สุดท้าย (pos := prev_pos + 48)
- เนื่องจากการเพิ่มตำแหน่ง 48 ไม่ได้รับประกันว่าเราจะไปถึงตำแหน่งของพัลส์อ้างอิงถัดไป (ข้อบกพร่องของเทป การทำงานที่ไม่เสถียรของกลไกเทปไดรฟ์ ฯลฯ) เราจึงต้องปรับตำแหน่งของพัลส์ pos เมื่อต้องการทำเช่นนี้ ให้ใช้ข้อมูลชิ้นเล็กๆ (pos-8;pos+8) และค้นหาค่าแอมพลิจูดสูงสุดจากข้อมูลนั้น ตำแหน่งที่สอดคล้องกับค่าสูงสุดจะถูกจัดเก็บไว้ในตำแหน่ง โดยที่ 8 = 48/6 เป็นค่าคงที่ที่ได้จากการทดลอง ซึ่งรับประกันว่าเราจะกำหนดค่าสูงสุดที่ถูกต้องและจะไม่ส่งผลต่อแรงกระตุ้นอื่นๆ ที่อาจอยู่ใกล้ๆ ในกรณีที่เลวร้ายมาก เมื่อระยะห่างระหว่างพัลส์น้อยกว่าหรือมากกว่า 48 มาก คุณสามารถใช้การค้นหาพัลส์แบบบังคับได้ แต่ภายในขอบเขตของบทความ ฉันจะไม่อธิบายสิ่งนี้ในอัลกอริทึม
- ในขั้นตอนก่อนหน้านี้ยังจำเป็นต้องตรวจสอบด้วยว่าพบพัลส์อ้างอิงเลย นั่นคือ หากคุณเพียงมองหาค่าสูงสุด สิ่งนี้ไม่ได้รับประกันว่าแรงกระตุ้นจะปรากฏในส่วนนี้ ในการใช้งานโปรแกรมการอ่านครั้งล่าสุดของฉัน ฉันจะตรวจสอบความแตกต่างระหว่างค่าแอมพลิจูดสูงสุดและต่ำสุดบนเซกเมนต์ และหากเกินขีดจำกัด ฉันจะนับการมีอยู่ของแรงกระตุ้น คำถามคือต้องทำอย่างไรหากไม่พบพัลส์อ้างอิง มี 2 ตัวเลือก: ข้อมูลสิ้นสุดแล้วและปิดเสียงต่อไป หรือนี่ควรถือเป็นข้อผิดพลาดในการอ่าน อย่างไรก็ตาม เราจะละเว้นสิ่งนี้เพื่อทำให้อัลกอริทึมง่ายขึ้น
- ในขั้นตอนต่อไป เราจำเป็นต้องพิจารณาว่ามีข้อมูลพัลส์อยู่หรือไม่ (บิต 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 เป็นค่าคงที่ที่ได้รับจากการทดลอง
- บันทึกตำแหน่งปัจจุบันใน prev_pos (prev_pos := pos)
- ทำซ้ำโดยเริ่มจากขั้นตอนที่ 3 จนกว่าเราจะอ่านไฟล์ทั้งหมด
- บิตอาร์เรย์ที่ได้จะต้องบันทึกเป็นชุดไบต์ เนื่องจากเราไม่ได้คำนึงถึงซิงค์ไบต์เมื่ออ่าน จำนวนบิตจึงอาจไม่เป็นผลคูณของ 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 และคอมพิวเตอร์อื่น ๆ ยอดนิยมในเวลานั้นด้วยซึ่งฉันสามารถค้นหาเอกสารได้ แต่ไม่ประสบความสำเร็จ - ความรู้ของฉันเกี่ยวกับคอมพิวเตอร์ย้อนยุคประเภทต่างๆ กลับกลายเป็นว่าไม่กว้างนัก
จากนั้นฉันก็ตัดสินใจไป
กระท่อมคอมพิวเตอร์ Tandy/วิทยุ TRS-80
มีโอกาสมากที่การบันทึกเสียงที่เป็นปัญหาซึ่งฉันยกตัวอย่างในตอนต้นของบทความนั้นถูกสร้างขึ้นบนคอมพิวเตอร์ในลักษณะนี้:
ปรากฎว่าคอมพิวเตอร์เครื่องนี้และรุ่นต่างๆ (รุ่น I/รุ่น III/รุ่น IV ฯลฯ) ได้รับความนิยมอย่างมากในคราวเดียว (แน่นอน ไม่ใช่ในรัสเซีย) เป็นที่น่าสังเกตว่าโปรเซสเซอร์ที่พวกเขาใช้ก็คือ Z80 เช่นกัน สำหรับคอมพิวเตอร์เครื่องนี้ คุณสามารถค้นหาได้บนอินเทอร์เน็ต
ฉันดาวน์โหลดโปรแกรมจำลอง
ฉันยังพบ
เมื่อทราบรูปแบบไฟล์ CAS (ซึ่งกลายเป็นเพียงสำเนาข้อมูลจากเทปที่ฉันมีอยู่แล้วทีละบิต ยกเว้นส่วนหัวที่มีซิงค์ไบต์) ฉันจึงสร้าง การเปลี่ยนแปลงเล็กน้อยในโปรแกรมของฉันและสามารถส่งออกไฟล์ CAS ที่ใช้งานได้ซึ่งทำงานในโปรแกรมจำลอง (TRS-80 Model III):
ฉันออกแบบยูทิลิตี้การแปลงเวอร์ชันล่าสุดพร้อมการกำหนดพัลส์แรกโดยอัตโนมัติและระยะห่างระหว่างพัลส์อ้างอิงเป็นแพ็คเกจ GEM ซอร์สโค้ดมีอยู่ที่
ข้อสรุป
เส้นทางที่เราเดินทางกลับกลายเป็นการเดินทางย้อนอดีตอันน่าหลงใหลและดีใจที่สุดท้ายก็พบคำตอบ เหนือสิ่งอื่นใด ฉัน:
- ฉันค้นหารูปแบบสำหรับการบันทึกข้อมูลใน ZX Spectrum และศึกษารูทีน ROM ในตัวสำหรับการบันทึก/อ่านข้อมูลจากเทปเสียง
- ฉันคุ้นเคยกับคอมพิวเตอร์ TRS-80 และรุ่นต่างๆของมันศึกษาระบบปฏิบัติการดูโปรแกรมตัวอย่างและยังมีโอกาสทำการดีบั๊กในรหัสเครื่อง (ท้ายที่สุดแล้วตัวช่วยจำ Z80 ทั้งหมดก็คุ้นเคยกับฉัน)
- เขียนยูทิลิตี้เต็มรูปแบบสำหรับการแปลงการบันทึกเสียงเป็นรูปแบบ CAS ซึ่งสามารถอ่านข้อมูลที่ยูทิลิตี้ "อย่างเป็นทางการ" ไม่ได้รับการยอมรับ
ที่มา: will.com