diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 0bf059704..3faa8397c 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -901,8 +901,8 @@ class ConcreteMachine: read_pointers_[3] = roms_[upper_rom_].data(); // Type whatever is required. - if(target.loadingCommand.length()) { - set_typer_for_string(target.loadingCommand.c_str()); + if(target.loading_command.length()) { + type_string(target.loading_command); } insert_media(target.media); @@ -953,9 +953,9 @@ class ConcreteMachine: // MARK: - Keyboard - void set_typer_for_string(const char *string) override final { + void type_string(const std::string &string) override final { std::unique_ptr mapper(new CharacterMapper()); - Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper)); + Utility::TypeRecipient::add_typer(string, std::move(mapper)); } HalfCycles get_typer_delay() override final { diff --git a/Machines/Commodore/Vic-20/Vic20.cpp b/Machines/Commodore/Vic-20/Vic20.cpp index 7555ab1c2..c1b5825e6 100644 --- a/Machines/Commodore/Vic-20/Vic20.cpp +++ b/Machines/Commodore/Vic-20/Vic20.cpp @@ -347,8 +347,8 @@ class ConcreteMachine: } void configure_as_target(const StaticAnalyser::Target &target) override final { - if(target.loadingCommand.length()) { - set_typer_for_string(target.loadingCommand.c_str()); + if(target.loading_command.length()) { + type_string(target.loading_command); } switch(target.vic20.memory_model) { @@ -653,9 +653,9 @@ class ConcreteMachine: m6502_.set_irq_line(keyboard_via_.get_interrupt_line()); } - void set_typer_for_string(const char *string) override final { + void type_string(const std::string &string) override final { std::unique_ptr mapper(new CharacterMapper()); - Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper)); + Utility::TypeRecipient::add_typer(string, std::move(mapper)); } void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) override final { diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp index 7022e7066..738b820d3 100644 --- a/Machines/Electron/Electron.cpp +++ b/Machines/Electron/Electron.cpp @@ -114,8 +114,8 @@ class ConcreteMachine: } void configure_as_target(const StaticAnalyser::Target &target) override final { - if(target.loadingCommand.length()) { - set_typer_for_string(target.loadingCommand.c_str()); + if(target.loading_command.length()) { + type_string(target.loading_command); } if(target.acorn.should_shift_restart) { @@ -414,9 +414,9 @@ class ConcreteMachine: return Cycles(625*128*2); // accept a new character every two frames } - void set_typer_for_string(const char *string) override final { + void type_string(const std::string &string) override final { std::unique_ptr mapper(new CharacterMapper()); - Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper)); + Utility::TypeRecipient::add_typer(string, std::move(mapper)); } KeyboardMapper &get_keyboard_mapper() override { diff --git a/Machines/KeyboardMachine.cpp b/Machines/KeyboardMachine.cpp index c6562bee4..ff21ddf44 100644 --- a/Machines/KeyboardMachine.cpp +++ b/Machines/KeyboardMachine.cpp @@ -27,3 +27,6 @@ void Machine::reset_all_keys(Inputs::Keyboard *keyboard) { Inputs::Keyboard &Machine::get_keyboard() { return keyboard_; } + +void Machine::type_string(const std::string &) { +} diff --git a/Machines/KeyboardMachine.hpp b/Machines/KeyboardMachine.hpp index a9522b2eb..da911412b 100644 --- a/Machines/KeyboardMachine.hpp +++ b/Machines/KeyboardMachine.hpp @@ -10,6 +10,7 @@ #define KeyboardMachine_h #include +#include #include "../Inputs/Keyboard.hpp" @@ -30,6 +31,13 @@ class Machine: public Inputs::Keyboard::Delegate { */ virtual void clear_all_keys() = 0; + /*! + Causes the machine to attempt to type the supplied string. + + This is best effort. Success or failure is permitted to be a function of machine and current state. + */ + virtual void type_string(const std::string &); + /*! Provides a destination for keyboard input. */ diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index b94958040..14ff65f82 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -17,6 +17,7 @@ #include "../../Components/8255/i8255.hpp" #include "../../Components/AY38910/AY38910.hpp" +#include "../../Storage/Tape/Parsers/MSX.hpp" #include "../../Storage/Tape/Tape.hpp" #include "../CRTMachine.hpp" @@ -27,8 +28,16 @@ #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Configurable/StandardOptions.hpp" + namespace MSX { +std::vector> get_options() { + return Configurable::standard_options( + static_cast(Configurable::DisplayRGBComposite | Configurable::QuickLoadTape) + ); +} + /*! Provides a sample source that can programmatically be set to one of two values. */ @@ -96,7 +105,8 @@ class ConcreteMachine: public CPU::Z80::BusHandler, public CRTMachine::Machine, public ConfigurationTarget::Machine, - public KeyboardMachine::Machine { + public KeyboardMachine::Machine, + public Configurable::Device { public: ConcreteMachine(): z80_(*this), @@ -138,6 +148,10 @@ class ConcreteMachine: void configure_as_target(const StaticAnalyser::Target &target) override { insert_media(target.media); + + if(target.loading_command.length()) { + type_string(target.loading_command); + } } bool insert_media(const StaticAnalyser::Media &media) override { @@ -159,6 +173,10 @@ class ConcreteMachine: return true; } + void type_string(const std::string &string) override final { + input_text_ += string; + } + void page_memory(uint8_t value) { for(size_t c = 0; c < 4; ++c) { read_pointers_[c] = memory_slots_[value & 3].read_pointers[c]; @@ -178,6 +196,60 @@ class ConcreteMachine: uint16_t address = cycle.address ? *cycle.address : 0x0000; switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: + if(use_fast_tape_) { + if(address == 0x1a63) { + // TAPION + + // Enable the tape motor. + i8255_.set_register(0xab, 0x8); + + // Disable interrupts. + z80_.set_value_of_register(CPU::Z80::Register::IFF1, 0); + z80_.set_value_of_register(CPU::Z80::Register::IFF2, 0); + + // Use the parser to find a header, and if one is found then populate + // LOWLIM and WINWID, and reset carry. Otherwise set carry. + using Parser = Storage::Tape::MSX::Parser; + std::unique_ptr new_speed = Parser::find_header(tape_player_); + if(new_speed) { + ram_[0xfca4] = new_speed->minimum_start_bit_duration; + ram_[0xfca5] = new_speed->low_high_disrimination_duration; + z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); + } else { + z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + } + + // RET. + *cycle.value = 0xc9; + break; + } + + if(address == 0x1abc) { + // TAPIN + + // Grab the current values of LOWLIM and WINWID. + using Parser = Storage::Tape::MSX::Parser; + Parser::FileSpeed tape_speed; + tape_speed.minimum_start_bit_duration = ram_[0xfca4]; + tape_speed.low_high_disrimination_duration = ram_[0xfca5]; + + // Ask the tape parser to grab a byte. + int next_byte = Parser::get_byte(tape_speed, tape_player_); + + // If a byte was found, return it with carry unset. Otherwise set carry to + // indicate error. + if(next_byte >= 0) { + z80_.set_value_of_register(CPU::Z80::Register::A, static_cast(next_byte)); + z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); + } else { + z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + } + + // RET. + *cycle.value = 0xc9; + break; + } + } case CPU::Z80::PartialMachineCycle::Read: *cycle.value = read_pointers_[address >> 14][address & 16383]; break; @@ -243,6 +315,32 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Interrupt: *cycle.value = 0xff; + + // Take this as a convenient moment to jump into the keyboard buffer, if desired. + if(!input_text_.empty()) { + // TODO: is it safe to assume these addresses? + const int buffer_start = 0xfbf0; + const int buffer_end = 0xfb18; + + int read_address = ram_[0xf3fa] | (ram_[0xf3fb] << 8); + int write_address = ram_[0xf3f8] | (ram_[0xf3f9] << 8); + + const int buffer_size = buffer_end - buffer_start; + int available_space = write_address + buffer_size - read_address - 1; + + const std::size_t characters_to_write = std::min(static_cast(available_space), input_text_.size()); + write_address -= buffer_start; + for(std::size_t c = 0; c < characters_to_write; ++c) { + char character = input_text_[c]; + ram_[write_address + buffer_start] = static_cast(character); + write_address = (write_address + 1) % buffer_size; + } + write_address += buffer_start; + input_text_.erase(input_text_.begin(), input_text_.begin() + static_cast(characters_to_write)); + + ram_[0xf3f8] = static_cast(write_address); + ram_[0xf3f9] = static_cast(write_address >> 8); + } break; default: break; @@ -321,6 +419,37 @@ class ConcreteMachine: return keyboard_mapper_; } + // MARK: - Configuration options. + std::vector> get_options() override { + return MSX::get_options(); + } + + void set_selections(const Configurable::SelectionSet &selections_by_option) override { + bool quickload; + if(Configurable::get_quick_load_tape(selections_by_option, quickload)) { + use_fast_tape_ = quickload; + } + + Configurable::Display display; + if(Configurable::get_display(selections_by_option, display)) { + get_crt()->set_output_device((display == Configurable::Display::RGB) ? Outputs::CRT::OutputDevice::Monitor : Outputs::CRT::OutputDevice::Television); + } + } + + Configurable::SelectionSet get_accurate_selections() override { + Configurable::SelectionSet selection_set; + Configurable::append_quick_load_tape_selection(selection_set, false); + Configurable::append_display_selection(selection_set, Configurable::Display::Composite); + return selection_set; + } + + Configurable::SelectionSet get_user_friendly_selections() override { + Configurable::SelectionSet selection_set; + Configurable::append_quick_load_tape_selection(selection_set, true); + Configurable::append_display_selection(selection_set, Configurable::Display::RGB); + return selection_set; + } + private: void update_audio() { speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2))); @@ -380,6 +509,7 @@ class ConcreteMachine: Outputs::Speaker::LowpassSpeaker> speaker_; Storage::Tape::BinaryTapePlayer tape_player_; + bool use_fast_tape_ = false; i8255PortHandler i8255_port_handler_; AYPortHandler ay_port_handler_; @@ -404,6 +534,7 @@ class ConcreteMachine: uint8_t key_states_[16]; int selected_key_line_ = 0; + std::string input_text_; MSX::KeyboardMapper keyboard_mapper_; }; diff --git a/Machines/MSX/MSX.hpp b/Machines/MSX/MSX.hpp index 103d635f8..f52c4befc 100644 --- a/Machines/MSX/MSX.hpp +++ b/Machines/MSX/MSX.hpp @@ -9,6 +9,8 @@ #ifndef MSX_hpp #define MSX_hpp +#include "../../Configurable/Configurable.hpp" + namespace MSX { class Machine { @@ -17,6 +19,8 @@ class Machine { static Machine *MSX(); }; +std::vector> get_options(); + } #endif /* MSX_hpp */ diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp index 03f2c48d6..63e7abb5c 100644 --- a/Machines/Oric/Oric.cpp +++ b/Machines/Oric/Oric.cpp @@ -261,8 +261,8 @@ class ConcreteMachine: microdisc_.set_delegate(this); } - if(target.loadingCommand.length()) { - set_typer_for_string(target.loadingCommand.c_str()); + if(target.loading_command.length()) { + type_string(target.loading_command); } if(target.oric.use_atmos_rom) { @@ -407,9 +407,9 @@ class ConcreteMachine: } // for Utility::TypeRecipient::Delegate - void set_typer_for_string(const char *string) override final { + void type_string(const std::string &string) override final { std::unique_ptr mapper(new CharacterMapper); - Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper)); + Utility::TypeRecipient::add_typer(string, std::move(mapper)); } // for Microdisc::Delegate diff --git a/Machines/Utility/MachineForTarget.cpp b/Machines/Utility/MachineForTarget.cpp index 5783317a0..730284163 100644 --- a/Machines/Utility/MachineForTarget.cpp +++ b/Machines/Utility/MachineForTarget.cpp @@ -64,6 +64,7 @@ std::map>> Machin std::map>> options; options.emplace(std::make_pair(LongNameForTargetMachine(StaticAnalyser::Target::Electron), Electron::get_options())); + options.emplace(std::make_pair(LongNameForTargetMachine(StaticAnalyser::Target::MSX), MSX::get_options())); options.emplace(std::make_pair(LongNameForTargetMachine(StaticAnalyser::Target::Oric), Oric::get_options())); options.emplace(std::make_pair(LongNameForTargetMachine(StaticAnalyser::Target::Vic20), Commodore::Vic20::get_options())); options.emplace(std::make_pair(LongNameForTargetMachine(StaticAnalyser::Target::ZX8081), ZX8081::get_options())); diff --git a/Machines/Utility/Typer.cpp b/Machines/Utility/Typer.cpp index 4160af136..09aa5585c 100644 --- a/Machines/Utility/Typer.cpp +++ b/Machines/Utility/Typer.cpp @@ -8,23 +8,22 @@ #include "Typer.hpp" -#include -#include +#include using namespace Utility; -Typer::Typer(const char *string, HalfCycles delay, HalfCycles frequency, std::unique_ptr character_mapper, Delegate *delegate) : +Typer::Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, std::unique_ptr character_mapper, Delegate *delegate) : frequency_(frequency), counter_(-delay), delegate_(delegate), character_mapper_(std::move(character_mapper)) { - std::size_t string_size = std::strlen(string) + 3; - string_ = (char *)std::malloc(string_size); - snprintf(string_, string_size, "%c%s%c", Typer::BeginString, string, Typer::EndString); + std::ostringstream string_stream; + string_stream << Typer::BeginString << string << Typer::EndString; + string_ = string_stream.str(); } void Typer::run_for(const HalfCycles duration) { - if(string_) { + if(string_pointer_ < string_.size()) { if(counter_ < 0 && counter_ + duration >= 0) { if(!type_next_character()) { delegate_->typer_reset(this); @@ -32,7 +31,7 @@ void Typer::run_for(const HalfCycles duration) { } counter_ += duration; - while(string_ && counter_ > frequency_) { + while(string_pointer_ < string_.size() && counter_ > frequency_) { counter_ -= frequency_; if(!type_next_character()) { delegate_->typer_reset(this); @@ -58,16 +57,10 @@ bool Typer::try_type_next_character() { } bool Typer::type_next_character() { - if(string_ == nullptr) return false; + if(string_pointer_ == string_.size()) return false; if(!try_type_next_character()) { phase_ = 0; - if(!string_[string_pointer_]) { - std::free(string_); - string_ = nullptr; - return false; - } - string_pointer_++; } else { phase_++; @@ -76,10 +69,6 @@ bool Typer::type_next_character() { return true; } -Typer::~Typer() { - std::free(string_); -} - // MARK: - Character mapper uint16_t *CharacterMapper::table_lookup_sequence_for_character(KeySequence *sequences, std::size_t length, char character) { diff --git a/Machines/Utility/Typer.hpp b/Machines/Utility/Typer.hpp index 78f3e1668..cbdca2421 100644 --- a/Machines/Utility/Typer.hpp +++ b/Machines/Utility/Typer.hpp @@ -10,6 +10,8 @@ #define Typer_hpp #include +#include + #include "../KeyboardMachine.hpp" #include "../../ClockReceiver/ClockReceiver.hpp" @@ -50,8 +52,7 @@ class Typer { virtual void typer_reset(Typer *typer) = 0; }; - Typer(const char *string, HalfCycles delay, HalfCycles frequency, std::unique_ptr character_mapper, Delegate *delegate); - ~Typer(); + Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, std::unique_ptr character_mapper, Delegate *delegate); void run_for(const HalfCycles duration); bool type_next_character(); @@ -62,7 +63,7 @@ class Typer { const char EndString = 0x03; // i.e. ASCII end of text private: - char *string_; + std::string string_; std::size_t string_pointer_ = 0; HalfCycles frequency_; @@ -82,16 +83,10 @@ class Typer { class TypeRecipient: public Typer::Delegate { public: /// Attaches a typer to this class that will type @c string using @c character_mapper as a source. - void set_typer_for_string(const char *string, std::unique_ptr character_mapper) { + void add_typer(const std::string &string, std::unique_ptr character_mapper) { typer_.reset(new Typer(string, get_typer_delay(), get_typer_frequency(), std::move(character_mapper), this)); } - /*! - Provided as a hook for subclasses to implement so that external callers can install a typer - without needing inside knowledge as to where the character mapper comes from. - */ - virtual void set_typer_for_string(const char *string) = 0; - /*! Provided in order to conform to that part of the Typer::Delegate interface that goes above and beyond KeyboardMachine::Machine; responds to the end of typing by clearing all keys. diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp index 8f8674c43..3061726af 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/ZX8081/ZX8081.cpp @@ -278,8 +278,8 @@ template class ConcreteMachine: } Memory::Fuzz(ram_); - if(target.loadingCommand.length()) { - set_typer_for_string(target.loadingCommand.c_str()); + if(target.loading_command.length()) { + type_string(target.loading_command); } insert_media(target.media); @@ -293,9 +293,9 @@ template class ConcreteMachine: return !media.tapes.empty(); } - void set_typer_for_string(const char *string) override final { + void type_string(const std::string &string) override final { std::unique_ptr mapper(new CharacterMapper(is_zx81_)); - Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper)); + Utility::TypeRecipient::add_typer(string, std::move(mapper)); } // Obtains the system ROMs. diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 13aeec8d4..d8ccf7d1f 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ 4B0E04F21FC9EAA800F43484 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04EC1FC9E88300F43484 /* StaticAnalyser.cpp */; }; 4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; 4B0E04FB1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; + 4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E61051FF34737002A9DBD /* MSX.cpp */; }; 4B121F951E05E66800BFDA12 /* PCMPatchedTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */; }; 4B121F9B1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */; }; 4B12C0ED1FCFA98D005BFD93 /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B12C0EB1FCFA98D005BFD93 /* Keyboard.cpp */; }; @@ -223,6 +224,8 @@ 4B5FADC01DE3BF2B00AEC565 /* Microdisc.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */; }; 4B643F3A1D77AD1900D431D6 /* CSStaticAnalyser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B643F391D77AD1900D431D6 /* CSStaticAnalyser.mm */; }; 4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B643F3E1D77B88000D431D6 /* DocumentController.swift */; }; + 4B651F9E1FF1B04100E18D9A /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B651F9C1FF1B04100E18D9A /* Tape.cpp */; }; + 4B651F9F1FF1B2AE00E18D9A /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B651F9C1FF1B04100E18D9A /* Tape.cpp */; }; 4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */; }; 4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */; }; 4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B69FB451C4D950F00B5F0AA /* libz.tbd */; }; @@ -269,6 +272,7 @@ 4BA22B071D8817CE0008C640 /* Disk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BA22B051D8817CE0008C640 /* Disk.cpp */; }; 4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BA61EAF1D91515900B3C876 /* NSData+StdVector.mm */; }; 4BA799951D8B656E0045123D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BA799931D8B656E0045123D /* StaticAnalyser.cpp */; }; + 4BAD13441FF709C700FD114A /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E61051FF34737002A9DBD /* MSX.cpp */; }; 4BB17D4E1ED7909F00ABD1E1 /* tests.expected.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BB17D4C1ED7909F00ABD1E1 /* tests.expected.json */; }; 4BB17D4F1ED7909F00ABD1E1 /* tests.in.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BB17D4D1ED7909F00ABD1E1 /* tests.in.json */; }; 4BB298F11B587D8400A49093 /* start in Resources */ = {isa = PBXBuildFile; fileRef = 4BB297E51B587D8300A49093 /* start */; }; @@ -647,6 +651,8 @@ 4B0E04ED1FC9E88300F43484 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = StaticAnalyser.hpp; path = ../../StaticAnalyser/MSX/StaticAnalyser.hpp; sourceTree = ""; }; 4B0E04F81FC9FA3000F43484 /* 9918.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 9918.hpp; path = 9918/9918.hpp; sourceTree = ""; }; 4B0E04F91FC9FA3100F43484 /* 9918.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = 9918.cpp; path = 9918/9918.cpp; sourceTree = ""; }; + 4B0E61051FF34737002A9DBD /* MSX.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MSX.cpp; path = Parsers/MSX.cpp; sourceTree = ""; }; + 4B0E61061FF34737002A9DBD /* MSX.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = MSX.hpp; path = Parsers/MSX.hpp; sourceTree = ""; }; 4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMPatchedTrackTests.mm; sourceTree = ""; }; 4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMSegmentEventSourceTests.mm; sourceTree = ""; }; 4B12C0EB1FCFA98D005BFD93 /* Keyboard.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = MSX/Keyboard.cpp; sourceTree = ""; }; @@ -817,6 +823,8 @@ 4B643F391D77AD1900D431D6 /* CSStaticAnalyser.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSStaticAnalyser.mm; path = StaticAnalyser/CSStaticAnalyser.mm; sourceTree = ""; }; 4B643F3C1D77AE5C00D431D6 /* CSMachine+Target.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSMachine+Target.h"; sourceTree = ""; }; 4B643F3E1D77B88000D431D6 /* DocumentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentController.swift; sourceTree = ""; }; + 4B651F9C1FF1B04100E18D9A /* Tape.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Tape.cpp; path = ../../StaticAnalyser/MSX/Tape.cpp; sourceTree = ""; }; + 4B651F9D1FF1B04100E18D9A /* Tape.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Tape.hpp; path = ../../StaticAnalyser/MSX/Tape.hpp; sourceTree = ""; }; 4B698D1A1FE768A100696C91 /* SampleSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SampleSource.hpp; sourceTree = ""; }; 4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Tape.cpp; sourceTree = ""; }; 4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tape.hpp; sourceTree = ""; }; @@ -1377,6 +1385,8 @@ children = ( 4B0E04EC1FC9E88300F43484 /* StaticAnalyser.cpp */, 4B0E04ED1FC9E88300F43484 /* StaticAnalyser.hpp */, + 4B651F9C1FF1B04100E18D9A /* Tape.cpp */, + 4B651F9D1FF1B04100E18D9A /* Tape.hpp */, ); name = MSX; sourceTree = ""; @@ -1995,10 +2005,12 @@ children = ( 4B8805EE1DCFC99C003085B1 /* Acorn.cpp */, 4B8805F21DCFD22A003085B1 /* Commodore.cpp */, + 4B0E61051FF34737002A9DBD /* MSX.cpp */, 4B8805F91DCFF807003085B1 /* Oric.cpp */, 4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */, 4B8805EF1DCFC99C003085B1 /* Acorn.hpp */, 4B8805F31DCFD22A003085B1 /* Commodore.hpp */, + 4B0E61061FF34737002A9DBD /* MSX.hpp */, 4B8805FA1DCFF807003085B1 /* Oric.hpp */, 4B4518A71F76004200926311 /* TapeParser.hpp */, 4BBFBB6B1EE8401E00C01E7A /* ZX8081.hpp */, @@ -3304,6 +3316,7 @@ 4B055A891FAE85580060FFFF /* StaticAnalyser.cpp in Sources */, 4B055ACE1FAE9B030060FFFF /* Plus3.cpp in Sources */, 4B055A8D1FAE85920060FFFF /* AsyncTaskQueue.cpp in Sources */, + 4BAD13441FF709C700FD114A /* MSX.cpp in Sources */, 4B055AC41FAE9AE80060FFFF /* Keyboard.cpp in Sources */, 4B055A941FAE85B50060FFFF /* CommodoreROM.cpp in Sources */, 4B055A971FAE85BB0060FFFF /* ZX8081.cpp in Sources */, @@ -3339,6 +3352,7 @@ 4B055AB91FAE86170060FFFF /* Acorn.cpp in Sources */, 4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */, 4B055AD61FAE9B130060FFFF /* MemoryFuzzer.cpp in Sources */, + 4B651F9F1FF1B2AE00E18D9A /* Tape.cpp in Sources */, 4B055AC21FAE9AE30060FFFF /* KeyboardMachine.cpp in Sources */, 4B055AD91FAE9B180060FFFF /* ZX8081.cpp in Sources */, 4B055AEB1FAE9BA20060FFFF /* PartialMachineCycle.cpp in Sources */, @@ -3358,6 +3372,7 @@ 4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */, 4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */, 4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */, + 4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */, 4BD14B111D74627C0088EAD6 /* StaticAnalyser.cpp in Sources */, 4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */, 4B4518A01F75FD1C00926311 /* CPCDSK.cpp in Sources */, @@ -3413,6 +3428,7 @@ 4B54C0C51F8D91D90050900F /* Keyboard.cpp in Sources */, 4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */, 4B86E25B1F8C628F006FAA45 /* Keyboard.cpp in Sources */, + 4B651F9E1FF1B04100E18D9A /* Tape.cpp in Sources */, 4B4518851F75E91A00926311 /* DiskController.cpp in Sources */, 4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */, 4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 9d8d25de5..d44d54994 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -188,9 +188,9 @@ struct MachineDelegate: CRTMachine::Machine::Delegate, public LockProtectedDeleg } - (void)paste:(NSString *)paste { - Utility::TypeRecipient *typeRecipient = _machine->type_recipient(); - if(typeRecipient) - typeRecipient->set_typer_for_string([paste UTF8String]); + KeyboardMachine::Machine *keyboardMachine = _machine->type_recipient(); + if(keyboardMachine) + keyboardMachine->type_string([paste UTF8String]); } - (void)applyTarget:(const StaticAnalyser::Target &)target { diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index 4704b8106..abcb46e77 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -439,9 +439,9 @@ int main(int argc, char *argv[]) { case SDL_KEYDOWN: // Syphon off the key-press if it's control+shift+V (paste). if(event.key.keysym.sym == SDLK_v && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) { - Utility::TypeRecipient *type_recipient = machine->type_recipient(); - if(type_recipient) { - type_recipient->set_typer_for_string(SDL_GetClipboardText()); + KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine(); + if(keyboard_machine) { + keyboard_machine->type_string(SDL_GetClipboardText()); break; } } diff --git a/StaticAnalyser/Acorn/StaticAnalyser.cpp b/StaticAnalyser/Acorn/StaticAnalyser.cpp index 8e0cda62d..588ffff42 100644 --- a/StaticAnalyser/Acorn/StaticAnalyser.cpp +++ b/StaticAnalyser/Acorn/StaticAnalyser.cpp @@ -96,7 +96,7 @@ void StaticAnalyser::Acorn::AddTargets(const Media &media, std::list &de // Inspect first file. If it's protected or doesn't look like BASIC // then the loading command is *RUN. Otherwise it's CHAIN"". - target.loadingCommand = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; + target.loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n"; target.media.tapes = media.tapes; } @@ -116,7 +116,7 @@ void StaticAnalyser::Acorn::AddTargets(const Media &media, std::list &de if(bootOption != Catalogue::BootOption::None) target.acorn.should_shift_restart = true; else - target.loadingCommand = "*CAT\n"; + target.loading_command = "*CAT\n"; } } diff --git a/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp index 07a3ee576..ea2f5c1fd 100644 --- a/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp +++ b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp @@ -95,7 +95,7 @@ static void InspectCatalogue( // If there's just one file, run that. if(candidate_files.size() == 1) { - target.loadingCommand = RunCommandFor(*candidate_files[0]); + target.loading_command = RunCommandFor(*candidate_files[0]); return; } @@ -126,7 +126,7 @@ static void InspectCatalogue( } if(basic_files == 1 || implicit_suffixed_files == 1) { std::size_t selected_file = (basic_files == 1) ? last_basic_file : last_implicit_suffixed_file; - target.loadingCommand = RunCommandFor(*candidate_files[selected_file]); + target.loading_command = RunCommandFor(*candidate_files[selected_file]); return; } @@ -143,14 +143,14 @@ static void InspectCatalogue( if(name_counts.size() == 2) { for(auto &pair : name_counts) { if(pair.second == 1) { - target.loadingCommand = RunCommandFor(*candidate_files[indices_by_name[pair.first]]); + target.loading_command = RunCommandFor(*candidate_files[indices_by_name[pair.first]]); return; } } } // Desperation. - target.loadingCommand = "cat\n"; + target.loading_command = "cat\n"; } static bool CheckBootSector(const std::shared_ptr &disk, StaticAnalyser::Target &target) { @@ -169,7 +169,7 @@ static bool CheckBootSector(const std::shared_ptr &disk, St // This is a system disk, then launch it as though it were CP/M. if(!matched) { - target.loadingCommand = "|cpm\n"; + target.loading_command = "|cpm\n"; return true; } } @@ -191,7 +191,7 @@ void StaticAnalyser::AmstradCPC::AddTargets(const Media &media, std::list string_stream << "1"; } string_stream << "\nRUN\n"; - target.loadingCommand = string_stream.str(); + target.loading_command = string_stream.str(); // make a first guess based on loading address switch(files.front().starting_address) { diff --git a/StaticAnalyser/MSX/StaticAnalyser.cpp b/StaticAnalyser/MSX/StaticAnalyser.cpp index a04756523..3d4d340a0 100644 --- a/StaticAnalyser/MSX/StaticAnalyser.cpp +++ b/StaticAnalyser/MSX/StaticAnalyser.cpp @@ -8,13 +8,17 @@ #include "StaticAnalyser.hpp" +#include "Tape.hpp" + /* -DEFB "AB" ; expansion ROM header -DEFW initcode ; start of the init code, 0 if no initcode -DEFW callstat; pointer to CALL statement handler, 0 if no such handler -DEFW device; pointer to expansion device handler, 0 if no such handler -DEFW basic ; pointer to the start of a tokenized basicprogram, 0 if no basicprogram -DEFS 6,0 ; room reserved for future extensions + Expected standard cartridge format: + + DEFB "AB" ; expansion ROM header + DEFW initcode ; start of the init code, 0 if no initcode + DEFW callstat; pointer to CALL statement handler, 0 if no such handler + DEFW device; pointer to expansion device handler, 0 if no such handler + DEFW basic ; pointer to the start of a tokenized basicprogram, 0 if no basicprogram + DEFS 6,0 ; room reserved for future extensions */ static std::list> MSXCartridgesFrom(const std::list> &cartridges) { @@ -29,31 +33,17 @@ static std::list> // Which must be a multiple of 16 kb in size. Storage::Cartridge::Cartridge::Segment segment = segments.front(); const size_t data_size = segment.data.size(); - if(data_size < 0x4000 || data_size & 0x3fff) continue; + if(data_size < 0x2000 || data_size & 0x3fff) continue; // Check for a ROM header at address 0; TODO: if it's not found then try 0x4000 // and consider swapping the image. // Check for the expansion ROM header and the reserved bytes. if(segment.data[0] != 0x41 || segment.data[1] != 0x42) continue; - bool all_zeroes = true; - for(size_t c = 0; c < 6; ++c) { - if(segment.data[10 + c] != 0) all_zeroes = false; - } - if(!all_zeroes) continue; - // Pick a paging address based on the four pointers. - uint16_t start_address = 0xc000; - for(size_t c = 0; c < 8; c += 2) { - uint16_t code_pointer = static_cast(segment.data[2 + c] | segment.data[3 + c] << 8); - if(code_pointer) { - start_address = std::min(static_cast(code_pointer &~ 0x3fff), start_address); - } - } - - // That'll do then, but apply the detected start address. + // Apply the standard MSX start address. msx_cartridges.emplace_back(new Storage::Cartridge::Cartridge({ - Storage::Cartridge::Cartridge::Segment(start_address, segment.data) + Storage::Cartridge::Cartridge::Segment(0x4000, segment.data) })); } @@ -63,10 +53,22 @@ static std::list> void StaticAnalyser::MSX::AddTargets(const Media &media, std::list &destination) { Target target; + // Obtain only those cartridges which it looks like an MSX would understand. target.media.cartridges = MSXCartridgesFrom(media.cartridges); - // TODO: tape parsing. Be dumb for now. - target.media.tapes = media.tapes; + // Check tapes for loadable files. + for(const auto &tape : media.tapes) { + std::vector files_on_tape = GetFiles(tape); + if(!files_on_tape.empty()) { + switch(files_on_tape.front().type) { + case File::Type::ASCII: target.loading_command = "RUN\"CAS:\r"; break; + case File::Type::TokenisedBASIC: target.loading_command = "CLOAD\rRUN\r"; break; + case File::Type::Binary: target.loading_command = "BLOAD\"CAS:\",R\r"; break; + default: break; + } + target.media.tapes.push_back(tape); + } + } if(!target.media.empty()) { target.machine = Target::MSX; diff --git a/StaticAnalyser/MSX/Tape.cpp b/StaticAnalyser/MSX/Tape.cpp new file mode 100644 index 000000000..5cfb4bc20 --- /dev/null +++ b/StaticAnalyser/MSX/Tape.cpp @@ -0,0 +1,163 @@ +// +// Tape.cpp +// Clock Signal +// +// Created by Thomas Harte on 25/12/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "Tape.hpp" + +#include "../../Storage/Tape/Parsers/MSX.hpp" + +using namespace StaticAnalyser::MSX; + +File::File(File &&rhs) : + name(std::move(rhs.name)), + type(rhs.type), + data(std::move(rhs.data)), + starting_address(rhs.starting_address), + entry_address(rhs.entry_address) {} + +File::File() : + type(Type::Binary), + starting_address(0), + entry_address(0) {} // For the sake of initialising in a defined state. + +std::vector StaticAnalyser::MSX::GetFiles(const std::shared_ptr &tape) { + std::vector files; + + Storage::Tape::BinaryTapePlayer tape_player(1000000); + tape_player.set_motor_control(true); + tape_player.set_tape(tape); + + using Parser = Storage::Tape::MSX::Parser; + + // Get all recognisable files from the tape. + while(!tape->is_at_end()) { + // Try to locate and measure a header. + std::unique_ptr file_speed = Parser::find_header(tape_player); + if(!file_speed) continue; + + // Check whether what follows is a recognisable file type. + uint8_t header[10] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + for(std::size_t c = 0; c < sizeof(header); ++c) { + int next_byte = Parser::get_byte(*file_speed, tape_player); + if(next_byte == -1) break; + header[c] = static_cast(next_byte); + } + + bool bytes_are_same = true; + for(std::size_t c = 1; c < sizeof(header); ++c) + bytes_are_same &= (header[c] == header[0]); + + if(!bytes_are_same) continue; + if(header[0] != 0xd0 && header[0] != 0xd3 && header[0] != 0xea) continue; + + File file; + + // Determine file type from information already collected. + switch(header[0]) { + case 0xd0: file.type = File::Type::Binary; break; + case 0xd3: file.type = File::Type::TokenisedBASIC; break; + case 0xea: file.type = File::Type::ASCII; break; + default: break; // Unreachable. + } + + // Read file name. + char name[7]; + for(std::size_t c = 1; c < 6; ++c) + name[c] = static_cast(Parser::get_byte(*file_speed, tape_player)); + name[6] = '\0'; + file.name = name; + + // ASCII: Read 256-byte segments until one ends with an end-of-file character. + if(file.type == File::Type::ASCII) { + while(true) { + file_speed = Parser::find_header(tape_player); + if(!file_speed) break; + int c = 256; + while(c--) { + int byte = Parser::get_byte(*file_speed, tape_player); + if(byte == -1) break; + file.data.push_back(static_cast(byte)); + } + if(c != -1) break; + if(file.data.back() == 0x1a) { + files.push_back(std::move(file)); + break; + } + } + continue; + } + + // Read a single additional segment, using the information at the begging to determine length. + file_speed = Parser::find_header(tape_player); + if(!file_speed) continue; + + // Binary: read start address, end address, entry address, then that many bytes. + if(file.type == File::Type::Binary) { + uint8_t locations[6]; + uint16_t end_address; + std::size_t c; + for(c = 0; c < sizeof(locations); ++c) { + int byte = Parser::get_byte(*file_speed, tape_player); + if(byte == -1) break; + locations[c] = static_cast(byte); + } + if(c != sizeof(locations)) continue; + + file.starting_address = static_cast(locations[0] | (locations[1] << 8)); + end_address = static_cast(locations[2] | (locations[3] << 8)); + file.entry_address = static_cast(locations[4] | (locations[5] << 8)); + + if(end_address < file.starting_address) continue; + + std::size_t length = end_address - file.starting_address; + while(length--) { + int byte = Parser::get_byte(*file_speed, tape_player); + if(byte == -1) continue; + file.data.push_back(static_cast(byte)); + } + + files.push_back(std::move(file)); + continue; + } + + // Tokenised BASIC, then: keep following 'next line' links from a hypothetical start of + // 0x8001, until finding the final line. + uint16_t current_address = 0x8001; + while(current_address) { + int next_address_buffer[2]; + next_address_buffer[0] = Parser::get_byte(*file_speed, tape_player); + next_address_buffer[1] = Parser::get_byte(*file_speed, tape_player); + + if(next_address_buffer[0] == -1 || next_address_buffer[1] == -1) break; + file.data.push_back(static_cast(next_address_buffer[0])); + file.data.push_back(static_cast(next_address_buffer[1])); + + uint16_t next_address = static_cast(next_address_buffer[0] | (next_address_buffer[1] << 8)); + if(!next_address) { + files.push_back(std::move(file)); + break; + } + if(next_address < current_address+2) break; + + // This line makes sense, so push it all in. + std::size_t length = next_address - current_address - 2; + current_address = next_address; + bool found_error = false; + while(length--) { + int byte = Parser::get_byte(*file_speed, tape_player); + if(byte == -1) { + found_error = true; + break; + } + file.data.push_back(static_cast(byte)); + } + if(found_error) break; + } + } + + return files; +} diff --git a/StaticAnalyser/MSX/Tape.hpp b/StaticAnalyser/MSX/Tape.hpp new file mode 100644 index 000000000..c7150852b --- /dev/null +++ b/StaticAnalyser/MSX/Tape.hpp @@ -0,0 +1,42 @@ +// +// Tape.hpp +// Clock Signal +// +// Created by Thomas Harte on 25/12/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef StaticAnalyser_MSX_Tape_hpp +#define StaticAnalyser_MSX_Tape_hpp + +#include "../../Storage/Tape/Tape.hpp" + +#include +#include + +namespace StaticAnalyser { +namespace MSX { + +struct File { + std::string name; + enum Type { + Binary, + TokenisedBASIC, + ASCII + } type; + + std::vector data; + + uint16_t starting_address; // Provided only for Type::Binary files. + uint16_t entry_address; // Provided only for Type::Binary files. + + File(File &&rhs); + File(); +}; + +std::vector GetFiles(const std::shared_ptr &tape); + +} +} + +#endif /* StaticAnalyser_MSX_Tape_hpp */ diff --git a/StaticAnalyser/Oric/StaticAnalyser.cpp b/StaticAnalyser/Oric/StaticAnalyser.cpp index 55b84b267..cfc8b22ac 100644 --- a/StaticAnalyser/Oric/StaticAnalyser.cpp +++ b/StaticAnalyser/Oric/StaticAnalyser.cpp @@ -97,7 +97,7 @@ void StaticAnalyser::Oric::AddTargets(const Media &media, std::list &des } target.media.tapes.push_back(tape); - target.loadingCommand = "CLOAD\"\"\n"; + target.loading_command = "CLOAD\"\"\n"; } } diff --git a/StaticAnalyser/StaticAnalyser.hpp b/StaticAnalyser/StaticAnalyser.hpp index 84f92c0b8..227d982bf 100644 --- a/StaticAnalyser/StaticAnalyser.hpp +++ b/StaticAnalyser/StaticAnalyser.hpp @@ -115,7 +115,7 @@ struct Target { } amstradcpc; }; - std::string loadingCommand; + std::string loading_command; Media media; }; diff --git a/StaticAnalyser/ZX8081/StaticAnalyser.cpp b/StaticAnalyser/ZX8081/StaticAnalyser.cpp index 8a15493f1..02fe4c37f 100644 --- a/StaticAnalyser/ZX8081/StaticAnalyser.cpp +++ b/StaticAnalyser/ZX8081/StaticAnalyser.cpp @@ -56,9 +56,9 @@ void StaticAnalyser::ZX8081::AddTargets(const Media &media, std::list &d // TODO: how to run software once loaded? Might require a BASIC detokeniser. if(target.zx8081.isZX81) { - target.loadingCommand = "J\"\"\n"; + target.loading_command = "J\"\"\n"; } else { - target.loadingCommand = "W\n"; + target.loading_command = "W\n"; } destination.push_back(target); diff --git a/Storage/Tape/Formats/CAS.cpp b/Storage/Tape/Formats/CAS.cpp index 6ea0b037a..cc6872acb 100644 --- a/Storage/Tape/Formats/CAS.cpp +++ b/Storage/Tape/Formats/CAS.cpp @@ -19,67 +19,41 @@ namespace { CAS::CAS(const char *file_name) { Storage::FileHolder file(file_name); - uint8_t lookahead[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + uint8_t lookahead[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - // Get the first header. - get_next(file, lookahead, 8); + // 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; - 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; + // Create a new chunk + chunks_.emplace_back(); + Chunk &chunk = chunks_.back(); - // This implies something has gone wrong with parsing. - default: throw ErrorNotCAS; - } + // 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]); - // Set the type and feed in the initial data. - files_.emplace_back(); - active_file = &files_.back(); - active_file->type = type; - } + chunk.long_header = bytes_are_equal && ((lookahead[0] == 0xd3) || (lookahead[0] == 0xd0) || (lookahead[0] == 0xea)); + chunk.has_gap = chunk.long_header && (chunks_.size() > 1); - // Add a new chunk for the new incoming data. - active_file->chunks.emplace_back(); - - // 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]); + // 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); } - // If the file ended, flush the lookahead. + // 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(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 - // 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; + for(std::size_t index = 0; index < sizeof(lookahead) - 1; index++) + chunk.data.push_back(lookahead[index]); } } } @@ -88,14 +62,14 @@ 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); +void CAS::get_next(Storage::FileHolder &file, uint8_t (&buffer)[10], std::size_t quantity) { + assert(quantity <= sizeof(buffer)); - if(quantity < 8) - std::memmove(buffer, &buffer[quantity], 8 - quantity); + if(quantity < sizeof(buffer)) + std::memmove(buffer, &buffer[quantity], sizeof(buffer) - quantity); while(quantity--) { - buffer[7 - quantity] = file.get8(); + buffer[sizeof(buffer) - 1 - quantity] = file.get8(); } } @@ -105,7 +79,6 @@ bool CAS::is_at_end() { void CAS::virtual_reset() { phase_ = Phase::Header; - file_pointer_ = 0; chunk_pointer_ = 0; distance_into_phase_ = 0; distance_into_bit_ = 0; @@ -126,7 +99,6 @@ Tape::Pulse CAS::virtual_get_next_pulse() { if(phase_ == Phase::Gap) { phase_ = Phase::Header; - chunk_pointer_ = 0; distance_into_phase_ = 0; } @@ -149,7 +121,7 @@ Tape::Pulse CAS::virtual_get_next_pulse() { // 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)) { + if(distance_into_phase_ == (chunks_[chunk_pointer_].long_header ? 31744 : 7936)) { phase_ = Phase::Bytes; distance_into_phase_ = 0; distance_into_bit_ = 0; @@ -159,7 +131,7 @@ Tape::Pulse CAS::virtual_get_next_pulse() { case Phase::Bytes: { // 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]; + uint8_t byte_value = chunks_[chunk_pointer_].data[distance_into_phase_ / 11]; int bit_offset = distance_into_phase_ % 11; switch(bit_offset) { case 0: bit = 0; break; @@ -168,28 +140,20 @@ 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. - // + // If bit is finished, and if all bytes in chunk have been posted then: + // - if this is the final chunk then note end of file. + // - otherwise, roll onto the next header or gap, depending on whether the next chunk has a gap. 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) { + if(distance_into_phase_ == chunks_[chunk_pointer_].data.size() * 11) { distance_into_phase_ = 0; chunk_pointer_++; - if(chunk_pointer_ == files_[file_pointer_].chunks.size()) { - chunk_pointer_ = 0; - file_pointer_++; - phase_ = (file_pointer_ == files_.size()) ? Phase::EndOfFile : Phase::Gap; + if(chunk_pointer_ == chunks_.size()) { + phase_ = Phase::EndOfFile; } else { - phase_ = Phase::Header; + phase_ = chunks_[chunk_pointer_].has_gap ? Phase::Gap : Phase::Header; } } } diff --git a/Storage/Tape/Formats/CAS.hpp b/Storage/Tape/Formats/CAS.hpp index beaca5843..fbd7e3a5f 100644 --- a/Storage/Tape/Formats/CAS.hpp +++ b/Storage/Tape/Formats/CAS.hpp @@ -42,22 +42,19 @@ class CAS: public Tape { Pulse virtual_get_next_pulse(); // Helper for populating the file list, below. - void get_next(Storage::FileHolder &file, uint8_t (&buffer)[8], std::size_t quantity); + void get_next(Storage::FileHolder &file, uint8_t (&buffer)[10], std::size_t quantity); - // Storage for the array of files to transcribe into audio. - enum class Block { - BSAVE, - CSAVE, - ASCII + // 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. + struct Chunk { + bool has_gap; + bool long_header; + std::vector data; }; - struct File { - Block type; - std::vector> chunks; - }; - std::vector files_; + std::vector chunks_; // Tracker for active state within the file list. - std::size_t file_pointer_ = 0; std::size_t chunk_pointer_ = 0; enum class Phase { Header, diff --git a/Storage/Tape/Parsers/MSX.cpp b/Storage/Tape/Parsers/MSX.cpp new file mode 100644 index 000000000..e06ef7f1c --- /dev/null +++ b/Storage/Tape/Parsers/MSX.cpp @@ -0,0 +1,193 @@ +// +// MSX.cpp +// Clock Signal +// +// Created by Thomas Harte on 26/12/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "MSX.hpp" + +#include + +using namespace Storage::Tape::MSX; + +std::unique_ptr Parser::find_header(Storage::Tape::BinaryTapePlayer &tape_player) { + if(!tape_player.get_motor_control()) { + return nullptr; + } + + /* + "When 1,111 cycles have been found with less than 35 µs variation in + their lengths a header has been located." + */ + bool last_level = tape_player.get_input(); + float low = std::numeric_limits::max(); + float high = std::numeric_limits::min(); + int samples = 0; + while(!tape_player.get_tape()->is_at_end()) { + float next_length = 0.0f; + do { + next_length += static_cast(tape_player.get_cycles_until_next_event()) / static_cast(tape_player.get_input_clock_rate()); + tape_player.run_for_input_pulse(); + } while(last_level == tape_player.get_input()); + last_level = tape_player.get_input(); + low = std::min(low, next_length); + high = std::max(high, next_length); + samples++; + if(high - low > 0.000035f) { + low = std::numeric_limits::max(); + high = std::numeric_limits::min(); + samples = 0; + } + if(samples == 1111*2) break; // Cycles are read, not half-cycles. + } + + if(tape_player.get_tape()->is_at_end()) return nullptr; + + /* + "The next 256 cycles are then read (1B34H) and averaged to determine the cassette HI cycle length." + */ + float total_length = 0.0f; + samples = 512; + while(!tape_player.get_tape()->is_at_end()) { + total_length += static_cast(tape_player.get_cycles_until_next_event()) / static_cast(tape_player.get_input_clock_rate()); + if(tape_player.get_input() != last_level) { + samples--; + if(!samples) break; + last_level = tape_player.get_input(); + } + tape_player.run_for_input_pulse(); + } + + if(tape_player.get_tape()->is_at_end()) return nullptr; + + /* + This figure is multiplied by 1.5 and placed in LOWLIM where it defines the minimum acceptable length + of a 0 start bit. The HI cycle length is placed in WINWID and will be used to discriminate + between LO and HI cycles." + */ + total_length = total_length / 256.0f; // To get the average, in microseconds. + // To convert to the loop count format used by the MSX BIOS. + uint8_t int_result = static_cast(total_length / (0.00001145f * 0.75f)); + + std::unique_ptr result(new FileSpeed); + result->minimum_start_bit_duration = int_result; + result->low_high_disrimination_duration = (int_result * 3) >> 2; + + return result; +} + +/*! + Attempts to read the next byte from the cassette, with data encoded + at the rate as defined by @c speed. + + Attempts exactly to duplicate the MSX's TAPIN function. + + @returns A value in the range 0–255 if a byte is found before the end of the tape; + -1 otherwise. +*/ +int Parser::get_byte(const FileSpeed &speed, Storage::Tape::BinaryTapePlayer &tape_player) { + if(!tape_player.get_motor_control()) { + return -1; + } + + /* + "The cassette is first read continuously until a start bit is found. + This is done by locating a negative transition, measuring the following + cycle length (1B1FH) and comparing this to see if it is greater than LOWLIM." + */ + float minimum_start_bit_duration = static_cast(speed.minimum_start_bit_duration) * 0.00001145f; + while(!tape_player.get_tape()->is_at_end()) { + // Find a negative transition. + while(!tape_player.get_tape()->is_at_end() && tape_player.get_input()) { + tape_player.run_for_input_pulse(); + } + + // Measure the following cycle (i.e. two transitions). + bool level = tape_player.get_input(); + float time_to_transition = 0.0f; + int transitions = 0; + while(!tape_player.get_tape()->is_at_end()) { + time_to_transition += static_cast(tape_player.get_cycles_until_next_event()) / static_cast(tape_player.get_input_clock_rate()); + tape_player.run_for_input_pulse(); + if(level != tape_player.get_input()) { + level = tape_player.get_input(); + transitions++; + if(transitions == 2) + break; + } + } + + // Check length against 'LOWLIM' (i.e. the minimum start bit duration). + if(time_to_transition > minimum_start_bit_duration) { + break; + } + } + + /* + "Each of the eight data bits is then read by counting the number of transitions within + a fixed period of time (1B03H). If zero or one transitions are found it is a 0 bit, if two + or three are found it is a 1 bit. If more than three transitions are found the routine + terminates with Flag C as this is presumed to be a hardware error of some sort. " + */ + int result = 0; + const int cycles_per_window = static_cast( + 0.5f + + static_cast(speed.low_high_disrimination_duration) * + 0.0000173f * + static_cast(tape_player.get_input_clock_rate()) + ); + int bits_left = 8; + bool level = tape_player.get_input(); + while(!tape_player.get_tape()->is_at_end() && bits_left--) { + // Count number of transitions within cycles_per_window. + int transitions = 0; + int cycles_remaining = cycles_per_window; + while(!tape_player.get_tape()->is_at_end() && cycles_remaining) { + const int cycles_until_next_event = static_cast(tape_player.get_cycles_until_next_event()); + const int cycles_to_run_for = std::min(cycles_until_next_event, cycles_remaining); + + cycles_remaining -= cycles_to_run_for; + tape_player.run_for(Cycles(cycles_to_run_for)); + + if(level != tape_player.get_input()) { + level = tape_player.get_input(); + transitions++; + } + } + + if(tape_player.get_tape()->is_at_end()) return -1; + + int next_bit = 0; + switch(transitions) { + case 0: case 1: + next_bit = 0x00; + break; + case 2: case 3: + next_bit = 0x80; + break; + default: + return -1; + } + result = (result >> 1) | next_bit; + + /* + "After the value of each bit has been determined a further one or two transitions are read (1B23H) + to retain synchronization. With an odd transition count one more will be read, with an even + transition count two more." + */ + int required_transitions = 2 - (transitions&1); + while(!tape_player.get_tape()->is_at_end()) { + tape_player.run_for_input_pulse(); + if(level != tape_player.get_input()) { + level = tape_player.get_input(); + required_transitions--; + if(!required_transitions) break; + } + } + + if(tape_player.get_tape()->is_at_end()) return -1; + } + return result; +} diff --git a/Storage/Tape/Parsers/MSX.hpp b/Storage/Tape/Parsers/MSX.hpp new file mode 100644 index 000000000..5146fb6c5 --- /dev/null +++ b/Storage/Tape/Parsers/MSX.hpp @@ -0,0 +1,56 @@ +// +// MSX.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/12/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef Storage_Tape_Parsers_MSX_hpp +#define Storage_Tape_Parsers_MSX_hpp + +#include "../Tape.hpp" + +#include +#include + +namespace Storage { +namespace Tape { +namespace MSX { + +class Parser { + public: + struct FileSpeed { + uint8_t minimum_start_bit_duration; // i.e. LOWLIM + uint8_t low_high_disrimination_duration; // i.e. WINWID + }; + + /*! + Finds the next header from the tape, determining constants for the + speed of file expected ahead. + + Attempts exactly to duplicate the MSX's TAPION function. + + @param tape_player The tape player containing the tape to search. + @returns An instance of FileSpeed if a header is found before the end of the tape; + @c nullptr otherwise. + */ + static std::unique_ptr find_header(Storage::Tape::BinaryTapePlayer &tape_player); + + /*! + Attempts to read the next byte from the cassette, with data encoded + at the rate as defined by @c speed. + + Attempts exactly to duplicate the MSX's TAPIN function. + + @returns A value in the range 0–255 if a byte is found before the end of the tape; + -1 otherwise. + */ + static int get_byte(const FileSpeed &speed, Storage::Tape::BinaryTapePlayer &tape_player); +}; + +} +} +} + +#endif /* Storage_Tape_Parsers_MSX_hpp */ diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 6722f6575..2c09dc228 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -130,11 +130,15 @@ void BinaryTapePlayer::set_motor_control(bool enabled) { } } +bool BinaryTapePlayer::get_motor_control() const { + return motor_is_running_; +} + void BinaryTapePlayer::set_tape_output(bool set) { // TODO } -bool BinaryTapePlayer::get_input() { +bool BinaryTapePlayer::get_input() const { return motor_is_running_ && input_level_; } diff --git a/Storage/Tape/Tape.hpp b/Storage/Tape/Tape.hpp index 1cc2aa435..829634ef5 100644 --- a/Storage/Tape/Tape.hpp +++ b/Storage/Tape/Tape.hpp @@ -132,8 +132,10 @@ class BinaryTapePlayer: public TapePlayer { public: BinaryTapePlayer(unsigned int input_clock_rate); void set_motor_control(bool enabled); + bool get_motor_control() const; + void set_tape_output(bool set); - bool get_input(); + bool get_input() const; void run_for(const Cycles cycles);