From d83178f29d4b43be93fb83f1f9f0177a7921bd4b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 22 Feb 2018 21:28:12 -0500 Subject: [PATCH] Makes an attempt at implementing all missing TZX 1.20 blocks. Towards that aim, simplifies CSW handling so that even regular RLE compression uses a static grab of file contents. --- Storage/Tape/Formats/CSW.cpp | 105 +++++++++++++------------------ Storage/Tape/Formats/CSW.hpp | 19 +++--- Storage/Tape/Formats/TZX.cpp | 96 +++++++++++++++++++++++++--- Storage/Tape/Formats/TZX.hpp | 15 ++++- Storage/Tape/PulseQueuedTape.cpp | 4 ++ Storage/Tape/PulseQueuedTape.hpp | 1 + 6 files changed, 159 insertions(+), 81 deletions(-) diff --git a/Storage/Tape/Formats/CSW.cpp b/Storage/Tape/Formats/CSW.cpp index ac70cb8b4..10d7d6402 100644 --- a/Storage/Tape/Formats/CSW.cpp +++ b/Storage/Tape/Formats/CSW.cpp @@ -13,21 +13,21 @@ using namespace Storage::Tape; CSW::CSW(const char *file_name) : - file_(file_name), source_data_pointer_(0) { - if(file_.stats().st_size < 0x20) throw ErrorNotCSW; + Storage::FileHolder file(file_name); + if(file.stats().st_size < 0x20) throw ErrorNotCSW; // Check signature. - if(!file_.check_signature("Compressed Square Wave")) { + if(!file.check_signature("Compressed Square Wave")) { throw ErrorNotCSW; } // Check terminating byte. - if(file_.get8() != 0x1a) throw ErrorNotCSW; + if(file.get8() != 0x1a) throw ErrorNotCSW; // Get version file number. - uint8_t major_version = file_.get8(); - uint8_t minor_version = file_.get8(); + uint8_t major_version = file.get8(); + uint8_t minor_version = file.get8(); // Reject if this is an unknown version. if(major_version > 2 || !major_version || minor_version > 1) throw ErrorNotCSW; @@ -35,41 +35,42 @@ CSW::CSW(const char *file_name) : // The header now diverges based on version. uint32_t number_of_waves = 0; if(major_version == 1) { - pulse_.length.clock_rate = file_.get16le(); + pulse_.length.clock_rate = file.get16le(); - if(file_.get8() != 1) throw ErrorNotCSW; + if(file.get8() != 1) throw ErrorNotCSW; compression_type_ = CompressionType::RLE; - pulse_.type = (file_.get8() & 1) ? Pulse::High : Pulse::Low; + pulse_.type = (file.get8() & 1) ? Pulse::High : Pulse::Low; - file_.seek(0x20, SEEK_SET); + file.seek(0x20, SEEK_SET); } else { - pulse_.length.clock_rate = file_.get32le(); - number_of_waves = file_.get32le(); - switch(file_.get8()) { + pulse_.length.clock_rate = file.get32le(); + number_of_waves = file.get32le(); + switch(file.get8()) { case 1: compression_type_ = CompressionType::RLE; break; case 2: compression_type_ = CompressionType::ZRLE; break; default: throw ErrorNotCSW; } - pulse_.type = (file_.get8() & 1) ? Pulse::High : Pulse::Low; - uint8_t extension_length = file_.get8(); + pulse_.type = (file.get8() & 1) ? Pulse::High : Pulse::Low; + uint8_t extension_length = file.get8(); - if(file_.stats().st_size < 0x34 + extension_length) throw ErrorNotCSW; - file_.seek(0x34 + extension_length, SEEK_SET); + if(file.stats().st_size < 0x34 + extension_length) throw ErrorNotCSW; + file.seek(0x34 + extension_length, SEEK_SET); } + // Grab all data remaining in the file. + std::vector file_data; + std::size_t remaining_data = static_cast(file.stats().st_size) - static_cast(file.tell()); + file_data.resize(remaining_data); + file.read(file_data.data(), remaining_data); + if(compression_type_ == CompressionType::ZRLE) { // The only clue given by CSW as to the output size in bytes is that there will be // number_of_waves waves. Waves are usually one byte, but may be five. So this code // is pessimistic. source_data_.resize(static_cast(number_of_waves) * 5); - std::vector file_data; - std::size_t remaining_data = static_cast(file_.stats().st_size) - static_cast(file_.tell()); - file_data.resize(remaining_data); - file_.read(file_data.data(), remaining_data); - // uncompress will tell how many compressed bytes there actually were, so use its // modification of output_length to throw away all the memory that isn't actually // needed. @@ -77,42 +78,34 @@ CSW::CSW(const char *file_name) : uncompress(source_data_.data(), &output_length, file_data.data(), file_data.size()); source_data_.resize(static_cast(output_length)); } else { - rle_start_ = file_.tell(); + source_data_ = std::move(file_data); } invert_pulse(); } -uint8_t CSW::get_next_byte() { - switch(compression_type_) { - case CompressionType::RLE: return file_.get8(); - case CompressionType::ZRLE: { - if(source_data_pointer_ == source_data_.size()) return 0xff; - uint8_t result = source_data_[source_data_pointer_]; - source_data_pointer_++; - return result; - } +CSW::CSW(const std::vector &&data, CompressionType compression_type, bool initial_level, uint32_t sampling_rate) { + pulse_.length.clock_rate = sampling_rate; + pulse_.type = initial_level ? Pulse::High : Pulse::Low; + source_data_ = std::move(data); +} - default: assert(false); break; - } +uint8_t CSW::get_next_byte() { + if(source_data_pointer_ == source_data_.size()) return 0xff; + uint8_t result = source_data_[source_data_pointer_]; + source_data_pointer_++; + return result; } uint32_t CSW::get_next_int32le() { - switch(compression_type_) { - case CompressionType::RLE: return file_.get32le(); - case CompressionType::ZRLE: { - if(source_data_pointer_ > source_data_.size() - 4) return 0xffff; - uint32_t result = (uint32_t)( - (source_data_[source_data_pointer_ + 0] << 0) | - (source_data_[source_data_pointer_ + 1] << 8) | - (source_data_[source_data_pointer_ + 2] << 16) | - (source_data_[source_data_pointer_ + 3] << 24)); - source_data_pointer_ += 4; - return result; - } - - default: assert(false); break; - } + if(source_data_pointer_ > source_data_.size() - 4) return 0xffff; + uint32_t result = (uint32_t)( + (source_data_[source_data_pointer_ + 0] << 0) | + (source_data_[source_data_pointer_ + 1] << 8) | + (source_data_[source_data_pointer_ + 2] << 16) | + (source_data_[source_data_pointer_ + 3] << 24)); + source_data_pointer_ += 4; + return result; } void CSW::invert_pulse() { @@ -120,21 +113,11 @@ void CSW::invert_pulse() { } bool CSW::is_at_end() { - switch(compression_type_) { - case CompressionType::RLE: return file_.eof(); - case CompressionType::ZRLE: return source_data_pointer_ == source_data_.size(); - - default: assert(false); break; - } + return source_data_pointer_ == source_data_.size(); } void CSW::virtual_reset() { - switch(compression_type_) { - case CompressionType::RLE: file_.seek(rle_start_, SEEK_SET); break; - case CompressionType::ZRLE: source_data_pointer_ = 0; break; - - default: assert(false); break; - } + source_data_pointer_ = 0; } Tape::Pulse CSW::virtual_get_next_pulse() { diff --git a/Storage/Tape/Formats/CSW.hpp b/Storage/Tape/Formats/CSW.hpp index f05df8c3a..fd1e0d179 100644 --- a/Storage/Tape/Formats/CSW.hpp +++ b/Storage/Tape/Formats/CSW.hpp @@ -30,6 +30,16 @@ class CSW: public Tape { */ CSW(const char *file_name); + enum class CompressionType { + RLE, + ZRLE + }; + + /*! + Constructs a @c CSW containing content as specified. Does not throw. + */ + CSW(const std::vector &&data, CompressionType compression_type, bool initial_level, uint32_t sampling_rate); + enum { ErrorNotCSW }; @@ -38,16 +48,11 @@ class CSW: public Tape { bool is_at_end(); private: - Storage::FileHolder file_; - void virtual_reset(); Pulse virtual_get_next_pulse(); Pulse pulse_; - enum class CompressionType { - RLE, - ZRLE - } compression_type_; + CompressionType compression_type_; uint8_t get_next_byte(); uint32_t get_next_int32le(); @@ -55,8 +60,6 @@ class CSW: public Tape { std::vector source_data_; std::size_t source_data_pointer_; - - long rle_start_; }; } diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 4c47c6082..d9641004b 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -8,6 +8,8 @@ #include "TZX.hpp" +#include "CSW.hpp" + using namespace Storage::Tape; namespace { @@ -59,6 +61,8 @@ void TZX::get_next_pulses() { case 0x12: get_pure_tone_data_block(); break; case 0x13: get_pulse_sequence(); break; case 0x14: get_pure_data_block(); break; + case 0x15: get_direct_recording_block(); break; + case 0x18: get_csw_recording_block(); break; case 0x19: get_generalised_data_block(); break; case 0x20: get_pause(); break; @@ -70,13 +74,20 @@ void TZX::get_next_pulses() { case 0x26: ignore_call_sequence(); break; case 0x27: ignore_return_from_sequence(); break; case 0x28: ignore_select_block(); break; + case 0x2a: ignore_stop_tape_if_in_48kb_mode(); break; + + case 0x2b: get_set_signal_level(); break; case 0x30: ignore_text_description(); break; case 0x31: ignore_message_block(); break; + case 0x32: ignore_archive_info(); break; case 0x33: get_hardware_type(); break; + case 0x35: ignore_custom_info_block(); break; case 0x4b: get_kansas_city_block(); break; + case 0x5a: ignore_glue_block(); 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. @@ -87,6 +98,26 @@ void TZX::get_next_pulses() { } } +void TZX::get_csw_recording_block() { + const uint32_t block_length = file_.get32le(); + const uint16_t pause_after_block = file_.get16le(); + const uint32_t sampling_rate = file_.get24le(); + const uint8_t compression_type = file_.get8(); + const uint32_t number_of_compressed_pulses = file_.get32le(); + + std::vector raw_block = file_.read(block_length - 10); + + CSW csw(std::move(raw_block), (compression_type == 2) ? CSW::CompressionType::ZRLE : CSW::CompressionType::RLE, current_level_, sampling_rate); + while(!csw.is_at_end()) { + Tape::Pulse next_pulse = csw.get_next_pulse(); + current_level_ = (next_pulse.type == Tape::Pulse::High); + emplace_back(std::move(next_pulse)); + } + + (void)number_of_compressed_pulses; + post_gap(pause_after_block); +} + void TZX::get_generalised_data_block() { uint32_t block_length = file_.get32le(); long endpoint = file_.tell() + static_cast(block_length); @@ -250,6 +281,36 @@ void TZX::get_pure_data_block() { get_data(data); } +void TZX::get_direct_recording_block() { + const Storage::Time length_per_sample(static_cast(file_.get16le()), StandardTZXClock); + const uint16_t pause_after_block = file_.get16le(); + uint8_t used_bits_in_final_byte = file_.get8(); + const uint32_t length_of_data = file_.get24le(); + + if(used_bits_in_final_byte < 1) used_bits_in_final_byte = 1; + if(used_bits_in_final_byte > 8) used_bits_in_final_byte = 8; + + uint8_t byte = 0; + unsigned int bits_at_level = 0; + uint8_t level = 0; + for(std::size_t bit = 0; bit < (length_of_data - 1) * 8 + used_bits_in_final_byte; ++bit) { + if(!(bit&7)) byte = file_.get8(); + if(!bit) level = byte&0x80; + + if((byte&0x80) != level) { + emplace_back(level ? Tape::Pulse::High : Tape::Pulse::Low, length_per_sample * bits_at_level); + bits_at_level = 0; + level = byte&0x80; + } + bits_at_level++; + } + + current_level_ = !!(level); + emplace_back(level ? Tape::Pulse::High : Tape::Pulse::Low, length_per_sample * bits_at_level); + + post_gap(pause_after_block); +} + void TZX::get_pulse_sequence() { uint8_t number_of_pulses = file_.get8(); while(number_of_pulses--) { @@ -266,6 +327,12 @@ void TZX::get_pause() { } } +void TZX::get_set_signal_level() { + file_.seek(4, SEEK_CUR); + const uint8_t level = file_.get8(); + current_level_ = !!level; +} + void TZX::get_kansas_city_block() { uint32_t block_length = file_.get32le(); @@ -350,7 +417,6 @@ void TZX::post_pulse(const Storage::Time &time) { // MARK: - Flow control; currently ignored void TZX::ignore_group_start() { - printf("Ignoring TZX group\n"); uint8_t length = file_.get8(); file_.seek(length, SEEK_CUR); } @@ -361,13 +427,11 @@ void TZX::ignore_group_end() { void TZX::ignore_jump_to_block() { uint16_t target = file_.get16le(); (void)target; - printf("Ignoring TZX jump\n"); } void TZX::ignore_loop_start() { uint16_t number_of_repetitions = file_.get16le(); (void)number_of_repetitions; - printf("Ignoring TZX loop\n"); } void TZX::ignore_loop_end() { @@ -376,17 +440,25 @@ void TZX::ignore_loop_end() { void TZX::ignore_call_sequence() { uint16_t number_of_entries = file_.get16le(); file_.seek(number_of_entries * sizeof(uint16_t), SEEK_CUR); - printf("Ignoring TZX call sequence\n"); } void TZX::ignore_return_from_sequence() { - printf("Ignoring TZX return from sequence\n"); } void TZX::ignore_select_block() { uint16_t length_of_block = file_.get16le(); file_.seek(length_of_block, SEEK_CUR); - printf("Ignoring TZX select block\n"); +} + +void TZX::ignore_stop_tape_if_in_48kb_mode() { + file_.seek(4, SEEK_CUR); +} + +void TZX::ignore_custom_info_block() { + // TODO: enquire about this; the TZX documentation is ambiguous as to whether this is really 10, or 0x10. + file_.seek(10, SEEK_CUR); + uint32_t length = file_.get32le(); + file_.seek(length, SEEK_CUR); } // MARK: - Messaging @@ -394,7 +466,6 @@ void TZX::ignore_select_block() { void TZX::ignore_text_description() { uint8_t length = file_.get8(); file_.seek(length, SEEK_CUR); - printf("Ignoring TZX text description\n"); } void TZX::ignore_message_block() { @@ -402,12 +473,19 @@ void TZX::ignore_message_block() { uint8_t length = file_.get8(); file_.seek(length, SEEK_CUR); (void)time_for_display; - printf("Ignoring TZX message\n"); +} + +void TZX::ignore_archive_info() { + uint16_t length = file_.get16le(); + file_.seek(length, SEEK_CUR); } void TZX::get_hardware_type() { // TODO: pick a way to retain and communicate this. uint8_t number_of_machines = file_.get8(); file_.seek(number_of_machines * 3, SEEK_CUR); - printf("Ignoring TZX hardware types (%d)\n", number_of_machines); +} + +void TZX::ignore_glue_block() { + file_.seek(9, SEEK_CUR); } diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index 6d061b582..fb459a035 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -44,11 +44,10 @@ class TZX: public PulseQueuedTape { void get_pure_tone_data_block(); void get_pulse_sequence(); void get_pure_data_block(); + void get_direct_recording_block(); + void get_csw_recording_block(); void get_generalised_data_block(); void get_pause(); - void get_kansas_city_block(); - - void get_hardware_type(); void ignore_group_start(); void ignore_group_end(); @@ -58,8 +57,18 @@ class TZX: public PulseQueuedTape { void ignore_call_sequence(); void ignore_return_from_sequence(); void ignore_select_block(); + void ignore_stop_tape_if_in_48kb_mode(); + + void get_set_signal_level(); + void ignore_text_description(); void ignore_message_block(); + void ignore_archive_info(); + void get_hardware_type(); + void ignore_custom_info_block(); + + void get_kansas_city_block(); + void ignore_glue_block(); struct Data { unsigned int length_of_zero_bit_pulse; diff --git a/Storage/Tape/PulseQueuedTape.cpp b/Storage/Tape/PulseQueuedTape.cpp index 4bdb4852b..7a3a343af 100644 --- a/Storage/Tape/PulseQueuedTape.cpp +++ b/Storage/Tape/PulseQueuedTape.cpp @@ -33,6 +33,10 @@ void PulseQueuedTape::emplace_back(Tape::Pulse::Type type, Time length) { queued_pulses_.emplace_back(type, length); } +void PulseQueuedTape::emplace_back(const Tape::Pulse &&pulse) { + queued_pulses_.emplace_back(pulse); +} + Tape::Pulse PulseQueuedTape::silence() { Pulse silence; silence.type = Pulse::Zero; diff --git a/Storage/Tape/PulseQueuedTape.hpp b/Storage/Tape/PulseQueuedTape.hpp index 274695e2c..637af4683 100644 --- a/Storage/Tape/PulseQueuedTape.hpp +++ b/Storage/Tape/PulseQueuedTape.hpp @@ -32,6 +32,7 @@ class PulseQueuedTape: public Tape { protected: void emplace_back(Tape::Pulse::Type type, Time length); + void emplace_back(const Tape::Pulse &&pulse); void clear(); bool empty();