1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-04 18:29:40 +00:00

Merge pull request #151 from TomHarte/TZX

Adds partial TZX file format support
This commit is contained in:
Thomas Harte 2017-07-21 20:47:08 -04:00 committed by GitHub
commit d6e60c8e3a
21 changed files with 647 additions and 194 deletions

View File

@ -24,11 +24,8 @@ Machine::Machine() :
tape_player_(ZX8081ClockRate),
use_fast_tape_hack_(false),
tape_advance_delay_(0),
tape_is_automatically_playing_(false),
tape_is_playing_(false),
has_latched_video_byte_(false) {
set_clock_rate(ZX8081ClockRate);
tape_player_.set_motor_control(true);
clear_all_keys();
}
@ -58,7 +55,7 @@ int Machine::perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
if(is_zx81_) horizontal_counter_ %= 207;
if(!tape_advance_delay_) {
if(tape_is_automatically_playing_ || tape_is_playing_) tape_player_.run_for_cycles(cycle.length);
tape_player_.run_for_cycles(cycle.length);
} else {
tape_advance_delay_ = std::max(tape_advance_delay_ - cycle.length, 0);
}
@ -133,7 +130,7 @@ int Machine::perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
case CPU::Z80::PartialMachineCycle::ReadOpcodeWait:
// Check for use of the fast tape hack.
if(use_fast_tape_hack_ && address == tape_trap_address_ && tape_player_.has_tape()) {
Storage::Time time = tape_player_.get_tape()->get_current_time();
uint64_t prior_offset = tape_player_.get_tape()->get_offset();
int next_byte = parser_.get_next_byte(tape_player_.get_tape());
if(next_byte != -1) {
uint16_t hl = get_value_of_register(CPU::Z80::Register::HL);
@ -147,12 +144,14 @@ int Machine::perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
tape_advance_delay_ = 1000;
return 0;
} else {
tape_player_.get_tape()->seek(time);
tape_player_.get_tape()->set_offset(prior_offset);
}
}
// Check for automatic tape control.
tape_is_automatically_playing_ = use_automatic_tape_motor_control_ && (address >= automatic_tape_motor_start_address_) && (address < automatic_tape_motor_end_address_);
if(use_automatic_tape_motor_control_) {
tape_player_.set_motor_control((address >= automatic_tape_motor_start_address_) && (address < automatic_tape_motor_end_address_));
}
is_opcode_read = true;
case CPU::Z80::PartialMachineCycle::Read:

View File

@ -69,9 +69,11 @@ class Machine:
inline void set_use_fast_tape_hack(bool activate) { use_fast_tape_hack_ = activate; }
inline void set_use_automatic_tape_motor_control(bool enabled) {
use_automatic_tape_motor_control_ = enabled;
if(!enabled) tape_is_automatically_playing_ = false;
if(!enabled) {
tape_player_.set_motor_control(false);
}
}
inline void set_tape_is_playing(bool is_playing) { tape_is_playing_ = is_playing; }
inline void set_tape_is_playing(bool is_playing) { tape_player_.set_motor_control(is_playing); }
// for Utility::TypeRecipient::Delegate
uint16_t *sequence_for_character(Utility::Typer *typer, char character);
@ -113,7 +115,6 @@ class Machine:
bool use_fast_tape_hack_;
bool use_automatic_tape_motor_control_;
bool tape_is_playing_, tape_is_automatically_playing_;
int tape_advance_delay_;
};

View File

@ -9,15 +9,15 @@
#ifndef Factors_hpp
#define Factors_hpp
#include <utility>
namespace NumberTheory {
/*!
@returns The greatest common divisor of @c a and @c b as computed by Euclid's algorithm.
*/
template<class T> T greatest_common_divisor(T a, T b) {
if(a < b) {
T swap = b;
b = a;
a = swap;
std::swap(a, b);
}
while(1) {

View File

@ -53,6 +53,8 @@
4B3BA0D11D318B44005DD7A7 /* TestMachine6502.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0CD1D318B44005DD7A7 /* TestMachine6502.mm */; };
4B3BF5B01F146265005B6C36 /* CSW.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BF5AE1F146264005B6C36 /* CSW.cpp */; };
4B3F1B461E0388D200DB26EE /* PCMPatchedTrack.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */; };
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */; };
4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */; };
4B44EBF51DC987AF00A7820C /* AllSuiteA.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */; };
4B44EBF71DC9883B00A7820C /* 6502_functional_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */; };
4B44EBF91DC9898E00A7820C /* BCDTEST_beeb in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */; };
@ -544,6 +546,10 @@
4B3BF5AF1F146264005B6C36 /* CSW.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CSW.hpp; sourceTree = "<group>"; };
4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMPatchedTrack.cpp; sourceTree = "<group>"; };
4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMPatchedTrack.hpp; sourceTree = "<group>"; };
4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = "<group>"; };
4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = "<group>"; };
4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = "<group>"; };
4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PulseQueuedTape.hpp; sourceTree = "<group>"; };
4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = AllSuiteA.bin; path = AllSuiteA/AllSuiteA.bin; sourceTree = "<group>"; };
4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 6502_functional_test.bin; path = "Klaus Dormann/6502_functional_test.bin"; sourceTree = "<group>"; };
4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */ = {isa = PBXFileReference; lastKnownFileType = file; name = BCDTEST_beeb; path = BCDTest/BCDTEST_beeb; sourceTree = "<group>"; };
@ -1388,6 +1394,8 @@
children = (
4B69FB411C4D941400B5F0AA /* Formats */,
4B8805F11DCFC9A2003085B1 /* Parsers */,
4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */,
4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */,
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */,
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */,
);
@ -1402,12 +1410,14 @@
4B59199A1DAC6C46005BB85C /* OricTAP.cpp */,
4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */,
4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */,
4B448E7F1F1C45A00009ABD6 /* TZX.cpp */,
4B1497861EE4A1DA00CE2596 /* ZX80O81P.cpp */,
4BC91B821D1F160E00884B76 /* CommodoreTAP.hpp */,
4B3BF5AF1F146264005B6C36 /* CSW.hpp */,
4B59199B1DAC6C46005BB85C /* OricTAP.hpp */,
4B2BFC5E1D613E0200BA3AA9 /* TapePRG.hpp */,
4B69FB431C4D941400B5F0AA /* TapeUEF.hpp */,
4B448E801F1C45A00009ABD6 /* TZX.hpp */,
4B1497871EE4A1DA00CE2596 /* ZX80O81P.hpp */,
4B69FB451C4D950F00B5F0AA /* libz.tbd */,
);
@ -2575,6 +2585,7 @@
4BC9DF4F1D04691600F44158 /* 6560.cpp in Sources */,
4B59199C1DAC6C46005BB85C /* OricTAP.cpp in Sources */,
4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */,
4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */,
4BD4A8CD1E077E8A0020D856 /* PCMSegment.cpp in Sources */,
4BD14B111D74627C0088EAD6 /* StaticAnalyser.cpp in Sources */,
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */,
@ -2605,6 +2616,7 @@
4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */,
4B2A53A11D117D36003C6002 /* CSAtari2600.mm in Sources */,
4BF829661D8F732B001BAE39 /* Disk.cpp in Sources */,
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */,
4BEA52631DF339D7007E74F2 /* Speaker.cpp in Sources */,
4BC5E4921D7ED365008CF980 /* StaticAnalyser.cpp in Sources */,
4BC830D11D6E7C690000A26F /* Tape.cpp in Sources */,

View File

@ -212,6 +212,20 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>tzx</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>cassette</string>
<key>CFBundleTypeName</key>
<string>Tape Image</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@ -34,6 +34,7 @@
#include "../Storage/Tape/Formats/OricTAP.hpp"
#include "../Storage/Tape/Formats/TapePRG.hpp"
#include "../Storage/Tape/Formats/TapeUEF.hpp"
#include "../Storage/Tape/Formats/TZX.hpp"
#include "../Storage/Tape/Formats/ZX80O81P.hpp"
typedef int TargetPlatformType;
@ -124,6 +125,7 @@ std::list<Target> StaticAnalyser::GetTargets(const char *file_name)
Format("ssd", disks, Disk::SSD, TargetPlatform::Acorn) // SSD
Format("tap", tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
Format("tap", tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
Format("tzx", tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
Format("uef", tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
#undef Format

View File

@ -40,9 +40,9 @@ void StaticAnalyser::ZX8081::AddTargets(
StaticAnalyser::Target target;
target.machine = Target::ZX8081;
target.zx8081.isZX81 = files.front().isZX81;
if(files.front().data.size() > 16384) {
/*if(files.front().data.size() > 16384) {
target.zx8081.memory_model = ZX8081MemoryModel::SixtyFourKB;
} else if(files.front().data.size() > 1024) {
} else*/ if(files.front().data.size() > 1024) {
target.zx8081.memory_model = ZX8081MemoryModel::SixteenKB;
} else {
target.zx8081.memory_model = ZX8081MemoryModel::Unexpanded;

View File

@ -50,8 +50,10 @@ static std::shared_ptr<File> ZX81FileFromData(const std::vector<uint8_t> &data)
// Look for a file name.
size_t data_pointer = 0;
std::vector<uint8_t> name_data;
int c = 11;
while(c < data.size() && c--) {
name_data.push_back(data[data_pointer] & 0x3f);
if(data[data_pointer] & 0x80) break;
data_pointer++;
}
@ -80,6 +82,7 @@ static std::shared_ptr<File> ZX81FileFromData(const std::vector<uint8_t> &data)
// TODO: check that the line numbers declared above exist (?)
std::shared_ptr<File> file(new File);
file->name = StringFromData(name_data, true);
file->data = data;
file->isZX81 = true;
return file;

View File

@ -19,7 +19,7 @@ namespace ZX8081 {
struct File {
std::vector<uint8_t> data;
std::string name;
std::wstring name;
bool isZX81;
};

View File

@ -71,6 +71,49 @@ class FileHolder {
*/
void ensure_file_is_at_least_length(long length);
class BitStream {
public:
BitStream(FILE *f, bool lsb_first) :
file_(f),
lsb_first_(lsb_first),
next_value_(0),
bits_remaining_(0) {}
uint8_t get_bits(int q) {
uint8_t result = 0;
while(q--) {
result = (uint8_t)((result << 1) | get_bit());
}
return result;
}
private:
FILE *file_;
bool lsb_first_;
uint8_t next_value_;
int bits_remaining_;
uint8_t get_bit() {
if(!bits_remaining_) {
bits_remaining_ = 8;
next_value_ = (uint8_t)fgetc(file_);
}
uint8_t bit;
if(lsb_first_) {
bit = next_value_ & 1;
next_value_ >>= 1;
} else {
bit = next_value_ >> 7;
next_value_ <<= 1;
}
bits_remaining_--;
return bit;
}
};
FILE *file_;
struct stat file_stats_;
bool is_read_only_;

View File

@ -25,15 +25,13 @@ struct Time {
Time() : length(0), clock_rate(1) {}
Time(unsigned int unsigned_int_value) : length(unsigned_int_value), clock_rate(1) {}
Time(int int_value) : Time((unsigned int)int_value) {}
Time(unsigned int length, unsigned int clock_rate) : length(length), clock_rate(clock_rate) { simplify(); }
Time(unsigned int length, unsigned int clock_rate) : length(length), clock_rate(clock_rate) {}
Time(int length, int clock_rate) : Time((unsigned int)length, (unsigned int)clock_rate) {}
Time(uint64_t length, uint64_t clock_rate) {
install_result(length, clock_rate);
simplify();
}
Time(float value) {
install_float(value);
simplify();
}
/*!
@ -94,6 +92,10 @@ struct Time {
inline Time &operator += (const Time &other) {
if(!other.length) return *this;
if(!length) {
*this = other;
return *this;
}
uint64_t result_length;
uint64_t result_clock_rate;
@ -207,6 +209,12 @@ struct Time {
private:
inline void install_result(uint64_t long_length, uint64_t long_clock_rate) {
if(long_length <= std::numeric_limits<unsigned int>::max() && long_clock_rate <= std::numeric_limits<unsigned int>::max()) {
length = (unsigned int)long_length;
clock_rate = (unsigned int)long_clock_rate;
return;
}
// TODO: switch to appropriate values if the result is too large or small to fit, even with trimmed accuracy.
if(!long_length) {
length = 0;
@ -214,6 +222,11 @@ struct Time {
return;
}
while(!(long_length&0xf) && !(long_clock_rate&0xf)) {
long_length >>= 4;
long_clock_rate >>= 4;
}
while(!(long_length&1) && !(long_clock_rate&1)) {
long_length >>= 1;
long_clock_rate >>= 1;

View File

@ -18,7 +18,7 @@ CSW::CSW(const char *file_name) :
// Check signature.
char identifier[22];
char signature[] = "Compressed Square Wave";
fread(identifier, 1, 22, file_);
fread(identifier, 1, strlen(signature), file_);
if(memcmp(identifier, signature, strlen(signature))) throw ErrorNotCSW;
// Check terminating byte.

View File

@ -0,0 +1,225 @@
//
// TZX.cpp
// Clock Signal
//
// Created by Thomas Harte on 16/07/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "TZX.hpp"
using namespace Storage::Tape;
namespace {
const unsigned int StandardTZXClock = 3500000;
const unsigned int TZXClockMSMultiplier = 3500;
}
TZX::TZX(const char *file_name) :
Storage::FileHolder(file_name),
current_level_(false) {
// Check for signature followed by a 0x1a
char identifier[7];
char signature[] = "ZXTape!";
fread(identifier, 1, strlen(signature), file_);
if(memcmp(identifier, signature, strlen(signature))) throw ErrorNotTZX;
if(fgetc(file_) != 0x1a) throw ErrorNotTZX;
// Get version number
uint8_t major_version = (uint8_t)fgetc(file_);
uint8_t minor_version = (uint8_t)fgetc(file_);
// Reject if an incompatible version
if(major_version != 1 || minor_version > 20) throw ErrorNotTZX;
virtual_reset();
}
void TZX::virtual_reset() {
clear();
set_is_at_end(false);
fseek(file_, 0x0a, SEEK_SET);
// This is a workaround for arguably dodgy ZX80/ZX81 TZXs; they launch straight
// into data but both machines require a gap before data begins. So impose
// an initial gap, in the form of a very long wave.
current_level_ = false;
post_gap(500);
}
void TZX::get_next_pulses() {
while(empty()) {
uint8_t chunk_id = (uint8_t)fgetc(file_);
if(feof(file_)) {
set_is_at_end(true);
return;
}
switch(chunk_id) {
case 0x10: get_standard_speed_data_block(); break;
case 0x11: get_turbo_speed_data_block(); break;
case 0x12: get_pure_tone_data_block(); break;
case 0x13: get_pulse_sequence(); break;
case 0x19: get_generalised_data_block(); break;
case 0x30: {
// Text description. Ripe for ignoring.
int length = fgetc(file_);
fseek(file_, length, SEEK_CUR);
} break;
default:
// In TZX each chunk has a different way of stating or implying its length,
// so there is no route past an unimplemented chunk.
printf("!!Unknown %02x!!", chunk_id);
set_is_at_end(true);
return;
}
}
}
void TZX::get_generalised_data_block() {
uint32_t block_length = fgetc32le();
long endpoint = ftell(file_) + (long)block_length;
uint16_t pause_after_block = fgetc16le();
uint32_t total_pilot_symbols = fgetc32le();
uint8_t maximum_pulses_per_pilot_symbol = (uint8_t)fgetc(file_);
uint8_t symbols_in_pilot_table = (uint8_t)fgetc(file_);
uint32_t total_data_symbols = fgetc32le();
uint8_t maximum_pulses_per_data_symbol = (uint8_t)fgetc(file_);
uint8_t symbols_in_data_table = (uint8_t)fgetc(file_);
get_generalised_segment(total_pilot_symbols, maximum_pulses_per_pilot_symbol, symbols_in_pilot_table, false);
get_generalised_segment(total_data_symbols, maximum_pulses_per_data_symbol, symbols_in_data_table, true);
post_gap(pause_after_block);
// This should be unnecessary, but intends to preserve sanity.
fseek(file_, endpoint, SEEK_SET);
}
void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols, bool is_data) {
if(!output_symbols) return;
// Construct the symbol table.
struct Symbol {
uint8_t flags;
std::vector<uint16_t> pulse_lengths;
};
std::vector<Symbol> symbol_table;
for(int c = 0; c < number_of_symbols; c++) {
Symbol symbol;
symbol.flags = (uint8_t)fgetc(file_);
for(int ic = 0; ic < max_pulses_per_symbol; ic++) {
symbol.pulse_lengths.push_back(fgetc16le());
}
symbol_table.push_back(symbol);
}
// Hence produce the output.
BitStream stream(file_, false);
int base = 2;
int bits = 1;
while(base < number_of_symbols) {
base <<= 1;
bits++;
}
for(int c = 0; c < output_symbols; c++) {
uint8_t symbol_value;
int count;
if(is_data) {
symbol_value = stream.get_bits(bits);
count = 1;
} else {
symbol_value = (uint8_t)fgetc(file_);
count = fgetc16le();
}
if(symbol_value > number_of_symbols) {
continue;
}
Symbol &symbol = symbol_table[symbol_value];
while(count--) {
// Mutate initial output level.
switch(symbol.flags & 3) {
case 0: break;
case 1: current_level_ ^= true; break;
case 2: current_level_ = true; break;
case 3: current_level_ = false; break;
}
// Output waves.
for(auto length : symbol.pulse_lengths) {
if(!length) break;
post_pulse(length);
}
}
}
}
void TZX::get_standard_speed_data_block() {
__unused uint16_t pause_after_block = fgetc16le();
uint16_t data_length = fgetc16le();
if(!data_length) return;
uint8_t first_byte = (uint8_t)fgetc(file_);
__unused int pilot_tone_pulses = (first_byte < 128) ? 8063 : 3223;
ungetc(first_byte, file_);
// TODO: output pilot_tone_pulses pulses
// TODO: output data_length bytes, in the Spectrum encoding
fseek(file_, data_length, SEEK_CUR);
// TODO: output a pause of length paused_after_block ms
}
void TZX::get_turbo_speed_data_block() {
__unused uint16_t length_of_pilot_pulse = fgetc16le();
__unused uint16_t length_of_sync_first_pulse = fgetc16le();
__unused uint16_t length_of_sync_second_pulse = fgetc16le();
__unused uint16_t length_of_zero_bit_pulse = fgetc16le();
__unused uint16_t length_of_one_bit_pulse = fgetc16le();
__unused uint16_t length_of_pilot_tone = fgetc16le();
__unused uint8_t number_of_bits_in_final_byte = (uint8_t)fgetc(file_);
__unused uint16_t pause_after_block = fgetc16le();
uint16_t data_length = fgetc16le();
// TODO output as described
fseek(file_, data_length, SEEK_CUR);
}
void TZX::get_pure_tone_data_block() {
__unused uint16_t length_of_pulse = fgetc16le();
__unused uint16_t nunber_of_pulses = fgetc16le();
while(nunber_of_pulses--) post_pulse(length_of_pulse);
}
void TZX::get_pulse_sequence() {
uint8_t number_of_pulses = (uint8_t)fgetc(file_);
while(number_of_pulses--) {
post_pulse(fgetc16le());
}
}
#pragma mark - Output
void TZX::post_pulse(unsigned int length) {
post_pulse(Storage::Time(length, StandardTZXClock));
}
void TZX::post_gap(unsigned int milliseconds) {
if(!milliseconds) return;
if(milliseconds > 1 && !current_level_) {
post_pulse(Storage::Time(TZXClockMSMultiplier, StandardTZXClock));
post_pulse(Storage::Time((milliseconds - 1u)*TZXClockMSMultiplier, StandardTZXClock));
} else {
post_pulse(Storage::Time(milliseconds*TZXClockMSMultiplier, StandardTZXClock));
}
}
void TZX::post_pulse(const Storage::Time &time) {
emplace_back(current_level_ ? Tape::Pulse::High : Tape::Pulse::Low, time);
current_level_ ^= true;
}

View File

@ -0,0 +1,56 @@
//
// TZX.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/07/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef TZX_hpp
#define TZX_hpp
#include "../PulseQueuedTape.hpp"
#include "../../FileHolder.hpp"
namespace Storage {
namespace Tape {
/*!
Provides a @c Tape containing a CSW tape image, which is a compressed 1-bit sampling.
*/
class TZX: public PulseQueuedTape, public Storage::FileHolder {
public:
/*!
Constructs a @c TZX containing content from the file with name @c file_name.
@throws ErrorNotTZX if this file could not be opened and recognised as a valid TZX file.
*/
TZX(const char *file_name);
enum {
ErrorNotTZX
};
private:
void virtual_reset();
void get_next_pulses();
bool current_level_;
void get_standard_speed_data_block();
void get_turbo_speed_data_block();
void get_pure_tone_data_block();
void get_pulse_sequence();
void get_generalised_data_block();
void get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols, bool is_data);
void post_pulse(unsigned int length);
void post_gap(unsigned int milliseconds);
void post_pulse(const Storage::Time &time);
};
}
}
#endif /* TZX_hpp */

View File

@ -14,8 +14,7 @@
#pragma mark - ZLib extensions
static float gzgetfloat(gzFile file)
{
static float gzgetfloat(gzFile file) {
uint8_t bytes[4];
gzread(file, bytes, 4);
@ -42,30 +41,26 @@ static float gzgetfloat(gzFile file)
return result;
}
static uint8_t gzget8(gzFile file)
{
static uint8_t gzget8(gzFile file) {
// This is a workaround for gzgetc, which seems to be broken in ZLib 1.2.8.
uint8_t result;
gzread(file, &result, 1);
return result;
}
static int gzget16(gzFile file)
{
static int gzget16(gzFile file) {
uint8_t bytes[2];
gzread(file, bytes, 2);
return bytes[0] | (bytes[1] << 8);
}
static int gzget24(gzFile file)
{
static int gzget24(gzFile file) {
uint8_t bytes[3];
gzread(file, bytes, 3);
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16);
}
static int gzget32(gzFile file)
{
static int gzget32(gzFile file) {
uint8_t bytes[4];
gzread(file, bytes, 4);
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
@ -75,28 +70,21 @@ using namespace Storage::Tape;
UEF::UEF(const char *file_name) :
time_base_(1200),
is_at_end_(false),
pulse_pointer_(0),
is_300_baud_(false)
{
is_300_baud_(false) {
file_ = gzopen(file_name, "rb");
char identifier[10];
int bytes_read = gzread(file_, identifier, 10);
if(bytes_read < 10 || strcmp(identifier, "UEF File!"))
{
if(bytes_read < 10 || strcmp(identifier, "UEF File!")) {
throw ErrorNotUEF;
}
uint8_t version[2];
gzread(file_, version, 2);
if(version[1] > 0 || version[0] > 10)
{
if(version[1] > 0 || version[0] > 10) {
throw ErrorNotUEF;
}
parse_next_tape_chunk();
}
UEF::~UEF()
@ -106,47 +94,16 @@ UEF::~UEF()
#pragma mark - Public methods
void UEF::virtual_reset()
{
void UEF::virtual_reset() {
gzseek(file_, 12, SEEK_SET);
is_at_end_ = false;
parse_next_tape_chunk();
}
bool UEF::is_at_end()
{
return is_at_end_;
}
Storage::Tape::Tape::Pulse UEF::virtual_get_next_pulse()
{
Pulse next_pulse;
if(is_at_end_)
{
next_pulse.type = Pulse::Zero;
next_pulse.length.length = time_base_ * 4;
next_pulse.length.clock_rate = time_base_ * 4;
return next_pulse;
}
next_pulse = queued_pulses_[pulse_pointer_];
pulse_pointer_++;
if(pulse_pointer_ == queued_pulses_.size())
{
queued_pulses_.clear();
pulse_pointer_ = 0;
parse_next_tape_chunk();
}
return next_pulse;
set_is_at_end(false);
clear();
}
#pragma mark - Chunk navigator
void UEF::parse_next_tape_chunk()
{
while(queued_pulses_.empty())
{
void UEF::get_next_pulses() {
while(empty()) {
// read chunk details
uint16_t chunk_id = (uint16_t)gzget16(file_);
uint32_t chunk_length = (uint32_t)gzget32(file_);
@ -154,14 +111,12 @@ void UEF::parse_next_tape_chunk()
// figure out where the next chunk will start
z_off_t start_of_next_chunk = gztell(file_) + chunk_length;
if(gzeof(file_))
{
is_at_end_ = true;
if(gzeof(file_)) {
set_is_at_end(true);
return;
}
switch(chunk_id)
{
switch(chunk_id) {
case 0x0100: queue_implicit_bit_pattern(chunk_length); break;
case 0x0102: queue_explicit_bit_pattern(chunk_length); break;
case 0x0112: queue_integer_gap(); break;
@ -173,16 +128,15 @@ void UEF::parse_next_tape_chunk()
case 0x0114: queue_security_cycles(); break;
case 0x0104: queue_defined_data(chunk_length); break;
case 0x0113: // change of base rate
{
// change of base rate
case 0x0113: {
// TODO: something smarter than just converting this to an int
float new_time_base = gzgetfloat(file_);
time_base_ = (unsigned int)roundf(new_time_base);
}
break;
case 0x0117:
{
case 0x0117: {
int baud_rate = gzget16(file_);
is_300_baud_ = (baud_rate == 300);
}
@ -199,16 +153,14 @@ void UEF::parse_next_tape_chunk()
#pragma mark - Chunk parsers
void UEF::queue_implicit_bit_pattern(uint32_t length)
{
void UEF::queue_implicit_bit_pattern(uint32_t length) {
while(length--)
{
queue_implicit_byte(gzget8(file_));
}
}
void UEF::queue_explicit_bit_pattern(uint32_t length)
{
void UEF::queue_explicit_bit_pattern(uint32_t length) {
size_t length_in_bits = (length << 3) - (size_t)gzget8(file_);
uint8_t current_byte = 0;
for(size_t bit = 0; bit < length_in_bits; bit++)
@ -219,31 +171,27 @@ void UEF::queue_explicit_bit_pattern(uint32_t length)
}
}
void UEF::queue_integer_gap()
{
void UEF::queue_integer_gap() {
Time duration;
duration.length = (unsigned int)gzget16(file_);
duration.clock_rate = time_base_;
queued_pulses_.emplace_back(Pulse::Zero, duration);
emplace_back(Pulse::Zero, duration);
}
void UEF::queue_floating_point_gap()
{
void UEF::queue_floating_point_gap() {
float length = gzgetfloat(file_);
Time duration;
duration.length = (unsigned int)(length * 4000000);
duration.clock_rate = 4000000;
queued_pulses_.emplace_back(Pulse::Zero, duration);
emplace_back(Pulse::Zero, duration);
}
void UEF::queue_carrier_tone()
{
void UEF::queue_carrier_tone() {
unsigned int number_of_cycles = (unsigned int)gzget16(file_);
while(number_of_cycles--) queue_bit(1);
}
void UEF::queue_carrier_tone_with_dummy()
{
void UEF::queue_carrier_tone_with_dummy() {
unsigned int pre_cycles = (unsigned int)gzget16(file_);
unsigned int post_cycles = (unsigned int)gzget16(file_);
while(pre_cycles--) queue_bit(1);
@ -251,15 +199,13 @@ void UEF::queue_carrier_tone_with_dummy()
while(post_cycles--) queue_bit(1);
}
void UEF::queue_security_cycles()
{
void UEF::queue_security_cycles() {
int number_of_cycles = gzget24(file_);
bool first_is_pulse = gzget8(file_) == 'P';
bool last_is_pulse = gzget8(file_) == 'P';
uint8_t current_byte = 0;
for(int cycle = 0; cycle < number_of_cycles; cycle++)
{
for(int cycle = 0; cycle < number_of_cycles; cycle++) {
if(!(cycle&7)) current_byte = gzget8(file_);
int bit = (current_byte >> 7);
current_byte <<= 1;
@ -268,24 +214,18 @@ void UEF::queue_security_cycles()
duration.length = bit ? 1 : 2;
duration.clock_rate = time_base_ * 4;
if(!cycle && first_is_pulse)
{
queued_pulses_.emplace_back(Pulse::High, duration);
}
else if(cycle == number_of_cycles-1 && last_is_pulse)
{
queued_pulses_.emplace_back(Pulse::Low, duration);
}
else
{
queued_pulses_.emplace_back(Pulse::Low, duration);
queued_pulses_.emplace_back(Pulse::High, duration);
if(!cycle && first_is_pulse) {
emplace_back(Pulse::High, duration);
} else if(cycle == number_of_cycles-1 && last_is_pulse) {
emplace_back(Pulse::Low, duration);
} else {
emplace_back(Pulse::Low, duration);
emplace_back(Pulse::High, duration);
}
}
}
void UEF::queue_defined_data(uint32_t length)
{
void UEF::queue_defined_data(uint32_t length) {
if(length < 3) return;
int bits_per_packet = gzget8(file_);
@ -296,8 +236,7 @@ void UEF::queue_defined_data(uint32_t length)
number_of_stop_bits = abs(number_of_stop_bits);
length -= 3;
while(length--)
{
while(length--) {
uint8_t byte = gzget8(file_);
uint8_t parity_value = byte;
@ -307,14 +246,12 @@ void UEF::queue_defined_data(uint32_t length)
queue_bit(0);
int c = bits_per_packet;
while(c--)
{
while(c--) {
queue_bit(byte&1);
byte >>= 1;
}
switch(parity_type)
{
switch(parity_type) {
default: break;
case 'E': queue_bit(parity_value&1); break;
case 'O': queue_bit((parity_value&1) ^ 1); break;
@ -322,45 +259,38 @@ void UEF::queue_defined_data(uint32_t length)
int stop_bits = number_of_stop_bits;
while(stop_bits--) queue_bit(1);
if(has_extra_stop_wave)
{
if(has_extra_stop_wave) {
Time duration;
duration.length = 1;
duration.clock_rate = time_base_ * 4;
queued_pulses_.emplace_back(Pulse::Low, duration);
queued_pulses_.emplace_back(Pulse::High, duration);
emplace_back(Pulse::Low, duration);
emplace_back(Pulse::High, duration);
}
}
}
#pragma mark - Queuing helpers
void UEF::queue_implicit_byte(uint8_t byte)
{
void UEF::queue_implicit_byte(uint8_t byte) {
queue_bit(0);
int c = 8;
while(c--)
{
while(c--) {
queue_bit(byte&1);
byte >>= 1;
}
queue_bit(1);
}
void UEF::queue_bit(int bit)
{
void UEF::queue_bit(int bit) {
int number_of_cycles;
Time duration;
duration.clock_rate = time_base_ * 4;
if(bit)
{
if(bit) {
// encode high-frequency waves
duration.length = 1;
number_of_cycles = 2;
}
else
{
} else {
// encode low-frequency waves
duration.length = 2;
number_of_cycles = 1;
@ -368,9 +298,8 @@ void UEF::queue_bit(int bit)
if(is_300_baud_) number_of_cycles *= 4;
while(number_of_cycles--)
{
queued_pulses_.emplace_back(Pulse::Low, duration);
queued_pulses_.emplace_back(Pulse::High, duration);
while(number_of_cycles--) {
emplace_back(Pulse::Low, duration);
emplace_back(Pulse::High, duration);
}
}

View File

@ -9,7 +9,7 @@
#ifndef TapeUEF_hpp
#define TapeUEF_hpp
#include "../Tape.hpp"
#include "../PulseQueuedTape.hpp"
#include <zlib.h>
#include <cstdint>
#include <vector>
@ -20,7 +20,7 @@ namespace Tape {
/*!
Provides a @c Tape containing a UEF tape image, a slightly-convoluted description of pulses.
*/
class UEF : public Tape {
class UEF : public PulseQueuedTape {
public:
/*!
Constructs a @c UEF containing content from the file with name @c file_name.
@ -34,22 +34,14 @@ class UEF : public Tape {
ErrorNotUEF
};
// implemented to satisfy @c Tape
bool is_at_end();
private:
void virtual_reset();
Pulse virtual_get_next_pulse();
gzFile file_;
unsigned int time_base_;
bool is_at_end_;
bool is_300_baud_;
std::vector<Pulse> queued_pulses_;
size_t pulse_pointer_;
void parse_next_tape_chunk();
void get_next_pulses();
void queue_implicit_bit_pattern(uint32_t length);
void queue_explicit_bit_pattern(uint32_t length);

View File

@ -13,30 +13,33 @@ using namespace Storage::Tape::ZX8081;
Parser::Parser() : pulse_was_high_(false), pulse_time_(0) {}
void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) {
pulse_time_ += pulse.length;
// If this is anything other than a transition from low to high, just add it to the
// count of time.
bool pulse_is_high = pulse.type == Storage::Tape::Tape::Pulse::High;
if(pulse_is_high == pulse_was_high_) return;
bool pulse_did_change = pulse_is_high != pulse_was_high_;
pulse_was_high_ = pulse_is_high;
if(!pulse_did_change || !pulse_is_high) {
pulse_time_ += pulse.length;
return;
}
// Otherwise post a new pulse.
post_pulse();
pulse_time_ = pulse.length;
}
void Parser::post_pulse() {
const float expected_pulse_length = 150.0f / 1000000.0f;
const float expected_pulse_length = 300.0f / 1000000.0f;
const float expected_gap_length = 1300.0f / 1000000.0f;
float pulse_time = pulse_time_.get_float();
pulse_time_.set_zero();
if(pulse_time > expected_gap_length * 1.25f) {
push_wave(WaveType::LongGap);
}
else if(pulse_time > expected_pulse_length * 1.25f) {
} else if(pulse_time > expected_pulse_length * 1.25f) {
push_wave(WaveType::Gap);
}
else if(pulse_time >= expected_pulse_length * 0.75f && pulse_time <= expected_pulse_length * 1.25f) {
} else if(pulse_time >= expected_pulse_length * 0.75f && pulse_time <= expected_pulse_length * 1.25f) {
push_wave(WaveType::Pulse);
}
else {
} else {
push_wave(WaveType::Unrecognised);
}
}
@ -55,7 +58,12 @@ void Parser::inspect_waves(const std::vector<WaveType> &waves) {
return;
}
if(waves.size() >= 9) {
if(waves[0] == WaveType::Unrecognised) {
push_symbol(SymbolType::Unrecognised, 1);
return;
}
if(waves.size() >= 4) {
size_t wave_offset = 0;
// If the very first thing is a gap, swallow it.
if(waves[0] == WaveType::Gap) {
@ -70,10 +78,7 @@ void Parser::inspect_waves(const std::vector<WaveType> &waves) {
// If those pulses were followed by a gap then they might be
// a recognised symbol.
if(number_of_pulses > 17 || number_of_pulses < 7) {
push_symbol(SymbolType::Unrecognised, 1);
}
else if(number_of_pulses + wave_offset < waves.size() &&
if(number_of_pulses + wave_offset < waves.size() &&
(waves[number_of_pulses + wave_offset] == WaveType::LongGap || waves[number_of_pulses + wave_offset] == WaveType::Gap)) {
// A 1 is 18 up/down waves, a 0 is 8. But the final down will be indistinguishable from
// the gap that follows the bit due to the simplified "high is high, everything else is low"
@ -81,9 +86,9 @@ void Parser::inspect_waves(const std::vector<WaveType> &waves) {
// 17 and/or 7 pulses.
size_t gaps_to_swallow = wave_offset + ((waves[number_of_pulses + wave_offset] == WaveType::Gap) ? 1 : 0);
switch(number_of_pulses) {
case 18: case 17: push_symbol(SymbolType::One, (int)(number_of_pulses + gaps_to_swallow)); break;
case 8: case 7: push_symbol(SymbolType::Zero, (int)(number_of_pulses + gaps_to_swallow)); break;
default: push_symbol(SymbolType::Unrecognised, 1); break;
case 8: push_symbol(SymbolType::One, (int)(number_of_pulses + gaps_to_swallow)); break;
case 3: push_symbol(SymbolType::Zero, (int)(number_of_pulses + gaps_to_swallow)); break;
default: push_symbol(SymbolType::Unrecognised, 1); break;
}
}
}
@ -92,17 +97,18 @@ void Parser::inspect_waves(const std::vector<WaveType> &waves) {
int Parser::get_next_byte(const std::shared_ptr<Storage::Tape::Tape> &tape) {
int c = 8;
int result = 0;
while(c--) {
while(c) {
if(is_at_end(tape)) return -1;
SymbolType symbol = get_next_symbol(tape);
if(symbol == SymbolType::FileGap) {
if(symbol != SymbolType::One && symbol != SymbolType::Zero) {
if(c == 8) continue;
return_symbol(symbol);
return -1;
}
if(symbol != SymbolType::One && symbol != SymbolType::Zero) {
return -1;
}
result = (result << 1) | (symbol == SymbolType::One ? 1 : 0);
c--;
}
return result;
}
@ -113,7 +119,7 @@ std::shared_ptr<std::vector<uint8_t>> Parser::get_next_file_data(const std::shar
if(symbol != SymbolType::FileGap) {
return nullptr;
}
while(symbol == SymbolType::FileGap && !is_at_end(tape)) {
while((symbol == SymbolType::FileGap || symbol == SymbolType::Unrecognised) && !is_at_end(tape)) {
symbol = get_next_symbol(tape);
}
if(is_at_end(tape)) return nullptr;
@ -131,6 +137,8 @@ std::shared_ptr<std::vector<uint8_t>> Parser::get_next_file_data(const std::shar
std::shared_ptr<Storage::Data::ZX8081::File> Parser::get_next_file(const std::shared_ptr<Storage::Tape::Tape> &tape) {
std::shared_ptr<std::vector<uint8_t>> file_data = get_next_file_data(tape);
if(!file_data) return nullptr;
if(!file_data) {
return nullptr;
}
return Storage::Data::ZX8081::FileFromData(*file_data);
}

View File

@ -0,0 +1,61 @@
//
// PulseQueuedTape.cpp
// Clock Signal
//
// Created by Thomas Harte on 16/07/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "PulseQueuedTape.hpp"
using namespace Storage::Tape;
PulseQueuedTape::PulseQueuedTape() : pulse_pointer_(0) {}
bool PulseQueuedTape::is_at_end() {
return is_at_end_;
}
void PulseQueuedTape::set_is_at_end(bool is_at_end) {
is_at_end_ = is_at_end;
}
void PulseQueuedTape::clear() {
queued_pulses_.clear();
pulse_pointer_ = 0;
}
bool PulseQueuedTape::empty() {
return queued_pulses_.empty();
}
void PulseQueuedTape::emplace_back(Tape::Pulse::Type type, Time length) {
queued_pulses_.emplace_back(type, length);
}
Tape::Pulse PulseQueuedTape::silence() {
Pulse silence;
silence.type = Pulse::Zero;
silence.length.length = 1;
silence.length.clock_rate = 1;
return silence;
}
Tape::Pulse PulseQueuedTape::virtual_get_next_pulse() {
if(is_at_end_) {
return silence();
}
if(pulse_pointer_ == queued_pulses_.size()) {
clear();
get_next_pulses();
if(is_at_end_ || pulse_pointer_ == queued_pulses_.size()) {
return silence();
}
}
size_t read_pointer = pulse_pointer_;
pulse_pointer_++;
return queued_pulses_[read_pointer];
}

View File

@ -0,0 +1,53 @@
//
// PulseQueuedTape.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/07/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef PulseQueuedTape_hpp
#define PulseQueuedTape_hpp
#include "Tape.hpp"
#include <vector>
namespace Storage {
namespace Tape {
/*!
Provides a @c Tape with a queue of upcoming pulses and an is-at-end flag.
If is-at-end is set then get_next_pulse() returns a second of silence and
is_at_end() returns true.
Otherwise get_next_pulse() returns something from the pulse queue if there is
anything there, and otherwise calls get_next_pulses(). get_next_pulses() is
virtual, giving subclasses a chance to provide the next batch of pulses.
*/
class PulseQueuedTape: public Tape {
public:
PulseQueuedTape();
bool is_at_end();
protected:
void emplace_back(Tape::Pulse::Type type, Time length);
void clear();
bool empty();
void set_is_at_end(bool);
virtual void get_next_pulses() = 0;
private:
Pulse virtual_get_next_pulse();
Pulse silence();
std::vector<Pulse> queued_pulses_;
size_t pulse_pointer_;
bool is_at_end_;
};
}
}
#endif /* PulseQueuedTape_hpp */

View File

@ -20,22 +20,47 @@ TapePlayer::TapePlayer(unsigned int input_clock_rate) :
#pragma mark - Seeking
void Storage::Tape::Tape::seek(Time &seek_time) {
current_time_.set_zero();
next_time_.set_zero();
while(next_time_ <= seek_time) get_next_pulse();
Time next_time(0);
reset();
while(next_time <= seek_time) {
get_next_pulse();
next_time += pulse_.length;
}
}
Storage::Time Tape::get_current_time() {
Time time(0);
uint64_t steps = get_offset();
reset();
while(steps--) {
get_next_pulse();
time += pulse_.length;
}
return time;
}
void Storage::Tape::Tape::reset() {
current_time_.set_zero();
next_time_.set_zero();
offset_ = 0;
virtual_reset();
}
Tape::Pulse Tape::get_next_pulse() {
Tape::Pulse pulse = virtual_get_next_pulse();
current_time_ = next_time_;
next_time_ += pulse.length;
return pulse;
pulse_ = virtual_get_next_pulse();
offset_++;
return pulse_;
}
uint64_t Tape::get_offset() {
return offset_;
}
void Tape::set_offset(uint64_t offset) {
if(offset == offset_) return;
if(offset < offset_) {
reset();
}
offset -= offset_;
while(offset--) get_next_pulse();
}
#pragma mark - Player
@ -85,7 +110,7 @@ void TapePlayer::process_next_event() {
#pragma mark - Binary Player
BinaryTapePlayer::BinaryTapePlayer(unsigned int input_clock_rate) :
TapePlayer(input_clock_rate), motor_is_running_(false)
TapePlayer(input_clock_rate), motor_is_running_(false), input_level_(false)
{}
void BinaryTapePlayer::set_motor_control(bool enabled) {
@ -97,7 +122,7 @@ void BinaryTapePlayer::set_tape_output(bool set) {
}
bool BinaryTapePlayer::get_input() {
return input_level_;
return motor_is_running_ && input_level_;
}
void BinaryTapePlayer::run_for_cycles(int number_of_cycles) {

View File

@ -52,16 +52,33 @@ class Tape {
/// @returns @c true if the tape has progressed beyond all recorded content; @c false otherwise.
virtual bool is_at_end() = 0;
/// @returns the amount of time preceeding the most recently-returned pulse.
virtual Time get_current_time() { return current_time_; }
/*!
Returns a numerical representation of progression into the tape. Precision is arbitrary but
required to be at least to the whole pulse. Greater numbers are later than earlier numbers,
but not necessarily continuous.
*/
virtual uint64_t get_offset();
/// Advances or reverses the tape to the last time before or at @c time from which a pulse starts.
/*!
Moves the tape to the first time at which the specified offset would be returned by get_offset.
*/
virtual void set_offset(uint64_t);
/*!
Calculates and returns the amount of time that has elapsed since the time began. Potentially expensive.
*/
virtual Time get_current_time();
/*!
Seeks to @c time. Potentially expensive.
*/
virtual void seek(Time &time);
virtual ~Tape() {};
private:
Time current_time_, next_time_;
uint64_t offset_;
Tape::Pulse pulse_;
virtual Pulse virtual_get_next_pulse() = 0;
virtual void virtual_reset() = 0;