//TODO: //"The first index of a file must start at 00:00:00" - if this isnt the case, we'll be doing nonsense for sure. so catch it //Recover the idea of TOCPoints maybe, as it's a more flexible way of generating the structure. //TODO //check for flags changing after a PREGAP is processed. the PREGAP can't correctly run if the flags aren't set //IN GENERAL: validate more pedantically (but that code gets in the way majorly) // - perhaps isolate validation checks into a different pass distinct from a Burn pass //NEW IDEA: //a cue file is a compressed representation of a more verbose format which is easier to understand //most fundamentally, it is organized with TRACK and INDEX commands alternating. //these should be flattened into individual records with CURRTRACK and CURRINDEX fields. //more generally, it's organized with 'register' settings and INDEX commands alternating. //whenever an INDEX command is received from the cue file, individual flattened records are written with the current 'register' settings //and an incrementing timestamp until the INDEX command appears (or the EOF happens) //PREGAP commands are special : at the moment it is received, emit flat records with a different pregap structure //POSTGAP commands are special : TBD using System; using System.Linq; using System.Text; using System.IO; using System.Collections.Generic; namespace BizHawk.Emulation.DiscSystem.CUE { /// /// Loads a cue file into a Disc. /// For this job, virtually all nonsense input is treated as errors, but the process will try to recover as best it can. /// The user should still reject any jobs which generated errors /// internal class LoadCueJob : DiscJob { /// /// The results of the compile job, a prerequisite for this /// public CompileCueJob IN_CompileJob; /// /// The resulting disc /// public Disc OUT_Disc; private enum BurnType { Normal, Pregap, Postgap } class BlobInfo { public IBlob Blob; public long Length; } //not sure if we need this... class TrackInfo { public int Length; public CompiledCueTrack CompiledCueTrack; } List BlobInfos; List TrackInfos = new List(); void MountBlobs() { IBlob file_blob = null; BlobInfos = new List(); foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles) { var bi = new BlobInfo(); BlobInfos.Add(bi); switch (ccf.Type) { case CompiledCueFileType.BIN: case CompiledCueFileType.Unknown: { //raw files: var blob = new Disc.Blob_RawFile { PhysicalPath = ccf.FullPath }; OUT_Disc.DisposableResources.Add(file_blob = blob); bi.Length = blob.Length; break; } case CompiledCueFileType.ECM: { var blob = new Disc.Blob_ECM(); OUT_Disc.DisposableResources.Add(file_blob = blob); blob.Load(ccf.FullPath); bi.Length = blob.Length; break; } case CompiledCueFileType.WAVE: { var blob = new Disc.Blob_WaveFile(); OUT_Disc.DisposableResources.Add(file_blob = blob); blob.Load(ccf.FullPath); bi.Length = blob.Length; break; } case CompiledCueFileType.DecodeAudio: { FFMpeg ffmpeg = new FFMpeg(); if (!ffmpeg.QueryServiceAvailable()) { throw new DiscReferenceException(ccf.FullPath, "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(ccf.FullPath); var blob = new Disc.Blob_WaveFile(); OUT_Disc.DisposableResources.Add(file_blob = blob); blob.Load(new MemoryStream(buf)); bi.Length = buf.Length; break; } default: throw new InvalidOperationException(); } //switch(file type) //wrap all the blobs with zero padding bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length); } } void AnalyzeTracks() { var compiledTracks = IN_CompileJob.OUT_CompiledCueTracks; for(int t=0;t 0) { //if burning through a specified pregap, count it down generateGap = true; specifiedPregapLength--; } else { //if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it for (; ; ) { if (curr_index == cct.Indexes.Count - 1) break; if (curr_blobMSF >= cct.Indexes[curr_index + 1].FileMSF.Sector) { curr_index++; if (curr_index == 1) { //WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track EmitRawTOCEntry(cct); } } else break; } } //select the track type for the subQ //it's obviously the same as the main track type usually, but during a pregap it can be different TrackInfo qTrack = ti; int qRelMSF = relMSF; if (curr_index == 0) { //tweak relMSF due to ambiguity/contradiction in yellowbook docs if (!context.DiscMountPolicy.CUE_PregapContradictionModeA) qRelMSF++; //[IEC10149] says there's two "intervals" of a pregap. //mednafen's pseudocode interpretation of this: //if this is a data track and the previous track was not data, the last 150 sectors of the pregap match this track and the earlier sectors (at least 75) math the previous track //I agree, so let's do it that way if (t != 1 && cct.TrackType != CueTrackType.Audio && TrackInfos[t - 1].CompiledCueTrack.TrackType == CueTrackType.Audio) { if (relMSF < -150) { qTrack = TrackInfos[t - 1]; } } } //generate the right kind of sector synth for this track SS_Base ss = null; if (generateGap) { var ss_gap = new SS_Gap(); ss_gap.TrackType = qTrack.CompiledCueTrack.TrackType; ss = ss_gap; } else { int sectorSize = int.MaxValue; switch (qTrack.CompiledCueTrack.TrackType) { case CueTrackType.Audio: case CueTrackType.CDI_2352: case CueTrackType.Mode1_2352: case CueTrackType.Mode2_2352: ss = new SS_2352(); sectorSize = 2352; break; case CueTrackType.Mode1_2048: ss = new SS_Mode1_2048(); sectorSize = 2048; break; default: case CueTrackType.Mode2_2336: throw new InvalidOperationException($"Not supported: {cct.TrackType}"); } ss.Blob = curr_blobInfo.Blob; ss.BlobOffset = curr_blobOffset; curr_blobOffset += sectorSize; curr_blobMSF++; } ss.Policy = context.DiscMountPolicy; //setup subQ byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption: ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags); ss.sq.q_tno = BCD2.FromDecimal(cct.Number); ss.sq.q_index = BCD2.FromDecimal(curr_index); ss.sq.AP_Timestamp = OUT_Disc._Sectors.Count; ss.sq.Timestamp = qRelMSF; //setup subP if (curr_index == 0) ss.Pause = true; OUT_Disc._Sectors.Add(ss); relMSF++; if (cct.IsFinalInFile) { //sometimes, break when the file is exhausted if (curr_blobOffset >= curr_blobInfo.Length) trackDone = true; } else { //other times, break when the track is done //(this check is safe because it's not the final track overall if it's not the final track in a file) if (curr_blobMSF >= TrackInfos[t + 1].CompiledCueTrack.Indexes[0].FileMSF.Sector) trackDone = true; } if (trackDone) break; } //--------------------------------- //gen postgap sectors int specifiedPostgapLength = cct.PostgapLength.Sector; for (int s = 0; s < specifiedPostgapLength; s++) { var ss = new SS_Gap(); ss.TrackType = cct.TrackType; //TODO - old track type in some < -150 cases? //-subq- byte ADR = 1; ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags); ss.sq.q_tno = BCD2.FromDecimal(cct.Number); ss.sq.q_index = BCD2.FromDecimal(curr_index); ss.sq.AP_Timestamp = OUT_Disc._Sectors.Count; ss.sq.Timestamp = relMSF; //-subP- //always paused--is this good enough? ss.Pause = true; OUT_Disc._Sectors.Add(ss); relMSF++; } } //end track loop //add RawTOCEntries A0 A1 A2 to round out the TOC var TOCMiscInfo = new Synthesize_A0A1A2_Job { IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber, IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber, IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat, IN_LeadoutTimestamp = OUT_Disc._Sectors.Count }; TOCMiscInfo.Run(OUT_Disc.RawTOCEntries); //TODO - generate leadout, or delegates at least //blech, old crap, maybe //OUT_Disc.Structure.Synthesize_TOCPointsFromSessions(); //FinishLog(); } //Run() } //class LoadCueJob } //namespace BizHawk.Emulation.DiscSystem