1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-10 08:04:45 +00:00

Merge pull request #323 from TomHarte/MSXCAS

Adds support for the MSX .CAS file format.
This commit is contained in:
Thomas Harte 2017-12-23 17:00:02 -08:00 committed by GitHub
commit f9f870ad2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 213 additions and 10 deletions

View File

@ -8,21 +8,197 @@
#include "CAS.hpp" #include "CAS.hpp"
#include <cassert>
#include <cstring>
using namespace Storage::Tape; using namespace Storage::Tape;
CAS::CAS(const char *file_name) : namespace {
file_(file_name) { const uint8_t header_signature[8] = {0x1f, 0xa6, 0xde, 0xba, 0xcc, 0x13, 0x7d, 0x74};
}
CAS::CAS(const char *file_name) {
Storage::FileHolder file(file_name);
uint8_t lookahead[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Get the first header.
get_next(file, lookahead, 8);
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;
// This implies something has gone wrong with parsing.
default: throw ErrorNotCAS;
}
// Set the type and feed in the initial data.
files_.emplace_back();
active_file = &files_.back();
active_file->type = type;
}
// 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]);
get_next(file, lookahead, 1);
}
// If the file ended, flush the lookahead.
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;
}
}
}
/*!
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);
if(quantity < 8)
std::memmove(buffer, &buffer[quantity], 8 - quantity);
while(quantity--) {
buffer[7 - quantity] = file.get8();
}
} }
bool CAS::is_at_end() { bool CAS::is_at_end() {
return true; return phase_ == Phase::EndOfFile;
} }
void CAS::virtual_reset() { void CAS::virtual_reset() {
phase_ = Phase::Header;
file_pointer_ = 0;
chunk_pointer_ = 0;
distance_into_phase_ = 0;
distance_into_bit_ = 0;
} }
Tape::Pulse CAS::virtual_get_next_pulse() { Tape::Pulse CAS::virtual_get_next_pulse() {
Pulse empty_pulse; Pulse pulse;
return empty_pulse; pulse.length.clock_rate = 9600;
// Clock rate is four times the baud rate (of 2400), because the quickest thing that might need
// to be communicated is a '1', which is two cycles at the baud rate, i.e. four events:
// high, low, high, low.
// If this is a gap, then that terminates a file. If this is already the end
// of the file then perpetual gaps await.
if(phase_ == Phase::Gap || phase_ == Phase::EndOfFile) {
pulse.length.length = pulse.length.clock_rate;
pulse.type = Pulse::Type::Zero;
if(phase_ == Phase::Gap) {
phase_ = Phase::Header;
chunk_pointer_ = 0;
distance_into_phase_ = 0;
}
return pulse;
}
// Determine which bit is now forthcoming.
int bit = 1;
switch(phase_) {
default: break;
case Phase::Header: {
// In the header, all bits are 1s, so let the default value stand. Just check whether the
// header is ended and, if so, move on to bytes.
distance_into_bit_++;
if(distance_into_bit_ == 2) {
distance_into_phase_++;
distance_into_bit_ = 0;
// 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)) {
phase_ = Phase::Bytes;
distance_into_phase_ = 0;
distance_into_bit_ = 0;
}
}
} break;
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];
int bit_offset = distance_into_phase_ % 11;
switch(bit_offset) {
case 0: bit = 0; break;
default: bit = (byte_value >> (bit_offset - 1)) & 1; break;
case 9:
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.
//
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) {
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;
} else {
phase_ = Phase::Header;
}
}
}
} break;
}
// A '1' is encoded with twice the frequency of a '0'.
pulse.length.length = static_cast<unsigned int>(2 - bit);
pulse.type = (distance_into_bit_ & 1) ? Pulse::Type::High : Pulse::Type::Low;
return pulse;
} }

View File

@ -12,6 +12,9 @@
#include "../Tape.hpp" #include "../Tape.hpp"
#include "../../FileHolder.hpp" #include "../../FileHolder.hpp"
#include <cstdint>
#include <vector>
namespace Storage { namespace Storage {
namespace Tape { namespace Tape {
@ -35,10 +38,35 @@ class CAS: public Tape {
bool is_at_end(); bool is_at_end();
private: private:
Storage::FileHolder file_;
void virtual_reset(); void virtual_reset();
Pulse virtual_get_next_pulse(); 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);
// Storage for the array of files to transcribe into audio.
enum class Block {
BSAVE,
CSAVE,
ASCII
};
struct File {
Block type;
std::vector<std::vector<std::uint8_t>> chunks;
};
std::vector<File> files_;
// Tracker for active state within the file list.
std::size_t file_pointer_ = 0;
std::size_t chunk_pointer_ = 0;
enum class Phase {
Header,
Bytes,
Gap,
EndOfFile
} phase_ = Phase::Header;
std::size_t distance_into_phase_ = 0;
std::size_t distance_into_bit_ = 0;
}; };
} }

View File

@ -89,8 +89,7 @@ void TapePlayer::get_next_pulse() {
if(tape_) { if(tape_) {
current_pulse_ = tape_->get_next_pulse(); current_pulse_ = tape_->get_next_pulse();
if(tape_->is_at_end()) update_sleep_observer(); if(tape_->is_at_end()) update_sleep_observer();
} } else {
else {
current_pulse_.length.length = 1; current_pulse_.length.length = 1;
current_pulse_.length.clock_rate = 1; current_pulse_.length.clock_rate = 1;
current_pulse_.type = Tape::Pulse::Zero; current_pulse_.type = Tape::Pulse::Zero;