/// <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);
        }
示例#3
0
        /// <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);
        }
示例#4
0
        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;
            }
        }