1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-04-09 00:37:27 +00:00

Merge pull request #595 from TomHarte/MSXTapeMotor

Fixes various MSX tape-handling bugs, and adds a status LED.
This commit is contained in:
Thomas Harte 2019-03-02 15:00:03 -05:00 committed by GitHub
commit 6a55d75b3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 156 additions and 49 deletions

View File

@ -395,6 +395,7 @@ class ConcreteMachine:
write_pointers_[c+1] = memory_slots_[value & 3].write_pointers[c+1];
value >>= 2;
}
set_use_fast_tape();
}
// MARK: Z80::BusHandler
@ -675,6 +676,7 @@ class ConcreteMachine:
if(disk_rom) {
disk_rom->set_activity_observer(observer);
}
i8255_port_handler_.set_activity_observer(observer);
}
// MARK: - Joysticks
@ -705,6 +707,7 @@ class ConcreteMachine:
// b4: cassette motor relay
tape_player_.set_motor_control(!(value & 0x10));
activity_observer_->set_led_status("Tape motor", !(value & 0x10));
// b7: keyboard click
bool new_audio_level = !!(value & 0x80);
@ -727,10 +730,19 @@ class ConcreteMachine:
return 0xff;
}
void set_activity_observer(Activity::Observer *observer) {
activity_observer_ = observer;
if(activity_observer_) {
activity_observer_->register_led("Tape motor");
activity_observer_->set_led_status("Tape motor", tape_player_.get_motor_control());
}
}
private:
ConcreteMachine &machine_;
Audio::Toggle &audio_toggle_;
Storage::Tape::BinaryTapePlayer &tape_player_;
Activity::Observer *activity_observer_ = nullptr;
};
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
@ -749,7 +761,7 @@ class ConcreteMachine:
bool allow_fast_tape_ = false;
bool use_fast_tape_ = false;
void set_use_fast_tape() {
use_fast_tape_ = !tape_player_is_sleeping_ && allow_fast_tape_ && tape_player_.has_tape();
use_fast_tape_ = !tape_player_is_sleeping_ && allow_fast_tape_ && tape_player_.has_tape() && !(paged_memory_&3);
}
i8255PortHandler i8255_port_handler_;

View File

@ -13,66 +13,161 @@
using namespace Storage::Tape;
/*
CAS files are a raw byte capture of tape content, with all solid tones transmuted to
the placeholder 1F A6 DE BA CC 13 7D 74 and gaps omitted.
Since that byte stream may also occur within files, and gaps and tone lengths need to be
reconstructed, knowledge of the MSX tape byte format is also required. Specifically:
Each tone followed by ten bytes that determine the file type:
ten bytes of value 0xD0 => a binary file;
ten bytes of value 0xD3 => it's a basic file;
ten bytes of value 0xEA => it's an ASCII file; and
any other pattern implies a raw data block.
Raw data blocks contain their two-byte length, then data.
Binary, Basic and ASCII files then have a six-byte file name, followed by a short tone, followed
by the file contents.
ASCII files:
... are a sequence of short tone/256-byte chunk pairs. For CAS purposes, these continue until
you hit another 1F A6 DE BA CC 13 7D 74 sequence.
Binary files:
... begin with three 16-bit values, the starting, ending and execution addresses. Then there is
the correct amount of data to fill memory from the starting to the ending address, inclusive.
BASIC files:
... are in Microsoft-standard BASIC form of (two bytes link to next line), (two bytes line number), [tokens],
starting from address 0x8001. These files continue until a next line address of 0x0000 is found, then
are usually padded by 0s for a period that I haven't yet determined a pattern for. The code below treats
everything to the next 0x1f as padding.
*/
namespace {
const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74};
#define TenX(x) {x, x, x, x, x, x, x, x, x, x}
const uint8_t binary_signature[] = TenX(0xd0);
const uint8_t basic_signature[] = TenX(0xd3);
const uint8_t ascii_signature[] = TenX(0xea);
}
CAS::CAS(const std::string &file_name) {
Storage::FileHolder file(file_name);
uint8_t lookahead[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Entirely fill the lookahead and verify that its start matches the header signature.
get_next(file, lookahead, 10);
if(std::memcmp(lookahead, header_signature, sizeof(header_signature))) throw ErrorNotCAS;
enum class Mode {
Seeking,
ASCII,
Binary,
BASIC
} parsing_mode_ = Mode::Seeking;
while(!file.eof()) {
// Just found a header, so flush the lookahead.
get_next(file, lookahead, 8);
while(true) {
// Churn through the file until the next header signature is found.
const auto header_position = file.tell();
const auto signature = file.read(8);
if(signature.size() != 8) break;
if(std::memcmp(signature.data(), header_signature, 8)) {
if(!chunks_.empty()) chunks_.back().data.push_back(signature[0]);
// Create a new chunk
chunks_.emplace_back();
Chunk &chunk = chunks_.back();
// Decide whether to award a long header and/or a gap.
bool bytes_are_equal = true;
for(std::size_t index = 0; index < sizeof(lookahead); index++)
bytes_are_equal &= (lookahead[index] == lookahead[0]);
chunk.long_header = bytes_are_equal && ((lookahead[0] == 0xd3) || (lookahead[0] == 0xd0) || (lookahead[0] == 0xea));
chunk.has_gap = chunk.long_header && (chunks_.size() > 1);
// Keep going until another header arrives or the file ends. Headers require the magic byte sequence,
// and also must be eight-byte aligned within the file.
while( !file.eof() &&
(std::memcmp(lookahead, header_signature, sizeof(header_signature)) || ((file.tell()-10)&7))) {
chunk.data.push_back(lookahead[0]);
get_next(file, lookahead, 1);
// Check for other 1fs in this stream, and repeat from there if any.
for(size_t c = 1; c < 8; ++c) {
if(signature[c] == 0x1f) {
file.seek(header_position + long(c), SEEK_SET);
break;
} else {
// Attach any unexpected bytes to the back of the most recent chunk.
// In effect this creates a linear search for the next explicit tone.
if(!chunks_.empty()) {
chunks_.back().data.push_back(signature[c]);
}
}
}
continue;
}
// If the file ended, flush the lookahead. The final thing in it will be a 0xff from the read that
// triggered the eof, so don't include that.
if(file.eof()) {
for(std::size_t index = 0; index < sizeof(lookahead) - 1; index++)
chunk.data.push_back(lookahead[index]);
// A header has definitely been found. Require from here at least 16 further bytes,
// being the type and a name.
const auto type = file.read(10);
if(type.size() != 10) break;
const bool is_binary = !std::memcmp(type.data(), binary_signature, type.size());
const bool is_basic = !std::memcmp(type.data(), basic_signature, type.size());
const bool is_ascii = !std::memcmp(type.data(), ascii_signature, type.size());
switch(parsing_mode_) {
case Mode::Seeking: {
if(is_ascii || is_binary || is_basic) {
file.seek(header_position + 8, SEEK_SET);
chunks_.emplace_back(!chunks_.empty(), true, file.read(10 + 6));
if(is_ascii) parsing_mode_ = Mode::ASCII;
if(is_binary) parsing_mode_ = Mode::Binary;
if(is_basic) parsing_mode_ = Mode::BASIC;
} else {
// Raw data appears now. Grab its length and keep going.
file.seek(header_position + 8, SEEK_SET);
const uint16_t length = file.get16le();
file.seek(header_position + 8, SEEK_SET);
chunks_.emplace_back(false, false, file.read(size_t(length) + 2));
}
} break;
case Mode::ASCII:
// Keep reading ASCII in 256-byte segments until a non-ASCII chunk arrives.
if(is_binary || is_basic || is_ascii) {
file.seek(header_position, SEEK_SET);
parsing_mode_ = Mode::Seeking;
} else {
file.seek(header_position + 8, SEEK_SET);
chunks_.emplace_back(false, false, file.read(256));
}
break;
case Mode::Binary: {
// Get the start and end addresses in order to figure out how much data
// is here.
file.seek(header_position + 8, SEEK_SET);
const uint16_t start_address = file.get16le();
const uint16_t end_address = file.get16le();
file.seek(header_position + 8, SEEK_SET);
const auto length = end_address - start_address + 1;
chunks_.emplace_back(false, false, file.read(size_t(length) + 6));
parsing_mode_ = Mode::Seeking;
} break;
case Mode::BASIC: {
// Horror of horrors, this will mean actually following the BASIC
// linked list of line contents.
file.seek(header_position + 8, SEEK_SET);
uint16_t address = 0x8001; // the BASIC start address.
while(true) {
const uint16_t next_line_address = file.get16le();
if(!next_line_address || file.eof()) break;
file.seek(next_line_address - address - 2, SEEK_CUR);
address = next_line_address;
}
const auto length = (file.tell() - 1) - (header_position + 8);
// Create the chunk and return to regular parsing.
file.seek(header_position + 8, SEEK_SET);
chunks_.emplace_back(false, false, file.read(size_t(length)));
parsing_mode_ = Mode::Seeking;
} break;
}
}
}
/*!
Treating @c buffer as a sliding lookahead, shifts it @c quantity elements to the left and
populates the new empty area to the right from @c file.
*/
void CAS::get_next(Storage::FileHolder &file, uint8_t (&buffer)[10], std::size_t quantity) {
assert(quantity <= sizeof(buffer));
if(quantity < sizeof(buffer))
std::memmove(buffer, &buffer[quantity], sizeof(buffer) - quantity);
while(quantity--) {
buffer[sizeof(buffer) - 1 - quantity] = file.get8();
}
}
bool CAS::is_at_end() {
return phase_ == Phase::EndOfFile;
}

View File

@ -42,9 +42,6 @@ class CAS: public Tape {
void virtual_reset();
Pulse virtual_get_next_pulse();
// Helper for populating the file list, below.
void get_next(Storage::FileHolder &file, uint8_t (&buffer)[10], std::size_t quantity);
// Storage for the array of data blobs to transcribe into audio;
// each chunk is preceded by a header which may be long, and is optionally
// also preceded by a gap.
@ -52,6 +49,9 @@ class CAS: public Tape {
bool has_gap;
bool long_header;
std::vector<std::uint8_t> data;
Chunk(bool has_gap, bool long_header, const std::vector<std::uint8_t> &data) :
has_gap(has_gap), long_header(long_header), data(std::move(data)) {}
};
std::vector<Chunk> chunks_;