From e8ddff0ee0d9475d0c178111468365b879ba35ba Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 21 Dec 2017 22:34:03 -0500 Subject: [PATCH 1/6] Makes a first, messy, attempt at serialising CAS files into audio. --- Storage/Tape/Formats/CAS.cpp | 84 ++++++++++++++++++++++++++++++++++-- Storage/Tape/Formats/CAS.hpp | 9 ++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index edce11dd2..f6a2dd23b 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -8,21 +8,97 @@ #include "CAS.hpp" +#include + using namespace Storage::Tape; +namespace { + const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74}; +} + CAS::CAS(const char *file_name) : file_(file_name) { + file_.read(input_, sizeof(input_)); + if(std::memcmp(input_, header_signature, sizeof(header_signature))) throw ErrorNotCAS; } bool CAS::is_at_end() { - return true; + return file_.eof(); } void CAS::virtual_reset() { - + file_.seek(0, SEEK_SET); + file_.read(input_, sizeof(input_)); + phase_ = Phase::Header; + distance_into_phase_ = 0; } Tape::Pulse CAS::virtual_get_next_pulse() { - Pulse empty_pulse; - return empty_pulse; + Pulse pulse; + switch(phase_) { + case Phase::Gap: { + // Leave a fixed 0.5 second gap between blocks. + pulse.length.length = 2400; + pulse.length.clock_rate = 4800; + pulse.type = Pulse::Type::Zero; + + if(!file_.eof()) phase_ = Phase::Header; + } break; + + case Phase::Header: { + // 1 bits are two complete cycle at 2400 hz + pulse.length.length = 1; + pulse.length.clock_rate = 4800; + pulse.type = (distance_into_phase_&1) ? Pulse::Type::Low : Pulse::Type::High; + + distance_into_phase_++; + if(distance_into_phase_ == 7936*4) { + distance_into_phase_ = 0; + phase_ = Phase::Bytes; + + for(int c = 1; c < 8; c++) { + input_[c] = file_.get8(); + } + } + } break; + + case Phase::Bytes: { + unsigned int bit; + const int bit_offset = distance_into_phase_ >> 2; + switch(bit_offset) { + case 0: bit = 0; break; + default: bit = (input_[0] >> (bit_offset - 1)) & 1; break; + case 9: + case 10: bit = 1; break; + } + + // 1 bits are two complete cycle at 2400 hz; 0 bits are one complete cycle at 1200 hz + pulse.length.length = 2 - bit; + pulse.length.clock_rate = 4800; + pulse.type = distance_into_phase_ ? Pulse::Type::High : Pulse::Type::Low; + + int adder = 1; + if(!bit && ((distance_into_phase_&3) == 1)) adder = 3; + distance_into_phase_ = (distance_into_phase_ + adder) % (11 * 4); + } break; + } + +// if(pulse.type == Pulse::Type::Zero) printf("\n---\n"); +// else { +// if(pulse.length.length == 1) printf("."); +// else if(pulse.length.length == 2) printf("+"); +// else printf("?"); +// } + + if(!distance_into_phase_ && phase_ == Phase::Bytes) { + std::memmove(input_, &input_[1], 7); + input_[7] = file_.get8(); + if(!is_at_end()) { + if(!std::memcmp(input_, header_signature, sizeof(header_signature))) { + phase_ = Phase::Header;//Gap; + } + } + } + + return pulse; } diff --git a/Storage/Tape/Formats/CAS.hpp b/Storage/Tape/Formats/CAS.hpp index 4b998e2b9..5aebffe72 100644 --- a/Storage/Tape/Formats/CAS.hpp +++ b/Storage/Tape/Formats/CAS.hpp @@ -39,6 +39,15 @@ class CAS: public Tape { void virtual_reset(); Pulse virtual_get_next_pulse(); + + uint8_t input_[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + enum class Phase { + Header, + Bytes, + Gap + } phase_ = Phase::Header; + int distance_into_phase_ = 0; }; } From b4bfcd42795360c41b225fef2e685b12d7064d9c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 23 Dec 2017 18:41:50 -0500 Subject: [PATCH 2/6] Switches to an attempt to break the .CAS into files ahead of time. Hopefully the better to insert appropriate lengths of header and gap. --- Storage/Tape/Formats/CAS.cpp | 177 ++++++++++++++++++++++++----------- Storage/Tape/Formats/CAS.hpp | 29 +++++- 2 files changed, 147 insertions(+), 59 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index f6a2dd23b..6b1b80971 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -8,6 +8,7 @@ #include "CAS.hpp" +#include #include using namespace Storage::Tape; @@ -16,89 +17,157 @@ namespace { const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74}; } -CAS::CAS(const char *file_name) : - file_(file_name) { - file_.read(input_, sizeof(input_)); - if(std::memcmp(input_, header_signature, sizeof(header_signature))) throw ErrorNotCAS; +CAS::CAS(const char *file_name) { + Storage::FileHolder file(file_name); + uint8_t lookahead[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + // Get the first header. + get_next(file, lookahead, 8); + if(std::memcmp(lookahead, header_signature, sizeof(header_signature))) throw ErrorNotCAS; + + File *active_file = nullptr; + while(!file.eof()) { + // Just found a header, so flush the lookahead. + get_next(file, lookahead, 8); + + // If no file is active, create one, as this must be an identification block. + if(!active_file) { + // Determine the new file type. + Block type; + switch(lookahead[0]) { + case 0xd3: type = Block::CSAVE; break; + case 0xd0: type = Block::BSAVE; break; + case 0xea: type = Block::ASCII; break; + + // This implies something has gone wrong with parsing. + default: throw ErrorNotCAS; + } + + // Set the type and feed in the initial data. + files_.emplace_back(); + active_file = &files_.back(); + active_file->type = type; + } + + // Add a new chunk for the new incoming data. + active_file->chunks.emplace_back(); + + // Keep going until another header arrives. + while(std::memcmp(lookahead, header_signature, sizeof(header_signature)) && !file.eof()) { + active_file->chunks.back().push_back(lookahead[0]); + get_next(file, lookahead, 1); + } + + switch(active_file->type) { + case Block::ASCII: + // ASCII files have as many chunks as necessary, the final one being back filled + // with 0x1a. + if(active_file->chunks.size() >= 2) { + std::vector &last_chunk = active_file->chunks.back(); + if(last_chunk.back() == 0x1a) + active_file = nullptr; + } + break; + + default: + // CSAVE and BSAVE files have exactly two chunks, the second being the data. + if(active_file->chunks.size() == 2) + active_file = nullptr; + break; + } + } +} + +void CAS::get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity) { + assert(quantity <= 8); + + if(quantity < 8) + std::memmove(buffer, &buffer[quantity], 8 - quantity); + + while(quantity--) { + buffer[7 - quantity] = file.get8(); + } } bool CAS::is_at_end() { - return file_.eof(); + return phase_ == Phase::EndOfFile; } void CAS::virtual_reset() { - file_.seek(0, SEEK_SET); - file_.read(input_, sizeof(input_)); phase_ = Phase::Header; + file_pointer_ = 0; + chunk_pointer_ = 0; distance_into_phase_ = 0; + distance_into_bit_ = 0; } Tape::Pulse CAS::virtual_get_next_pulse() { Pulse pulse; - switch(phase_) { - case Phase::Gap: { - // Leave a fixed 0.5 second gap between blocks. - pulse.length.length = 2400; - pulse.length.clock_rate = 4800; - pulse.type = Pulse::Type::Zero; + pulse.length.clock_rate = 4800; - if(!file_.eof()) phase_ = Phase::Header; - } break; + // If this is a gap, then that terminates a file. If this is already the end + // of the file then perpetual gaps await. + if(phase_ == Phase::Gap || phase_ == Phase::EndOfFile) { + pulse.length.length = pulse.length.clock_rate; + pulse.type = Pulse::Type::Zero; + + if(phase_ == Phase::Gap) { + phase_ = Phase::Header; + file_pointer_ ++; + chunk_pointer_ = 0; + distance_into_phase_ = 0; + } + + return pulse; + } + + // Determine which bit is now forthcoming. + int bit = 1; + + switch(phase_) { + default: break; case Phase::Header: { - // 1 bits are two complete cycle at 2400 hz - pulse.length.length = 1; - pulse.length.clock_rate = 4800; - pulse.type = (distance_into_phase_&1) ? Pulse::Type::Low : Pulse::Type::High; - - distance_into_phase_++; - if(distance_into_phase_ == 7936*4) { - distance_into_phase_ = 0; - phase_ = Phase::Bytes; - - for(int c = 1; c < 8; c++) { - input_[c] = file_.get8(); + distance_into_bit_++; + if(distance_into_bit_ == 2) { + distance_into_phase_++; + if(distance_into_phase_ == (chunk_pointer_ ? 15360 : 3840)) { + phase_ = Phase::Bytes; + distance_into_phase_ = 0; + distance_into_bit_ = 0; } } } break; case Phase::Bytes: { - unsigned int bit; - const int bit_offset = distance_into_phase_ >> 2; + int byte_value = files_[file_pointer_].chunks[chunk_pointer_][distance_into_phase_ / 11]; + int bit_offset = distance_into_phase_ % 11; switch(bit_offset) { case 0: bit = 0; break; - default: bit = (input_[0] >> (bit_offset - 1)) & 1; break; + default: bit = (byte_value >> (bit_offset - 1)) & 1; break; case 9: case 10: bit = 1; break; } - // 1 bits are two complete cycle at 2400 hz; 0 bits are one complete cycle at 1200 hz - pulse.length.length = 2 - bit; - pulse.length.clock_rate = 4800; - pulse.type = distance_into_phase_ ? Pulse::Type::High : Pulse::Type::Low; - - int adder = 1; - if(!bit && ((distance_into_phase_&3) == 1)) adder = 3; - distance_into_phase_ = (distance_into_phase_ + adder) % (11 * 4); + distance_into_bit_++; + if(distance_into_bit_ == (bit ? 4 : 2)) { + distance_into_bit_ = 0; + distance_into_phase_++; + if(distance_into_phase_ == files_[file_pointer_].chunks[chunk_pointer_].size() * 11) { + distance_into_phase_ = 0; + chunk_pointer_++; + if(chunk_pointer_ == files_[file_pointer_].chunks.size()) { + chunk_pointer_ = 0; + file_pointer_++; + phase_ = (chunk_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; + } + } + } } break; } -// if(pulse.type == Pulse::Type::Zero) printf("\n---\n"); -// else { -// if(pulse.length.length == 1) printf("."); -// else if(pulse.length.length == 2) printf("+"); -// else printf("?"); -// } - - if(!distance_into_phase_ && phase_ == Phase::Bytes) { - std::memmove(input_, &input_[1], 7); - input_[7] = file_.get8(); - if(!is_at_end()) { - if(!std::memcmp(input_, header_signature, sizeof(header_signature))) { - phase_ = Phase::Header;//Gap; - } - } - } + pulse.length.length = static_cast(2 - bit); + pulse.type = (distance_into_bit_ & 1) ? Pulse::Type::High : Pulse::Type::Low; return pulse; } diff --git a/Storage/Tape/Formats/CAS.hpp b/Storage/Tape/Formats/CAS.hpp index 5aebffe72..beaca5843 100644 --- a/Storage/Tape/Formats/CAS.hpp +++ b/Storage/Tape/Formats/CAS.hpp @@ -12,6 +12,9 @@ #include "../Tape.hpp" #include "../../FileHolder.hpp" +#include +#include + namespace Storage { namespace Tape { @@ -35,19 +38,35 @@ class CAS: public Tape { bool is_at_end(); private: - Storage::FileHolder file_; - void virtual_reset(); Pulse virtual_get_next_pulse(); - uint8_t input_[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + // Helper for populating the file list, below. + void get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity); + // Storage for the array of files to transcribe into audio. + enum class Block { + BSAVE, + CSAVE, + ASCII + }; + struct File { + Block type; + std::vector> chunks; + }; + std::vector files_; + + // Tracker for active state within the file list. + std::size_t file_pointer_ = 0; + std::size_t chunk_pointer_ = 0; enum class Phase { Header, Bytes, - Gap + Gap, + EndOfFile } phase_ = Phase::Header; - int distance_into_phase_ = 0; + std::size_t distance_into_phase_ = 0; + std::size_t distance_into_bit_ = 0; }; } From f4d414d6e41f57cb326eea5ad8471543d3fbbfb0 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 23 Dec 2017 18:42:04 -0500 Subject: [PATCH 3/6] Removes stray line break. --- Storage/Tape/Tape.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 0d5ce1504..6722f6575 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -89,8 +89,7 @@ void TapePlayer::get_next_pulse() { if(tape_) { current_pulse_ = tape_->get_next_pulse(); if(tape_->is_at_end()) update_sleep_observer(); - } - else { + } else { current_pulse_.length.length = 1; current_pulse_.length.clock_rate = 1; current_pulse_.type = Tape::Pulse::Zero; From 9a7e9745795d029b5adda59f98deae0148f67035 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 23 Dec 2017 19:20:04 -0500 Subject: [PATCH 4/6] Corrects skipping of every other file, and transition from bytes back into header. --- Storage/Tape/Formats/CAS.cpp | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 6b1b80971..8176e2570 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -78,6 +78,10 @@ CAS::CAS(const char *file_name) { } } +/*! + Treating @c buffer as a sliding lookahead, shifts it @c quantity elements to the left and + populates the new empty area to the right from @c file. +*/ void CAS::get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity) { assert(quantity <= 8); @@ -103,7 +107,10 @@ void CAS::virtual_reset() { Tape::Pulse CAS::virtual_get_next_pulse() { Pulse pulse; - pulse.length.clock_rate = 4800; + pulse.length.clock_rate = 9600; + // Clock rate is four times the baud rate (of 2400), because the quickest thing that might need + // to be communicated is a '1', which is two cycles at the baud rate, i.e. four events: + // high, low, high, low. // If this is a gap, then that terminates a file. If this is already the end // of the file then perpetual gaps await. @@ -128,10 +135,16 @@ Tape::Pulse CAS::virtual_get_next_pulse() { default: break; case Phase::Header: { + // In the header, all bits are 1s, so let the default value stand. Just check whether the + // header is ended and, if so, move on to bytes. distance_into_bit_++; if(distance_into_bit_ == 2) { distance_into_phase_++; - if(distance_into_phase_ == (chunk_pointer_ ? 15360 : 3840)) { + distance_into_bit_ = 0; + + // This code always produces a 2400 baud signal; so use the appropriate Red Book-supplied + // constants to check whether the header has come to an end. + if(distance_into_phase_ == (chunk_pointer_ ? 7936 : 31744)) { phase_ = Phase::Bytes; distance_into_phase_ = 0; distance_into_bit_ = 0; @@ -140,7 +153,8 @@ Tape::Pulse CAS::virtual_get_next_pulse() { } break; case Phase::Bytes: { - int byte_value = files_[file_pointer_].chunks[chunk_pointer_][distance_into_phase_ / 11]; + // Provide bits with a single '0' start bit and two '1' stop bits. + uint8_t byte_value = files_[file_pointer_].chunks[chunk_pointer_][distance_into_phase_ / 11]; int bit_offset = distance_into_phase_ % 11; switch(bit_offset) { case 0: bit = 0; break; @@ -149,6 +163,15 @@ Tape::Pulse CAS::virtual_get_next_pulse() { case 10: bit = 1; break; } + // Lots of branches below, to the effect that: + // + // if bit is finished, and if all bytes in chunk have been posted then: + // + // - if this is the final chunk in the file then, if there are further files switch to a gap. + // Otherwise note end of file. + // + // - otherwise, roll onto the next header. + // distance_into_bit_++; if(distance_into_bit_ == (bit ? 4 : 2)) { distance_into_bit_ = 0; @@ -158,14 +181,16 @@ Tape::Pulse CAS::virtual_get_next_pulse() { chunk_pointer_++; if(chunk_pointer_ == files_[file_pointer_].chunks.size()) { chunk_pointer_ = 0; - file_pointer_++; phase_ = (chunk_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; + } else { + phase_ = Phase::Header; } } } } break; } + // A '1' is encoded with twice the frequency of a '0'. pulse.length.length = static_cast(2 - bit); pulse.type = (distance_into_bit_ & 1) ? Pulse::Type::High : Pulse::Type::Low; From 0a079b0f94805c657d1b52a2ebbd1342dbddac71 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 23 Dec 2017 19:32:24 -0500 Subject: [PATCH 5/6] Attempts to fix failure to distinguish end-of-file. --- Storage/Tape/Formats/CAS.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 8176e2570..22b572aa0 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -120,7 +120,6 @@ Tape::Pulse CAS::virtual_get_next_pulse() { if(phase_ == Phase::Gap) { phase_ = Phase::Header; - file_pointer_ ++; chunk_pointer_ = 0; distance_into_phase_ = 0; } @@ -181,7 +180,8 @@ Tape::Pulse CAS::virtual_get_next_pulse() { chunk_pointer_++; if(chunk_pointer_ == files_[file_pointer_].chunks.size()) { chunk_pointer_ = 0; - phase_ = (chunk_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; + file_pointer_++; + phase_ = (file_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; } else { phase_ = Phase::Header; } From cbba6a55951ff4aa91aefac46f557bffc98c1cf5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 23 Dec 2017 19:54:42 -0500 Subject: [PATCH 6/6] Ensures final few bytes of a CAS file aren't dropped. --- Storage/Tape/Formats/CAS.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 22b572aa0..6ea0b037a 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -52,12 +52,18 @@ CAS::CAS(const char *file_name) { // Add a new chunk for the new incoming data. active_file->chunks.emplace_back(); - // Keep going until another header arrives. + // Keep going until another header arrives or the file ends. while(std::memcmp(lookahead, header_signature, sizeof(header_signature)) && !file.eof()) { active_file->chunks.back().push_back(lookahead[0]); get_next(file, lookahead, 1); } + // If the file ended, flush the lookahead. + if(file.eof()) { + for(int index = 0; index < 8; index++) + active_file->chunks.back().push_back(lookahead[index]); + } + switch(active_file->type) { case Block::ASCII: // ASCII files have as many chunks as necessary, the final one being back filled