From ef085e3f9355a2126df89c8b01b92b5ffbe4ab2b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 1 Mar 2019 18:49:21 -0500 Subject: [PATCH 1/6] MSX: introduces a tape motor LED, and limits the fast-tape hack to the BIOS. --- Machines/MSX/MSX.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index e286fde05..883e430a7 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -415,7 +415,7 @@ class ConcreteMachine: switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: if(use_fast_tape_) { - if(address == 0x1a63) { + if(address == 0x1a63 && read_pointers_[0x1a63 >> 13] == &memory_slots_[0].source[0x1a63 >> 13]) { // TAPION // Enable the tape motor. @@ -442,7 +442,7 @@ class ConcreteMachine: break; } - if(address == 0x1abc) { + if(address == 0x1abc && read_pointers_[0x1a63 >> 13] == &memory_slots_[0].source[0x1a63 >> 13]) { // TAPIN // Grab the current values of LOWLIM and WINWID. @@ -675,6 +675,7 @@ class ConcreteMachine: if(disk_rom) { disk_rom->set_activity_observer(observer); } + i8255_port_handler_.set_activity_observer(observer); } // MARK: - Joysticks @@ -705,6 +706,7 @@ class ConcreteMachine: // b4: cassette motor relay tape_player_.set_motor_control(!(value & 0x10)); + activity_observer_->set_led_status("Tape motor", !(value & 0x10)); // b7: keyboard click bool new_audio_level = !!(value & 0x80); @@ -727,10 +729,19 @@ class ConcreteMachine: return 0xff; } + void set_activity_observer(Activity::Observer *observer) { + activity_observer_ = observer; + if(activity_observer_) { + activity_observer_->register_led("Tape motor"); + activity_observer_->set_led_status("Tape motor", tape_player_.get_motor_control()); + } + } + private: ConcreteMachine &machine_; Audio::Toggle &audio_toggle_; Storage::Tape::BinaryTapePlayer &tape_player_; + Activity::Observer *activity_observer_ = nullptr; }; CPU::Z80::Processor z80_; From 1ccee036c42e263769c50900a9f99910d2d2c5de Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 2 Mar 2019 14:19:54 -0500 Subject: [PATCH 2/6] Switches complete logic behind CAS to wave conversion to parsing tape files. --- Storage/Tape/Formats/CAS.cpp | 182 ++++++++++++++++++++++++++--------- Storage/Tape/Formats/CAS.hpp | 6 +- 2 files changed, 139 insertions(+), 49 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index ef9e129f3..19c233a87 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -13,66 +13,156 @@ using namespace Storage::Tape; +/* + CAS files are a raw byte capture of tape content, with all solid tones transmuted to + the placeholder 1F A6 DE BA CC 13 7D 74 and gaps omitted. + + Since that byte stream may also occur within files, and gaps and tone lengths need to be + reconstructed, knowledge of the MSX tape byte format is also required. Specifically: + + Each tone followed by ten bytes that determine the file type: + + ten bytes of value 0xD0 => a binary file; + ten bytes of value 0xD3 => it's a basic file; + ten bytes of value 0xEA => it's an ASCII file; and + any other pattern implies a raw data block. + + Raw data blocks contain their two-byte length, then data. + + Binary, Basic and ASCII files then have a six-byte file name, followed by a short tone, followed + by the file contents. + + ASCII files: + + ... are a sequence of short tone/256-byte chunk pairs. For CAS purposes, these continue until + you hit another 1F A6 DE BA CC 13 7D 74 sequence. + + Binary files: + + ... begin with three 16-bit values, the starting, ending and execution addresses. Then there is + the correct amount of data to fill memory from the starting to the ending address, inclusive. + + BASIC files: + + ... are in Microsoft-standard BASIC form of (two bytes link to next line), (two bytes line number), [tokens], + starting from address 0x8001. These files continue until a next line address of 0x0000 is found, then + are usually padded by 0s for a period that I haven't yet determined a pattern for. The code below treats + everything to the next 0x1f as padding. +*/ + namespace { const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74}; + + #define TenX(x) {x, x, x, x, x, x, x, x, x, x} + const uint8_t binary_signature[] = TenX(0xd0); + const uint8_t basic_signature[] = TenX(0xd3); + const uint8_t ascii_signature[] = TenX(0xea); } CAS::CAS(const std::string &file_name) { Storage::FileHolder file(file_name); - uint8_t lookahead[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - // Entirely fill the lookahead and verify that its start matches the header signature. - get_next(file, lookahead, 10); - if(std::memcmp(lookahead, header_signature, sizeof(header_signature))) throw ErrorNotCAS; + enum class Mode { + Seeking, + ASCII, + Binary, + BASIC + } parsing_mode_ = Mode::Seeking; - while(!file.eof()) { - // Just found a header, so flush the lookahead. - get_next(file, lookahead, 8); - - // Create a new chunk - chunks_.emplace_back(); - Chunk &chunk = chunks_.back(); - - // Decide whether to award a long header and/or a gap. - bool bytes_are_equal = true; - for(std::size_t index = 0; index < sizeof(lookahead); index++) - bytes_are_equal &= (lookahead[index] == lookahead[0]); - - chunk.long_header = bytes_are_equal && ((lookahead[0] == 0xd3) || (lookahead[0] == 0xd0) || (lookahead[0] == 0xea)); - chunk.has_gap = chunk.long_header && (chunks_.size() > 1); - - // Keep going until another header arrives or the file ends. Headers require the magic byte sequence, - // and also must be eight-byte aligned within the file. - while( !file.eof() && - (std::memcmp(lookahead, header_signature, sizeof(header_signature)) || ((file.tell()-10)&7))) { - chunk.data.push_back(lookahead[0]); - get_next(file, lookahead, 1); + while(true) { + // Churn through the file until the next header signature is found. + const auto header_position = file.tell(); + const auto signature = file.read(8); + if(signature.size() != 8) break; + if(std::memcmp(signature.data(), header_signature, 8)) { + // Check for other 1fs in this stream, and repeat from there if any. + for(size_t c = 1; c < 8; ++c) { + if(signature[c] == 0x1f) { + file.seek(header_position + long(c), SEEK_SET); + break; + } + } + continue; } - // If the file ended, flush the lookahead. The final thing in it will be a 0xff from the read that - // triggered the eof, so don't include that. - if(file.eof()) { - for(std::size_t index = 0; index < sizeof(lookahead) - 1; index++) - chunk.data.push_back(lookahead[index]); + // A header has definitely been found. Require from here at least 16 further bytes, + // being the type and a name. + const auto type = file.read(10); + if(type.size() != 10) break; + + const bool is_binary = !std::memcmp(type.data(), binary_signature, type.size()); + const bool is_basic = !std::memcmp(type.data(), basic_signature, type.size()); + const bool is_ascii = !std::memcmp(type.data(), ascii_signature, type.size()); + + switch(parsing_mode_) { + case Mode::Seeking: { + if(is_ascii || is_binary || is_basic) { + file.seek(header_position + 8, SEEK_SET); + chunks_.emplace_back(!chunks_.empty(), true, file.read(10 + 6)); + + if(is_ascii) parsing_mode_ = Mode::ASCII; + if(is_binary) parsing_mode_ = Mode::Binary; + if(is_basic) parsing_mode_ = Mode::BASIC; + } else { + // Raw data appears now. Grab its length and keep going. + file.seek(header_position + 8, SEEK_SET); + const uint16_t length = file.get16le(); + + file.seek(header_position, SEEK_SET); + chunks_.emplace_back(false, false, file.read(size_t(length) + 2 + 8)); + } + } break; + + case Mode::ASCII: + // Keep reading ASCII in 256-byte segments until a non-ASCII chunk arrives. + if(is_binary || is_basic || is_ascii) { + file.seek(header_position, SEEK_SET); + parsing_mode_ = Mode::Seeking; + } else { + file.seek(header_position + 8, SEEK_SET); + chunks_.emplace_back(false, false, file.read(256)); + } + break; + + case Mode::Binary: { + // Get the start and end addresses in order to figure out how much data + // is here. + file.seek(header_position + 8, SEEK_SET); + const uint16_t start_address = file.get16le(); + const uint16_t end_address = file.get16le(); + + file.seek(header_position + 8, SEEK_SET); + const auto length = end_address - start_address + 1; + chunks_.emplace_back(false, false, file.read(size_t(length) + 6)); + + parsing_mode_ = Mode::Seeking; + } break; + + case Mode::BASIC: { + // Horror of horrors, this will mean actually following the BASIC + // linked list of line contents. + file.seek(header_position + 8, SEEK_SET); + uint16_t address = 0x8001; // the BASIC start address. + while(true) { + const uint16_t next_line_address = file.get16le(); + if(!next_line_address || file.eof()) break; + file.seek(next_line_address - address - 2, SEEK_CUR); + address = next_line_address; + } + + // Retain also any padding that follows the BASIC. + while(file.get8() != 0x1f); + const auto length = (file.tell() - 1) - (header_position + 8); + + // Create the chunk and return to regular parsing. + file.seek(header_position + 8, SEEK_SET); + chunks_.emplace_back(false, false, file.read(size_t(length))); + parsing_mode_ = Mode::Seeking; + } break; } } } -/*! - 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)[10], std::size_t quantity) { - assert(quantity <= sizeof(buffer)); - - if(quantity < sizeof(buffer)) - std::memmove(buffer, &buffer[quantity], sizeof(buffer) - quantity); - - while(quantity--) { - buffer[sizeof(buffer) - 1 - quantity] = file.get8(); - } -} - bool CAS::is_at_end() { return phase_ == Phase::EndOfFile; } diff --git a/Storage/Tape/Formats/CAS.hpp b/Storage/Tape/Formats/CAS.hpp index a20bc8a2b..e47ad3ab9 100644 --- a/Storage/Tape/Formats/CAS.hpp +++ b/Storage/Tape/Formats/CAS.hpp @@ -42,9 +42,6 @@ class CAS: public Tape { void virtual_reset(); Pulse virtual_get_next_pulse(); - // Helper for populating the file list, below. - void get_next(Storage::FileHolder &file, uint8_t (&buffer)[10], std::size_t quantity); - // Storage for the array of data blobs to transcribe into audio; // each chunk is preceded by a header which may be long, and is optionally // also preceded by a gap. @@ -52,6 +49,9 @@ class CAS: public Tape { bool has_gap; bool long_header; std::vector data; + + Chunk(bool has_gap, bool long_header, const std::vector &data) : + has_gap(has_gap), long_header(long_header), data(std::move(data)) {} }; std::vector chunks_; From ddce4fb46b64ccc6672d2b3df8d971aa4a0db2c1 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 2 Mar 2019 14:35:16 -0500 Subject: [PATCH 3/6] Ensures that unexpected padding goes somewhere. --- Storage/Tape/Formats/CAS.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 19c233a87..3b3b6f2c7 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -76,10 +76,16 @@ CAS::CAS(const std::string &file_name) { if(signature.size() != 8) break; if(std::memcmp(signature.data(), header_signature, 8)) { // Check for other 1fs in this stream, and repeat from there if any. - for(size_t c = 1; c < 8; ++c) { + for(size_t c = 0; c < 8; ++c) { if(signature[c] == 0x1f) { file.seek(header_position + long(c), SEEK_SET); break; + } else { + // Attach any unexpected bytes to the back of the most recent chunk. + // In effect this creates a linear search for the next explicit tone. + if(!chunks_.empty()) { + chunks_.back().data.push_back(signature[c]); + } } } continue; @@ -149,9 +155,6 @@ CAS::CAS(const std::string &file_name) { file.seek(next_line_address - address - 2, SEEK_CUR); address = next_line_address; } - - // Retain also any padding that follows the BASIC. - while(file.get8() != 0x1f); const auto length = (file.tell() - 1) - (header_position + 8); // Create the chunk and return to regular parsing. From 84d7157dfb88a120a2d20f83fbede380f5a43ec2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 2 Mar 2019 14:40:48 -0500 Subject: [PATCH 4/6] Corrects arithmetic on raw data blocks. --- 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 3b3b6f2c7..9b6b477d2 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -114,8 +114,8 @@ CAS::CAS(const std::string &file_name) { file.seek(header_position + 8, SEEK_SET); const uint16_t length = file.get16le(); - file.seek(header_position, SEEK_SET); - chunks_.emplace_back(false, false, file.read(size_t(length) + 2 + 8)); + file.seek(header_position + 8, SEEK_SET); + chunks_.emplace_back(false, false, file.read(size_t(length) + 2)); } } break; From 9c8a2265b5ecb5d10846eabb1e2e29adad4f715d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 2 Mar 2019 14:47:52 -0500 Subject: [PATCH 5/6] Breaks infinite loop where signature[0] == 0x1f but some of the rest doesn't match. --- Storage/Tape/Formats/CAS.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 9b6b477d2..8662e1bef 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -75,8 +75,10 @@ CAS::CAS(const std::string &file_name) { const auto signature = file.read(8); if(signature.size() != 8) break; if(std::memcmp(signature.data(), header_signature, 8)) { + if(!chunks_.empty()) chunks_.back().data.push_back(signature[0]); + // Check for other 1fs in this stream, and repeat from there if any. - for(size_t c = 0; c < 8; ++c) { + for(size_t c = 1; c < 8; ++c) { if(signature[c] == 0x1f) { file.seek(header_position + long(c), SEEK_SET); break; From d5b4ddd9e5492f2e585ecb876d1c895ad838c6c9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 2 Mar 2019 14:54:26 -0500 Subject: [PATCH 6/6] Simplifies use_fast_tape_ logic. --- Machines/MSX/MSX.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 883e430a7..f6dc8f90a 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -395,6 +395,7 @@ class ConcreteMachine: write_pointers_[c+1] = memory_slots_[value & 3].write_pointers[c+1]; value >>= 2; } + set_use_fast_tape(); } // MARK: Z80::BusHandler @@ -415,7 +416,7 @@ class ConcreteMachine: switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: if(use_fast_tape_) { - if(address == 0x1a63 && read_pointers_[0x1a63 >> 13] == &memory_slots_[0].source[0x1a63 >> 13]) { + if(address == 0x1a63) { // TAPION // Enable the tape motor. @@ -442,7 +443,7 @@ class ConcreteMachine: break; } - if(address == 0x1abc && read_pointers_[0x1a63 >> 13] == &memory_slots_[0].source[0x1a63 >> 13]) { + if(address == 0x1abc) { // TAPIN // Grab the current values of LOWLIM and WINWID. @@ -760,7 +761,7 @@ class ConcreteMachine: bool allow_fast_tape_ = false; bool use_fast_tape_ = false; void set_use_fast_tape() { - use_fast_tape_ = !tape_player_is_sleeping_ && allow_fast_tape_ && tape_player_.has_tape(); + use_fast_tape_ = !tape_player_is_sleeping_ && allow_fast_tape_ && tape_player_.has_tape() && !(paged_memory_&3); } i8255PortHandler i8255_port_handler_;