void FromCuePathInternal(string cuePath, CueBinPrefs prefs) { string cueDir = Path.GetDirectoryName(cuePath); var cue = new Cue(); cue.LoadFromPath(cuePath); FromCueInternal(cue, cueDir, prefs); }
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs) { var ret = new Disc(); ret.FromCuePathInternal(cuePath, prefs); ret.TOC.GeneratePoints(); ret.PopulateQSubchannel(); return(ret); }
public static CueBinPrefs GetCuePrefs() { var prefs = new CueBinPrefs(); prefs.AnnotateCue = true; // TODO? checkCueProp_Annotations.Checked; prefs.OneBlobPerTrack = false; //TODO? checkCueProp_OneBlobPerTrack.Checked; prefs.ReallyDumpBin = false; prefs.SingleSession = true; prefs.ExtensionAware = true; return prefs; }
public string GenerateCUE_OneBin(CueBinPrefs prefs) { if (prefs.OneBlobPerTrack) throw new InvalidOperationException("OneBinPerTrack passed to GenerateCUE_OneBin"); //this generates a single-file cue!!!!!!! dont expect it to generate bin-per-track! StringBuilder sb = new StringBuilder(); foreach (var session in Sessions) { if (!prefs.SingleSession) { //dont want to screw around with sessions for now if (prefs.AnnotateCue) sb.AppendFormat("SESSION {0:D2} (length={1})\n", session.num, session.length_aba); else sb.AppendFormat("SESSION {0:D2}\n", session.num); } foreach (var track in session.Tracks) { ETrackType trackType = track.TrackType; //mutate track type according to our principle of canonicalization if (trackType == ETrackType.Mode1_2048 && prefs.DumpECM) trackType = ETrackType.Mode1_2352; if (prefs.AnnotateCue) sb.AppendFormat(" TRACK {0:D2} {1} (length={2})\n", track.num, Cue.TrackTypeStringForTrackType(trackType), track.length_aba); else sb.AppendFormat(" TRACK {0:D2} {1}\n", track.num, Cue.TrackTypeStringForTrackType(trackType)); foreach (var index in track.Indexes) { //cue+bin has an implicit 150 sector pregap which neither the cue nor the bin has any awareness of //except for the baked-in sector addressing. //but, if there is an extra-long pregap, we want to reflect it this way int lba = index.aba - 150; if (lba <= 0 && index.num == 0 && track.num == 1) { } //dont emit index 0 when it is the same as index 1, it is illegal for some reason else if (index.num == 0 && index.aba == track.Indexes[1].aba) { //dont emit index 0 when it is the same as index 1, it confuses some cue parsers } else { sb.AppendFormat(" INDEX {0:D2} {1}\n", index.num, new Timestamp(lba).Value); } } } } return sb.ToString(); }
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs) { var ret = new Disc(); ret.FromCuePathInternal(cuePath, prefs); ret.Structure.Synthesize_TOCPointsFromSessions(); ret.Synthesize_SubcodeFromStructure(); ret.Synthesize_TOCRawFromStructure(); //try loading SBI. make sure its done after the subcode is synthesized! string sbiPath = Path.ChangeExtension(cuePath, "sbi"); if (File.Exists(sbiPath) && SBI_Format.QuickCheckISSBI(sbiPath)) { var sbi = new SBI_Format().LoadSBIPath(sbiPath); ret.ApplySBI(sbi); } return(ret); }
bool Dump(CueBin cueBin, string directoryTo, CueBinPrefs prefs) { ProgressReport pr = new ProgressReport(); Thread workThread = new Thread(() => { cueBin.Dump(directoryTo, prefs, pr); }); ProgressDialog pd = new ProgressDialog(pr); pd.Show(this); this.Enabled = false; workThread.Start(); for (; ; ) { Application.DoEvents(); Thread.Sleep(10); if (workThread.ThreadState != ThreadState.Running) break; pd.Update(); } this.Enabled = true; pd.Dispose(); return !pr.CancelSignal; }
CueBinPrefs GetCuePrefs() { var prefs = new CueBinPrefs(); prefs.AnnotateCue = checkCueProp_Annotations.Checked; prefs.OneBlobPerTrack = checkCueProp_OneBlobPerTrack.Checked; prefs.ReallyDumpBin = false; prefs.SingleSession = true; return prefs; }
public CueBin DumpCueBin(string baseName, CueBinPrefs prefs) { if (Structure.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 = new CUE_Format().GenerateCUE_OneBin(Structure,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 < Structure.LengthInSectors; 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 data at the same time StringBuilder sbCue = new StringBuilder(); for (int i = 0; i < Structure.Sessions[0].Tracks.Count; i++) { var track = Structure.Sessions[0].Tracks[i]; var bfd = new CueBin.BinFileDescriptor { name = baseName + string.Format(" (Track {0:D2}).bin", track.Number), 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.LengthInSectors; 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.Number, Cue.TrackTypeStringForTrackType(track.TrackType)); foreach (var index in track.Indexes) { int x = index.aba - track.Indexes[0].aba; if (index.Number == 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.Number, new Timestamp(x).Value); } } } ret.cue = sbCue.ToString(); } return ret; }
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs) { var ret = new Disc(); ret.FromCuePathInternal(cuePath, prefs); ret.Structure.Synthesize_TOCPointsFromSessions(); ret.Synthesize_SubcodeFromStructure(); ret.Synthesize_TOCRawFromStructure(); //try loading SBI. make sure its done after the subcode is synthesized! string sbiPath = Path.ChangeExtension(cuePath, "sbi"); if (File.Exists(sbiPath) && SBI_Format.QuickCheckISSBI(sbiPath)) { var sbi = new SBI_Format().LoadSBIPath(sbiPath); ret.ApplySBI(sbi); } return ret; }
public void Dump(string directory, CueBinPrefs prefs, ProgressReport progress) { byte[] subcodeTemp = new byte[96]; 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].SubcodeSector.ReadSubcodeDeinterleaved(subcodeTemp, 0); fsSubQ.Write(subcodeTemp, 12, 12); } } } } finally { fs.Dispose(); if (fsSubQ != null) fsSubQ.Dispose(); } } }
//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); }
/// <summary> /// Generates the CUE file for the provided DiscStructure /// </summary> public string GenerateCUE_OneBin(DiscStructure structure, CueBinPrefs prefs) { if (prefs.OneBlobPerTrack) { throw new InvalidOperationException("OneBinPerTrack passed to GenerateCUE_OneBin"); } //this generates a single-file cue!!!!!!! dont expect it to generate bin-per-track! StringBuilder sb = new StringBuilder(); foreach (var session in structure.Sessions) { if (!prefs.SingleSession) { //dont want to screw around with sessions for now sb.AppendFormat("SESSION {0:D2}\n", session.num); if (prefs.AnnotateCue) { sb.AppendFormat("REM ; session (length={0})\n", session.length_aba); } } foreach (var track in session.Tracks) { ETrackType trackType = track.TrackType; //mutate track type according to our principle of canonicalization if (trackType == ETrackType.Mode1_2048 && prefs.DumpECM) { trackType = ETrackType.Mode1_2352; } sb.AppendFormat(" TRACK {0:D2} {1}\n", track.Number, Cue.TrackTypeStringForTrackType(trackType)); if (prefs.AnnotateCue) { sb.AppendFormat(" REM ; track (length={0})\n", track.LengthInSectors); } foreach (var index in track.Indexes) { //cue+bin has an implicit 150 sector pregap which neither the cue nor the bin has any awareness of //except for the baked-in sector addressing. //but, if there is an extra-long pregap, we want to reflect it this way int lba = index.aba - 150; if (lba <= 0 && index.Number == 0 && track.Number == 1) { } //dont emit index 0 when it is the same as index 1, it is illegal for some reason else if (index.Number == 0 && index.aba == track.Indexes[1].aba) { //dont emit index 0 when it is the same as index 1, it confuses some cue parsers } else { sb.AppendFormat(" INDEX {0:D2} {1}\n", index.Number, new Timestamp(lba).Value); } } } } return(sb.ToString()); }
void FromCueInternal(Cue cue, string cueDir, CueBinPrefs prefs) { //TODO - add cue directory to CueBinPrefs???? could make things cleaner... var session = new DiscTOC.Session {num = 1}; TOC.Sessions.Add(session); var pregap_sector = new Sector_Zero(); int curr_track = 1; foreach (var cue_file in cue.Files) { //structural validation if (cue_file.Tracks.Count < 1) throw new Cue.CueBrokenException("`You must specify at least one track per file.`"); string blobPath = Path.Combine(cueDir, cue_file.Path); if (CueFileResolver.ContainsKey(cue_file.Path)) blobPath = CueFileResolver[cue_file.Path]; int blob_sectorsize = Cue.BINSectorSizeForTrackType(cue_file.Tracks[0].TrackType); int blob_length_aba; long blob_length_bytes; IBlob cue_blob; //try any way we can to acquire a file if (!File.Exists(blobPath) && prefs.ExtensionAware) { blobPath = FindAlternateExtensionFile(blobPath, prefs.CaseSensitive, cueDir); } if (!File.Exists(blobPath)) throw new DiscReferenceException(blobPath, ""); //some simple rules to mutate the file type if we received something fishy string blobPathExt = Path.GetExtension(blobPath).ToLower(); if (blobPathExt == ".ape") cue_file.FileType = Cue.CueFileType.Wave; if (blobPathExt == ".mp3") cue_file.FileType = Cue.CueFileType.Wave; if (blobPathExt == ".mpc") cue_file.FileType = Cue.CueFileType.Wave; if (blobPathExt == ".flac") cue_file.FileType = Cue.CueFileType.Wave; if (blobPathExt == ".ecm") cue_file.FileType = Cue.CueFileType.ECM; if (cue_file.FileType == Cue.CueFileType.Binary || cue_file.FileType == Cue.CueFileType.Unspecified) { //make a blob for the file Blob_RawFile blob = new Blob_RawFile {PhysicalPath = blobPath}; Blobs.Add(blob); blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; cue_blob = blob; } else if (cue_file.FileType == Cue.CueFileType.ECM) { if (!Blob_ECM.IsECM(blobPath)) { throw new DiscReferenceException(blobPath, "an ECM file was specified or detected, but it isn't a valid ECM file. You've got issues. Consult your iso vendor."); } Blob_ECM blob = new Blob_ECM(); Blobs.Add(blob); blob.Parse(blobPath); cue_blob = blob; blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; } else if (cue_file.FileType == Cue.CueFileType.Wave) { Blob_WaveFile blob = new Blob_WaveFile(); Blobs.Add(blob); try { //check whether we can load the wav directly bool loaded = false; if (File.Exists(blobPath) && Path.GetExtension(blobPath).ToUpper() == ".WAV") { try { blob.Load(blobPath); loaded = true; } catch { } } //if that didnt work or wasnt possible, try loading it through ffmpeg if (!loaded) { FFMpeg ffmpeg = new FFMpeg(); if (!ffmpeg.QueryServiceAvailable()) { throw new DiscReferenceException(blobPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)"); } AudioDecoder dec = new AudioDecoder(); byte[] buf = dec.AcquireWaveData(blobPath); blob.Load(new MemoryStream(buf)); WasSlowLoad = true; } } catch (Exception ex) { throw new DiscReferenceException(blobPath, ex); } blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; cue_blob = blob; } else throw new Exception("Internal error - Unhandled cue blob type"); //TODO - make CueTimestamp better, and also make it a struct, and also just make it DiscTimestamp //start timekeeping for the blob. every time we hit an index, this will advance int blob_timestamp = 0; //because we can have different size sectors in a blob, we need to keep a file cursor within the blob long blob_cursor = 0; //the aba that this cue blob starts on int blob_disc_aba_start = Sectors.Count; //for each track within the file, create an index 0 if it is missing. //also check to make sure there is an index 1 for (int t = 0; t < cue_file.Tracks.Count; t++) { var cue_track = cue_file.Tracks[t]; if (!cue_track.Indexes.ContainsKey(1)) throw new Cue.CueBrokenException("Track was missing an index 01"); if (!cue_track.Indexes.ContainsKey(0)) { //index 0 will default to the same as index 1. //i am not sure whether it is valid to have two indexes with the same timestamp. //we will do this to simplify some processing, but we can purge it in a later pass if we need to. var cti = new Cue.CueTrackIndex(0); cue_track.Indexes[0] = cti; cti.Timestamp = cue_track.Indexes[1].Timestamp; } } //validate that the first index in the file is 00:00:00 if (cue_file.Tracks[0].Indexes[0].Timestamp.ABA != 0) throw new Cue.CueBrokenException("`The first index of a blob must start at 00:00:00.`"); //for each track within the file: for (int t = 0; t < cue_file.Tracks.Count; t++) { var cue_track = cue_file.Tracks[t]; //record the disc ABA that this track started on int track_disc_aba_start = Sectors.Count; //record the pregap location. it will default to the start of the track unless we supplied a pregap command int track_disc_pregap_aba = track_disc_aba_start; int blob_track_start = blob_timestamp; //once upon a time we had a check here to prevent a single blob from containing variant sector sizes. but we support that now. //check integrity of track sequence and setup data structures //TODO - check for skipped tracks in cue parser instead if (cue_track.TrackNum != curr_track) throw new Cue.CueBrokenException("Found a cue with skipped tracks"); var toc_track = new DiscTOC.Track(); toc_track.num = curr_track; toc_track.TrackType = cue_track.TrackType; session.Tracks.Add(toc_track); if (curr_track == 1) { if (cue_track.PreGap.ABA != 0) throw new InvalidOperationException("not supported (yet): cue files with track 1 pregaps"); //but now we add one anyway, because every existing cue+bin seems to implicitly specify this cue_track.PreGap = new Timestamp(150); } //check whether a pregap is requested. //this causes empty sectors to get generated without consuming data from the blob if (cue_track.PreGap.ABA > 0) { for (int i = 0; i < cue_track.PreGap.ABA; i++) { Sectors.Add(new SectorEntry(pregap_sector)); } } //look ahead to the next track's index 1 so we can see how long this track's last index is //or, for the last track, use the length of the file int track_length_aba; if (t == cue_file.Tracks.Count - 1) track_length_aba = blob_length_aba - blob_timestamp; else track_length_aba = cue_file.Tracks[t + 1].Indexes[1].Timestamp.ABA - blob_timestamp; //toc_track.length_aba = track_length_aba; //xxx //find out how many indexes we have int num_indexes = 0; for (num_indexes = 0; num_indexes <= 99; num_indexes++) if (!cue_track.Indexes.ContainsKey(num_indexes)) break; //for each index, calculate length of index and then emit it for (int index = 0; index < num_indexes; index++) { bool is_last_index = index == num_indexes - 1; //install index into hierarchy var toc_index = new DiscTOC.Index {num = index}; toc_track.Indexes.Add(toc_index); if (index == 0) { toc_index.aba = track_disc_pregap_aba - (cue_track.Indexes[1].Timestamp.ABA - cue_track.Indexes[0].Timestamp.ABA); } else toc_index.aba = Sectors.Count; //calculate length of the index //if it is the last index then we use our calculation from before, otherwise we check the next index int index_length_aba; if (is_last_index) index_length_aba = track_length_aba - (blob_timestamp - blob_track_start); else index_length_aba = cue_track.Indexes[index + 1].Timestamp.ABA - blob_timestamp; //emit sectors for (int aba = 0; aba < index_length_aba; aba++) { bool is_last_aba_in_index = (aba == index_length_aba - 1); bool is_last_aba_in_track = is_last_aba_in_index && is_last_index; switch (cue_track.TrackType) { case ETrackType.Audio: //all 2352 bytes are present case ETrackType.Mode1_2352: //2352 bytes are present, containing 2048 bytes of user data as well as ECM case ETrackType.Mode2_2352: //2352 bytes are present, containing the entirety of a mode2 sector (could be form0,1,2) { //these cases are all 2352 bytes //in all these cases, either no ECM is present or ECM is provided. //so we just emit a Sector_Raw Sector_RawBlob sector_rawblob = new Sector_RawBlob { Blob = cue_blob, Offset = blob_cursor }; blob_cursor += 2352; Sector_Mode1_or_Mode2_2352 sector_raw; if(cue_track.TrackType == ETrackType.Mode1_2352) sector_raw = new Sector_Mode1_2352(); else if (cue_track.TrackType == ETrackType.Audio) sector_raw = new Sector_Mode1_2352(); //TODO should probably make a new sector adapter which errors if 2048B are requested else if (cue_track.TrackType == ETrackType.Mode2_2352) sector_raw = new Sector_Mode2_2352(); else throw new InvalidOperationException(); sector_raw.BaseSector = sector_rawblob; Sectors.Add(new SectorEntry(sector_raw)); break; } case ETrackType.Mode1_2048: //2048 bytes are present. ECM needs to be generated to create a full sector { //ECM needs to know the sector number so we have to record that here int curr_disc_aba = Sectors.Count; var sector_2048 = new Sector_Mode1_2048(curr_disc_aba + 150) { Blob = new ECMCacheBlob(cue_blob), Offset = blob_cursor }; blob_cursor += 2048; Sectors.Add(new SectorEntry(sector_2048)); break; } } //switch(TrackType) //we've emitted an ABA, so consume it from the blob blob_timestamp++; } //aba emit loop } //index loop //check whether a postgap is requested. if it is, we need to generate silent sectors for (int i = 0; i < cue_track.PostGap.ABA; i++) { Sectors.Add(new SectorEntry(pregap_sector)); } //we're done with the track now. //record its length: toc_track.length_aba = Sectors.Count - toc_track.Indexes[1].aba; curr_track++; //if we ran off the end of the blob, pad it with zeroes, I guess if (blob_cursor > blob_length_bytes) { //mutate the blob to an encapsulating Blob_ZeroPadAdapter Blobs[Blobs.Count - 1] = new Blob_ZeroPadAdapter(Blobs[Blobs.Count - 1], blob_length_bytes, blob_cursor - blob_length_bytes); } } //track loop } //file loop //finally, analyze the length of the sessions and the entire disc by summing the lengths of the tracks //this is a little more complex than it looks, because the length of a thing is not determined by summing it //but rather by the difference in abas between start and end TOC.length_aba = 0; foreach (var toc_session in TOC.Sessions) { var firstTrack = toc_session.Tracks[0]; //track 0, index 0 is actually -150. but cue sheets will never say that //firstTrack.Indexes[0].aba -= 150; var lastTrack = toc_session.Tracks[toc_session.Tracks.Count - 1]; session.length_aba = lastTrack.Indexes[1].aba + lastTrack.length_aba - firstTrack.Indexes[0].aba; TOC.length_aba += toc_session.length_aba; } }
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs) { var ret = new Disc(); ret.FromCuePathInternal(cuePath, prefs); ret.TOC.GeneratePoints(); ret.PopulateQSubchannel(); return ret; }
void FromCueInternal(Cue cue, string cueDir, CueBinPrefs prefs) { //TODO - add cue directory to CueBinPrefs???? could make things cleaner... var session = new DiscTOC.Session { num = 1 }; TOC.Sessions.Add(session); var pregap_sector = new Sector_Zero(); int curr_track = 1; foreach (var cue_file in cue.Files) { //structural validation if (cue_file.Tracks.Count < 1) { throw new Cue.CueBrokenException("`You must specify at least one track per file.`"); } string blobPath = Path.Combine(cueDir, cue_file.Path); if (CueFileResolver.ContainsKey(cue_file.Path)) { blobPath = CueFileResolver[cue_file.Path]; } int blob_sectorsize = Cue.BINSectorSizeForTrackType(cue_file.Tracks[0].TrackType); int blob_length_aba; long blob_length_bytes; IBlob cue_blob; //try any way we can to acquire a file if (!File.Exists(blobPath) && prefs.ExtensionAware) { blobPath = FindAlternateExtensionFile(blobPath, prefs.CaseSensitive, cueDir); } if (!File.Exists(blobPath)) { throw new DiscReferenceException(blobPath, ""); } //some simple rules to mutate the file type if we received something fishy string blobPathExt = Path.GetExtension(blobPath).ToLower(); if (blobPathExt == ".ape") { cue_file.FileType = Cue.CueFileType.Wave; } if (blobPathExt == ".mp3") { cue_file.FileType = Cue.CueFileType.Wave; } if (blobPathExt == ".mpc") { cue_file.FileType = Cue.CueFileType.Wave; } if (blobPathExt == ".flac") { cue_file.FileType = Cue.CueFileType.Wave; } if (blobPathExt == ".ecm") { cue_file.FileType = Cue.CueFileType.ECM; } if (cue_file.FileType == Cue.CueFileType.Binary || cue_file.FileType == Cue.CueFileType.Unspecified) { //make a blob for the file Blob_RawFile blob = new Blob_RawFile { PhysicalPath = blobPath }; Blobs.Add(blob); blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; cue_blob = blob; } else if (cue_file.FileType == Cue.CueFileType.ECM) { if (!Blob_ECM.IsECM(blobPath)) { throw new DiscReferenceException(blobPath, "an ECM file was specified or detected, but it isn't a valid ECM file. You've got issues. Consult your iso vendor."); } Blob_ECM blob = new Blob_ECM(); Blobs.Add(blob); blob.Parse(blobPath); cue_blob = blob; blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; } else if (cue_file.FileType == Cue.CueFileType.Wave) { Blob_WaveFile blob = new Blob_WaveFile(); Blobs.Add(blob); try { //check whether we can load the wav directly bool loaded = false; if (File.Exists(blobPath) && Path.GetExtension(blobPath).ToUpper() == ".WAV") { try { blob.Load(blobPath); loaded = true; } catch { } } //if that didnt work or wasnt possible, try loading it through ffmpeg if (!loaded) { FFMpeg ffmpeg = new FFMpeg(); if (!ffmpeg.QueryServiceAvailable()) { throw new DiscReferenceException(blobPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)"); } AudioDecoder dec = new AudioDecoder(); byte[] buf = dec.AcquireWaveData(blobPath); blob.Load(new MemoryStream(buf)); WasSlowLoad = true; } } catch (Exception ex) { throw new DiscReferenceException(blobPath, ex); } blob_length_aba = (int)(blob.Length / blob_sectorsize); blob_length_bytes = blob.Length; cue_blob = blob; } else { throw new Exception("Internal error - Unhandled cue blob type"); } //TODO - make CueTimestamp better, and also make it a struct, and also just make it DiscTimestamp //start timekeeping for the blob. every time we hit an index, this will advance int blob_timestamp = 0; //because we can have different size sectors in a blob, we need to keep a file cursor within the blob long blob_cursor = 0; //the aba that this cue blob starts on int blob_disc_aba_start = Sectors.Count; //for each track within the file, create an index 0 if it is missing. //also check to make sure there is an index 1 for (int t = 0; t < cue_file.Tracks.Count; t++) { var cue_track = cue_file.Tracks[t]; if (!cue_track.Indexes.ContainsKey(1)) { throw new Cue.CueBrokenException("Track was missing an index 01"); } if (!cue_track.Indexes.ContainsKey(0)) { //index 0 will default to the same as index 1. //i am not sure whether it is valid to have two indexes with the same timestamp. //we will do this to simplify some processing, but we can purge it in a later pass if we need to. var cti = new Cue.CueTrackIndex(0); cue_track.Indexes[0] = cti; cti.Timestamp = cue_track.Indexes[1].Timestamp; } } //validate that the first index in the file is 00:00:00 if (cue_file.Tracks[0].Indexes[0].Timestamp.ABA != 0) { throw new Cue.CueBrokenException("`The first index of a blob must start at 00:00:00.`"); } //for each track within the file: for (int t = 0; t < cue_file.Tracks.Count; t++) { var cue_track = cue_file.Tracks[t]; //record the disc ABA that this track started on int track_disc_aba_start = Sectors.Count; //record the pregap location. it will default to the start of the track unless we supplied a pregap command int track_disc_pregap_aba = track_disc_aba_start; int blob_track_start = blob_timestamp; //once upon a time we had a check here to prevent a single blob from containing variant sector sizes. but we support that now. //check integrity of track sequence and setup data structures //TODO - check for skipped tracks in cue parser instead if (cue_track.TrackNum != curr_track) { throw new Cue.CueBrokenException("Found a cue with skipped tracks"); } var toc_track = new DiscTOC.Track(); toc_track.num = curr_track; toc_track.TrackType = cue_track.TrackType; session.Tracks.Add(toc_track); if (curr_track == 1) { if (cue_track.PreGap.ABA != 0) { throw new InvalidOperationException("not supported (yet): cue files with track 1 pregaps"); } //but now we add one anyway, because every existing cue+bin seems to implicitly specify this cue_track.PreGap = new Timestamp(150); } //check whether a pregap is requested. //this causes empty sectors to get generated without consuming data from the blob if (cue_track.PreGap.ABA > 0) { for (int i = 0; i < cue_track.PreGap.ABA; i++) { Sectors.Add(new SectorEntry(pregap_sector)); } } //look ahead to the next track's index 1 so we can see how long this track's last index is //or, for the last track, use the length of the file int track_length_aba; if (t == cue_file.Tracks.Count - 1) { track_length_aba = blob_length_aba - blob_timestamp; } else { track_length_aba = cue_file.Tracks[t + 1].Indexes[1].Timestamp.ABA - blob_timestamp; } //toc_track.length_aba = track_length_aba; //xxx //find out how many indexes we have int num_indexes = 0; for (num_indexes = 0; num_indexes <= 99; num_indexes++) { if (!cue_track.Indexes.ContainsKey(num_indexes)) { break; } } //for each index, calculate length of index and then emit it for (int index = 0; index < num_indexes; index++) { bool is_last_index = index == num_indexes - 1; //install index into hierarchy var toc_index = new DiscTOC.Index { num = index }; toc_track.Indexes.Add(toc_index); if (index == 0) { toc_index.aba = track_disc_pregap_aba - (cue_track.Indexes[1].Timestamp.ABA - cue_track.Indexes[0].Timestamp.ABA); } else { toc_index.aba = Sectors.Count; } //calculate length of the index //if it is the last index then we use our calculation from before, otherwise we check the next index int index_length_aba; if (is_last_index) { index_length_aba = track_length_aba - (blob_timestamp - blob_track_start); } else { index_length_aba = cue_track.Indexes[index + 1].Timestamp.ABA - blob_timestamp; } //emit sectors for (int aba = 0; aba < index_length_aba; aba++) { bool is_last_aba_in_index = (aba == index_length_aba - 1); bool is_last_aba_in_track = is_last_aba_in_index && is_last_index; switch (cue_track.TrackType) { case ETrackType.Audio: //all 2352 bytes are present case ETrackType.Mode1_2352: //2352 bytes are present, containing 2048 bytes of user data as well as ECM case ETrackType.Mode2_2352: //2352 bytes are present, containing the entirety of a mode2 sector (could be form0,1,2) { //these cases are all 2352 bytes //in all these cases, either no ECM is present or ECM is provided. //so we just emit a Sector_Raw Sector_RawBlob sector_rawblob = new Sector_RawBlob { Blob = cue_blob, Offset = blob_cursor }; blob_cursor += 2352; Sector_Mode1_or_Mode2_2352 sector_raw; if (cue_track.TrackType == ETrackType.Mode1_2352) { sector_raw = new Sector_Mode1_2352(); } else if (cue_track.TrackType == ETrackType.Audio) { sector_raw = new Sector_Mode1_2352(); //TODO should probably make a new sector adapter which errors if 2048B are requested } else if (cue_track.TrackType == ETrackType.Mode2_2352) { sector_raw = new Sector_Mode2_2352(); } else { throw new InvalidOperationException(); } sector_raw.BaseSector = sector_rawblob; Sectors.Add(new SectorEntry(sector_raw)); break; } case ETrackType.Mode1_2048: //2048 bytes are present. ECM needs to be generated to create a full sector { //ECM needs to know the sector number so we have to record that here int curr_disc_aba = Sectors.Count; var sector_2048 = new Sector_Mode1_2048(curr_disc_aba + 150) { Blob = new ECMCacheBlob(cue_blob), Offset = blob_cursor }; blob_cursor += 2048; Sectors.Add(new SectorEntry(sector_2048)); break; } } //switch(TrackType) //we've emitted an ABA, so consume it from the blob blob_timestamp++; } //aba emit loop } //index loop //check whether a postgap is requested. if it is, we need to generate silent sectors for (int i = 0; i < cue_track.PostGap.ABA; i++) { Sectors.Add(new SectorEntry(pregap_sector)); } //we're done with the track now. //record its length: toc_track.length_aba = Sectors.Count - toc_track.Indexes[1].aba; curr_track++; //if we ran off the end of the blob, pad it with zeroes, I guess if (blob_cursor > blob_length_bytes) { //mutate the blob to an encapsulating Blob_ZeroPadAdapter Blobs[Blobs.Count - 1] = new Blob_ZeroPadAdapter(Blobs[Blobs.Count - 1], blob_length_bytes, blob_cursor - blob_length_bytes); } } //track loop } //file loop //finally, analyze the length of the sessions and the entire disc by summing the lengths of the tracks //this is a little more complex than it looks, because the length of a thing is not determined by summing it //but rather by the difference in abas between start and end TOC.length_aba = 0; foreach (var toc_session in TOC.Sessions) { var firstTrack = toc_session.Tracks[0]; //track 0, index 0 is actually -150. but cue sheets will never say that //firstTrack.Indexes[0].aba -= 150; var lastTrack = toc_session.Tracks[toc_session.Tracks.Count - 1]; session.length_aba = lastTrack.Indexes[1].aba + lastTrack.length_aba - firstTrack.Indexes[0].aba; TOC.length_aba += toc_session.length_aba; } }
public CueBin DumpCueBin(string baseName, CueBinPrefs prefs) { if (Structure.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 = new CUE_Format().GenerateCUE_OneBin(Structure, 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 < Structure.LengthInSectors; 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 data at the same time StringBuilder sbCue = new StringBuilder(); for (int i = 0; i < Structure.Sessions[0].Tracks.Count; i++) { var track = Structure.Sessions[0].Tracks[i]; var bfd = new CueBin.BinFileDescriptor { name = baseName + string.Format(" (Track {0:D2}).bin", track.Number), 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.LengthInSectors; 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.Number, Cue.TrackTypeStringForTrackType(track.TrackType)); foreach (var index in track.Indexes) { int x = index.aba - track.Indexes[0].aba; if (index.Number == 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.Number, new Timestamp(x).Value); } } } ret.cue = sbCue.ToString(); } return(ret); }
public void Dump(string directory, CueBinPrefs prefs, ProgressReport progress) { byte[] subcodeTemp = new byte[96]; 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].SubcodeSector.ReadSubcodeDeinterleaved(subcodeTemp, 0); fsSubQ.Write(subcodeTemp, 12, 12); } } } } finally { fs.Dispose(); if (fsSubQ != null) { fsSubQ.Dispose(); } } } }