From 238348c885bcf51d4aa0e5175cf34837a4ddb507 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 16 Jul 2017 21:33:11 -0400 Subject: [PATCH 01/22] Performed the initial wiring to announce that this application supports TZX files and to route them to the ZX80/81 static analyser. The TZX class itself does not yet do much beyond basic validation. I think it'll be easiest if it follows in UEF's footsteps in queuing up pulses ahead of time, so some factoring out is now required. --- .../Clock Signal.xcodeproj/project.pbxproj | 6 +++ OSBindings/Mac/Clock Signal/Info.plist | 14 +++++ StaticAnalyser/StaticAnalyser.cpp | 2 + Storage/Tape/Formats/CSW.cpp | 2 +- Storage/Tape/Formats/TZX.cpp | 49 ++++++++++++++++++ Storage/Tape/Formats/TZX.hpp | 51 +++++++++++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 Storage/Tape/Formats/TZX.cpp create mode 100644 Storage/Tape/Formats/TZX.hpp diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index cf82f9e73..886946b5b 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 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 */; }; 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 +545,8 @@ 4B3BF5AF1F146264005B6C36 /* CSW.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CSW.hpp; sourceTree = ""; }; 4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMPatchedTrack.cpp; sourceTree = ""; }; 4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMPatchedTrack.hpp; sourceTree = ""; }; + 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = ""; }; + 4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = ""; }; 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = AllSuiteA.bin; path = AllSuiteA/AllSuiteA.bin; sourceTree = ""; }; 4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 6502_functional_test.bin; path = "Klaus Dormann/6502_functional_test.bin"; sourceTree = ""; }; 4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */ = {isa = PBXFileReference; lastKnownFileType = file; name = BCDTEST_beeb; path = BCDTest/BCDTEST_beeb; sourceTree = ""; }; @@ -1402,12 +1405,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 */, ); @@ -2605,6 +2610,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 */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 4ca666175..bd6e68c48 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -212,6 +212,20 @@ NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument + + CFBundleTypeExtensions + + tzx + + CFBundleTypeIconFile + cassette + CFBundleTypeName + Tape Image + CFBundleTypeRole + Viewer + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + CFBundleExecutable $(EXECUTABLE_NAME) diff --git a/StaticAnalyser/StaticAnalyser.cpp b/StaticAnalyser/StaticAnalyser.cpp index 41c1b87ce..2d42fffbe 100644 --- a/StaticAnalyser/StaticAnalyser.cpp +++ b/StaticAnalyser/StaticAnalyser.cpp @@ -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 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 diff --git a/Storage/Tape/Formats/CSW.cpp b/Storage/Tape/Formats/CSW.cpp index 2a065cb03..de3a493d4 100644 --- a/Storage/Tape/Formats/CSW.cpp +++ b/Storage/Tape/Formats/CSW.cpp @@ -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. diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp new file mode 100644 index 000000000..0083d8e37 --- /dev/null +++ b/Storage/Tape/Formats/TZX.cpp @@ -0,0 +1,49 @@ +// +// 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; + +TZX::TZX(const char *file_name) : + Storage::FileHolder(file_name), + is_high_(false), + is_at_end_(false), + pulse_pointer_(0) { + + // 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; + + // seed initial block contents + parse_next_chunk(); +} + +bool TZX::is_at_end() { + return true; +} + +Tape::Pulse TZX::virtual_get_next_pulse() { + return Tape::Pulse(); +} + +void TZX::virtual_reset() { +} + +void TZX::parse_next_chunk() { +} diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp new file mode 100644 index 000000000..f6adf1b3d --- /dev/null +++ b/Storage/Tape/Formats/TZX.hpp @@ -0,0 +1,51 @@ +// +// 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 "../Tape.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 Tape, 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 + }; + + // implemented to satisfy @c Tape + bool is_at_end(); + + private: + Pulse virtual_get_next_pulse(); + void virtual_reset(); + +// std::vector queued_pulses_; + size_t pulse_pointer_; + bool is_at_end_; + + bool is_high_; + void parse_next_chunk(); +}; + +} +} +#endif /* TZX_hpp */ From 8f72fc4a442c8e58d122b0e1b18aecee934b1656 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 16 Jul 2017 22:04:40 -0400 Subject: [PATCH 02/22] Factored out from the UEF implementation the concept of being a tape that has a queue of pending pulses and manages that queue. --- .../Clock Signal.xcodeproj/project.pbxproj | 6 + Storage/Tape/Formats/TapeUEF.cpp | 173 ++++++------------ Storage/Tape/Formats/TapeUEF.hpp | 14 +- Storage/Tape/PulseQueuedTape.cpp | 61 ++++++ Storage/Tape/PulseQueuedTape.hpp | 53 ++++++ 5 files changed, 174 insertions(+), 133 deletions(-) create mode 100644 Storage/Tape/PulseQueuedTape.cpp create mode 100644 Storage/Tape/PulseQueuedTape.hpp diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 886946b5b..fe0f78dc4 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 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 */; }; @@ -547,6 +548,8 @@ 4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMPatchedTrack.hpp; sourceTree = ""; }; 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = ""; }; 4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = ""; }; + 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = ""; }; + 4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PulseQueuedTape.hpp; sourceTree = ""; }; 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = AllSuiteA.bin; path = AllSuiteA/AllSuiteA.bin; sourceTree = ""; }; 4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 6502_functional_test.bin; path = "Klaus Dormann/6502_functional_test.bin"; sourceTree = ""; }; 4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */ = {isa = PBXFileReference; lastKnownFileType = file; name = BCDTEST_beeb; path = BCDTest/BCDTEST_beeb; sourceTree = ""; }; @@ -1391,6 +1394,8 @@ children = ( 4B69FB411C4D941400B5F0AA /* Formats */, 4B8805F11DCFC9A2003085B1 /* Parsers */, + 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */, + 4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */, 4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */, 4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */, ); @@ -2580,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 */, diff --git a/Storage/Tape/Formats/TapeUEF.cpp b/Storage/Tape/Formats/TapeUEF.cpp index 7a6926eb8..a17883044 100644 --- a/Storage/Tape/Formats/TapeUEF.cpp +++ b/Storage/Tape/Formats/TapeUEF.cpp @@ -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); } } diff --git a/Storage/Tape/Formats/TapeUEF.hpp b/Storage/Tape/Formats/TapeUEF.hpp index 300dff29a..58f6b5ab8 100644 --- a/Storage/Tape/Formats/TapeUEF.hpp +++ b/Storage/Tape/Formats/TapeUEF.hpp @@ -9,7 +9,7 @@ #ifndef TapeUEF_hpp #define TapeUEF_hpp -#include "../Tape.hpp" +#include "../PulseQueuedTape.hpp" #include #include #include @@ -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 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); diff --git a/Storage/Tape/PulseQueuedTape.cpp b/Storage/Tape/PulseQueuedTape.cpp new file mode 100644 index 000000000..82f2d40e9 --- /dev/null +++ b/Storage/Tape/PulseQueuedTape.cpp @@ -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]; +} diff --git a/Storage/Tape/PulseQueuedTape.hpp b/Storage/Tape/PulseQueuedTape.hpp new file mode 100644 index 000000000..634412c50 --- /dev/null +++ b/Storage/Tape/PulseQueuedTape.hpp @@ -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 + +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 queued_pulses_; + size_t pulse_pointer_; + bool is_at_end_; +}; + +} +} + +#endif /* PulseQueuedTape_hpp */ From 7327da63506e0dbcc6d14baee02b9d69e153ea35 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 16 Jul 2017 22:06:56 -0400 Subject: [PATCH 03/22] Switched the nascent `TZX` to use the new `PulseQueuedTape`. --- Storage/Tape/Formats/TZX.cpp | 19 ++++--------------- Storage/Tape/Formats/TZX.hpp | 14 +++----------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 0083d8e37..6ece1672b 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -12,9 +12,7 @@ using namespace Storage::Tape; TZX::TZX(const char *file_name) : Storage::FileHolder(file_name), - is_high_(false), - is_at_end_(false), - pulse_pointer_(0) { + is_high_(false) { // Check for signature followed by a 0x1a char identifier[7]; @@ -29,21 +27,12 @@ TZX::TZX(const char *file_name) : // Reject if an incompatible version if(major_version != 1 || minor_version > 20) throw ErrorNotTZX; - - // seed initial block contents - parse_next_chunk(); -} - -bool TZX::is_at_end() { - return true; -} - -Tape::Pulse TZX::virtual_get_next_pulse() { - return Tape::Pulse(); } void TZX::virtual_reset() { + clear(); + fseek(file_, SEEK_SET, 0x0a); } -void TZX::parse_next_chunk() { +void TZX::get_next_pulses() { } diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index f6adf1b3d..446e91d43 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -9,7 +9,7 @@ #ifndef TZX_hpp #define TZX_hpp -#include "../Tape.hpp" +#include "../PulseQueuedTape.hpp" #include "../../FileHolder.hpp" namespace Storage { @@ -18,7 +18,7 @@ namespace Tape { /*! Provides a @c Tape containing a CSW tape image, which is a compressed 1-bit sampling. */ -class TZX: public Tape, public Storage::FileHolder { +class TZX: public PulseQueuedTape, public Storage::FileHolder { public: /*! Constructs a @c TZX containing content from the file with name @c file_name. @@ -31,19 +31,11 @@ class TZX: public Tape, public Storage::FileHolder { ErrorNotTZX }; - // implemented to satisfy @c Tape - bool is_at_end(); - private: - Pulse virtual_get_next_pulse(); void virtual_reset(); - -// std::vector queued_pulses_; - size_t pulse_pointer_; - bool is_at_end_; + void get_next_pulses(); bool is_high_; - void parse_next_chunk(); }; } From b63971caf78c1346298bc5e3407f0c82afa242a5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 16 Jul 2017 22:40:38 -0400 Subject: [PATCH 04/22] Took a first, incorrect, shot at TZX chunk 0x19, the generalised data block. --- Storage/Tape/Formats/TZX.cpp | 87 +++++++++++++++++++++++++++++++++++- Storage/Tape/Formats/TZX.hpp | 3 ++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 6ece1672b..f2cc1f62a 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -10,6 +10,10 @@ using namespace Storage::Tape; +namespace { +const unsigned int StandardTZXClock = 3500000; +} + TZX::TZX(const char *file_name) : Storage::FileHolder(file_name), is_high_(false) { @@ -31,8 +35,89 @@ TZX::TZX(const char *file_name) : void TZX::virtual_reset() { clear(); - fseek(file_, SEEK_SET, 0x0a); + fseek(file_, 0x0a, SEEK_SET); } 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 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(); + 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); + get_generalised_segment(total_data_symbols, maximum_pulses_per_data_symbol, symbols_in_data_table); + emplace_back(Tape::Pulse::Zero, Storage::Time((unsigned int)pause_after_block, 1000u)); +} + +void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols) { + if(!output_symbols) return; + + // Construct the symbol table. + struct Symbol { + uint8_t flags; + std::vector pulse_lengths; + }; + std::vector 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()); + } + } + + // Hence produce the output. + for(int c = 0; c < output_symbols; c++) { + uint8_t symbol_value = (uint8_t)fgetc(file_); + Symbol &symbol = symbol_table[symbol_value]; + + // Mutate initial output level. + switch(symbol.flags & 3) { + case 0: break; + case 1: is_high_ ^= true; break; + case 2: is_high_ = true; break; + case 3: is_high_ = false; break; + } + + // Output waves. + for(auto length : symbol.pulse_lengths) { + if(!length) break; + + is_high_ ^= true; + emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time((unsigned int)length, StandardTZXClock)); + } + } } diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index 446e91d43..174109556 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -36,6 +36,9 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { void get_next_pulses(); bool is_high_; + + void get_generalised_data_block(); + void get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols); }; } From fa617eac6bff9d55cc70544ffd5c406d3a6fc664 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 07:34:10 -0400 Subject: [PATCH 05/22] =?UTF-8?q?Spotted=20that=20pilot=20and=20data=20seg?= =?UTF-8?q?ments=20have=20different=20encodings=20=E2=80=94=20of=20course!?= =?UTF-8?q?=20=E2=80=94=20and=20attempted=20to=20adapt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Storage/FileHolder.hpp | 43 +++++++++++++++++++++++++++++ Storage/Tape/Formats/TZX.cpp | 53 +++++++++++++++++++++++++----------- Storage/Tape/Formats/TZX.hpp | 2 +- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Storage/FileHolder.hpp b/Storage/FileHolder.hpp index 458927608..a2be837bd 100644 --- a/Storage/FileHolder.hpp +++ b/Storage/FileHolder.hpp @@ -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_; diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index f2cc1f62a..4100996c8 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -77,12 +77,12 @@ void TZX::get_generalised_data_block() { 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); - get_generalised_segment(total_data_symbols, maximum_pulses_per_data_symbol, symbols_in_data_table); + 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); emplace_back(Tape::Pulse::Zero, Storage::Time((unsigned int)pause_after_block, 1000u)); } -void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols) { +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. @@ -97,27 +97,48 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe 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 = (uint8_t)fgetc(file_); + 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]; - // Mutate initial output level. - switch(symbol.flags & 3) { - case 0: break; - case 1: is_high_ ^= true; break; - case 2: is_high_ = true; break; - case 3: is_high_ = false; break; - } + while(count--) { + // Mutate initial output level. + switch(symbol.flags & 3) { + case 0: break; + case 1: is_high_ ^= true; break; + case 2: is_high_ = true; break; + case 3: is_high_ = false; break; + } - // Output waves. - for(auto length : symbol.pulse_lengths) { - if(!length) break; + // Output waves. + for(auto length : symbol.pulse_lengths) { + if(!length) break; - is_high_ ^= true; - emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time((unsigned int)length, StandardTZXClock)); + is_high_ ^= true; + emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time((unsigned int)length, StandardTZXClock)); + } } } } diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index 174109556..e2e055c66 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -38,7 +38,7 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { bool is_high_; void get_generalised_data_block(); - void get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols); + void get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols, bool is_data); }; } From 9108495586d52ad1f51527790a4deadd386d15c5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 07:35:53 -0400 Subject: [PATCH 06/22] Added a safety seek. --- Storage/Tape/Formats/TZX.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 4100996c8..be1b0dbfd 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -67,6 +67,7 @@ void TZX::get_next_pulses() { 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(); @@ -80,6 +81,9 @@ void TZX::get_generalised_data_block() { 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); emplace_back(Tape::Pulse::Zero, Storage::Time((unsigned int)pause_after_block, 1000u)); + + // 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) { From 2179edc7fe22393434759622ac8e827018175a7a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 07:43:47 -0400 Subject: [PATCH 07/22] Adjusted to allow the very first thing found to be data, and ensured that unrecognised symbols break files just as gaps do. --- Storage/Tape/Parsers/ZX8081.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Storage/Tape/Parsers/ZX8081.cpp b/Storage/Tape/Parsers/ZX8081.cpp index 960992dd5..336ccdb46 100644 --- a/Storage/Tape/Parsers/ZX8081.cpp +++ b/Storage/Tape/Parsers/ZX8081.cpp @@ -95,11 +95,8 @@ int Parser::get_next_byte(const std::shared_ptr &tape) { while(c--) { if(is_at_end(tape)) return -1; SymbolType symbol = get_next_symbol(tape); - if(symbol == SymbolType::FileGap) { - return_symbol(symbol); - return -1; - } if(symbol != SymbolType::One && symbol != SymbolType::Zero) { + return_symbol(symbol); return -1; } result = (result << 1) | (symbol == SymbolType::One ? 1 : 0); @@ -110,10 +107,7 @@ int Parser::get_next_byte(const std::shared_ptr &tape) { std::shared_ptr> Parser::get_next_file_data(const std::shared_ptr &tape) { if(is_at_end(tape)) return nullptr; SymbolType symbol = get_next_symbol(tape); - 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; From 127b584e794ffe5f3ca0efb6a81060ae21f2c577 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 07:52:28 -0400 Subject: [PATCH 08/22] Ensured that resetting a TZX resets the is-at-end flag. --- Storage/Tape/Formats/TZX.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index be1b0dbfd..71cb9cea1 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -35,6 +35,7 @@ TZX::TZX(const char *file_name) : void TZX::virtual_reset() { clear(); + set_is_at_end(false); fseek(file_, 0x0a, SEEK_SET); } From 0350925c1e4cb57e76a876ce1df88d4be402e582 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 19:38:15 -0400 Subject: [PATCH 09/22] Started sketching out greater support. Mostly TODOs right now, but pulse sequence and pure tone are implemented. I probably also need at least the CSW block to hit everything you might see in a ZX80 or ZX81 tape. Then I can worry about the rest when I have a way to test them. --- Storage/Tape/Formats/TZX.cpp | 59 +++++++++++++++++++++++++++++++++--- Storage/Tape/Formats/TZX.hpp | 7 +++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 71cb9cea1..7960ae549 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -48,7 +48,11 @@ void TZX::get_next_pulses() { } switch(chunk_id) { - case 0x19: get_generalised_data_block(); break; + 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. @@ -140,10 +144,57 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe // Output waves. for(auto length : symbol.pulse_lengths) { if(!length) break; - - is_high_ ^= true; - emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time((unsigned int)length, StandardTZXClock)); + post_pulse(length); } } } } + +void TZX::post_pulse(unsigned int length) { + is_high_ ^= true; + emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time(length, StandardTZXClock)); +} + +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()); + } +} diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index e2e055c66..c37334043 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -37,8 +37,15 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { bool is_high_; + 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); }; } From 130d598ec91d3bc2676dfafcef7c712939509864 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 19:52:54 -0400 Subject: [PATCH 10/22] Corrected some minor out-of-style breaks, and ensured that the name of ZX81 files is returned. --- Storage/Data/ZX8081.cpp | 3 +++ Storage/Data/ZX8081.hpp | 2 +- Storage/Tape/Parsers/ZX8081.cpp | 9 +++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Storage/Data/ZX8081.cpp b/Storage/Data/ZX8081.cpp index 836c85dde..d706dfc4b 100644 --- a/Storage/Data/ZX8081.cpp +++ b/Storage/Data/ZX8081.cpp @@ -50,8 +50,10 @@ static std::shared_ptr ZX81FileFromData(const std::vector &data) // Look for a file name. size_t data_pointer = 0; + std::vector 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 ZX81FileFromData(const std::vector &data) // TODO: check that the line numbers declared above exist (?) std::shared_ptr file(new File); + file->name = StringFromData(name_data, true); file->data = data; file->isZX81 = true; return file; diff --git a/Storage/Data/ZX8081.hpp b/Storage/Data/ZX8081.hpp index 148479556..780cc8a15 100644 --- a/Storage/Data/ZX8081.hpp +++ b/Storage/Data/ZX8081.hpp @@ -19,7 +19,7 @@ namespace ZX8081 { struct File { std::vector data; - std::string name; + std::wstring name; bool isZX81; }; diff --git a/Storage/Tape/Parsers/ZX8081.cpp b/Storage/Tape/Parsers/ZX8081.cpp index 336ccdb46..d131d07cd 100644 --- a/Storage/Tape/Parsers/ZX8081.cpp +++ b/Storage/Tape/Parsers/ZX8081.cpp @@ -29,14 +29,11 @@ void Parser::post_pulse() { 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); } } From 35296017b5d2331f97228a9d5eeaebf107a030b2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 17 Jul 2017 21:36:05 -0400 Subject: [PATCH 11/22] Clarified meaning of is_high_ flag and ensured it is honoured properly. --- Storage/Tape/Formats/TZX.cpp | 13 +++++++------ Storage/Tape/Formats/TZX.hpp | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 7960ae549..ede3b011a 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -16,7 +16,7 @@ const unsigned int StandardTZXClock = 3500000; TZX::TZX(const char *file_name) : Storage::FileHolder(file_name), - is_high_(false) { + next_is_high_(false) { // Check for signature followed by a 0x1a char identifier[7]; @@ -36,6 +36,7 @@ TZX::TZX(const char *file_name) : void TZX::virtual_reset() { clear(); set_is_at_end(false); + next_is_high_ = false; fseek(file_, 0x0a, SEEK_SET); } @@ -136,9 +137,9 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe // Mutate initial output level. switch(symbol.flags & 3) { case 0: break; - case 1: is_high_ ^= true; break; - case 2: is_high_ = true; break; - case 3: is_high_ = false; break; + case 1: next_is_high_ ^= true; break; + case 2: next_is_high_ = false; break; + case 3: next_is_high_ = true; break; } // Output waves. @@ -151,8 +152,8 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe } void TZX::post_pulse(unsigned int length) { - is_high_ ^= true; - emplace_back(is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time(length, StandardTZXClock)); + emplace_back(next_is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time(length, StandardTZXClock)); + next_is_high_ ^= true; } void TZX::get_standard_speed_data_block() { diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index c37334043..e0eb6b432 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -35,7 +35,7 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { void virtual_reset(); void get_next_pulses(); - bool is_high_; + bool next_is_high_; void get_standard_speed_data_block(); void get_turbo_speed_data_block(); From c8cee88e3386b0ead620fa54409f37881d112c41 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 18 Jul 2017 22:49:11 -0400 Subject: [PATCH 12/22] Ensured that a stopped tape outputs a `false` level and took an extra safety check in instantiation. --- Storage/Tape/Tape.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index b615fcf50..24528e58b 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -85,7 +85,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 +97,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) { From 44e5a03cf2449477288eca80b86161a8c4fcb9a2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 19 Jul 2017 19:21:27 -0400 Subject: [PATCH 13/22] Removed just-don't-power-the-tape approach to pausing and playing, in favour of being fully communicative. --- Machines/ZX8081/ZX8081.cpp | 9 ++++----- Machines/ZX8081/ZX8081.hpp | 7 ++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp index 6bf9cab22..83f6b8e54 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/ZX8081/ZX8081.cpp @@ -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); } @@ -152,7 +149,9 @@ int Machine::perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { } // 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: diff --git a/Machines/ZX8081/ZX8081.hpp b/Machines/ZX8081/ZX8081.hpp index 23d773dbf..4b6bf9dda 100644 --- a/Machines/ZX8081/ZX8081.hpp +++ b/Machines/ZX8081/ZX8081.hpp @@ -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_; }; From 70af0750129c8b70dfed45c12d187c7aa27de16b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 19 Jul 2017 21:28:33 -0400 Subject: [PATCH 14/22] Determined what appears to be an appropriate workaround for the ZX81 TZX that I've managed to obtain. --- Storage/Tape/Formats/TZX.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index ede3b011a..6829b6c43 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -31,10 +31,19 @@ TZX::TZX(const char *file_name) : // Reject if an incompatible version if(major_version != 1 || minor_version > 20) throw ErrorNotTZX; + + virtual_reset(); } void TZX::virtual_reset() { clear(); + + // 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. + emplace_back(Tape::Pulse::Low, Storage::Time(1, 4)); + emplace_back(Tape::Pulse::High, Storage::Time(1, 4)); + set_is_at_end(false); next_is_high_ = false; fseek(file_, 0x0a, SEEK_SET); @@ -152,7 +161,7 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe } void TZX::post_pulse(unsigned int length) { - emplace_back(next_is_high_ ? Tape::Pulse::High : Tape::Pulse::Low, Storage::Time(length, StandardTZXClock)); + emplace_back(next_is_high_ ? Tape::Pulse::Low : Tape::Pulse::High, Storage::Time(length, StandardTZXClock)); next_is_high_ ^= true; } From 9e975a46a2edc90299d901c2983d18de6982fa14 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 18:18:45 -0400 Subject: [PATCH 15/22] Mildly simplified syntax. --- NumberTheory/Factors.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NumberTheory/Factors.hpp b/NumberTheory/Factors.hpp index 03ffc8593..98832d247 100644 --- a/NumberTheory/Factors.hpp +++ b/NumberTheory/Factors.hpp @@ -9,15 +9,15 @@ #ifndef Factors_hpp #define Factors_hpp +#include + namespace NumberTheory { /*! @returns The greatest common divisor of @c a and @c b as computed by Euclid's algorithm. */ template T greatest_common_divisor(T a, T b) { if(a < b) { - T swap = b; - b = a; - a = swap; + std::swap(a, b); } while(1) { From e152ed2e616d591b7999cb5518c427e891bde7f9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 18:20:27 -0400 Subject: [PATCH 16/22] Made an attempt to avoid GCD costs when accumulating `Time`s with a common clock rate (/divisor). --- Storage/Storage.hpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Storage/Storage.hpp b/Storage/Storage.hpp index d2eb84b36..bb6ccd3aa 100644 --- a/Storage/Storage.hpp +++ b/Storage/Storage.hpp @@ -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::max() && long_clock_rate <= std::numeric_limits::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; From d9a2c32acaa01922aa422308ebbaa147911c5d46 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 18:21:12 -0400 Subject: [PATCH 17/22] Made an attempt to obey the proper TZX rules on gaps, and to hit the common-clock-rate `Time` optimisation. --- Storage/Tape/Formats/TZX.cpp | 47 ++++++++++++++++++++++++------------ Storage/Tape/Formats/TZX.hpp | 5 +++- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 6829b6c43..37b0cb169 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -12,11 +12,12 @@ 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), - next_is_high_(false) { + current_level_(false) { // Check for signature followed by a 0x1a char identifier[7]; @@ -37,16 +38,14 @@ TZX::TZX(const char *file_name) : 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. - emplace_back(Tape::Pulse::Low, Storage::Time(1, 4)); - emplace_back(Tape::Pulse::High, Storage::Time(1, 4)); - - set_is_at_end(false); - next_is_high_ = false; - fseek(file_, 0x0a, SEEK_SET); + current_level_ = false; + post_gap(500); } void TZX::get_next_pulses() { @@ -95,7 +94,7 @@ void TZX::get_generalised_data_block() { 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); - emplace_back(Tape::Pulse::Zero, Storage::Time((unsigned int)pause_after_block, 1000u)); + post_gap(pause_after_block); // This should be unnecessary, but intends to preserve sanity. fseek(file_, endpoint, SEEK_SET); @@ -146,9 +145,9 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe // Mutate initial output level. switch(symbol.flags & 3) { case 0: break; - case 1: next_is_high_ ^= true; break; - case 2: next_is_high_ = false; break; - case 3: next_is_high_ = true; break; + case 1: current_level_ ^= true; break; + case 2: current_level_ = true; break; + case 3: current_level_ = false; break; } // Output waves. @@ -160,11 +159,6 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe } } -void TZX::post_pulse(unsigned int length) { - emplace_back(next_is_high_ ? Tape::Pulse::Low : Tape::Pulse::High, Storage::Time(length, StandardTZXClock)); - next_is_high_ ^= true; -} - void TZX::get_standard_speed_data_block() { __unused uint16_t pause_after_block = fgetc16le(); uint16_t data_length = fgetc16le(); @@ -208,3 +202,24 @@ void TZX::get_pulse_sequence() { 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; +} diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index e0eb6b432..d0a3eedfd 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -35,7 +35,7 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { void virtual_reset(); void get_next_pulses(); - bool next_is_high_; + bool current_level_; void get_standard_speed_data_block(); void get_turbo_speed_data_block(); @@ -46,6 +46,9 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { 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); }; } From a3e00249805fa4758d9abe101119c7f33607b913 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 18:55:03 -0400 Subject: [PATCH 18/22] Chopped time accumulation out of the default `Tape` process because it's proving to be sufficiently expensive for a TZX as not to be worthwhile. Introduced a cheaper position capturing/restoring method. --- Machines/ZX8081/ZX8081.cpp | 4 ++-- Storage/Tape/Tape.cpp | 39 +++++++++++++++++++++++++++++--------- Storage/Tape/Tape.hpp | 25 ++++++++++++++++++++---- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp index 83f6b8e54..23fe0db44 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/ZX8081/ZX8081.cpp @@ -130,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); @@ -144,7 +144,7 @@ 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); } } diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 24528e58b..de4892b35 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -20,22 +20,43 @@ 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) { + reset(); + while(offset--) get_next_pulse(); } #pragma mark - Player diff --git a/Storage/Tape/Tape.hpp b/Storage/Tape/Tape.hpp index 2cd6c028b..6f72ee4f7 100644 --- a/Storage/Tape/Tape.hpp +++ b/Storage/Tape/Tape.hpp @@ -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; From d69b1e2d11f84a0ee0d38bb88e2538cb59064f10 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 19:39:38 -0400 Subject: [PATCH 19/22] Switched parsing logic to looking only for upward zero crossings, that being my new understanding of the ROM. --- Storage/Tape/Parsers/ZX8081.cpp | 36 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Storage/Tape/Parsers/ZX8081.cpp b/Storage/Tape/Parsers/ZX8081.cpp index d131d07cd..0ad025d5f 100644 --- a/Storage/Tape/Parsers/ZX8081.cpp +++ b/Storage/Tape/Parsers/ZX8081.cpp @@ -13,19 +13,25 @@ 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); @@ -52,7 +58,7 @@ void Parser::inspect_waves(const std::vector &waves) { return; } - if(waves.size() >= 9) { + if(waves.size() >= 4) { size_t wave_offset = 0; // If the very first thing is a gap, swallow it. if(waves[0] == WaveType::Gap) { @@ -67,10 +73,7 @@ void Parser::inspect_waves(const std::vector &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" @@ -78,9 +81,9 @@ void Parser::inspect_waves(const std::vector &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; } } } @@ -104,6 +107,9 @@ int Parser::get_next_byte(const std::shared_ptr &tape) { std::shared_ptr> Parser::get_next_file_data(const std::shared_ptr &tape) { if(is_at_end(tape)) return nullptr; SymbolType symbol = get_next_symbol(tape); + if(symbol != SymbolType::FileGap) { + return nullptr; + } while((symbol == SymbolType::FileGap || symbol == SymbolType::Unrecognised) && !is_at_end(tape)) { symbol = get_next_symbol(tape); } @@ -122,6 +128,8 @@ std::shared_ptr> Parser::get_next_file_data(const std::shar std::shared_ptr Parser::get_next_file(const std::shared_ptr &tape) { std::shared_ptr> file_data = get_next_file_data(tape); - if(!file_data) return nullptr; + if(!file_data) { + return nullptr; + } return Storage::Data::ZX8081::FileFromData(*file_data); } From 06ea81fdb29a99ce199f6ae29b15c7e167459d56 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 20:23:26 -0400 Subject: [PATCH 20/22] Made sure that unrecognised waves don't block the symbol queue, and allowed any type of nonsense to be skipped before finding a byte. --- Storage/Tape/Parsers/ZX8081.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Parsers/ZX8081.cpp b/Storage/Tape/Parsers/ZX8081.cpp index 0ad025d5f..68d3fc4b1 100644 --- a/Storage/Tape/Parsers/ZX8081.cpp +++ b/Storage/Tape/Parsers/ZX8081.cpp @@ -58,6 +58,11 @@ void Parser::inspect_waves(const std::vector &waves) { return; } + 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. @@ -92,14 +97,18 @@ void Parser::inspect_waves(const std::vector &waves) { int Parser::get_next_byte(const std::shared_ptr &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::One && symbol != SymbolType::Zero) { + if(c == 8) continue; return_symbol(symbol); return -1; } + result = (result << 1) | (symbol == SymbolType::One ? 1 : 0); + c--; } return result; } From a0269986827eb3c67f4de22798348a8c0aef51e2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 20:43:20 -0400 Subject: [PATCH 21/22] Marginally optimised `set_offset` to avoid `reset`s when possible. --- Storage/Tape/Tape.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index de4892b35..e0e3e81e3 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -55,7 +55,11 @@ uint64_t Tape::get_offset() { } void Tape::set_offset(uint64_t offset) { - reset(); + if(offset == offset_) return; + if(offset < offset_) { + reset(); + } + offset -= offset_; while(offset--) get_next_pulse(); } From 3c8bf52fb8136efc7df718af266d316e9d6a2aea Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 21 Jul 2017 20:43:56 -0400 Subject: [PATCH 22/22] Believing my 64kb memory map not currently to work, temporarily disabled reference to it in the static analyser. --- StaticAnalyser/ZX8081/StaticAnalyser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StaticAnalyser/ZX8081/StaticAnalyser.cpp b/StaticAnalyser/ZX8081/StaticAnalyser.cpp index 93fd276c0..2451d1aee 100644 --- a/StaticAnalyser/ZX8081/StaticAnalyser.cpp +++ b/StaticAnalyser/ZX8081/StaticAnalyser.cpp @@ -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;