/// <exception cref="MDSParseException">path reference no longer points to file</exception> private Dictionary <int, IBlob> MountBlobs(AFile mdsf, Disc disc) { Dictionary <int, IBlob> BlobIndex = new Dictionary <int, IBlob>(); 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 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); }
/// <exception cref="MDSParseException">no file found at <paramref name="mdsPath"/> or BLOB error</exception> 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 <int, IBlob> BlobIndex = MountBlobs(loadResults.ParsedMDSFile, disc); var mdsf = loadResults.ParsedMDSFile; //generate DiscTOCRaw items from the ones specified in the MDS file disc.RawTOCEntries = new List <RawTOCEntry>(); 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(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.FirstOrDefault(t => t.Point == i); 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 // it's probably a safe assumption that there will be only one blob per track, but I'm still not 100% sure on this var tr = mdsf.TOCEntries.FirstOrDefault(a => a.Point == i) ?? throw new MDSParseException("BLOB Error!"); #if true if (tr.ImageFileNamePaths.Count == 0) { throw new MDSParseException("BLOB Error!"); } #else // this is the worst use of lists and LINQ I've seen in this god-forsaken codebase, I hope for all our sakes that it's not a workaround for some race condition --yoshi List <string> blobstrings = new List <string>(); 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(); #endif 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 Blob_RawFile currBlob = (Blob_RawFile)BlobIndex[currBlobIndex]; long currBlobLength = currBlob.Length; long currBlobPosition = sector; if (currBlobPosition == currBlobLength) { currBlobIndex++; } mdfBlob = disc.DisposableResources[currBlobIndex] as 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); }
/// <exception cref="CCDParseException">file <paramref name="ccdPath"/> not found, nonexistent IMG file, nonexistent SUB file, IMG or SUB file not multiple of <c>2352 B</c>, or IMG and SUB files differ in length</exception> public Disc LoadCCDToDisc(string ccdPath, DiscMountPolicy IN_DiscMountPolicy) { var loadResults = LoadCCDPath(ccdPath); if (!loadResults.Valid) { throw loadResults.FailureException; } Disc disc = new Disc(); IBlob imgBlob = null, subBlob = null; long imgLen = -1, subLen; //mount the IMG file //first check for a .ecm in place of the img var imgPath = loadResults.ImgPath; if (!File.Exists(imgPath)) { var ecmPath = Path.ChangeExtension(imgPath, ".img.ecm"); if (File.Exists(ecmPath)) { if (Blob_ECM.IsECM(ecmPath)) { var ecm = new Blob_ECM(); ecm.Load(ecmPath); imgBlob = ecm; imgLen = ecm.Length; } } } if (imgBlob == null) { if (!File.Exists(loadResults.ImgPath)) { throw new CCDParseException("Malformed CCD format: nonexistent IMG file!"); } var imgFile = new Blob_RawFile() { PhysicalPath = loadResults.ImgPath }; imgLen = imgFile.Length; imgBlob = imgFile; } disc.DisposableResources.Add(imgBlob); //mount the SUB file if (!File.Exists(loadResults.SubPath)) { throw new CCDParseException("Malformed CCD format: nonexistent SUB file!"); } var subFile = new Blob_RawFile() { PhysicalPath = loadResults.SubPath }; subBlob = subFile; disc.DisposableResources.Add(subBlob); subLen = subFile.Length; //quick integrity check of file sizes if (imgLen % 2352 != 0) { throw new CCDParseException("Malformed CCD format: IMG file length not multiple of 2352"); } int NumImgSectors = (int)(imgLen / 2352); if (subLen != NumImgSectors * 96) { throw new CCDParseException("Malformed CCD format: SUB file length not matching IMG"); } var ccdf = loadResults.ParsedCCDFile; //the only instance of a sector synthesizer we'll need SS_CCD synth = new SS_CCD(); //generate DiscTOCRaw items from the ones specified in the CCD file //TODO - range validate these (too many truncations to byte) disc.RawTOCEntries = new List <RawTOCEntry>(); foreach (var entry in ccdf.TOCEntries) { 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; } var q = new SubchannelQ { q_status = SubchannelQ.ComputeStatus(entry.ADR, (EControlQ)(entry.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 }; disc.RawTOCEntries.Add(new RawTOCEntry { QData = q }); } //analyze the RAWTocEntries to figure out what type of track track 1 is var tocSynth = new Synthesize_DiscTOC_From_RawTOCEntries_Job(disc.RawTOCEntries); tocSynth.Run(); //Add sectors for the mandatory track 1 pregap, which isn't stored in the CCD file //We reuse some CUE code for this. //If we load other formats later we might should abstract this even further (to a synthesizer job) //It can't really be abstracted from cue files though due to the necessity of merging this with other track 1 pregaps 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 i = 0; i < 150; i++) { var ss_gap = new CUE.SS_Gap() { Policy = IN_DiscMountPolicy, TrackType = pregapTrackType }; disc._Sectors.Add(ss_gap); int qRelMSF = i - 150; //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 = i; ss_gap.sq.Timestamp = qRelMSF; //setup subP ss_gap.Pause = true; } //build the sectors: //set up as many sectors as we have img/sub for, even if the TOC doesnt reference them //(the TOC is unreliable, and the Track records are redundant) for (int i = 0; i < NumImgSectors; i++) { disc._Sectors.Add(synth); } return(disc); }
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; } }