using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
using System.Linq;
namespace BizHawk.Emulation.DiscSystem
{
///
/// Parsing Alcohol 120% files
/// Info taken from:
/// * http://forum.redump.org/post/41803/#p41803
/// * Libmirage image-mds parser - https://sourceforge.net/projects/cdemu/files/libmirage/
/// * DiscImageChef - https://github.com/claunia/DiscImageChef/blob/master/DiscImageChef.DiscImages/Alcohol120.cs
///
public class MDS_Format
{
///
/// A loose representation of an Alcohol 120 .mds file (with a few extras)
///
public class AFile
{
///
/// Full path to the MDS file
///
public string MDSPath;
///
/// MDS Header
///
public AHeader Header = new AHeader();
///
/// List of MDS session blocks
///
public List Sessions = new List();
///
/// List of track blocks
///
public List Tracks = new List();
///
/// Current parsed session objects
///
public List ParsedSession = new List();
///
/// Calculated MDS TOC entries (still to be parsed into BizHawk)
///
public List TOCEntries = new List();
}
public class AHeader
{
///
/// Standard alcohol 120% signature - usually "MEDIA DESCRIPTOR"
///
public string Signature; // 16 bytes
///
/// Alcohol version?
///
public byte[] Version; // 2 bytes
///
/// The medium type
/// * 0x00 - CD
/// * 0x01 - CD-R
/// * 0x02 - CD-RW
/// * 0x10 - DVD
/// * 0x12 - DVD-R
///
public int Medium;
///
/// Number of sessions
///
public int SessionCount;
///
/// Burst Cutting Area length
///
public int BCALength;
///
/// Burst Cutting Area data offset
///
public long BCAOffset;
///
/// Offset to disc (DVD?) structures
///
public long StructureOffset;
///
/// Offset to the first session block
///
public long SessionOffset;
///
/// Data Position Measurement offset
///
public long DPMOffset;
///
/// Parse mds stream for the header
///
public AHeader Parse(Stream stream)
{
EndianBitConverter bc = EndianBitConverter.CreateForLittleEndian();
EndianBitConverter bcBig = EndianBitConverter.CreateForBigEndian();
byte[] header = new byte[88];
stream.Read(header, 0, 88);
this.Signature = Encoding.ASCII.GetString(header.Take(16).ToArray());
this.Version = header.Skip(16).Take(2).ToArray();
this.Medium = bc.ToInt16(header.Skip(18).Take(2).ToArray());
this.SessionCount = bc.ToInt16(header.Skip(20).Take(2).ToArray());
this.BCALength = bc.ToInt16(header.Skip(26).Take(2).ToArray());
this.BCAOffset = bc.ToInt32(header.Skip(36).Take(4).ToArray());
this.StructureOffset = bc.ToInt32(header.Skip(64).Take(4).ToArray());
this.SessionOffset = bc.ToInt32(header.Skip(80).Take(4).ToArray());
this.DPMOffset = bc.ToInt32(header.Skip(84).Take(4).ToArray());
return this;
}
}
///
/// MDS session block representation
///
public class ASession
{
public int SessionStart; /* Session's start address */
public int SessionEnd; /* Session's end address */
public int SessionNumber; /* Session number */
public byte AllBlocks; /* Number of all data blocks. */
public byte NonTrackBlocks; /* Number of lead-in data blocks */
public int FirstTrack; /* First track in session */
public int LastTrack; /* Last track in session */
public long TrackOffset; /* Offset of lead-in+regular track data blocks. */
}
///
/// Representation of an MDS track block
/// For convenience (and extra confusion) this also holds the track extrablock, filename(footer) block infos
/// as well as the calculated image filepath as specified in the MDS file
///
public class ATrack
{
///
/// The specified data mode
/// 0x00 - None (no data)
/// 0x02 - DVD
/// 0xA9 - Audio
/// 0xAA - Mode1
/// 0xAB - Mode2
/// 0xAC - Mode2 Form1
/// 0xAD - Mode2 Form2
///
public byte Mode; /* Track mode */
///
/// Subchannel mode for the track (0x00 = None, 0x08 = Interleaved)
///
public byte SubMode; /* Subchannel mode */
/* These are the fields from Sub-channel Q information, which are
also returned in full TOC by READ TOC/PMA/ATIP command */
public int ADR_Control; /* Adr/Ctl */
public int TrackNo; /* Track number field */
public int Point; /* Point field (= track number for track entries) */
public int AMin; /* Min */
public int ASec; /* Sec */
public int AFrame; /* Frame */
public int Zero; /* Zero */
public int PMin; /* PMin */
public int PSec; /* PSec */
public int PFrame; /* PFrame */
public long ExtraOffset; /* Start offset of this track's extra block. */
public int SectorSize; /* Sector size. */
public long PLBA; /* Track start sector (PLBA). */
public ulong StartOffset; /* Track start offset (from beginning of MDS file) */
public long Files; /* Number of filenames for this track */
public long FooterOffset; /* Start offset of footer (from beginning of MDS file) */
///
/// Track extra block
///
public ATrackExtra ExtraBlock = new ATrackExtra();
///
/// List of footer(filename) blocks for this track
///
public List FooterBlocks = new List();
///
/// List of the calculated full paths to this track's image file
/// The MDS file itself may contain a filename, or just an *.extension
///
public List ImageFileNamePaths = new List();
public int BlobIndex;
}
///
/// Extra track block
///
public class ATrackExtra
{
public long Pregap; /* Number of sectors in pregap. */
public long Sectors; /* Number of sectors in track. */
}
///
/// Footer (filename) block - potentially one for every track
///
public class AFooter
{
public long FilenameOffset; /* Start offset of image filename string (from beginning of mds file) */
public long WideChar; /* Seems to be set to 1 if widechar filename is used */
}
///
/// Represents a parsed MDS TOC entry
///
public class ATOCEntry
{
public ATOCEntry(int entryNum)
{
EntryNum = entryNum;
}
///
/// these should be 0-indexed
///
public int EntryNum;
///
/// 1-indexed - the session that this entry belongs to
///
public int Session;
// ///
// /// this seems just to be the LBA corresponding to AMIN:ASEC:AFRAME (give or take 150). It's not stored on the disc, and it's redundant.
// ///
// public int ALBA;
///
/// this seems just to be the LBA corresponding to PMIN:PSEC:PFRAME (give or take 150).
///
public int PLBA;
//these correspond pretty directly to values in the Q subchannel fields
//NOTE: they're specified as absolute MSF. That means, they're 2 seconds off from what they should be when viewed as final TOC values
public int ADR_Control;
public int TrackNo;
public int Point;
public int AMin;
public int ASec;
public int AFrame;
public int Zero;
public int PMin;
public int PSec;
public int PFrame;
public int SectorSize;
public long TrackOffset;
///
/// List of the calculated full paths to this track's image file
/// The MDS file itself may contain a filename, or just an *.extension
///
public List ImageFileNamePaths = new List();
///
/// Track extra block
///
public ATrackExtra ExtraBlock = new ATrackExtra();
public int BlobIndex;
}
/// header is malformed or identifies file as MDS 2.x, or any track has a DVD mode
public AFile Parse(Stream stream)
{
EndianBitConverter bc = EndianBitConverter.CreateForLittleEndian();
EndianBitConverter bcBig = EndianBitConverter.CreateForBigEndian();
bool isDvd = false;
var aFile = new AFile
{
MDSPath = (stream as FileStream).Name
};
stream.Seek(0, SeekOrigin.Begin);
// check whether the header in the mds file is long enough
if (stream.Length < 88) throw new MDSParseException("Malformed MDS format: The descriptor file does not appear to be long enough.");
// parse header
aFile.Header = aFile.Header.Parse(stream);
// check version to make sure this is only v1.x
// currently NO support for version 2.x
if (aFile.Header.Version[0] > 1)
{
throw new MDSParseException($"MDS Parse Error: Only MDS version 1.x is supported!\nDetected version: {aFile.Header.Version[0]}.{aFile.Header.Version[1]}");
}
// parse sessions
Dictionary aSessions = new Dictionary();
stream.Seek(aFile.Header.SessionOffset, SeekOrigin.Begin);
for (int se = 0; se < aFile.Header.SessionCount; se++)
{
byte[] sessionHeader = new byte[24];
stream.Read(sessionHeader, 0, 24);
//sessionHeader.Reverse().ToArray();
var session = new ASession
{
SessionStart = bc.ToInt32(sessionHeader.Take(4).ToArray()),
SessionEnd = bc.ToInt32(sessionHeader.Skip(4).Take(4).ToArray()),
SessionNumber = bc.ToInt16(sessionHeader.Skip(8).Take(2).ToArray()),
AllBlocks = sessionHeader[10],
NonTrackBlocks = sessionHeader[11],
FirstTrack = bc.ToInt16(sessionHeader.Skip(12).Take(2).ToArray()),
LastTrack = bc.ToInt16(sessionHeader.Skip(14).Take(2).ToArray()),
TrackOffset = bc.ToInt32(sessionHeader.Skip(20).Take(4).ToArray())
};
//mdsf.Sessions.Add(session);
aSessions.Add(session.SessionNumber, session);
}
long footerOffset = 0;
// parse track blocks
Dictionary aTracks = new Dictionary();
// iterate through each session block
foreach (ASession session in aSessions.Values)
{
stream.Seek(session.TrackOffset, SeekOrigin.Begin);
//Dictionary sessionToc = new Dictionary();
// iterate through every block specified in each session
for (int bl = 0; bl < session.AllBlocks; bl++)
{
byte[] trackHeader;
ATrack track = new ATrack();
trackHeader = new byte[80];
stream.Read(trackHeader, 0, 80);
track.Mode = trackHeader[0];
track.SubMode = trackHeader[1];
track.ADR_Control = trackHeader[2];
track.TrackNo = trackHeader[3];
track.Point = trackHeader[4];
track.AMin = trackHeader[5];
track.ASec = trackHeader[6];
track.AFrame = trackHeader[7];
track.Zero = trackHeader[8];
track.PMin = trackHeader[9];
track.PSec = trackHeader[10];
track.PFrame = trackHeader[11];
track.ExtraOffset = bc.ToInt32(trackHeader.Skip(12).Take(4).ToArray());
track.SectorSize = bc.ToInt16(trackHeader.Skip(16).Take(2).ToArray());
track.PLBA = bc.ToInt32(trackHeader.Skip(36).Take(4).ToArray());
track.StartOffset = BitConverter.ToUInt64(trackHeader.Skip(40).Take(8).ToArray(), 0);
track.Files = bc.ToInt32(trackHeader.Skip(48).Take(4).ToArray());
track.FooterOffset = bc.ToInt32(trackHeader.Skip(52).Take(4).ToArray());
if (track.Mode == 0x02)
{
isDvd = true;
throw new MDSParseException("DVD Detected. Not currently supported!");
}
// check for track extra block - this can probably be handled in a separate loop,
// but I'll just store the current stream position then seek forward to the extra block for this track
long currPos = stream.Position;
// Only CDs have extra blocks - for DVDs ExtraOffset = track length
if (track.ExtraOffset > 0 && !isDvd)
{
byte[] extHeader = new byte[8];
stream.Seek(track.ExtraOffset, SeekOrigin.Begin);
stream.Read(extHeader, 0, 8);
track.ExtraBlock.Pregap = bc.ToInt32(extHeader.Take(4).ToArray());
track.ExtraBlock.Sectors = bc.ToInt32(extHeader.Skip(4).Take(4).ToArray());
stream.Seek(currPos, SeekOrigin.Begin);
}
else if (isDvd == true)
{
track.ExtraBlock.Sectors = track.ExtraOffset;
}
// read the footer/filename block for this track
currPos = stream.Position;
long numOfFilenames = track.Files;
for (long fi = 1; fi <= numOfFilenames; fi++)
{
// skip leadin/out info tracks
if (track.FooterOffset == 0)
continue;
byte[] foot = new byte[16];
stream.Seek(track.FooterOffset, SeekOrigin.Begin);
stream.Read(foot, 0, 16);
var f = new AFooter
{
FilenameOffset = bc.ToInt32(foot.Take(4).ToArray()),
WideChar = bc.ToInt32(foot.Skip(4).Take(4).ToArray())
};
track.FooterBlocks.Add(f);
track.FooterBlocks = track.FooterBlocks.Distinct().ToList();
// parse the filename string
string fileName = "*.mdf";
if (f.FilenameOffset > 0)
{
// filename offset is present
stream.Seek(f.FilenameOffset, SeekOrigin.Begin);
byte[] fname;
if (numOfFilenames == 1)
{
if (aFile.Header.DPMOffset == 0)
{
// filename is in the remaining space to EOF
fname = new byte[stream.Length - stream.Position];
}
else
{
// filename is in the remaining space to EOF + dpm offset
fname = new byte[aFile.Header.DPMOffset - stream.Position];
}
}
else
{
// looks like each filename string is 6 bytes with a trailing \0
fname = new byte[6];
}
// read the filename
stream.Read(fname, 0, fname.Length);
// if widechar is 1 filename is stored using 16-bit, otherwise 8-bit is used
if (f.WideChar == 1)
fileName = Encoding.Unicode.GetString(fname).TrimEnd('\0');
else
fileName = Encoding.Default.GetString(fname).TrimEnd('\0');
}
else
{
// assume an MDF file with the same name as the MDS
}
string dir = Path.GetDirectoryName(aFile.MDSPath);
if (f.FilenameOffset == 0 ||
string.Compare(fileName, "*.mdf", StringComparison.InvariantCultureIgnoreCase) == 0)
{
fileName = $@"{dir}\{Path.GetFileNameWithoutExtension(aFile.MDSPath)}.mdf";
}
else
{
fileName = $@"{dir}\{fileName}";
}
track.ImageFileNamePaths.Add(fileName);
track.ImageFileNamePaths = track.ImageFileNamePaths.Distinct().ToList();
}
stream.Position = currPos;
aTracks.Add(track.Point, track);
aFile.Tracks.Add(track);
if (footerOffset == 0)
footerOffset = track.FooterOffset;
}
}
// build custom session object
aFile.ParsedSession = new List();
foreach (var s in aSessions.Values)
{
Session session = new Session();
if (!aTracks.TryGetValue(s.FirstTrack, out var startTrack))
{
break;
}
if (!aTracks.TryGetValue(s.LastTrack, out var endTrack))
{
break;
}
session.StartSector = startTrack.PLBA;
session.StartTrack = s.FirstTrack;
session.SessionSequence = s.SessionNumber;
session.EndSector = endTrack.PLBA + endTrack.ExtraBlock.Sectors - 1;
session.EndTrack = s.LastTrack;
aFile.ParsedSession.Add(session);
}
// now build the TOC object
foreach (var se in aFile.ParsedSession)
foreach (var t in aTracks.Values
.Where(a => se.StartTrack <= a.TrackNo && a.TrackNo <= se.EndTrack)
.OrderBy(a => a.TrackNo))
{
aFile.TOCEntries.Add(new ATOCEntry(t.Point)
{
ADR_Control = t.ADR_Control,
AFrame = t.AFrame,
AMin = t.AMin,
ASec = t.ASec,
BlobIndex = t.BlobIndex,
EntryNum = t.TrackNo,
ExtraBlock = t.ExtraBlock,
ImageFileNamePaths = t.ImageFileNamePaths,
PFrame = t.PFrame,
PLBA = Convert.ToInt32(t.PLBA),
PMin = t.PMin,
Point = t.Point,
PSec = t.PSec,
SectorSize = t.SectorSize,
Session = se.SessionSequence,
TrackOffset = Convert.ToInt64(t.StartOffset),
Zero = t.Zero
});
}
return aFile;
}
///
/// Custom session object
///
public class Session
{
public long StartSector;
public int StartTrack;
public int SessionSequence;
public long EndSector;
public int EndTrack;
}
public class MDSParseException : Exception
{
public MDSParseException(string message) : base(message) { }
}
public class LoadResults
{
public List RawTOCEntries;
public AFile ParsedMDSFile;
public bool Valid;
public MDSParseException FailureException;
public string MdsPath;
}
public static LoadResults LoadMDSPath(string path)
{
var ret = new LoadResults { MdsPath = path };
//ret.MdfPath = Path.ChangeExtension(path, ".mdf");
try
{
if (!File.Exists(path)) throw new MDSParseException("Malformed MDS format: nonexistent MDS file!");
AFile mdsf;
using (var infMDS = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
mdsf = new MDS_Format().Parse(infMDS);
ret.ParsedMDSFile = mdsf;
ret.Valid = true;
}
catch (MDSParseException ex)
{
ret.FailureException = ex;
}
return ret;
}
/// path reference no longer points to file
Dictionary MountBlobs(AFile mdsf, Disc disc)
{
Dictionary BlobIndex = new Dictionary();
int count = 0;
foreach (var track in mdsf.Tracks)
{
foreach (var file in track.ImageFileNamePaths.Distinct())
{
if (!File.Exists(file))
throw new MDSParseException($"Malformed MDS format: nonexistent image file: {file}");
IBlob mdfBlob = null;
long mdfLen = -1;
//mount the file
if (mdfBlob == null)
{
var mdfFile = new Disc.Blob_RawFile() { PhysicalPath = file };
mdfLen = mdfFile.Length;
mdfBlob = mdfFile;
}
bool dupe = false;
foreach (var re in disc.DisposableResources)
{
if (re.ToString() == mdfBlob.ToString())
dupe = true;
}
if (!dupe)
{
// wrap in zeropadadapter
disc.DisposableResources.Add(mdfBlob);
BlobIndex[count++] = mdfBlob;
}
}
}
return BlobIndex;
}
RawTOCEntry EmitRawTOCEntry(ATOCEntry entry)
{
BCD2 tno, ino;
//this should actually be zero. im not sure if this is stored as BCD2 or not
tno = BCD2.FromDecimal(entry.TrackNo);
//these are special values.. I think, taken from this:
//http://www.staff.uni-mainz.de/tacke/scsi/SCSI2-14.html
//the CCD will contain Points as decimal values except for these specially converted decimal values which should stay as BCD.
//Why couldn't they all be BCD? I don't know. I guess because BCD is inconvenient, but only A0 and friends have special meaning. It's confusing.
ino = BCD2.FromDecimal(entry.Point);
if (entry.Point == 0xA0) ino.BCDValue = 0xA0;
else if (entry.Point == 0xA1) ino.BCDValue = 0xA1;
else if (entry.Point == 0xA2) ino.BCDValue = 0xA2;
// get ADR & Control from ADR_Control byte
byte adrc = Convert.ToByte(entry.ADR_Control);
var Control = adrc & 0x0F;
var ADR = adrc >> 4;
var q = new SubchannelQ
{
q_status = SubchannelQ.ComputeStatus(ADR, (EControlQ)(Control & 0xF)),
q_tno = tno,
q_index = ino,
min = BCD2.FromDecimal(entry.AMin),
sec = BCD2.FromDecimal(entry.ASec),
frame = BCD2.FromDecimal(entry.AFrame),
zero = (byte)entry.Zero,
ap_min = BCD2.FromDecimal(entry.PMin),
ap_sec = BCD2.FromDecimal(entry.PSec),
ap_frame = BCD2.FromDecimal(entry.PFrame),
q_crc = 0, //meaningless
};
return new RawTOCEntry { QData = q };
}
/// no file found at or BLOB error
public Disc LoadMDSToDisc(string mdsPath, DiscMountPolicy IN_DiscMountPolicy)
{
var loadResults = LoadMDSPath(mdsPath);
if (!loadResults.Valid)
throw loadResults.FailureException;
Disc disc = new Disc();
// load all blobs
Dictionary BlobIndex = MountBlobs(loadResults.ParsedMDSFile, disc);
var mdsf = loadResults.ParsedMDSFile;
//generate DiscTOCRaw items from the ones specified in the MDS file
disc.RawTOCEntries = new List();
foreach (var entry in mdsf.TOCEntries)
{
disc.RawTOCEntries.Add(EmitRawTOCEntry(entry));
}
//analyze the RAWTocEntries to figure out what type of track track 1 is
var tocSynth = new Synthesize_DiscTOC_From_RawTOCEntries_Job() { Entries = disc.RawTOCEntries };
tocSynth.Run();
// now build the sectors
int currBlobIndex = 0;
foreach (var session in mdsf.ParsedSession)
{
for (int i = session.StartTrack; i <= session.EndTrack; i++)
{
int relMSF = -1;
var track = mdsf.TOCEntries.Where(t => t.Point == i).FirstOrDefault();
if (track == null)
break;
// ignore the info entries
if (track.Point == 0xA0 ||
track.Point == 0xA1 ||
track.Point == 0xA2)
{
continue;
}
// get the blob(s) for this track
// its probably a safe assumption that there will be only one blob per track,
// but i'm still not 100% sure on this
var tr = (from a in mdsf.TOCEntries
where a.Point == i
select a).FirstOrDefault();
if (tr == null)
throw new MDSParseException("BLOB Error!");
List blobstrings = new List();
foreach (var t in tr.ImageFileNamePaths)
{
if (!blobstrings.Contains(t))
blobstrings.Add(t);
}
var tBlobs = (from a in tr.ImageFileNamePaths
select a).ToList();
if (tBlobs.Count < 1)
throw new MDSParseException("BLOB Error!");
// is the currBlob valid for this track, or do we need to increment?
string bString = tBlobs.First();
IBlob mdfBlob = null;
// check for track pregap and create if necessary
// this is specified in the track extras block
if (track.ExtraBlock.Pregap > 0)
{
CUE.CueTrackType pregapTrackType = CUE.CueTrackType.Audio;
if (tocSynth.Result.TOCItems[1].IsData)
{
if (tocSynth.Result.Session1Format == SessionFormat.Type20_CDXA)
pregapTrackType = CUE.CueTrackType.Mode2_2352;
else if (tocSynth.Result.Session1Format == SessionFormat.Type10_CDI)
pregapTrackType = CUE.CueTrackType.CDI_2352;
else if (tocSynth.Result.Session1Format == SessionFormat.Type00_CDROM_CDDA)
pregapTrackType = CUE.CueTrackType.Mode1_2352;
}
for (int pre = 0; pre < track.ExtraBlock.Pregap; pre++)
{
relMSF++;
var ss_gap = new CUE.SS_Gap()
{
Policy = IN_DiscMountPolicy,
TrackType = pregapTrackType
};
disc._Sectors.Add(ss_gap);
int qRelMSF = pre - Convert.ToInt32(track.ExtraBlock.Pregap);
//tweak relMSF due to ambiguity/contradiction in yellowbook docs
if (!IN_DiscMountPolicy.CUE_PregapContradictionModeA)
qRelMSF++;
//setup subQ
byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption:
ss_gap.sq.SetStatus(ADR, tocSynth.Result.TOCItems[1].Control);
ss_gap.sq.q_tno = BCD2.FromDecimal(1);
ss_gap.sq.q_index = BCD2.FromDecimal(0);
ss_gap.sq.AP_Timestamp = pre;
ss_gap.sq.Timestamp = qRelMSF;
//setup subP
ss_gap.Pause = true;
}
// pregap processing completed
}
// create track sectors
long currBlobOffset = track.TrackOffset;
for (long sector = session.StartSector; sector <= session.EndSector; sector++)
{
CUE.SS_Base sBase = null;
// get the current blob from the BlobIndex
Disc.Blob_RawFile currBlob = BlobIndex[currBlobIndex] as Disc.Blob_RawFile;
long currBlobLength = currBlob.Length;
long currBlobPosition = sector;
if (currBlobPosition == currBlobLength)
currBlobIndex++;
mdfBlob = disc.DisposableResources[currBlobIndex] as Disc.Blob_RawFile;
//int userSector = 2048;
switch (track.SectorSize)
{
case 2448:
sBase = new CUE.SS_2352()
{
Policy = IN_DiscMountPolicy
};
//userSector = 2352;
break;
case 2048:
default:
sBase = new CUE.SS_Mode1_2048()
{
Policy = IN_DiscMountPolicy
};
//userSector = 2048;
break;
//throw new Exception($"Not supported: Sector Size {track.SectorSize}");
}
// configure blob
sBase.Blob = mdfBlob;
sBase.BlobOffset = currBlobOffset;
currBlobOffset += track.SectorSize; // userSector;
// add subchannel data
relMSF++;
BCD2 tno, ino;
//this should actually be zero. im not sure if this is stored as BCD2 or not
tno = BCD2.FromDecimal(track.TrackNo);
//these are special values.. I think, taken from this:
//http://www.staff.uni-mainz.de/tacke/scsi/SCSI2-14.html
//the CCD will contain Points as decimal values except for these specially converted decimal values which should stay as BCD.
//Why couldn't they all be BCD? I don't know. I guess because BCD is inconvenient, but only A0 and friends have special meaning. It's confusing.
ino = BCD2.FromDecimal(track.Point);
if (track.Point == 0xA0) ino.BCDValue = 0xA0;
else if (track.Point == 0xA1) ino.BCDValue = 0xA1;
else if (track.Point == 0xA2) ino.BCDValue = 0xA2;
// get ADR & Control from ADR_Control byte
byte adrc = Convert.ToByte(track.ADR_Control);
var Control = adrc & 0x0F;
var ADR = adrc >> 4;
var q = new SubchannelQ
{
q_status = SubchannelQ.ComputeStatus(ADR, (EControlQ)(Control & 0xF)),
q_tno = BCD2.FromDecimal(track.Point),
q_index = ino,
AP_Timestamp = disc._Sectors.Count,
Timestamp = relMSF - Convert.ToInt32(track.ExtraBlock.Pregap)
};
sBase.sq = q;
disc._Sectors.Add(sBase);
}
}
}
return disc;
}
} //class MDS_Format
}