Merge pull request #925 from aaru-dps/fakeshemp/scp-refactor

Refactor SCP
This commit is contained in:
2026-04-04 13:53:14 +01:00
committed by GitHub
7 changed files with 355 additions and 54 deletions

View File

@@ -47,4 +47,10 @@ public sealed partial class SuperCardPro
readonly byte[] _scpSignature = "SCP"u8.ToArray();
/// <summary>SuperCardPro track header signature: "TRK"</summary>
readonly byte[] _trkSignature = "TRK"u8.ToArray();
/// <summary>Per SCP spec: Extended mode TDH table offset (when FLAGS bit 6 is set)</summary>
const uint EXTENDED_MODE_TDH_OFFSET = 0x80;
/// <summary>Per SCP spec: Extended mode reserved bytes size (bytes 0x10-0x7F when FLAGS bit 6 is set)</summary>
const uint EXTENDED_MODE_RESERVED_SIZE = 0x70;
}

View File

@@ -51,6 +51,8 @@ public sealed partial class SuperCardPro
AtariFMEx = 0x12,
AtariSTSS = 0x14,
AtariSTDS = 0x15,
AtariSTSSHD = 0x16, // Per SCP spec v2.5: Atari ST SS HD
AtariSTDSHD = 0x17, // Per SCP spec v2.5: Atari ST DS HD
AppleII = 0x20,
AppleIIPro = 0x21,
Apple400K = 0x24,
@@ -85,21 +87,21 @@ public sealed partial class SuperCardPro
[Flags]
public enum ScpFlags : byte
{
/// <summary>If set, flux starts at index pulse</summary>
StartsAtIndex = 0x00,
/// <summary>If set, drive is 96tpi, else 48tpi</summary>
/// <summary>Per SCP spec: If set (bit 0), flux starts at index pulse</summary>
StartsAtIndex = 0x01,
/// <summary>Per SCP spec: If set (bit 1), drive is 96tpi, else 48tpi</summary>
Tpi = 0x02,
/// <summary>If set, drive is 360rpm, else 300rpm</summary>
/// <summary>Per SCP spec: If set (bit 2), drive is 360rpm, else 300rpm</summary>
Rpm = 0x04,
/// <summary>If set, image contains normalized data</summary>
/// <summary>Per SCP spec: If set (bit 3), image contains normalized data</summary>
Normalized = 0x08,
/// <summary>If set, image is read/write capable</summary>
/// <summary>Per SCP spec: If set (bit 4), image is read/write capable</summary>
Writable = 0x10,
/// <summary>If set, image has footer</summary>
/// <summary>Per SCP spec: If set (bit 5), image has footer</summary>
HasFooter = 0x20,
/// <summary>If set, image is not floppy</summary>
/// <summary>Per SCP spec: If set (bit 6), image is not floppy (extended mode)</summary>
NotFloppy = 0x40,
/// <summary>If set, image was not generated by SuperCard Pro device</summary>
/// <summary>Per SCP spec: If set (bit 7), image was not generated by SuperCard Pro device</summary>
CreatedByOtherDevice = 0x80
}

View File

@@ -59,17 +59,28 @@ public sealed partial class SuperCardPro
/// <summary>
/// Takes a Head, Track and Sub-Track representation and converts it to the Track representation used by SCP.
/// For single-sided disks: side 0 uses even entries (0,2,4...), side 1 uses odd entries (1,3,5...).
/// For double-sided disks: standard head + track * 2.
/// </summary>
/// <param name="head">The head number</param>
/// <param name="track">The track number</param>
/// <param name="subTrack">The sub-track number</param>
/// <param name="heads">Per SCP spec: 0=both heads, 1=side 0 only, 2=side 1 only</param>
/// <returns>SCP format track number</returns>
static long HeadTrackSubToScpTrack(uint head, ushort track, byte subTrack, byte heads)
{
// Per SCP spec: For single-sided disks, entries are skipped
// Side 0 only: uses even entries (0,2,4,6...)
// Side 1 only: uses odd entries (1,3,5,7...)
if(heads == 1) // Side 0 only
return track * 2;
// ReSharper disable once UnusedParameter.Local
static long HeadTrackSubToScpTrack(uint head, ushort track, byte subTrack) =>
if(heads == 2) // Side 1 only
return track * 2 + 1;
// TODO: Support single-sided disks
head + track * 2;
// Double-sided: standard head + track * 2
return head + track * 2;
}
static byte[] UInt32ToFluxRepresentation(uint ticks)
{
@@ -164,4 +175,134 @@ public sealed partial class SuperCardPro
return sum;
}
/// <summary>
/// Reads flux data with overflow handling.
/// When a bit cell time is 0x0000, it indicates no flux transition for at least 65536*25ns.
/// Multiple consecutive 0x0000 entries accumulate (each adds 65536*25ns).
/// </summary>
static void ReadFluxDataWithOverflow(BinaryReader reader, ulong trackLength, List<byte> output)
{
for(ulong j = 0; j < trackLength; j++)
{
ushort rawValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
// Per SCP spec: 0x0000 indicates overflow (no flux transition for >= 65536*25ns)
// When this occurs, the next bit cell time will be added to 65536 (or more if multiple 0x0000)
// e.g., 0x0000, 0x0000, 0x7FFF = 65536 + 65536 + 32767 = 163839
if(rawValue == 0)
{
// Count consecutive 0x0000 entries to accumulate overflow
ulong overflowCount = 1;
// Look ahead to find next non-zero value or end of track
while(j + overflowCount < trackLength)
{
long savedPosition = reader.BaseStream.Position;
ushort nextValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
if(nextValue == 0)
{
overflowCount++;
}
else
{
// Found non-zero value - restore position so we can read it again
reader.BaseStream.Position = savedPosition;
// Per SCP spec: Next non-zero value is added to accumulated overflow
// overflowCount * 65536 + nextValue = total bit cell time
nextValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
uint overflowTotal = (uint)(overflowCount * 65536) + nextValue;
output.AddRange(UInt32ToFluxRepresentation(overflowTotal));
// We've consumed overflowCount overflow entries (0x0000) plus 1 next value entry
// Total entries consumed: overflowCount + 1
// j will be incremented by loop, so advance by (overflowCount + 1 - 1) = overflowCount
j += overflowCount;
goto continueLoop;
}
}
// End of track reached while in overflow - output just the overflow
// This shouldn't normally happen in valid SCP files, but handle it gracefully
uint overflowTotalOnly = (uint)(overflowCount * 65536);
output.AddRange(UInt32ToFluxRepresentation(overflowTotalOnly));
// We've consumed overflowCount entries, j will increment, so advance by overflowCount - 1
j += overflowCount - 1;
if(j + 1 >= trackLength) break;
continueLoop:
continue;
}
output.AddRange(UInt16ToFluxRepresentation(rawValue));
}
}
/// <summary>
/// Reads ASCII timestamp string between track data and footer.
/// Timestamp format varies by locale (e.g., "1/05/2014 5:15:21 PM").
/// Valid ASCII range is 0x30-0x5F. Returns null if no valid timestamp found.
/// </summary>
static string ReadTimestamp(Stream stream, long afterTrackDataPosition)
{
// Per SCP spec: Timestamp appears after track data, before footer
// Check if we're at a valid position to read timestamp
if(stream.Position < afterTrackDataPosition)
stream.Position = afterTrackDataPosition;
// Per SCP spec: Timestamp is ASCII, valid range 0x30-0x5F
// Check if first byte is valid ASCII
long savedPosition = stream.Position;
if(stream.Position >= stream.Length) return null;
int firstByte = stream.ReadByte();
if(firstByte < 0) return null;
// Per SCP spec: Valid ASCII timestamp characters are 0x30-0x5F
// But we should allow wider range for actual timestamp strings (printable ASCII)
if(firstByte < 0x20 || firstByte > 0x7E)
{
stream.Position = savedPosition;
return null;
}
stream.Position = savedPosition;
// Read until we hit invalid ASCII or null terminator or reasonable length limit
var timestampBytes = new List<byte>();
int maxLength = 256; // Reasonable max timestamp length
for(int i = 0; i < maxLength; i++)
{
if(stream.Position >= stream.Length) break;
int b = stream.ReadByte();
if(b < 0) break; // EOF
if(b == 0) break; // Null terminator
// Per SCP spec: ASCII characters - allow printable range
if(b < 0x20 || b > 0x7E) // Outside printable ASCII
{
stream.Position--; // Unread this byte
break;
}
timestampBytes.Add((byte)b);
}
if(timestampBytes.Count == 0) return null;
return Encoding.ASCII.GetString(timestampBytes.ToArray()).Trim();
}
}

View File

@@ -88,8 +88,66 @@ public sealed partial class SuperCardPro
/// <inheritdoc />
public IEnumerable<MediaType> SupportedMediaTypes =>
[
// TODO: SCP supports a lot more formats, please add more whence tested.
MediaType.DOS_35_DS_DD_9, MediaType.DOS_35_HD, MediaType.DOS_525_DS_DD_9, MediaType.DOS_525_HD,
// Apple formats
MediaType.Apple32SS, MediaType.Apple32DS, MediaType.Apple33SS, MediaType.Apple33DS,
MediaType.AppleSonySS, MediaType.AppleSonyDS, MediaType.AppleFileWare,
// IBM PC/DOS formats - 5.25"
MediaType.DOS_525_SS_DD_8, MediaType.DOS_525_SS_DD_9,
MediaType.DOS_525_DS_DD_8, MediaType.DOS_525_DS_DD_9, MediaType.DOS_525_HD,
// IBM PC/DOS formats - 3.5"
MediaType.DOS_35_SS_DD_8, MediaType.DOS_35_SS_DD_9,
MediaType.DOS_35_DS_DD_8, MediaType.DOS_35_DS_DD_9, MediaType.DOS_35_HD, MediaType.DOS_35_ED,
// Microsoft formats
MediaType.DMF, MediaType.DMF_82, MediaType.XDF_525, MediaType.XDF_35,
// Atari formats
MediaType.ATARI_525_SD, MediaType.ATARI_525_DD, MediaType.ATARI_525_ED,
MediaType.ATARI_35_SS_DD, MediaType.ATARI_35_DS_DD,
MediaType.ATARI_35_SS_DD_11, MediaType.ATARI_35_DS_DD_11,
// Commodore/Amiga formats
MediaType.CBM_35_DD, MediaType.CBM_AMIGA_35_DD, MediaType.CBM_AMIGA_35_HD,
MediaType.CBM_1540, MediaType.CBM_1540_Ext, MediaType.CBM_1571,
// NEC/Sharp formats
MediaType.NEC_525_SS, MediaType.NEC_525_DS, MediaType.NEC_525_HD,
MediaType.NEC_35_HD_8, MediaType.NEC_35_HD_15, MediaType.NEC_35_TD,
MediaType.SHARP_525, MediaType.SHARP_525_9, MediaType.SHARP_35, MediaType.SHARP_35_9,
// 8" formats
MediaType.NEC_8_SD, MediaType.NEC_8_DD,
MediaType.ECMA_99_8, MediaType.ECMA_69_8,
// IBM formats
MediaType.IBM23FD, MediaType.IBM33FD_128, MediaType.IBM33FD_256, MediaType.IBM33FD_512,
MediaType.IBM43FD_128, MediaType.IBM43FD_256,
MediaType.IBM53FD_256, MediaType.IBM53FD_512, MediaType.IBM53FD_1024,
// DEC formats
MediaType.RX01, MediaType.RX02, MediaType.RX03, MediaType.RX50,
// Acorn formats
MediaType.ACORN_525_SS_SD_40, MediaType.ACORN_525_SS_SD_80,
MediaType.ACORN_525_SS_DD_40, MediaType.ACORN_525_SS_DD_80, MediaType.ACORN_525_DS_DD,
MediaType.ACORN_35_DS_DD, MediaType.ACORN_35_DS_HD,
// ECMA standard formats
MediaType.ECMA_54, MediaType.ECMA_59, MediaType.ECMA_66, MediaType.ECMA_69_8,
MediaType.ECMA_69_15, MediaType.ECMA_69_26, MediaType.ECMA_70, MediaType.ECMA_78,
MediaType.ECMA_78_2, MediaType.ECMA_99_15, MediaType.ECMA_99_26, MediaType.ECMA_100,
MediaType.ECMA_125, MediaType.ECMA_147,
// FDFORMAT formats
MediaType.FDFORMAT_525_DD, MediaType.FDFORMAT_525_HD,
MediaType.FDFORMAT_35_DD, MediaType.FDFORMAT_35_HD,
// Other formats
MediaType.Apricot_35, MediaType.MetaFloppy_Mod_I, MediaType.MetaFloppy_Mod_II,
// Unknown/fallback
MediaType.Unknown
];

View File

@@ -48,6 +48,7 @@ public sealed partial class SuperCardPro
#region IWritableFluxImage Members
/// <inheritdoc />
/// <summary>Opens and parses SuperCard Pro flux image file</summary>
public ErrorNumber Open(IFilter imageFilter)
{
Header = new ScpHeader();
@@ -56,8 +57,12 @@ public sealed partial class SuperCardPro
_scpFilter = imageFilter;
// Per SCP spec: Minimum file size is header size (bytes 0x00-0x0F + TDH offset table)
if(_scpStream.Length < Marshal.SizeOf<ScpHeader>()) return ErrorNumber.InvalidArgument;
// Per SCP spec: Read header starting at byte 0x00
// Bytes 0x00-0x02: "SCP" signature
// Byte 0x03: Version/revision (Version<<4|Revision)
var hdr = new byte[Marshal.SizeOf<ScpHeader>()];
_scpStream.EnsureRead(hdr, 0, Marshal.SizeOf<ScpHeader>());
@@ -76,32 +81,59 @@ public sealed partial class SuperCardPro
AaruLogging.Debug(MODULE_NAME, "header.resolution = {0}", Header.resolution);
AaruLogging.Debug(MODULE_NAME, "header.checksum = 0x{0:X8}", Header.checksum);
AaruLogging.Debug(MODULE_NAME, "header.flags.StartsAtIndex = {0}", Header.flags == ScpFlags.StartsAtIndex);
// Per SCP spec: FLAGS byte (0x08) bit definitions
// Bit 0 (INDEX): cleared = no index reference, set = flux starts at index
// Bit 1 (TPI): cleared = 48TPI, set = 96TPI (5.25" drives only)
// Bit 2 (RPM): cleared = 300 RPM, set = 360 RPM
// Bit 3 (TYPE): cleared = original flux, set = normalized flux
// Bit 4 (MODE): cleared = read-only, set = read/write capable
// Bit 5 (FOOTER): cleared = no footer, set = footer present
// Bit 6 (EXTENDED MODE): cleared = floppy only, set = extended mode (tapes/hard drives)
// Bit 7 (FLUX CREATOR): cleared = SuperCard Pro, set = other device
AaruLogging.Debug(MODULE_NAME, "header.flags.StartsAtIndex = {0}", Header.flags.HasFlag(ScpFlags.StartsAtIndex));
AaruLogging.Debug(MODULE_NAME, "header.flags.Tpi = {0}", Header.flags == ScpFlags.Tpi ? "96tpi" : "48tpi");
AaruLogging.Debug(MODULE_NAME, "header.flags.Tpi = {0}", Header.flags.HasFlag(ScpFlags.Tpi) ? "96tpi" : "48tpi");
AaruLogging.Debug(MODULE_NAME, "header.flags.Rpm = {0}", Header.flags == ScpFlags.Rpm ? "360rpm" : "300rpm");
AaruLogging.Debug(MODULE_NAME, "header.flags.Rpm = {0}", Header.flags.HasFlag(ScpFlags.Rpm) ? "360rpm" : "300rpm");
AaruLogging.Debug(MODULE_NAME, "header.flags.Normalized = {0}", Header.flags == ScpFlags.Normalized);
AaruLogging.Debug(MODULE_NAME, "header.flags.Normalized = {0}", Header.flags.HasFlag(ScpFlags.Normalized));
AaruLogging.Debug(MODULE_NAME, "header.flags.Writable = {0}", Header.flags == ScpFlags.Writable);
AaruLogging.Debug(MODULE_NAME, "header.flags.Writable = {0}", Header.flags.HasFlag(ScpFlags.Writable));
AaruLogging.Debug(MODULE_NAME, "header.flags.HasFooter = {0}", Header.flags == ScpFlags.HasFooter);
AaruLogging.Debug(MODULE_NAME, "header.flags.HasFooter = {0}", Header.flags.HasFlag(ScpFlags.HasFooter));
AaruLogging.Debug(MODULE_NAME, "header.flags.NotFloppy = {0}", Header.flags == ScpFlags.NotFloppy);
AaruLogging.Debug(MODULE_NAME, "header.flags.NotFloppy = {0}", Header.flags.HasFlag(ScpFlags.NotFloppy));
AaruLogging.Debug(MODULE_NAME,
"header.flags.CreatedByOtherDevice = {0}",
Header.flags == ScpFlags.CreatedByOtherDevice);
Header.flags.HasFlag(ScpFlags.CreatedByOtherDevice));
// Per SCP spec: First 3 bytes must be "SCP" signature
if(!_scpSignature.SequenceEqual(Header.signature)) return ErrorNumber.InvalidArgument;
// Extended mode (FLAGS bit 6) indicates non-floppy media (tapes/hard drives)
// These remain unimplemented for now
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
// Since we only support floppy disks, always set MetadataMediaType to BlockMedia
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
ScpTracks = new Dictionary<byte, TrackHeader>();
// Per SCP spec: For single-sided disks, skip TDH entries appropriately
// heads = 0: both heads (read all tracks)
// heads = 1: side 0 only (read even entries: 0,2,4,6...)
// heads = 2: side 1 only (read odd entries: 1,3,5,7...)
for(byte t = Header.start; t <= Header.end; t++)
{
if(t >= Header.offsets.Length) break;
// Skip entries based on single-sided disk configuration
if(Header.heads == 1 && (t % 2) != 0) continue; // Side 0 only - skip odd entries
if(Header.heads == 2 && (t % 2) == 0) continue; // Side 1 only - skip even entries
if(Header.offsets[t] == 0) continue; // Per SCP spec: 0x00000000 means no flux data for this track
_scpStream.Position = Header.offsets[t];
var trk = new TrackHeader
@@ -110,9 +142,11 @@ public sealed partial class SuperCardPro
Entries = new TrackEntry[Header.revolutions]
};
// Per SCP spec: Track Data Header (TDH) starts with "TRK" signature (3 bytes) + track number (1 byte)
_scpStream.EnsureRead(trk.Signature, 0, trk.Signature.Length);
trk.TrackNumber = (byte)_scpStream.ReadByte();
// Per SCP spec: Validate TDH signature for recovery from corrupt files
if(!trk.Signature.SequenceEqual(_trkSignature))
{
AaruLogging.Debug(MODULE_NAME,
@@ -135,6 +169,8 @@ public sealed partial class SuperCardPro
AaruLogging.Debug(MODULE_NAME, Localization.Found_track_0_at_1, t, Header.offsets[t]);
// Per SCP spec: Each revolution has 3 longwords: indexTime, trackLength, dataOffset
// dataOffset is relative to start of TDH (not file start)
for(byte r = 0; r < Header.revolutions; r++)
{
var rev = new byte[Marshal.SizeOf<TrackEntry>()];
@@ -142,15 +178,13 @@ public sealed partial class SuperCardPro
trk.Entries[r] = Marshal.ByteArrayToStructureLittleEndian<TrackEntry>(rev);
// De-relative offsets
// Per SCP spec: dataOffset is relative to TDH start, convert to absolute file offset
trk.Entries[r].dataOffset += Header.offsets[t];
}
ScpTracks.Add(t, trk);
}
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
switch(Header.type)
{
case ScpDiskType.Commodore64:
@@ -184,7 +218,7 @@ public sealed partial class SuperCardPro
break;
case ScpDiskType.AtariFMEx:
break;
return ErrorNumber.NotImplemented;
case ScpDiskType.AtariSTSS:
_imageInfo.MediaType = MediaType.ATARI_35_SS_DD;
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 80);
@@ -197,6 +231,9 @@ public sealed partial class SuperCardPro
_imageInfo.Heads = 2;
break;
case ScpDiskType.AtariSTSSHD: // Per SCP spec v2.5: Atari ST SS HD
case ScpDiskType.AtariSTDSHD: // Per SCP spec v2.5: Atari ST DS HD
return ErrorNumber.NotImplemented;
case ScpDiskType.AppleII:
_imageInfo.MediaType = MediaType.Apple32DS;
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 40);
@@ -204,7 +241,7 @@ public sealed partial class SuperCardPro
break;
case ScpDiskType.AppleIIPro:
break;
return ErrorNumber.NotImplemented;
case ScpDiskType.Apple400K:
_imageInfo.MediaType = MediaType.AppleSonySS;
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 80);
@@ -253,15 +290,10 @@ public sealed partial class SuperCardPro
break;
case ScpDiskType.TandySSDD:
return ErrorNumber.NotImplemented;
case ScpDiskType.TandyDSSD:
return ErrorNumber.NotImplemented;
case ScpDiskType.TandyDSDD:
return ErrorNumber.NotImplemented;
case ScpDiskType.Ti994A:
return ErrorNumber.NotImplemented;
case ScpDiskType.RolandD20:
return ErrorNumber.NotImplemented;
case ScpDiskType.AmstradCPC:
return ErrorNumber.NotImplemented;
case ScpDiskType.Generic360K:
@@ -292,14 +324,12 @@ public sealed partial class SuperCardPro
case ScpDiskType.TapeGCR1:
case ScpDiskType.TapeGCR2:
case ScpDiskType.TapeMFM:
_imageInfo.MediaType = MediaType.UnknownTape;
break;
// Tapes remain unimplemented for now
return ErrorNumber.NotImplemented;
case ScpDiskType.HddMFM:
case ScpDiskType.HddRLL:
_imageInfo.MediaType = MediaType.GENERIC_HDD;
break;
// Hard drives remain unimplemented for now
return ErrorNumber.NotImplemented;
default:
_imageInfo.MediaType = MediaType.Unknown;
@@ -311,9 +341,21 @@ public sealed partial class SuperCardPro
break;
}
// Per SCP spec: Timestamp (if present) appears after track data, before footer
// Read timestamp if present
long lastTrackDataPosition = _scpStream.Position;
string timestamp = ReadTimestamp(_scpStream, lastTrackDataPosition);
if(timestamp != null)
{
AaruLogging.Debug(MODULE_NAME, "Found timestamp: \"{0}\"", timestamp);
// Timestamp is informative only - we use footer timestamps if footer is present
}
// Per SCP spec: Footer detection - look for "FPCS" signature
if(Header.flags.HasFlag(ScpFlags.HasFooter))
{
long position = _scpStream.Position;
long position = timestamp != null ? _scpStream.Position : lastTrackDataPosition;
_scpStream.Seek(-4, SeekOrigin.End);
while(_scpStream.Position >= position)
@@ -416,7 +458,18 @@ public sealed partial class SuperCardPro
_imageInfo.DriveFirmwareRevision =
$"{(footer.firmwareVersion & 0xF0) >> 4}.{footer.firmwareVersion & 0xF}";
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
// Per SCP spec: When FOOTER bit is set and version byte is 0, use footer version
if(Header.version == 0)
{
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
AaruLogging.Debug(MODULE_NAME,
"Using footer version (header version was 0): {0}",
_imageInfo.Version);
}
else
{
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
}
break;
}
@@ -430,7 +483,6 @@ public sealed partial class SuperCardPro
_imageInfo.ApplicationVersion = $"{(Header.version & 0xF0) >> 4}.{Header.version & 0xF}";
_imageInfo.CreationTime = imageFilter.CreationTime;
_imageInfo.LastModificationTime = imageFilter.LastWriteTime;
_imageInfo.Version = "2.4";
}
return ErrorNumber.NoError;
@@ -485,16 +537,22 @@ public sealed partial class SuperCardPro
{
buffer = null;
// Non-floppy media (tapes/hard drives) not supported
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
if(captureIndex > 0) return ErrorNumber.OutOfRange;
List<byte> tmpBuffer = [];
// Per SCP spec: If flux starts at index, add initial 0 marker
if(Header.flags.HasFlag(ScpFlags.StartsAtIndex)) tmpBuffer.Add(0);
TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)];
byte scpTrackNum = (byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
if(!ScpTracks.TryGetValue(scpTrackNum, out TrackHeader scpTrack))
return ErrorNumber.OutOfRange;
// Per SCP spec: indexTime is duration in nanoseconds/25ns for one revolution
for(var i = 0; i < Header.revolutions; i++)
tmpBuffer.AddRange(UInt32ToFluxRepresentation(scpTrack.Entries[i].indexTime));
@@ -507,15 +565,20 @@ public sealed partial class SuperCardPro
{
buffer = null;
// Non-floppy media (tapes/hard drives) not supported
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
if(HeadTrackSubToScpTrack(head, track, subTrack) > Header.end) return ErrorNumber.OutOfRange;
byte scpTrackNum = (byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
if(scpTrackNum > Header.end) return ErrorNumber.OutOfRange;
if(captureIndex > 0) return ErrorNumber.OutOfRange;
// Per SCP spec: bitCellEncoding (byte 0x09) = 0 means 16 bits, other values are for future expansion
if(Header.bitCellEncoding != 0 && Header.bitCellEncoding != 16) return ErrorNumber.NotImplemented;
TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)];
if(!ScpTracks.TryGetValue(scpTrackNum, out TrackHeader scpTrack))
return ErrorNumber.OutOfRange;
Stream stream = _scpFilter.GetDataForkStream();
var br = new BinaryReader(stream);
@@ -526,9 +589,8 @@ public sealed partial class SuperCardPro
{
br.BaseStream.Seek(scpTrack.Entries[i].dataOffset, SeekOrigin.Begin);
// TODO: Check for 0x0000
for(ulong j = 0; j < scpTrack.Entries[i].trackLength; j++)
tmpBuffer.AddRange(UInt16ToFluxRepresentation(BigEndianBitConverter.ToUInt16(br.ReadBytes(2), 0)));
// Per SCP spec: Handle 0x0000 overflow entries in flux data (strongbits protection)
ReadFluxDataWithOverflow(br, scpTrack.Entries[i].trackLength, tmpBuffer);
}
buffer = tmpBuffer.ToArray();
@@ -569,11 +631,28 @@ public sealed partial class SuperCardPro
{
byte scpTrack = kvp.Key;
// Reverse HeadTrackSubToScpTrack: scpTrack = head + track * 2
uint head = (uint)(scpTrack % 2);
ushort track = (ushort)(scpTrack / 2);
// Reverse HeadTrackSubToScpTrack based on heads configuration
// Per SCP spec: Single-sided disks use specific entry patterns
uint head;
ushort track;
const byte subTrack = 0; // SuperCardPro always has subTrack = 0
if(Header.heads == 1) // Side 0 only - even entries
{
track = (ushort)(scpTrack / 2);
head = 0;
}
else if(Header.heads == 2) // Side 1 only - odd entries
{
track = (ushort)(scpTrack / 2);
head = 1;
}
else // Double-sided - standard mapping
{
head = (uint)(scpTrack % 2);
track = (ushort)(scpTrack / 2);
}
return new FluxCapture
{
Head = head,

View File

@@ -58,6 +58,21 @@ public sealed partial class SuperCardPro
#endregion
#region Nested type: ExtendedModeHeader
/// <summary>
/// Per SCP spec: Extended mode header structure (bytes 0x10-0x7F when FLAGS bit 6 is set).
/// Reserved for future use with hard drives and tape drives.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
readonly struct ExtendedModeHeader
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)]
public readonly byte[] reserved; // 0x70 bytes reserved for extended mode variables
}
#endregion
#region Nested type: ScpHeader
[StructLayout(LayoutKind.Sequential, Pack = 1)]

View File

@@ -58,8 +58,8 @@ public sealed partial class SuperCardPro
if(subTrack != 0) return ErrorNumber.NotSupported;
Header.start = byte.Min((byte)HeadTrackSubToScpTrack(head, track, subTrack), Header.start);
Header.end = byte.Max((byte)HeadTrackSubToScpTrack(head, track, subTrack), Header.end);
Header.start = byte.Min((byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads), Header.start);
Header.end = byte.Max((byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads), Header.end);
ulong scpResolution = dataResolution / DEFAULT_RESOLUTION - 1;
@@ -72,7 +72,7 @@ public sealed partial class SuperCardPro
// SCP can only have one resolution for all tracks
if(Header.resolution != scpResolution) return ErrorNumber.NotSupported;
long scpTrack = HeadTrackSubToScpTrack(head, track, subTrack);
long scpTrack = HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
_writingStream.Seek(0x10 + 4 * scpTrack, SeekOrigin.Begin);
_writingStream.Write(BitConverter.GetBytes(_trackOffset), 0, 4);