using System; using System.Linq; using System.Text; 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: mode2 (could be 2336[vanilla mode2], 2048[xa mode2 form1], 2324[xa mode2 form2]) //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) //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.Emulation.DiscSystem { public partial class Disc : IDisposable { public interface ISector { /// /// reads the entire sector, raw /// int Read_2352(byte[] buffer, int offset); /// /// reads 2048 bytes of userdata.. precisely what this means isnt always 100% certain (for instance mode2 form 0 has 2336 bytes of userdata instead of 2048).. /// ..but its certain enough for this to be useful /// int Read_2048(byte[] buffer, int offset); } /// /// Presently, an IBlob doesn't need to work multithreadedly. It's quite an onerous demand. /// This should probably be managed by the Disc class somehow, or by the user making another Disc. /// public interface IBlob : IDisposable { /// /// what a weird parameter order. normally the dest buffer would be first. weird. /// /// location in the blob to read from /// destination buffer for read data /// offset into destination buffer /// amount to read int Read(long byte_pos, byte[] buffer, int offset, int count); } public sealed class Blob_ZeroPadAdapter : IBlob { public Blob_ZeroPadAdapter(IBlob baseBlob, long padFrom, long padLen) { this.baseBlob = baseBlob; this.padFrom = padFrom; this.padLen = padLen; } public int Read(long byte_pos, byte[] buffer, int offset, int count) { throw new NotImplementedException("Blob_ZeroPadAdapter hasnt been tested yet! please report this!"); //something about this seems unnecessarily complex, but... i dunno. /* //figure out how much remains until the zero-padding begins long remain = byte_pos - padFrom; int todo; if (remain < count) todo = (int)remain; else todo = count; //read up until the zero-padding int totalRead = 0; int readed = baseBlob.Read(byte_pos, buffer, offset, todo); totalRead += readed; offset += todo; //if we didnt read enough, we certainly shouldnt try to read any more if (readed < todo) return readed; //if that was all we needed, then we're done count -= todo; if (count == 0) return totalRead; //if we need more, it must come from zero-padding remain = padLen; if (remain < count) todo = (int)remain; else todo = count; Array.Clear(buffer, offset, todo); totalRead += todo; return totalRead; */ } public void Dispose() { baseBlob.Dispose(); } private readonly IBlob baseBlob; private long padFrom; private long padLen; } private 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 = 0; 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; } } } /// /// this ISector is dumb and only knows how to drag chunks off a source blob /// class Sector_RawBlob : ISector { public IBlob Blob; public long Offset; public int Read_2352(byte[] buffer, int offset) { return Blob.Read(Offset, buffer, offset, 2352); } public int Read_2048(byte[] buffer, int offset) { return Blob.Read(Offset, buffer, offset, 2048); } } /// /// this ISector always returns zeroes /// class Sector_Zero : ISector { public int Read_2352(byte[] buffer, int offset) { Array.Clear(buffer, 0, 2352); return 2352; } public int Read_2048(byte[] buffer, int offset) { Array.Clear(buffer, 0, 2048); return 2048; } } // ------ replaced by Blob_ZeroPadAdapter ///// ///// this ISector adapts another ISector by always returning zeroes ///// TODO I dont like the way this works. I think blobs should get adapted instead to zero-pad to a certain length. ///// //class Sector_ZeroPad : ISector //{ // public ISector BaseSector; // public int BaseLength; // public int Read(byte[] buffer, int offset) // { // return _Read(buffer, offset, 2352); // } // public int Read_2048(byte[] buffer, int offset) // { // return _Read(buffer, offset, 2352); // } // int _Read(byte[] buffer, int offset, int amount) // { // int read = BaseSector.Read(buffer, offset); // if(read < BaseLength) return read; // for (int i = BaseLength; i < amount; i++) // buffer[offset + i] = 0; // return amount; // } //} abstract class Sector_Mode1_or_Mode2_2352 : ISector { public ISector BaseSector; public abstract int Read_2352(byte[] buffer, int offset); public abstract int Read_2048(byte[] buffer, int offset); } /// /// This ISector is a raw MODE1 sector /// class Sector_Mode1_2352 : Sector_Mode1_or_Mode2_2352 { public override int Read_2352(byte[] buffer, int offset) { return BaseSector.Read_2352(buffer, offset); } public override int Read_2048(byte[] buffer, int offset) { //to get 2048 bytes out of this sector type, start 16 bytes in int ret = BaseSector.Read_2352(TempSector, 0); Buffer.BlockCopy(TempSector, 16, buffer, offset, 2048); System.Diagnostics.Debug.Assert(buffer != TempSector); return 2048; } [ThreadStatic] static byte[] TempSector = new byte[2352]; } /// /// this ISector is a raw MODE2 sector. could be form 0,1,2... who can say? supposedly: /// To tell the different Mode 2s apart you have to examine bytes 16-23 of the sector (the first 8 bytes of Mode Data). /// If bytes 16-19 are not the same as 20-23, then it is Mode 2. If they are equal and bit 5 is on (0x20), then it is Mode 2 Form 2. Otherwise it is Mode 2 Form 1. /// ...but we're not using this information in any way /// class Sector_Mode2_2352 : Sector_Mode1_or_Mode2_2352 { public override int Read_2352(byte[] buffer, int offset) { return BaseSector.Read_2352(buffer, offset); } public override int Read_2048(byte[] buffer, int offset) { //to get 2048 bytes out of this sector type, start 24 bytes in int ret = BaseSector.Read_2352(TempSector, 0); Buffer.BlockCopy(TempSector, 24, buffer, offset, 2048); System.Diagnostics.Debug.Assert(buffer != TempSector); return 2048; } [ThreadStatic] static byte[] TempSector = new byte[2352]; } private 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; } /// /// this ISector is a MODE1 sector that is generating itself from an underlying MODE1/2048 userdata piece /// 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_2048(byte[] buffer, int offset) { //this is easy. we only have 2048 bytes, and 2048 bytes were requested return Blob.BaseBlob.Read(Offset, buffer, offset, 2048); } public int Read_2352(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; //calculate EDC and poke into the sector uint edc = ECM.EDC_Calc(buffer, offset, 2064); ECM.PokeUint(buffer, 2064, edc); //intermediate for (int i = 0; i < 8; i++) buffer[offset + 2068 + i] = 0; //ECC ECM.ECC_Populate(buffer, offset, buffer, offset, false); //VALIDATION - check our homemade algorithms against code derived from ECM ////EDC //GPL_ECM.edc_validateblock(buffer, 2064, buffer, offset + 2064); ////ECC //GPL_ECM.ecc_validate(buffer, offset, false); //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) { 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) { //make a fake cue file to represent this iso file const string isoCueWrapper = @" FILE ""xarp.barp.marp.farp"" BINARY TRACK 01 MODE1/2048 INDEX 01 00:00:00 "; string cueDir = String.Empty; var cue = new Cue(); CueFileResolver["xarp.barp.marp.farp"] = isoPath; cue.LoadFromString(isoCueWrapper); FromCueInternal(cue, cueDir, new CueBinPrefs()); //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 {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 { name = baseName + string.Format(" (Track {0:D2}).bin", track.num), 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, CueBinPrefs prefs) { var ret = new Disc(); ret.FromCuePathInternal(cuePath, prefs); 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) { return new BCD2 {DecimalValue = d}; } 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) { 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 bins to bitbucket instead of disk /// public bool DumpToBitbucket; /// /// 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; /// /// enables various extension-aware behaviours. /// enables auto-search for files with the same name but differing extension. /// enables auto-detection of situations where cue blobfiles are indicating the wrong type in the cuefile /// public bool ExtensionAware = false; /// /// whenever we have a choice, use case sensitivity in searching for files /// public bool CaseSensitive = false; /// /// 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"); if (prefs.DumpToBitbucket) { } else 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"); Stream fsSubQ = null; Stream fs; if(prefs.DumpToBitbucket) fs = Stream.Null; else fs = new FileStream(trackBinPath, FileMode.Create, FileAccess.Write, FileShare.None); try { if (prefs.DumpSubchannelQ) if (prefs.DumpToBitbucket) fsSubQ = Stream.Null; else 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(); } } } } }