using System; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.IO; using System.Collections.Generic; //http://www.pctechguide.com/iso-9660-data-format-for-cds-cd-roms-cd-rs-and-cd-rws //http://linux.die.net/man/1/cue2toc //http://cdemu.sourceforge.net/project.php#sf //apparently cdrdao is the ultimate linux tool for doing this stuff but it doesnt support DAO96 (or other DAO modes) that would be necessary to extract P-Q subchannels //(cdrdao only supports R-W) //here is a featureset list of windows cd burning programs (useful for cuesheet compatibility info) //http://www.dcsoft.com/cue_mastering_progs.htm //good //http://linux-sxs.org/bedtime/cdapi.html //http://en.wikipedia.org/wiki/Track_%28CD%29 //http://docs.google.com/viewer?a=v&q=cache:imNKye05zIEJ:www.13thmonkey.org/documentation/SCSI/mmc-r10a.pdf+q+subchannel+TOC+format&hl=en&gl=us&pid=bl&srcid=ADGEEShtYqlluBX2lgxTL3pVsXwk6lKMIqSmyuUCX4RJ3DntaNq5vI2pCvtkyze-fumj7vvrmap6g1kOg5uAVC0IxwU_MRhC5FB0c_PQ2BlZQXDD7P3GeNaAjDeomelKaIODrhwOoFNb&sig=AHIEtbRXljAcFjeBn3rMb6tauHWjSNMYrw //r:\consoles\~docs\yellowbook //http://digitalx.org/cue-sheet/examples/ // //"qemu cdrom emulator" //http://www.koders.com/c/fid7171440DEC7C18B932715D671DEE03743111A95A.aspx //less good //http://www.cyberciti.biz/faq/getting-volume-information-from-cds-iso-images/ //http://www.cims.nyu.edu/cgi-systems/man.cgi?section=7I&topic=cdio //ideas: /* * do some stuff asynchronously. for example, decoding mp3 sectors. * keep a list of 'blobs' (giant bins or decoded wavs likely) which can reference the disk * keep a list of sectors and the blob/offset from which they pull -- also whether the sector is available * if it is not available and something requests it then it will have to block while that sector gets generated * perhaps the blobs know how to resolve themselves and the requested sector can be immediately resolved (priority boost) * mp3 blobs should be hashed and dropped in %TEMP% as a wav decode */ //here is an MIT licensed C mp3 decoder //http://core.fluendo.com/gstreamer/src/gst-fluendo-mp3/ /*information on saturn TOC and session data structures is on pdf page 58 of System Library User's Manual; * as seen in yabause, there are 1000 u32s in this format: * Ctrl[4bit] Adr[4bit] StartFrameAddressFAD[24bit] (nonexisting tracks are 0xFFFFFFFF) * Followed by Fist Track Information, Last Track Information.. * Ctrl[4bit] Adr[4bit] FirstTrackNumber/LastTrackNumber[8bit] and then some stuff I dont understand * ..and Read Out Information: * Ctrl[4bit] Adr[4bit] ReadOutStartFrameAddress[24bit] * * Also there is some stuff about FAD of sessions. * This should be generated by the saturn core, but we need to make sure we pass down enough information to do it */ //2048 bytes packed into 2352: //12 bytes sync(00 ff ff ff ff ff ff ff ff ff ff 00) //3 bytes sector address (min+A0),sec,frac //does this correspond to ccd `point` field in the TOC entries? //sector mode byte (0: silence; 1: 2048Byte mode (EDC,ECC,CIRC), 2: 2352Byte mode (CIRC only) //user data: 2336 bytes //cue sheets may use mode1_2048 (and the error coding needs to be regenerated to get accurate raw data) or mode1_2352 (the entire sector is present) //mode2_2352 is the only kind of mode2, by necessity //audio is a different mode, seems to be just 2352 bytes with no sync, header or error correction. i guess the CIRC error correction is still there namespace BizHawk.DiscSystem { public partial class Disc : IDisposable { //TODO - separate these into Read_2352 and Read_2048 (optimizations can be made by ISector implementors depending on what is requested) //(for example, avoiding the 2048 byte sector creating the ECC data and then immediately discarding it) public interface ISector { int Read(byte[] buffer, int offset); } public interface IBlob : IDisposable { int Read(long byte_pos, byte[] buffer, int offset, int count); void Dispose(); } class Blob_RawFile : IBlob { public string PhysicalPath { get { return physicalPath; } set { physicalPath = value; length = new FileInfo(physicalPath).Length; } } string physicalPath; long length; public long Offset; BufferedStream fs; public void Dispose() { if (fs != null) { fs.Dispose(); fs = null; } } public int Read(long byte_pos, byte[] buffer, int offset, int count) { //use quite a large buffer, because normally we will be reading these sequentially but in small chunks. //this enhances performance considerably const int buffersize = 2352 * 75 * 2; if (fs == null) fs = new BufferedStream(new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read), buffersize); long target = byte_pos + Offset; if(fs.Position != target) fs.Position = target; return fs.Read(buffer, offset, count); } public long Length { get { return length; } } } class Sector_RawSilence : ISector { public int Read(byte[] buffer, int offset) { Array.Clear(buffer, 0, 2352); return 2352; } } class Sector_RawBlob : ISector { public IBlob Blob; public long Offset; public int Read(byte[] buffer, int offset) { return Blob.Read(Offset, buffer, offset, 2352); } } class Sector_Zero : ISector { public int Read(byte[] buffer, int offset) { for (int i = 0; i < 2352; i++) buffer[offset + i] = 0; return 2352; } } class Sector_ZeroPad : ISector { public ISector BaseSector; public int BaseLength; public int Read(byte[] buffer, int offset) { int read = BaseSector.Read(buffer, offset); if(read < BaseLength) return read; for (int i = BaseLength; i < 2352; i++) buffer[offset + i] = 0; return 2352; } } class Sector_Raw : ISector { public ISector BaseSector; public int Read(byte[] buffer, int offset) { return BaseSector.Read(buffer, offset); } } protected static byte BCD_Byte(byte val) { byte ret = (byte)(val % 10); ret += (byte)(16 * (val / 10)); return ret; } //a blob that also has an ECM cache associated with it. maybe one day. class ECMCacheBlob { public ECMCacheBlob(IBlob blob) { BaseBlob = blob; } public IBlob BaseBlob; } class Sector_Mode1_2048 : ISector { public Sector_Mode1_2048(int ABA) { byte aba_min = (byte)(ABA / 60 / 75); byte aba_sec = (byte)((ABA / 75) % 60); byte aba_frac = (byte)(ABA % 75); bcd_aba_min = BCD_Byte(aba_min); bcd_aba_sec = BCD_Byte(aba_sec); bcd_aba_frac = BCD_Byte(aba_frac); } byte bcd_aba_min, bcd_aba_sec, bcd_aba_frac; public ECMCacheBlob Blob; public long Offset; byte[] extra_data; bool has_extra_data; public int Read(byte[] buffer, int offset) { //user data int read = Blob.BaseBlob.Read(Offset, buffer, offset + 16, 2048); //if we read the 2048 physical bytes OK, then return the complete sector if (read == 2048 && has_extra_data) { Buffer.BlockCopy(extra_data, 0, buffer, offset, 16); Buffer.BlockCopy(extra_data, 16, buffer, offset + 2064, 4 + 8 + 172 + 104); return 2352; } //sync buffer[offset + 0] = 0x00; buffer[offset + 1] = 0xFF; buffer[offset + 2] = 0xFF; buffer[offset + 3] = 0xFF; buffer[offset + 4] = 0xFF; buffer[offset + 5] = 0xFF; buffer[offset + 6] = 0xFF; buffer[offset + 7] = 0xFF; buffer[offset + 8] = 0xFF; buffer[offset + 9] = 0xFF; buffer[offset + 10] = 0xFF; buffer[offset + 11] = 0x00; //sector address buffer[offset + 12] = bcd_aba_min; buffer[offset + 13] = bcd_aba_sec; buffer[offset + 14] = bcd_aba_frac; //mode 1 buffer[offset + 15] = 1; //EDC ECM.edc_computeblock(buffer, offset+2064, buffer, offset+2064); //intermediate for (int i = 0; i < 8; i++) buffer[offset + 2068 + i] = 0; //ECC ECM.ecc_generate(buffer, offset, false, buffer, offset+2076); //if we read the 2048 physical bytes OK, then return the complete sector if (read == 2048) { extra_data = new byte[16 + 4 + 8 + 172 + 104]; Buffer.BlockCopy(buffer, 0, extra_data, 0, 16); Buffer.BlockCopy(buffer, 2064, extra_data, 16, 4 + 8 + 172 + 104); has_extra_data = true; return 2352; } //otherwise, return a smaller value to indicate an error else return read; } } //this is a physical 2352 byte sector. public class SectorEntry { public SectorEntry(ISector sec) { this.Sector = sec; } public ISector Sector; //todo - add some PARAMETER fields to this, so that the ISector can use them (so that each ISector doesnt have to be constructed also) //also then, maybe this could be a struct //q-subchannel stuff. can be returned directly, or built into the entire subcode sector if you want /// /// ADR and CONTROL /// public byte q_status; /// /// BCD indications of the current track number and index /// public BCD2 q_tno, q_index; /// /// track-relative timestamp /// public BCD2 q_min, q_sec, q_frame; /// /// absolute timestamp /// public BCD2 q_amin, q_asec, q_aframe; public void Read_SubchannelQ(byte[] buffer, int offset) { buffer[offset + 0] = q_status; buffer[offset + 1] = q_tno.BCDValue; buffer[offset + 2] = q_index.BCDValue; buffer[offset + 3] = q_min.BCDValue; buffer[offset + 4] = q_sec.BCDValue; buffer[offset + 5] = q_frame.BCDValue; buffer[offset + 6] = 0; buffer[offset + 7] = q_amin.BCDValue; buffer[offset + 8] = q_asec.BCDValue; buffer[offset + 9] = q_aframe.BCDValue; ushort crc16 = CRC16_CCITT.Calculate(buffer, 0, 10); //CRC is stored inverted and big endian buffer[offset + 10] = (byte)(~(crc16 >> 8)); buffer[offset + 11] = (byte)(~(crc16)); } } public List Blobs = new List(); public List Sectors = new List(); public DiscTOC TOC = new DiscTOC(); public void Dispose() { foreach (var blob in Blobs) { blob.Dispose(); } } void FromIsoPathInternal(string isoPath) { var session = new DiscTOC.Session(); session.num = 1; TOC.Sessions.Add(session); var track = new DiscTOC.Track(); track.num = 1; session.Tracks.Add(track); var index = new DiscTOC.Index(); index.num = 0; track.Indexes.Add(index); index = new DiscTOC.Index(); index.num = 1; track.Indexes.Add(index); var fiIso = new FileInfo(isoPath); Blob_RawFile blob = new Blob_RawFile(); blob.PhysicalPath = fiIso.FullName; Blobs.Add(blob); int num_aba = (int)(fiIso.Length / 2048); track.length_aba = num_aba; if (fiIso.Length % 2048 != 0) throw new InvalidOperationException("invalid iso file (size not multiple of 2048)"); //TODO - handle this with Final Fantasy 9 cd1.iso var ecmCacheBlob = new ECMCacheBlob(blob); for (int i = 0; i < num_aba; i++) { Sector_Mode1_2048 sector = new Sector_Mode1_2048(i+150); sector.Blob = ecmCacheBlob; sector.Offset = i * 2048; Sectors.Add(new SectorEntry(sector)); } TOC.AnalyzeLengthsFromIndexLengths(); } public CueBin DumpCueBin(string baseName, CueBinPrefs prefs) { if (TOC.Sessions.Count > 1) throw new NotSupportedException("can't dump cue+bin with more than 1 session yet"); CueBin ret = new CueBin(); ret.baseName = baseName; ret.disc = this; if (!prefs.OneBlobPerTrack) { //this is the preferred mode of dumping things. we will always write full sectors. string cue = TOC.GenerateCUE_OneBin(prefs); var bfd = new CueBin.BinFileDescriptor(); bfd.name = baseName + ".bin"; ret.cue = string.Format("FILE \"{0}\" BINARY\n", bfd.name) + cue; ret.bins.Add(bfd); bfd.SectorSize = 2352; //skip the mandatory track 1 pregap! cue+bin files do not contain it for (int i = 150; i < TOC.length_aba; i++) { bfd.abas.Add(i); bfd.aba_zeros.Add(false); } } else { //we build our own cue here (unlike above) because we need to build the cue and the output dat aat the same time StringBuilder sbCue = new StringBuilder(); for (int i = 0; i < TOC.Sessions[0].Tracks.Count; i++) { var track = TOC.Sessions[0].Tracks[i]; var bfd = new CueBin.BinFileDescriptor(); bfd.name = baseName + string.Format(" (Track {0:D2}).bin", track.num); bfd.SectorSize = Cue.BINSectorSizeForTrackType(track.TrackType); ret.bins.Add(bfd); int aba = 0; //skip the mandatory track 1 pregap! cue+bin files do not contain it if (i == 0) aba = 150; for (; aba < track.length_aba; aba++) { int thisaba = track.Indexes[0].aba + aba; bfd.abas.Add(thisaba); bfd.aba_zeros.Add(false); } sbCue.AppendFormat("FILE \"{0}\" BINARY\n", bfd.name); sbCue.AppendFormat(" TRACK {0:D2} {1}\n", track.num, Cue.TrackTypeStringForTrackType(track.TrackType)); foreach (var index in track.Indexes) { int x = index.aba - track.Indexes[0].aba; if (index.num == 0 && index.aba == track.Indexes[1].aba) { //dont emit index 0 when it is the same as index 1, it is illegal for some reason } //else if (i==0 && index.num == 0) //{ // //don't generate the first index, it is illogical //} else { //track 1 included the lead-in at the beginning of it. sneak past that. //if (i == 0) x -= 150; sbCue.AppendFormat(" INDEX {0:D2} {1}\n", index.num, new Timestamp(x).Value); } } } ret.cue = sbCue.ToString(); } return ret; } /// /// NOT USED RIGHT NOW. AMBIGUOUS, ANYWAY. /// "bin" is an ill-defined concept. /// [Obsolete] void DumpBin_2352(string binPath) { byte[] temp = new byte[2352]; //a cue's bin probably doesn't contain the first 150 sectors, so skip it using (FileStream fs = new FileStream(binPath, FileMode.Create, FileAccess.Write, FileShare.None)) for (int i = 150; i < Sectors.Count; i++) { ReadLBA_2352(i, temp, 0); fs.Write(temp, 0, 2352); } } public static Disc FromCuePath(string cuePath) { var ret = new Disc(); ret.FromCuePathInternal(cuePath); ret.TOC.GeneratePoints(); ret.PopulateQSubchannel(); return ret; } /// /// THIS HASNT BEEN TESTED IN A LONG TIME. DOES IT WORK? /// public static Disc FromIsoPath(string isoPath) { var ret = new Disc(); ret.FromIsoPathInternal(isoPath); ret.TOC.GeneratePoints(); ret.PopulateQSubchannel(); return ret; } /// /// creates subchannel Q data track for this disc /// void PopulateQSubchannel() { int aba = 0; int dpIndex = 0; while (aba < Sectors.Count) { if (dpIndex < TOC.Points.Count - 1) { if (aba >= TOC.Points[dpIndex + 1].ABA) { dpIndex++; } } var dp = TOC.Points[dpIndex]; var se = Sectors[aba]; int control = 0; //choose a control byte depending on whether this is an audio or data track if(dp.Track.TrackType == ETrackType.Audio) control = (int)Q_Control.StereoNoPreEmph; else control = (int)Q_Control.DataUninterrupted; //we always use ADR=1 (mode-1 q block) //this could be more sophisticated but it is almost useless for emulation (only useful for catalog/ISRC numbers) int adr = 1; se.q_status = (byte)(adr | (control << 4)); se.q_tno = BCD2.FromDecimal(dp.TrackNum); se.q_index = BCD2.FromDecimal(dp.IndexNum); int track_relative_aba = aba - dp.Track.Indexes[1].aba; track_relative_aba = Math.Abs(track_relative_aba); Timestamp track_relative_timestamp = new Timestamp(track_relative_aba); se.q_min = BCD2.FromDecimal(track_relative_timestamp.MIN); se.q_sec = BCD2.FromDecimal(track_relative_timestamp.SEC); se.q_frame = BCD2.FromDecimal(track_relative_timestamp.FRAC); Timestamp absolute_timestamp = new Timestamp(aba); se.q_amin = BCD2.FromDecimal(absolute_timestamp.MIN); se.q_asec = BCD2.FromDecimal(absolute_timestamp.SEC); se.q_aframe = BCD2.FromDecimal(absolute_timestamp.FRAC); aba++; } } static byte IntToBCD(int n) { int ones; int tens = Math.DivRem(n,10,out ones); return (byte)((tens<<4)|ones); } private enum Q_Control { StereoNoPreEmph = 0, StereoPreEmph = 1, MonoNoPreemph = 8, MonoPreEmph = 9, DataUninterrupted = 4, DataIncremental = 5, CopyProhibitedMask = 0, CopyPermittedMask = 2, } } /// /// encapsulates a 2 digit BCD number as used various places in the CD specs /// public struct BCD2 { /// /// The raw BCD value. you can't do math on this number! but you may be asked to supply it to a game program. /// The largest number it can logically contain is 99 /// public byte BCDValue; /// /// The derived decimal value. you can do math on this! the largest number it can logically contain is 99. /// public int DecimalValue { get { return (BCDValue & 0xF) + ((BCDValue >> 4) & 0xF) * 10; } set { BCDValue = IntToBCD(value); } } /// /// makes a BCD2 from a decimal number. don't supply a number > 99 or you might not like the results /// public static BCD2 FromDecimal(int d) { BCD2 ret = new BCD2(); ret.DecimalValue = d; return ret; } static byte IntToBCD(int n) { int ones; int tens = Math.DivRem(n, 10, out ones); return (byte)((tens << 4) | ones); } } public class Timestamp { /// /// creates timestamp of 00:00:00 /// public Timestamp() { Value = "00:00:00"; } /// /// creates a timestamp from a string in the form mm:ss:ff /// public Timestamp(string value) { this.Value = value; MIN = int.Parse(value.Substring(0, 2)); SEC = int.Parse(value.Substring(3, 2)); FRAC = int.Parse(value.Substring(6, 2)); ABA = MIN * 60 * 75 + SEC * 75 + FRAC; } public readonly string Value; public readonly int MIN, SEC, FRAC, ABA; /// /// creates timestamp from supplied ABA /// public Timestamp(int ABA) { this.ABA = ABA; MIN = ABA / (60 * 75); SEC = (ABA / 75) % 60; FRAC = ABA % 75; Value = string.Format("{0:D2}:{1:D2}:{2:D2}", MIN, SEC, FRAC); } } public enum ETrackType { Mode1_2352, Mode1_2048, Mode2_2352, Audio } public class CueBinPrefs { /// /// Controls general operations: should the output be split into several blobs, or just use one? /// public bool OneBlobPerTrack; /// /// NOT SUPPORTED YET (just here as a reminder) If choosing OneBinPerTrack, you may wish to write wave files for audio tracks. /// //public bool DumpWaveFiles; /// /// turn this on to dump bins instead of just cues /// public bool ReallyDumpBin; /// /// dump a .sub.q along with bins. one day we'll want to dump the entire subcode but really Q is all thats important for debugging most things /// public bool DumpSubchannelQ; /// /// generate remarks and other annotations to help humans understand whats going on, but which will confuse many cue parsers /// public bool AnnotateCue; /// /// EVIL: in theory this would attempt to generate pregap commands to save disc space, but I think this is a bad idea. /// it would also be useful for OneBinPerTrack mode in making wave files. /// HOWEVER - by the time we've loaded things up into our canonical format, we don't know which 'pregaps' are safe for turning back into pregaps /// Because they might sometimes contain data (gapless audio discs). So we would have to inspect a series of sectors to look for silence. /// And even still, the ECC information might be important. So, forget it. /// NEVER USE OR IMPLEMENT THIS /// //public bool PreferPregapCommand = false; /// /// some cue parsers cant handle sessions. better not emit a session command then. multi-session discs will then be broken /// public bool SingleSession; //THIS IS WRONG-HEADED. track 1 index 0 must never equal index 1! apparently. /// /// DO NOT CHANGE THIS! All sectors will be written with ECM data. It's a waste of space, but it is exact. (not completely supported yet) /// public bool DumpECM = true; } /// /// Encapsulates an in-memory cue+bin (complete cuesheet and a little registry of files) /// it will be based on a disc (fro mwhich it can read sectors to avoid burning through extra memory) /// TODO - we must merge this with whatever reads in cue+bin /// public class CueBin { public string cue; public string baseName; public Disc disc; public class BinFileDescriptor { public string name; public List abas = new List(); //todo - do we really need this? i dont think so... public List aba_zeros = new List(); public int SectorSize; } public List bins = new List(); //NOT SUPPORTED RIGHT NOW //public string CreateRedumpReport() //{ // if (disc.TOC.Sessions[0].Tracks.Count != bins.Count) // throw new InvalidOperationException("Cannot generate redump report on CueBin lacking OneBinPerTrack property"); // StringBuilder sb = new StringBuilder(); // for (int i = 0; i < disc.TOC.Sessions[0].Tracks.Count; i++) // { // var track = disc.TOC.Sessions[0].Tracks[i]; // var bfd = bins[i]; // //dump the track // byte[] dump = new byte[track.length_aba * 2352]; // //TODO ????????? post-ABA unknown // //for (int aba = 0; aba < track.length_aba; aba++) // // disc.ReadLBA_2352(bfd.lbas[lba],dump,lba*2352); // string crc32 = string.Format("{0:X8}", CRC32.Calculate(dump)); // string md5 = Util.Hash_MD5(dump, 0, dump.Length); // string sha1 = Util.Hash_SHA1(dump, 0, dump.Length); // int pregap = track.Indexes[1].lba - track.Indexes[0].lba; // Timestamp pregap_ts = new Timestamp(pregap); // Timestamp len_ts = new Timestamp(track.length_lba); // sb.AppendFormat("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}\n", // i, // Cue.RedumpTypeStringForTrackType(track.TrackType), // pregap_ts.Value, // len_ts.Value, // track.length_lba, // track.length_lba*Cue.BINSectorSizeForTrackType(track.TrackType), // crc32, // md5, // sha1 // ); // } // return sb.ToString(); //} public void Dump(string directory, CueBinPrefs prefs) { ProgressReport pr = new ProgressReport(); Dump(directory, prefs, pr); } public void Dump(string directory, CueBinPrefs prefs, ProgressReport progress) { byte[] subQ_temp = new byte[12]; progress.TaskCount = 2; progress.Message = "Generating Cue"; progress.ProgressEstimate = 1; progress.ProgressCurrent = 0; progress.InfoPresent = true; string cuePath = Path.Combine(directory, baseName + ".cue"); File.WriteAllText(cuePath, cue); progress.Message = "Writing bin(s)"; progress.TaskCurrent = 1; progress.ProgressEstimate = bins.Sum((bfd) => bfd.abas.Count); progress.ProgressCurrent = 0; if(!prefs.ReallyDumpBin) return; foreach (var bfd in bins) { int sectorSize = bfd.SectorSize; byte[] temp = new byte[2352]; byte[] empty = new byte[2352]; string trackBinFile = bfd.name; string trackBinPath = Path.Combine(directory, trackBinFile); string subQPath = Path.ChangeExtension(trackBinPath, ".sub.q"); FileStream fsSubQ = null; FileStream fs = new FileStream(trackBinPath, FileMode.Create, FileAccess.Write, FileShare.None); try { if (prefs.DumpSubchannelQ) fsSubQ = new FileStream(subQPath, FileMode.Create, FileAccess.Write, FileShare.None); for (int i = 0; i < bfd.abas.Count; i++) { if (progress.CancelSignal) return; progress.ProgressCurrent++; int aba = bfd.abas[i]; if (bfd.aba_zeros[i]) { fs.Write(empty, 0, sectorSize); } else { if (sectorSize == 2352) disc.ReadABA_2352(aba, temp, 0); else if (sectorSize == 2048) disc.ReadABA_2048(aba, temp, 0); else throw new InvalidOperationException(); fs.Write(temp, 0, sectorSize); //write subQ if necessary if (fsSubQ != null) { disc.Sectors[aba].Read_SubchannelQ(subQ_temp, 0); fsSubQ.Write(subQ_temp, 0, 12); } } } } finally { fs.Dispose(); if (fsSubQ != null) fsSubQ.Dispose(); } } } } }