Files
2026-04-22 14:07:22 -04:00

192 lines
4.8 KiB
C++

//
// K7.cpp
// Clock Signal
//
// Created by Thomas Harte on 23/03/2026.
// Copyright © 2026 Thomas Harte. All rights reserved.
//
#include "K7.hpp"
using namespace Storage::Tape;
/*
K7 files are a raw dump of source bytes that were encoded in the standard Thomson layout.
They assume a high-level ROM hack from the emulator, the primary consequence of which is
failure to include realistic amounts of time between blocks. Consequently the process of
turning them back into audio that a real machine can use requires block-level parsing,
in order to insert gaps.
# TO machines:
That layout is:
0: start bit
LSB...MSB: byte content
1 1: stop bits.
Each bit is composed of square waves — low, then high — of idealised duration:
7 / 31500ths of a second, 5 times over: 0 bit
5 / 31500ths of a second, 7 times over: 1 bit
So baud rate is 31500 / (5 * 7) = 900.
# MO machines:
Basic FM encoding is used with an idealised 833µs window length; each window begins
with a level transition after which:
any transitions in that window: 1 bit
no transitions: 0 bit.
Specifically, the ROM watches for an edge to synchronise, then waits for two thirds of the
bit length, then samples again to decide a 0 or a 1. And repeat.
There are not start and stop bits; bits are stored MSB first.
*/
K7::K7(const std::string &file_name) : file_name_(file_name) {}
std::unique_ptr<FormatSerialiser> K7::format_serialiser() const {
return std::make_unique<Serialiser>(file_name_);
}
K7::Serialiser::Serialiser(const std::string &name) : file_(name, FileMode::Read) {}
void K7::Serialiser::set_target_platforms(const TargetPlatform::Type type) {
target_ = type;
}
void K7::Serialiser::reset() {
file_.seek(0, Whence::SET);
state_ = State::LeadIn;
state_length_ = 0;
}
uint8_t K7::Serialiser::next() {
// The state machine below forces gaps between files; the K7 file format is predicated on emulator hacks
// underneath — it assumes a high-level capturing of the relevant ROM routines.
//
// This attempts to ameliorate for that.
switch(state_) {
// Some files fail to start with enough synchronisation bytes.
case State::LeadIn: {
++state_length_;
if(state_length_ == 10) {
state_ = State::Seeking;
}
return 0x01;
} break;
case State::Seeking: {
const uint8_t byte = file_.get();
byte_history_ = uint16_t((byte_history_ << 8) | byte);
if(byte_history_ == 0x3c5a) {
state_ = State::Header;
state_length_ = 0;
}
return byte;
} break;
case State::Header: {
const uint8_t byte = file_.get();
++state_length_;
if(state_length_ == 2) {
state_ = State::Body;
state_length_ = uint8_t(byte - 2) + 1; // Because the test in ::Body is posthoc.
}
return byte;
} break;
case State::Body: {
const uint8_t byte = file_.get();
--state_length_;
if(!state_length_) {
state_ = State::PostBodyPause;
state_length_ = 50; // Arbitrary; selected to be 'long enough'.
}
return byte;
} break;
case State::PostBodyPause: {
--state_length_;
if(!state_length_) {
state_ = State::Seeking;
}
return 0x01; // Almost arbitrary: matches the standard synchronisation byte.
} break;
default: __builtin_unreachable();
}
}
void K7::Serialiser::push_next_pulses() {
if(file_.eof()) {
set_is_at_end(true);
return;
}
// The TO and MO have entirely-distinct encodings.
if(target_ == TargetPlatform::ThomsonTO) {
static constexpr int LengthDenominator = 31'500 * 2;
static constexpr auto ZeroLength = Time(7, LengthDenominator);
static constexpr int ZeroRepetitions = 5;
static constexpr auto OneLength = Time(5, LengthDenominator);
static constexpr int OneRepetitions = 7;
const auto post = [&](const bool bit) {
const auto output = [&](const Time length, const int repetitions) {
for(int c = 0; c < repetitions; c++) {
emplace_back(Pulse::Low, length);
emplace_back(Pulse::High, length);
}
};
if(bit) {
output(OneLength, OneRepetitions);
} else {
output(ZeroLength, ZeroRepetitions);
}
};
post(0);
uint8_t byte = next();
for(int c = 0; c < 8; c++) {
post(byte & 1);
byte >>= 1;
}
post(1);
post(1);
} else {
const auto pulse_type = [&]() {
current_type_ = current_type_ == Pulse::Type::High ? Pulse::Type::Low : Pulse::Type::High;
return current_type_;
};
const auto post = [&](const bool bit) {
// Provide a perfect 1200 baud.
static constexpr auto FullPulse = Time(1, 1200);
static constexpr auto HalfPulse = Time(1, 2400);
if(bit) {
emplace_back(pulse_type(), HalfPulse);
emplace_back(pulse_type(), HalfPulse);
} else {
emplace_back(pulse_type(), FullPulse);
}
};
uint8_t byte = next();
for(int c = 0; c < 8; c++) {
post(byte & 0x80);
byte <<= 1;
}
}
}