1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-07 08:28:57 +00:00

Merge pull request #324 from TomHarte/MSXAnalysis

Introduces basic tape analysis for the MSX
This commit is contained in:
Thomas Harte 2017-12-29 15:45:21 -08:00 committed by GitHub
commit eef34adcbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 749 additions and 179 deletions

View File

@ -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<CharacterMapper> 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 {

View File

@ -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<CharacterMapper> 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 {

View File

@ -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<CharacterMapper> 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 {

View File

@ -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 &) {
}

View File

@ -10,6 +10,7 @@
#define KeyboardMachine_h
#include <cstdint>
#include <string>
#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.
*/

View File

@ -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<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(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<Parser::FileSpeed> 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<uint16_t>(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<std::size_t>(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<uint8_t>(character);
write_address = (write_address + 1) % buffer_size;
}
write_address += buffer_start;
input_text_.erase(input_text_.begin(), input_text_.begin() + static_cast<std::string::difference_type>(characters_to_write));
ram_[0xf3f8] = static_cast<uint8_t>(write_address);
ram_[0xf3f9] = static_cast<uint8_t>(write_address >> 8);
}
break;
default: break;
@ -321,6 +419,37 @@ class ConcreteMachine:
return keyboard_mapper_;
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> 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<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, AudioToggle>> 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_;
};

View File

@ -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<std::unique_ptr<Configurable::Option>> get_options();
}
#endif /* MSX_hpp */

View File

@ -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<CharacterMapper> mapper(new CharacterMapper);
Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper));
Utility::TypeRecipient::add_typer(string, std::move(mapper));
}
// for Microdisc::Delegate

View File

@ -64,6 +64,7 @@ std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> Machin
std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> 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()));

View File

@ -8,23 +8,22 @@
#include "Typer.hpp"
#include <cstdlib>
#include <cstring>
#include <sstream>
using namespace Utility;
Typer::Typer(const char *string, HalfCycles delay, HalfCycles frequency, std::unique_ptr<CharacterMapper> character_mapper, Delegate *delegate) :
Typer::Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, std::unique_ptr<CharacterMapper> 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) {

View File

@ -10,6 +10,8 @@
#define Typer_hpp
#include <memory>
#include <string>
#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<CharacterMapper> character_mapper, Delegate *delegate);
~Typer();
Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, std::unique_ptr<CharacterMapper> 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<CharacterMapper> character_mapper) {
void add_typer(const std::string &string, std::unique_ptr<CharacterMapper> 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.

View File

@ -278,8 +278,8 @@ template<bool is_zx81> 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<bool is_zx81> 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<CharacterMapper> 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.

View File

@ -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 = "<group>"; };
4B0E04F81FC9FA3000F43484 /* 9918.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 9918.hpp; path = 9918/9918.hpp; sourceTree = "<group>"; };
4B0E04F91FC9FA3100F43484 /* 9918.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = 9918.cpp; path = 9918/9918.cpp; sourceTree = "<group>"; };
4B0E61051FF34737002A9DBD /* MSX.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MSX.cpp; path = Parsers/MSX.cpp; sourceTree = "<group>"; };
4B0E61061FF34737002A9DBD /* MSX.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = MSX.hpp; path = Parsers/MSX.hpp; sourceTree = "<group>"; };
4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMPatchedTrackTests.mm; sourceTree = "<group>"; };
4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMSegmentEventSourceTests.mm; sourceTree = "<group>"; };
4B12C0EB1FCFA98D005BFD93 /* Keyboard.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = MSX/Keyboard.cpp; sourceTree = "<group>"; };
@ -817,6 +823,8 @@
4B643F391D77AD1900D431D6 /* CSStaticAnalyser.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSStaticAnalyser.mm; path = StaticAnalyser/CSStaticAnalyser.mm; sourceTree = "<group>"; };
4B643F3C1D77AE5C00D431D6 /* CSMachine+Target.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSMachine+Target.h"; sourceTree = "<group>"; };
4B643F3E1D77B88000D431D6 /* DocumentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentController.swift; sourceTree = "<group>"; };
4B651F9C1FF1B04100E18D9A /* Tape.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Tape.cpp; path = ../../StaticAnalyser/MSX/Tape.cpp; sourceTree = "<group>"; };
4B651F9D1FF1B04100E18D9A /* Tape.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Tape.hpp; path = ../../StaticAnalyser/MSX/Tape.hpp; sourceTree = "<group>"; };
4B698D1A1FE768A100696C91 /* SampleSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SampleSource.hpp; sourceTree = "<group>"; };
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Tape.cpp; sourceTree = "<group>"; };
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tape.hpp; sourceTree = "<group>"; };
@ -1377,6 +1385,8 @@
children = (
4B0E04EC1FC9E88300F43484 /* StaticAnalyser.cpp */,
4B0E04ED1FC9E88300F43484 /* StaticAnalyser.hpp */,
4B651F9C1FF1B04100E18D9A /* Tape.cpp */,
4B651F9D1FF1B04100E18D9A /* Tape.hpp */,
);
name = MSX;
sourceTree = "<group>";
@ -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 */,

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -96,7 +96,7 @@ void StaticAnalyser::Acorn::AddTargets(const Media &media, std::list<Target> &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<Target> &de
if(bootOption != Catalogue::BootOption::None)
target.acorn.should_shift_restart = true;
else
target.loadingCommand = "*CAT\n";
target.loading_command = "*CAT\n";
}
}

View File

@ -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<Storage::Disk::Disk> &disk, StaticAnalyser::Target &target) {
@ -169,7 +169,7 @@ static bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &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<Target
// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing
// enter and responding to the follow-on prompt to press a key, so just type for
// a while. Yuck!
target.loadingCommand = "|tape\nrun\"\n1234567890";
target.loading_command = "|tape\nrun\"\n1234567890";
}
if(!target.media.disks.empty()) {

View File

@ -82,7 +82,7 @@ void StaticAnalyser::Commodore::AddTargets(const Media &media, std::list<Target>
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) {

View File

@ -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<std::shared_ptr<Storage::Cartridge::Cartridge>>
MSXCartridgesFrom(const std::list<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) {
@ -29,31 +33,17 @@ static std::list<std::shared_ptr<Storage::Cartridge::Cartridge>>
// 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<uint16_t>(segment.data[2 + c] | segment.data[3 + c] << 8);
if(code_pointer) {
start_address = std::min(static_cast<uint16_t>(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<std::shared_ptr<Storage::Cartridge::Cartridge>>
void StaticAnalyser::MSX::AddTargets(const Media &media, std::list<Target> &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<File> 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;

163
StaticAnalyser/MSX/Tape.cpp Normal file
View File

@ -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<File> StaticAnalyser::MSX::GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape) {
std::vector<File> 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<Parser::FileSpeed> 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<uint8_t>(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<char>(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<uint8_t>(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<uint8_t>(byte);
}
if(c != sizeof(locations)) continue;
file.starting_address = static_cast<uint16_t>(locations[0] | (locations[1] << 8));
end_address = static_cast<uint16_t>(locations[2] | (locations[3] << 8));
file.entry_address = static_cast<uint16_t>(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<uint8_t>(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<uint8_t>(next_address_buffer[0]));
file.data.push_back(static_cast<uint8_t>(next_address_buffer[1]));
uint16_t next_address = static_cast<uint16_t>(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<uint8_t>(byte));
}
if(found_error) break;
}
}
return files;
}

View File

@ -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 <string>
#include <vector>
namespace StaticAnalyser {
namespace MSX {
struct File {
std::string name;
enum Type {
Binary,
TokenisedBASIC,
ASCII
} type;
std::vector<uint8_t> 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<File> GetFiles(const std::shared_ptr<Storage::Tape::Tape> &tape);
}
}
#endif /* StaticAnalyser_MSX_Tape_hpp */

View File

@ -97,7 +97,7 @@ void StaticAnalyser::Oric::AddTargets(const Media &media, std::list<Target> &des
}
target.media.tapes.push_back(tape);
target.loadingCommand = "CLOAD\"\"\n";
target.loading_command = "CLOAD\"\"\n";
}
}

View File

@ -115,7 +115,7 @@ struct Target {
} amstradcpc;
};
std::string loadingCommand;
std::string loading_command;
Media media;
};

View File

@ -56,9 +56,9 @@ void StaticAnalyser::ZX8081::AddTargets(const Media &media, std::list<Target> &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);

View File

@ -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<uint8_t> &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;
}
}
}

View File

@ -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<std::uint8_t> data;
};
struct File {
Block type;
std::vector<std::vector<std::uint8_t>> chunks;
};
std::vector<File> files_;
std::vector<Chunk> 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,

View File

@ -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 <algorithm>
using namespace Storage::Tape::MSX;
std::unique_ptr<Parser::FileSpeed> 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<float>::max();
float high = std::numeric_limits<float>::min();
int samples = 0;
while(!tape_player.get_tape()->is_at_end()) {
float next_length = 0.0f;
do {
next_length += static_cast<float>(tape_player.get_cycles_until_next_event()) / static_cast<float>(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<float>::max();
high = std::numeric_limits<float>::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<float>(tape_player.get_cycles_until_next_event()) / static_cast<float>(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<uint8_t>(total_length / (0.00001145f * 0.75f));
std::unique_ptr<FileSpeed> 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 0255 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<float>(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<float>(tape_player.get_cycles_until_next_event()) / static_cast<float>(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<int>(
0.5f +
static_cast<float>(speed.low_high_disrimination_duration) *
0.0000173f *
static_cast<float>(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<int>(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;
}

View File

@ -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 <memory>
#include <cstdint>
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<FileSpeed> 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 0255 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 */

View File

@ -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_;
}

View File

@ -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);