diff --git a/doc/using.md b/doc/using.md index 8deb1399..1aeb8abe 100644 --- a/doc/using.md +++ b/doc/using.md @@ -221,11 +221,19 @@ directory. file format in a non-backwards-compatible way; this tool will upgrade flux files to the new format. - - `fluxengine convert`: converts various formats to various other formats. - You can use this to convert Catweasel or Supercard Pro flux files to - FluxEngine's native format, for flux files to various other formats useful - for debugging (including VCD which can be loaded into - [sigrok](http://sigrok.org)). + - `fluxengine convert`: converts flux files from various formats to various + other formats. You can use this to convert Catweasel flux files to + FluxEngine's native format, FluxEngine flux files to various other formats + useful for debugging (including VCD which can be loaded into + [sigrok](http://sigrok.org)), and bidirectional conversion to and from + Supercard Pro `.scp` format. + + **Important SCP note:** import (`fluxengine convert scptoflux`) should be + fairly robust, but export (`fluxengine convert fluxtoscp`) should only be + done with great caution as FluxEngine files contain features which can't be + represented very well in `.scp` format and they're probably pretty dubious. + As ever, please [get in + touch](https://github.com/davidgiven/fluxengine/issues/new) with any reports. Commands which normally take `--source` or `--dest` get a sensible default if left unspecified. `fluxengine read ibm` on its own will read drive 0 and diff --git a/lib/flags.cc b/lib/flags.cc index ba7803a3..adb41547 100644 --- a/lib/flags.cc +++ b/lib/flags.cc @@ -29,7 +29,7 @@ void FlagGroup::addFlag(Flag* flag) _flags.push_back(flag); } -void FlagGroup::parseFlags(int argc, const char* argv[]) +std::vector FlagGroup::parseFlagsWithFilenames(int argc, const char* argv[]) { if (_initialised) throw std::runtime_error("called parseFlags() twice"); @@ -66,6 +66,7 @@ void FlagGroup::parseFlags(int argc, const char* argv[]) /* Now actually parse them. */ + std::vector filenames; int index = 1; while (index < argc) { @@ -76,52 +77,73 @@ void FlagGroup::parseFlags(int argc, const char* argv[]) std::string value; bool usesthat = false; - if ((thisarg.size() == 0) || (thisarg[0] != '-')) - Error() << "non-option parameter " << thisarg << " seen (try --help)"; - if ((thisarg.size() > 1) && (thisarg[1] == '-')) + if (thisarg.size() == 0) { - /* Long option. */ - - auto equals = thisarg.rfind('='); - if (equals != std::string::npos) - { - key = thisarg.substr(0, equals); - value = thisarg.substr(equals+1); - } - else - { - key = thisarg; - value = thatarg; - usesthat = true; - } + /* Ignore this argument. */ + } + else if (thisarg[0] != '-') + { + /* This is a filename. */ + filenames.push_back(thisarg); } else { - /* Short option. */ + /* This is a flag. */ - if (thisarg.size() > 2) + if ((thisarg.size() > 1) && (thisarg[1] == '-')) { - key = thisarg.substr(0, 2); - value = thisarg.substr(2); + /* Long option. */ + + auto equals = thisarg.rfind('='); + if (equals != std::string::npos) + { + key = thisarg.substr(0, equals); + value = thisarg.substr(equals+1); + } + else + { + key = thisarg; + value = thatarg; + usesthat = true; + } } else { - key = thisarg; - value = thatarg; - usesthat = true; + /* Short option. */ + + if (thisarg.size() > 2) + { + key = thisarg.substr(0, 2); + value = thisarg.substr(2); + } + else + { + key = thisarg; + value = thatarg; + usesthat = true; + } } + + auto flag = flags_by_name.find(key); + if (flag == flags_by_name.end()) + Error() << "unknown flag '" << key << "'; try --help"; + + flag->second->set(value); + if (usesthat && flag->second->hasArgument()) + index++; } - auto flag = flags_by_name.find(key); - if (flag == flags_by_name.end()) - Error() << "unknown flag '" << key << "'; try --help"; - - flag->second->set(value); - index++; - if (usesthat && flag->second->hasArgument()) - index++; } + + return filenames; +} + +void FlagGroup::parseFlags(int argc, const char* argv[]) +{ + auto filenames = parseFlagsWithFilenames(argc, argv); + if (!filenames.empty()) + Error() << "non-option parameter " << *filenames.begin() << " seen (try --help)"; } void FlagGroup::checkInitialised() const diff --git a/lib/flags.h b/lib/flags.h index bd41bfbf..df584e72 100644 --- a/lib/flags.h +++ b/lib/flags.h @@ -14,6 +14,7 @@ public: public: void parseFlags(int argc, const char* argv[]); + std::vector parseFlagsWithFilenames(int argc, const char* argv[]); void addFlag(Flag* flag); void checkInitialised() const; diff --git a/mkninja.sh b/mkninja.sh index 0bc1d782..eec4ecc2 100644 --- a/mkninja.sh +++ b/mkninja.sh @@ -190,6 +190,7 @@ buildlibrary libfrontend.a \ src/fe-cwftoflux.cc \ src/fe-erase.cc \ src/fe-fluxtoau.cc \ + src/fe-fluxtoscp.cc \ src/fe-fluxtovcd.cc \ src/fe-inspect.cc \ src/fe-readadfs.cc \ diff --git a/src/fe-fluxtoscp.cc b/src/fe-fluxtoscp.cc new file mode 100644 index 00000000..1275269b --- /dev/null +++ b/src/fe-fluxtoscp.cc @@ -0,0 +1,188 @@ +#include "globals.h" +#include "flags.h" +#include "fluxmap.h" +#include "sql.h" +#include "bytes.h" +#include "protocol.h" +#include "dataspec.h" +#include "fmt/format.h" +#include "decoders/fluxmapreader.h" +#include "scp.h" +#include +#include + +static FlagGroup flags { }; + +static SettableFlag fortyTrackMode( + { "--48", "-4" }, + "set 48 tpi mode; only every other physical track is emitted" +); + +static SettableFlag singleSided( + { "--single-sided", "-s" }, + "only emit side 0" +); + +static IntFlag diskType( + { "--disk-type" }, + "sets the SCP disk type byte", + 0xff +); + +static sqlite3* inputDb; + +static void syntax() +{ + std::cout << "Syntax: fluxengine convert fluxtoscp \n"; + exit(0); +} + +static void write_le32(uint8_t dest[4], uint32_t v) +{ + dest[0] = v; + dest[1] = v >> 8; + dest[2] = v >> 16; + dest[3] = v >> 24; +} + +static int strackno(int track, int side) +{ + if (fortyTrackMode) + track /= 2; + if (singleSided) + return track; + else + return (track << 1) | side; +} + +int mainConvertFluxToScp(int argc, const char* argv[]) +{ + auto filenames = flags.parseFlagsWithFilenames(argc, argv); + if (filenames.size() != 2) + syntax(); + + inputDb = sqlOpen(filenames[0], SQLITE_OPEN_READONLY); + auto tracks = sqlFindFlux(inputDb); + + int maxTrack = 0; + int maxSide = 0; + for (auto p : tracks) + { + if (singleSided && (p.second == 1)) + continue; + maxTrack = std::max(maxTrack, (int)p.first); + maxSide = std::max(maxSide, (int)p.second); + } + int maxStrack = strackno(maxTrack, maxSide); + + std::cout << fmt::format("Writing {} {} SCP file containing {} SCP tracks\n", + fortyTrackMode ? "48 tpi" : "96 tpi", + singleSided ? "single sided" : "double sided", + maxStrack + 1 + ); + + ScpHeader fileheader = {0}; + fileheader.file_id[0] = 'S'; + fileheader.file_id[1] = 'C'; + fileheader.file_id[2] = 'P'; + fileheader.file_id[3] = 0x18; /* Version 1.8 of the spec */ + fileheader.type = diskType; + fileheader.revolutions = 5; + fileheader.start_track = 0; + fileheader.end_track = maxStrack; + fileheader.flags = SCP_FLAG_INDEXED | (fortyTrackMode ? 0 : SCP_FLAG_96TPI); + fileheader.cell_width = 0; + fileheader.heads = singleSided ? 1 : 0; + + Bytes trackdata; + ByteWriter trackdataWriter(trackdata); + + int trackstep = 1 + fortyTrackMode; + int maxside = singleSided ? 0 : 1; + for (int track = 0; track <= maxTrack; track += trackstep) + { + for (int side = 0; side <= maxside; side++) + { + int strack = strackno(track, side); + std::cout << fmt::format("FE track {}.{}, SCP track {}: ", track, side, strack) << std::flush; + + auto fluxmap = sqlReadFlux(inputDb, track, side); + ScpTrack trackheader = {0}; + trackheader.track_id[0] = 'T'; + trackheader.track_id[1] = 'R'; + trackheader.track_id[2] = 'K'; + trackheader.strack = strack; + + FluxmapReader fmr(*fluxmap); + Bytes fluxdata; + ByteWriter fluxdataWriter(fluxdata); + + int revolution = 0; + unsigned revTicks = 0; + unsigned totalTicks = 0; + unsigned ticksSinceLastPulse = 0; + uint32_t startOffset = 0; + while (revolution < 5) + { + unsigned ticks; + int opcode = fmr.readOpcode(ticks); + if (ticks) + { + ticksSinceLastPulse += ticks; + totalTicks += ticks; + revTicks += ticks; + } + + switch (opcode) + { + case -1: /* end of flux, treat like an index marker */ + case F_OP_INDEX: + { + auto* revheader = &trackheader.revolution[revolution]; + write_le32(revheader->offset, startOffset + sizeof(ScpTrack)); + write_le32(revheader->length, (fluxdataWriter.pos - startOffset) / 2); + write_le32(revheader->index, revTicks * NS_PER_TICK / 25); + revolution++; + revheader++; + revTicks = 0; + startOffset = fluxdataWriter.pos; + break; + } + + case F_OP_PULSE: + { + unsigned t = ticksSinceLastPulse * NS_PER_TICK / 25; + while (t >= 0x10000) + { + fluxdataWriter.write_be16(0); + t -= 0x10000; + } + fluxdataWriter.write_be16(t); + ticksSinceLastPulse = 0; + break; + } + } + } + + write_le32(fileheader.track[strack], trackdataWriter.pos + sizeof(ScpHeader)); + trackdataWriter += Bytes((uint8_t*)&trackheader, sizeof(trackheader)); + trackdataWriter += fluxdata; + + std::cout << fmt::format("{} ms in {} bytes\n", + totalTicks * MS_PER_TICK, + fluxdata.size()); + } + } + + sqlClose(inputDb); + + std::cout << "Writing output file...\n"; + std::ofstream of(filenames[1], std::ios::out | std::ios::binary); + if (!of.is_open()) + Error() << "cannot open output file"; + of.write((const char*) &fileheader, sizeof(fileheader)); + of.write((const char*) trackdata.begin(), trackdata.size()); + of.close(); + + return 0; +} diff --git a/src/fe-scptoflux.cc b/src/fe-scptoflux.cc index 362e0b32..31853b25 100644 --- a/src/fe-scptoflux.cc +++ b/src/fe-scptoflux.cc @@ -4,37 +4,9 @@ #include "bytes.h" #include "protocol.h" #include "fmt/format.h" +#include "scp.h" #include -struct ScpHeader -{ - char file_id[3]; // file ID - 'SCP' - uint8_t version; // major/minor in nibbles - uint8_t type; // disk type - subclass/class in nibbles - uint8_t revolutions; // up to 5 - uint8_t start_track; // 0..165 - uint8_t end_track; // 0..165 - uint8_t flags; // see below - uint8_t cell_width; // in bits, 0 meaning 16 - uint8_t heads; // 0 = both, 1 = side 0 only, 2 = side 1 only - uint8_t resolution; // 25ns * (resolution+1) - uint8_t checksum[4]; // of data after this point - uint8_t track[165][4]; // track offsets, not necessarily 165 -}; - -struct ScpTrack -{ - char track_id[3]; // 'TRK' - uint8_t strack; // SCP track number - struct - { - uint8_t index[4]; // time for one revolution - uint8_t length[4]; // number of bitcells - uint8_t offset[4]; // offset to bitcell data, relative to track header - } - revolution[5]; -}; - static std::ifstream inputFile; static sqlite3* outputDb; static ScpHeader header; @@ -109,6 +81,7 @@ static void read_track(int strack) Fluxmap fluxmap; nanoseconds_t pending = 0; + unsigned inputBytes = 0; for (int revolution = 0; revolution < header.revolutions; revolution++) { if (revolution != 0) @@ -133,12 +106,14 @@ static void read_track(int strack) pending = 0; } else - pending += interval; + pending += 0x10000; } + + inputBytes += datalength*2; } - std::cout << fmt::format(" {} ms in {} output bytes\n", - fluxmap.duration() / 1e6, fluxmap.bytes()); + std::cout << fmt::format(" {} ms in {} input bytes and {} output bytes\n", + fluxmap.duration() / 1e6, inputBytes, fluxmap.bytes()); sqlWriteFlux(outputDb, trackno(strack), headno(strack), fluxmap); } diff --git a/src/fluxengine.cc b/src/fluxengine.cc index 479eca85..1bf3498f 100644 --- a/src/fluxengine.cc +++ b/src/fluxengine.cc @@ -5,6 +5,7 @@ typedef int command_cb(int agrc, const char* argv[]); extern command_cb mainErase; extern command_cb mainConvertCwfToFlux; extern command_cb mainConvertFluxToAu; +extern command_cb mainConvertFluxToScp; extern command_cb mainConvertFluxToVcd; extern command_cb mainConvertScpToFlux; extern command_cb mainInspect; @@ -86,6 +87,7 @@ static std::vector convertables = { "cwftoflux", mainConvertCwfToFlux, "Converts CatWeasel stream files to flux.", }, { "scptoflux", mainConvertScpToFlux, "Converts Supercard Pro stream files to flux.", }, { "fluxtoau", mainConvertFluxToAu, "Converts (one track of a) flux file to an .au audio file.", }, + { "fluxtoscp", mainConvertFluxToScp, "Converrt a flux file to a Supercard Pro file.", }, { "fluxtovcd", mainConvertFluxToVcd, "Converts (one track of a) flux file to a VCD file.", }, }; diff --git a/src/scp.h b/src/scp.h new file mode 100644 index 00000000..7df3be31 --- /dev/null +++ b/src/scp.h @@ -0,0 +1,43 @@ +#ifndef SCP_H +#define SCP_H + +struct ScpHeader +{ + char file_id[3]; // file ID - 'SCP' + uint8_t version; // major/minor in nibbles + uint8_t type; // disk type - subclass/class in nibbles + uint8_t revolutions; // up to 5 + uint8_t start_track; // 0..165 + uint8_t end_track; // 0..165 + uint8_t flags; // see below + uint8_t cell_width; // in bits, 0 meaning 16 + uint8_t heads; // 0 = both, 1 = side 0 only, 2 = side 1 only + uint8_t resolution; // 25ns * (resolution+1) + uint8_t checksum[4]; // of data after this point + uint8_t track[165][4]; // track offsets, not necessarily 165 +}; + +enum +{ + SCP_FLAG_INDEXED = (1<<0), + SCP_FLAG_96TPI = (1<<1), + SCP_FLAG_360RPM = (1<<2), + SCP_FLAG_NORMALIZED = (1<<3), + SCP_FLAG_READWRITE = (1<<4), + SCP_FLAG_FOOTER = (1<<5) +}; + +struct ScpTrack +{ + char track_id[3]; // 'TRK' + uint8_t strack; // SCP track number + struct + { + uint8_t index[4]; // time for one revolution + uint8_t length[4]; // number of bitcells + uint8_t offset[4]; // offset to bitcell data, relative to track header + } + revolution[5]; +}; + +#endif