Пример #1
0
        /// <summary>
        /// Loads an SBI file from the specified path
        /// </summary>
        public SBIFile LoadSBIPath(string path)
        {
            using(var fs = File.OpenRead(path))
            {
                BinaryReader br = new BinaryReader(fs);
                string sig = br.ReadStringFixedAscii(4);
                if (sig != "SBI\0")
                    throw new SBIParseException("Missing magic number");

                SBIFile ret = new SBIFile();
                List<short> bytes = new List<short>();

                //read records until done
                for (; ; )
                {
                    //graceful end
                    if (fs.Position == fs.Length)
                        break;

                    if (fs.Position+4 > fs.Length) throw new SBIParseException("Broken record");
                    var m = BCD2.BCDToInt(br.ReadByte());
                    var s = BCD2.BCDToInt(br.ReadByte());
                    var f = BCD2.BCDToInt(br.ReadByte());
                    var ts = new Timestamp(m, s, f);
                    ret.ABAs.Add(ts.Sector);
                    int type = br.ReadByte();
                    switch (type)
                    {
                        case 1: //Q0..Q9
                            if (fs.Position + 10 > fs.Length) throw new SBIParseException("Broken record");
                            for (int i = 0; i <= 9; i++) bytes.Add(br.ReadByte());
                            for (int i = 10; i <= 11; i++) bytes.Add(-1);
                            break;
                        case 2: //Q3..Q5
                            if (fs.Position + 3 > fs.Length) throw new SBIParseException("Broken record");
                            for (int i = 0; i <= 2; i++) bytes.Add(-1);
                            for (int i = 3; i <= 5; i++) bytes.Add(br.ReadByte());
                            for (int i = 6; i <= 11; i++) bytes.Add(-1);
                            break;
                        case 3: //Q7..Q9
                            if (fs.Position + 3 > fs.Length) throw new SBIParseException("Broken record");
                            for (int i = 0; i <= 6; i++) bytes.Add(-1);
                            for (int i = 7; i <= 9; i++) bytes.Add(br.ReadByte());
                            for (int i = 10; i <= 11; i++) bytes.Add(-1);
                            break;
                        default:
                            throw new SBIParseException("Broken record");
                    }
                }

                ret.subq = bytes.ToArray();
                return ret;

            }
        }
Пример #2
0
		/// <summary>
		/// Synthesizes a data sector header
		/// </summary>
		public static void SectorHeader(byte[] buffer16, int offset, int LBA, byte mode)
		{
			buffer16[offset + 0] = 0x00;
			for (int i = 1; i < 11; i++) buffer16[offset + i] = 0xFF;
			buffer16[offset + 11] = 0x00;
			Timestamp ts = new Timestamp(LBA + 150);
			buffer16[offset + 12] = BCD2.IntToBCD(ts.MIN);
			buffer16[offset + 13] = BCD2.IntToBCD(ts.SEC);
			buffer16[offset + 14] = BCD2.IntToBCD(ts.FRAC);
			buffer16[offset + 15] = mode;
		}
Пример #3
0
        /// <summary>
        /// Creates the subcode (really, just subchannel Q) for this disc from its current TOC.
        /// Depends on the TOCPoints existing in the structure
        /// TODO - do we need a fully 0xFF P-subchannel for PSX?
        /// </summary>
        void Synthesize_SubcodeFromStructure()
        {
            int aba = 0;
            int dpIndex = 0;

            //TODO - from mednafen (on PC-FX chip chan kick)
            //If we're more than 2 seconds(150 sectors) from the real "start" of the track/INDEX 01, and the track is a data track,
            //and the preceding track is an audio track, encode it as audio(by taking the SubQ control field from the preceding

            //NOTE: discs may have subcode which is nonsense or possibly not recoverable from a sensible disc structure.
            //but this function does what it says.

            //SO: heres the main idea of how this works.
            //we have the Structure.Points (whose name we dont like) which is a list of sectors where the tno/index changes.
            //So for each sector, we see if we've advanced to the next point.
            //TODO - check if this is synthesized correctly when producing a structure from a TOCRaw
            while (aba < Sectors.Count)
            {
                if (dpIndex < Structure.Points.Count - 1)
                {
                    while (aba >= Structure.Points[dpIndex + 1].ABA)
                    {
                        dpIndex++;
                    }
                }
                var dp = Structure.Points[dpIndex];

                var se = Sectors[aba];

                EControlQ control = dp.Control;
                bool pause = true;
                if (dp.Num != 0) //TODO - shouldnt this be IndexNum?
                    pause = false;
                if ((dp.Control & EControlQ.DataUninterrupted)!=0)
                    pause = false;

                int adr = dp.ADR;

                SubchannelQ sq = new SubchannelQ();
                sq.q_status = SubchannelQ.ComputeStatus(adr, control);
                sq.q_tno = BCD2.FromDecimal(dp.TrackNum).BCDValue;
                sq.q_index = BCD2.FromDecimal(dp.IndexNum).BCDValue;

                int track_relative_aba = aba - dp.Track.Indexes[1].aba;
                track_relative_aba = Math.Abs(track_relative_aba);
                Timestamp track_relative_timestamp = new Timestamp(track_relative_aba);
                sq.min = BCD2.FromDecimal(track_relative_timestamp.MIN);
                sq.sec = BCD2.FromDecimal(track_relative_timestamp.SEC);
                sq.frame = BCD2.FromDecimal(track_relative_timestamp.FRAC);
                sq.zero = 0;
                Timestamp absolute_timestamp = new Timestamp(aba);
                sq.ap_min = BCD2.FromDecimal(absolute_timestamp.MIN);
                sq.ap_sec = BCD2.FromDecimal(absolute_timestamp.SEC);
                sq.ap_frame = BCD2.FromDecimal(absolute_timestamp.FRAC);

                var bss = new BufferedSubcodeSector();
                bss.Synthesize_SubchannelQ(ref sq, true);

                //TEST: need this for psx?
                if(pause) bss.Synthesize_SubchannelP(true);

                se.SubcodeSector = bss;

                aba++;
            }
        }
Пример #4
0
		void RunMednaDisc()
		{
			var disc = new Disc();
			OUT_Disc = disc;

			//create a MednaDisc and give it to the disc for ownership
			var md = new MednaDisc(IN_FromPath);
			disc.DisposableResources.Add(md);

			//"length of disc" for bizhawk's purposes (NOT a robust concept!) is determined by beginning of leadout track
			var m_leadoutTrack = md.TOCTracks[100];
			int nSectors = (int)m_leadoutTrack.lba;

			//make synth param memos
			disc.SynthParams.MednaDisc = md;

			//this is the sole sector synthesizer we'll need
			var synth = new SS_MednaDisc();
			OUT_Disc.SynthProvider = new SimpleSectorSynthProvider() { SS = synth };

			//ADR (q-Mode) is necessarily 0x01 for a RawTOCEntry
			const int kADR = 1;
			const int kUnknownControl = 0;

			//mednafen delivers us what is essentially but not exactly (or completely) a TOCRaw.
			//we need to synth RawTOCEntries from this and then turn it into a proper TOCRaw
			//when coming from mednafen, there are 101 entries.
			//entry[0] is placeholder junk, not to be used
			//entry[100] is the leadout track (A0)
			//A1 and A2 are in the form of FirstRecordedTrackNumber and LastRecordedTrackNumber
			for (int i = 1; i < 101; i++)
			{
				var m_te = md.TOCTracks[i];

				//dont add invalid (absent) items
				if (!m_te.Valid)
					continue;

				var m_ts = new Timestamp((int)m_te.lba + 150); //these are supposed to be absolute timestamps

				var q = new SubchannelQ
				{
					q_status = SubchannelQ.ComputeStatus(kADR, (EControlQ)m_te.control), 
					q_tno = BCD2.FromDecimal(0), //unknown with mednadisc
					q_index = BCD2.FromDecimal(i),
					min = BCD2.FromDecimal(0), //unknown with mednadisc
					sec = BCD2.FromDecimal(0), //unknown with mednadisc
					frame = BCD2.FromDecimal(0), //unknown with mednadisc
					zero = 0, //unknown with mednadisc
					ap_min = BCD2.FromDecimal(m_ts.MIN),
					ap_sec = BCD2.FromDecimal(m_ts.SEC),
					ap_frame = BCD2.FromDecimal(m_ts.FRAC),
					q_crc = 0 //meaningless
				};

				//a special fixup: mednafen's entry 100 is the lead-out track, so change it into the A2 raw toc entry
				if (i == 100)
				{
					q.q_index.BCDValue = 0xA2;
				}

				disc.RawTOCEntries.Add(new RawTOCEntry { QData = q });
			}

			//synth A0 and A1 entries (indicating first and last recorded tracks and also session type)
			var qA0 = new SubchannelQ
			{
				q_status = SubchannelQ.ComputeStatus(kADR, kUnknownControl),
				q_tno = BCD2.FromDecimal(0), //unknown with mednadisc
				q_index = BCD2.FromBCD(0xA0),
				min = BCD2.FromDecimal(0), //unknown with mednadisc
				sec = BCD2.FromDecimal(0), //unknown with mednadisc
				frame = BCD2.FromDecimal(0), //unknown with mednadisc
				zero = 0, //unknown with mednadisc
				ap_min = BCD2.FromDecimal(md.TOC.first_track),
				ap_sec = BCD2.FromDecimal(md.TOC.disc_type),
				ap_frame = BCD2.FromDecimal(0),
				q_crc = 0, //meaningless
			};
			disc.RawTOCEntries.Add(new RawTOCEntry { QData = qA0 });
			var qA1 = new SubchannelQ
			{
				q_status = SubchannelQ.ComputeStatus(kADR, kUnknownControl),
				q_tno = BCD2.FromDecimal(0), //unknown with mednadisc
				q_index = BCD2.FromBCD(0xA1),
				min = BCD2.FromDecimal(0), //unknown with mednadisc
				sec = BCD2.FromDecimal(0), //unknown with mednadisc
				frame = BCD2.FromDecimal(0), //unknown with mednadisc
				zero = 0, //unknown with mednadisc
				ap_min = BCD2.FromDecimal(md.TOC.last_track),
				ap_sec = BCD2.FromDecimal(0),
				ap_frame = BCD2.FromDecimal(0),
				q_crc = 0, //meaningless
			};
			disc.RawTOCEntries.Add(new RawTOCEntry { QData = qA1 });

		}
Пример #5
0
		/// <summary>
		/// creates subchannel Q data track for this disc
		/// </summary>
		void PopulateQSubchannel()
		{
			int aba = 0;
			int dpIndex = 0;

			while (aba < Sectors.Count)
			{
				if (dpIndex < TOC.Points.Count - 1)
				{
					if (aba >= TOC.Points[dpIndex + 1].ABA)
					{
						dpIndex++;
					}
				}
				var dp = TOC.Points[dpIndex];

				var se = Sectors[aba];

				int control = 0;
				//choose a control byte depending on whether this is an audio or data track
				if(dp.Track.TrackType == ETrackType.Audio)
					control = (int)Q_Control.StereoNoPreEmph;
				else control = (int)Q_Control.DataUninterrupted;

				//we always use ADR=1 (mode-1 q block)
				//this could be more sophisticated but it is almost useless for emulation (only useful for catalog/ISRC numbers)
				int adr = 1;
				se.q_status = (byte)(adr | (control << 4));
				se.q_tno = BCD2.FromDecimal(dp.TrackNum);
				se.q_index = BCD2.FromDecimal(dp.IndexNum);

				int track_relative_aba = aba - dp.Track.Indexes[1].aba;
				track_relative_aba = Math.Abs(track_relative_aba);
				Timestamp track_relative_timestamp = new Timestamp(track_relative_aba);
				se.q_min = BCD2.FromDecimal(track_relative_timestamp.MIN);
				se.q_sec = BCD2.FromDecimal(track_relative_timestamp.SEC);
				se.q_frame = BCD2.FromDecimal(track_relative_timestamp.FRAC);
				Timestamp absolute_timestamp = new Timestamp(aba);
				se.q_amin = BCD2.FromDecimal(absolute_timestamp.MIN);
				se.q_asec = BCD2.FromDecimal(absolute_timestamp.SEC);
				se.q_aframe = BCD2.FromDecimal(absolute_timestamp.FRAC);

				aba++;
			}
		}
Пример #6
0
        void FromCueInternal(Cue cue, string cueDir, CueBinPrefs prefs)
        {
            //TODO - add cue directory to CueBinPrefs???? could make things cleaner...

            Structure = new DiscStructure();
            var session = new DiscStructure.Session {num = 1};
            Structure.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;

                //this is a bit dodgy.. lets fixup the indices so we have something for index 0
                //TODO - I WISH WE DIDNT HAVE TO DO THIS. WE SHOULDNT PAY SO MUCH ATTENTION TO THE INTEGRITY OF THE INDEXES
                Timestamp blob_ts = new Timestamp(0);
                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");
                    for (int i = 0; i <= 99; i++)
                    {
                        if (cue_track.Indexes.ContainsKey(i))
                        {
                            blob_ts = cue_track.Indexes[i].Timestamp;
                        }
                        else if (i == 0)
                        {
                            var cti = new Cue.CueTrackIndex(0);
                            cue_track.Indexes[0] = cti;
                            cti.Timestamp = blob_ts;
                        }
                    }
                }

                //validate that the first index in the file is 00:00:00
                //"The first index of a file must start at 00:00:00"
                //zero 20-dec-2014 - NOTE - index 0 is OK. we've seen files that 'start' at non-zero but thats only with index 1 -- an index 0 was explicitly listed at time 0
                if (cue_file.Tracks[0].Indexes[0].Timestamp.Sector != 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 DiscStructure.Track();
                    session.Tracks.Add(toc_track);
                    toc_track.Number = curr_track;
                    toc_track.TrackType = cue_track.TrackType;
                    toc_track.ADR = 1; //safe assumption. CUE can't store this.

                    //choose a Control value based on track type and other flags from cue
                    //TODO - this might need to be controlled by cue loading prefs
                    toc_track.Control = cue_track.Control;
                    if (toc_track.TrackType == ETrackType.Audio)
                        toc_track.Control |= EControlQ.StereoNoPreEmph;
                    else toc_track.Control |= EControlQ.DataUninterrupted;

                    if (curr_track == 1)
                    {
                        if (cue_track.PreGap.Sector != 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.Sector > 0)
                    {
                        for (int i = 0; i < cue_track.PreGap.Sector; 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.Sector - 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 DiscStructure.Index {Number = index};
                        toc_track.Indexes.Add(toc_index);
                        if (index == 0)
                        {
                            //zero 18-dec-2014 - uhhhh cant make sense of this.
                            //toc_index.aba = track_disc_pregap_aba - (cue_track.Indexes[1].Timestamp.Sector - cue_track.Indexes[0].Timestamp.Sector);
                            toc_index.aba = track_disc_pregap_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.Sector - 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.Sector; i++)
                    {
                        Sectors.Add(new SectorEntry(pregap_sector));
                    }

                    //we're done with the track now.
                    //record its length:
                    toc_track.LengthInSectors = 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
            //EDIT - or is the above nonsense? it should be the amount of data present, full stop.
            Structure.LengthInSectors = 0;
            foreach (var toc_session in Structure.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.LengthInSectors - firstTrack.Indexes[0].aba;
                Structure.LengthInSectors += toc_session.length_aba;
            }
        }