public void Synth(SectorSynthJob job) { //be lazy, just generate the whole sector unconditionally //this is mostly based on mednafen's approach, which was probably finely tailored for PSX //heres the comments on the subject: // I'm not trusting that the "control" field for the TOC leadout entry will always be set properly, so | the control fields for the last track entry // and the leadout entry together before extracting the D2 bit. Audio track->data leadout is fairly benign though maybe noisy(especially if we ever implement // data scrambling properly), but data track->audio leadout could break things in an insidious manner for the more accurate drive emulation code). var ses = job.Disc.Structure.Sessions[SessionNumber]; int lba_relative = job.LBA - ses.LeadoutTrack.LBA; //data is zero int ts = lba_relative; int ats = job.LBA; const int ADR = 0x1; // Q channel data encodes position EControlQ control = ses.LeadoutTrack.Control; //ehhh? CDI? //if(toc.tracks[toc.last_track].valid) // control |= toc.tracks[toc.last_track].control & 0x4; //else if(toc.disc_type == DISC_TYPE_CD_I) // control |= 0x4; control |= (EControlQ)(((int)ses.LastInformationTrack.Control) & 4); SubchannelQ sq = new SubchannelQ(); sq.SetStatus(ADR, control); sq.q_tno.BCDValue = 0xAA; sq.q_index.BCDValue = 0x01; sq.Timestamp = ts; sq.AP_Timestamp = ats; sq.zero = 0; //finally, rely on a gap sector to do the heavy lifting to synthesize this CUE.CueTrackType TrackType = CUE.CueTrackType.Audio; if (ses.LeadoutTrack.IsData) { if (job.Disc.TOC.Session1Format == SessionFormat.Type20_CDXA || job.Disc.TOC.Session1Format == SessionFormat.Type10_CDI) { TrackType = CUE.CueTrackType.Mode2_2352; } else { TrackType = CUE.CueTrackType.Mode1_2352; } } CUE.SS_Gap ss_gap = new CUE.SS_Gap() { Policy = Policy, sq = sq, TrackType = TrackType, Pause = true //? }; ss_gap.Synth(job); }
/// <summary> /// Fills this subcode buffer with subchannel Q data. calculates the required CRC, as well. /// Returns the crc, calculated or otherwise. /// </summary> public ushort Synthesize_SubchannelQ(ref SubchannelQ sq, bool calculateCRC) { int offset = 12; //Q subchannel begins after P, 12 bytes in SubcodeDeinterleaved[offset + 0] = sq.q_status; SubcodeDeinterleaved[offset + 1] = sq.q_tno; SubcodeDeinterleaved[offset + 2] = sq.q_index; SubcodeDeinterleaved[offset + 3] = sq.min.BCDValue; SubcodeDeinterleaved[offset + 4] = sq.sec.BCDValue; SubcodeDeinterleaved[offset + 5] = sq.frame.BCDValue; SubcodeDeinterleaved[offset + 6] = sq.zero; SubcodeDeinterleaved[offset + 7] = sq.ap_min.BCDValue; SubcodeDeinterleaved[offset + 8] = sq.ap_sec.BCDValue; SubcodeDeinterleaved[offset + 9] = sq.ap_frame.BCDValue; ushort crc16; if (calculateCRC) { crc16 = CRC16_CCITT.Calculate(SubcodeDeinterleaved, offset, 10); } else { crc16 = sq.q_crc; } //CRC is stored inverted and big endian SubcodeDeinterleaved[offset + 10] = (byte)(~(crc16 >> 8)); SubcodeDeinterleaved[offset + 11] = (byte)(~(crc16)); return(crc16); }
/// <summary>appends the new entries to the provided list</summary> /// <exception cref="InvalidOperationException"><see cref="IN_Session1Format"/> is <see cref="SessionFormat.None"/> or a non-member</exception> public void Run(List <RawTOCEntry> entries) { //NOTE: entries are inserted at the beginning due to observations of CCD indicating they might need to be that way //Since I'm being asked to synthesize them here, I guess I can put them in whatever order I want, can't I? SubchannelQ sq = new SubchannelQ(); //ADR (q-Mode) is necessarily 0x01 for a RawTOCEntry const int kADR = 1; const int kUnknownControl = 0; sq.SetStatus(kADR, (EControlQ)kUnknownControl); //first recorded track number: sq.q_index.BCDValue = 0xA0; sq.ap_min.DecimalValue = IN_FirstRecordedTrackNumber; switch (IN_Session1Format) { //TODO these probably shouldn't be decimal values case SessionFormat.Type00_CDROM_CDDA: sq.ap_sec.DecimalValue = 0x00; break; case SessionFormat.Type10_CDI: sq.ap_sec.DecimalValue = 0x10; break; case SessionFormat.Type20_CDXA: sq.ap_sec.DecimalValue = 0x20; break; default: throw new InvalidOperationException("Invalid Session1Format"); } sq.ap_frame.DecimalValue = 0; entries.Insert(0, new RawTOCEntry { QData = sq }); //last recorded track number: sq.q_index.BCDValue = 0xA1; sq.ap_min.DecimalValue = IN_LastRecordedTrackNumber; sq.ap_sec.DecimalValue = 0; sq.ap_frame.DecimalValue = 0; entries.Insert(1, new RawTOCEntry { QData = sq }); //leadout: sq.q_index.BCDValue = 0xA2; sq.AP_Timestamp = IN_LeadoutTimestamp; entries.Insert(2, new RawTOCEntry { QData = sq }); }
private RawTOCEntry EmitRawTOCEntry(ATOCEntry entry) { 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; } // get ADR & Control from ADR_Control byte byte adrc = Convert.ToByte(entry.ADR_Control); var Control = adrc & 0x0F; var ADR = adrc >> 4; var q = new SubchannelQ { q_status = SubchannelQ.ComputeStatus(ADR, (EControlQ)(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 }; return(new RawTOCEntry { QData = q }); }
/// <summary> /// Serializes the provided SubchannelQ structure into a buffer /// Returns the crc, calculated or otherwise. /// </summary> public static ushort SubQ_Serialize(byte[] buf12, int offset, ref SubchannelQ sq) { buf12[offset + 0] = sq.q_status; buf12[offset + 1] = sq.q_tno.BCDValue; buf12[offset + 2] = sq.q_index.BCDValue; buf12[offset + 3] = sq.min.BCDValue; buf12[offset + 4] = sq.sec.BCDValue; buf12[offset + 5] = sq.frame.BCDValue; buf12[offset + 6] = sq.zero; buf12[offset + 7] = sq.ap_min.BCDValue; buf12[offset + 8] = sq.ap_sec.BCDValue; buf12[offset + 9] = sq.ap_frame.BCDValue; return(SubQ_SynthChecksum(buf12, offset)); }
/// <summary> /// Serializes the provided SubchannelQ structure into a buffer /// Returns the crc, calculated or otherwise. /// </summary> public static ushort SubQ_Serialize(byte[] buf12, int offset, ref SubchannelQ sq) { buf12[offset + 0] = sq.q_status; buf12[offset + 1] = sq.q_tno.BCDValue; buf12[offset + 2] = sq.q_index.BCDValue; buf12[offset + 3] = sq.min.BCDValue; buf12[offset + 4] = sq.sec.BCDValue; buf12[offset + 5] = sq.frame.BCDValue; buf12[offset + 6] = sq.zero; buf12[offset + 7] = sq.ap_min.BCDValue; buf12[offset + 8] = sq.ap_sec.BCDValue; buf12[offset + 9] = sq.ap_frame.BCDValue; return SubQ_SynthChecksum(buf12, offset); }
/// <summary> /// Appends the new entries to the provided list /// </summary> public void Run(List<RawTOCEntry> entries) { //NOTE: entries are inserted at the beginning due to observations of CCD indicating they might need to be that way //Since I'm being asked to synthesize them here, I guess I can put them in whatever order I want, can't I? SubchannelQ sq = new SubchannelQ(); //ADR (q-Mode) is necessarily 0x01 for a RawTOCEntry const int kADR = 1; const int kUnknownControl = 0; sq.SetStatus(kADR, (EControlQ)kUnknownControl); //first recorded track number: sq.q_index.BCDValue = 0xA0; sq.ap_min.DecimalValue = IN_FirstRecordedTrackNumber; switch(IN_Session1Format) { //TODO these probably shouldn't be decimal values case SessionFormat.Type00_CDROM_CDDA: sq.ap_sec.DecimalValue = 0x00; break; case SessionFormat.Type10_CDI: sq.ap_sec.DecimalValue = 0x10; break; case SessionFormat.Type20_CDXA: sq.ap_sec.DecimalValue = 0x20; break; default: throw new InvalidOperationException("Invalid Session1Format"); } sq.ap_frame.DecimalValue = 0; entries.Insert(0, new RawTOCEntry { QData = sq }); //last recorded track number: sq.q_index.BCDValue = 0xA1; sq.ap_min.DecimalValue = IN_LastRecordedTrackNumber; sq.ap_sec.DecimalValue = 0; sq.ap_frame.DecimalValue = 0; entries.Insert(1, new RawTOCEntry { QData = sq }); //leadout: sq.q_index.BCDValue = 0xA2; sq.AP_Timestamp = IN_LeadoutTimestamp; entries.Insert(2, new RawTOCEntry { QData = sq }); }
/// <summary> /// Reads 12 bytes of subQ data from a sector and stores it unpacked into the provided struct /// TODO - make use of deserialize code elsewhere /// </summary> public void ReadLBA_SubQ(int lba, out SubchannelQ sq) { ReadLBA_SubQ(lba, buf12, 0); sq.q_status = buf12[0]; sq.q_tno.BCDValue = buf12[1]; sq.q_index.BCDValue = buf12[2]; sq.min.BCDValue = buf12[3]; sq.sec.BCDValue = buf12[4]; sq.frame.BCDValue = buf12[5]; sq.zero = buf12[6]; sq.ap_min.BCDValue = buf12[7]; sq.ap_sec.BCDValue = buf12[8]; sq.ap_frame.BCDValue = buf12[9]; //CRC is stored inverted and big endian.. so... do the opposite byte hibyte = (byte)(~buf12[10]); byte lobyte = (byte)(~buf12[11]); sq.q_crc = (ushort)((hibyte << 8) | lobyte); }
public void Run() { //TODO: encode_mode2_form2_sector var leadoutTs = Disc.TOC.LeadoutLBA; var lastTrackTOCItem = Disc.TOC.TOCItems[Disc.TOC.LastRecordedTrackNumber]; //NOTE: in case LastRecordedTrackNumber is al ie, this will malfunction //leadout flags.. let's set them the same as the last track. //THIS IS NOT EXACTLY THE SAME WAY MEDNAFEN DOES IT EControlQ leadoutFlags = lastTrackTOCItem.Control; //TODO - needs to be encoded as a certain mode (mode 2 form 2 for psx... i guess...) for (int i = 0; i < Length; i++) { //var se = new SectorEntry(sz); //Disc.Sectors.Add(se); SubchannelQ sq = new SubchannelQ(); int track_relative_msf = i; sq.min = BCD2.FromDecimal(new Timestamp(track_relative_msf).MIN); sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC); sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC); int absolute_msf = i + leadoutTs.Sector; sq.ap_min = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).MIN); sq.ap_sec = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).SEC); sq.ap_frame = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).FRAC); sq.q_tno.DecimalValue = 0xAA; //special value for leadout sq.q_index.DecimalValue = 1; byte ADR = 1; sq.SetStatus(ADR, leadoutFlags); //TODO - actually stash the subQ } }
public void Run() { //TODO: encode_mode2_form2_sector var leadoutTs = Disc.TOC.LeadoutLBA; var lastTrackTOCItem = Disc.TOC.TOCItems[Disc.TOC.LastRecordedTrackNumber]; //NOTE: in case LastRecordedTrackNumber is al ie, this will malfunction //leadout flags.. let's set them the same as the last track. //THIS IS NOT EXACTLY THE SAME WAY MEDNAFEN DOES IT EControlQ leadoutFlags = lastTrackTOCItem.Control; //TODO - needs to be encoded as a certain mode (mode 2 form 2 for psx... i guess...) for (int i = 0; i < Length; i++) { //var se = new SectorEntry(sz); //Disc.Sectors.Add(se); SubchannelQ sq = new SubchannelQ(); int track_relative_msf = i; sq.min = BCD2.FromDecimal(new Timestamp(track_relative_msf).MIN); sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC); sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC); int absolute_msf = i + leadoutTs; sq.ap_min = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).MIN); sq.ap_sec = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).SEC); sq.ap_frame = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).FRAC); sq.q_tno.DecimalValue = 0xAA; //special value for leadout sq.q_index.DecimalValue = 1; byte ADR = 1; sq.SetStatus(ADR, leadoutFlags); //TODO - actually stash the subQ } }
public void ReadLBA_SubchannelQ(int lba, ref SubchannelQ sq) { var se = disc.ReadLBA_SectorEntry(lba); se.SubcodeSector.ReadSubcodeChannel(1, buffer, 0); int offset = 0; sq.q_status = buffer[offset + 0]; sq.q_tno = buffer[offset + 1]; sq.q_index = buffer[offset + 2]; sq.min.BCDValue = buffer[offset + 3]; sq.sec.BCDValue = buffer[offset + 4]; sq.frame.BCDValue = buffer[offset + 5]; //nothing in byte[6] sq.ap_min.BCDValue = buffer[offset + 7]; sq.ap_sec.BCDValue = buffer[offset + 8]; sq.ap_frame.BCDValue = buffer[offset + 9]; //CRC is stored inverted and big endian.. so... do the opposite byte hibyte = (byte)(~buffer[offset + 10]); byte lobyte = (byte)(~buffer[offset + 11]); sq.q_crc = (ushort)((hibyte << 8) | lobyte); }
/// <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++; } }
/// <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++; } }
/// <summary> /// Fills this subcode buffer with subchannel Q data. calculates the required CRC, as well. /// Returns the crc, calculated or otherwise. /// </summary> public ushort Synthesize_SubchannelQ(ref SubchannelQ sq, bool calculateCRC) { int offset = 12; //Q subchannel begins after P, 12 bytes in SubcodeDeinterleaved[offset + 0] = sq.q_status; SubcodeDeinterleaved[offset + 1] = sq.q_tno; SubcodeDeinterleaved[offset + 2] = sq.q_index; SubcodeDeinterleaved[offset + 3] = sq.min.BCDValue; SubcodeDeinterleaved[offset + 4] = sq.sec.BCDValue; SubcodeDeinterleaved[offset + 5] = sq.frame.BCDValue; SubcodeDeinterleaved[offset + 6] = sq.zero; SubcodeDeinterleaved[offset + 7] = sq.ap_min.BCDValue; SubcodeDeinterleaved[offset + 8] = sq.ap_sec.BCDValue; SubcodeDeinterleaved[offset + 9] = sq.ap_frame.BCDValue; ushort crc16; if (calculateCRC) crc16 = CRC16_CCITT.Calculate(SubcodeDeinterleaved, offset, 10); else crc16 = sq.q_crc; //CRC is stored inverted and big endian SubcodeDeinterleaved[offset + 10] = (byte)(~(crc16 >> 8)); SubcodeDeinterleaved[offset + 11] = (byte)(~(crc16)); return crc16; }
/// <summary> /// Loads a CCD at the specified path to a Disc object /// </summary> public Disc LoadCCDToDisc(string ccdPath) { var loadResults = LoadCCDPath(ccdPath); if (!loadResults.Valid) { throw loadResults.FailureException; } Disc disc = new Disc(); var ccdf = loadResults.ParsedCCDFile; var imgBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.ImgPath }; var subBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.SubPath }; disc.Blobs.Add(imgBlob); disc.Blobs.Add(subBlob); //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>(); BufferedSubcodeSector bss = new BufferedSubcodeSector(); foreach (var entry in ccdf.TOCEntries) { var q = new SubchannelQ { q_status = SubchannelQ.ComputeStatus(entry.ADR, (EControlQ)(entry.Control & 0xF)), q_tno = (byte)entry.TrackNo, q_index = (byte)entry.Point, 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), }; //CRC cant be calculated til we've got all the fields setup q.q_crc = bss.Synthesize_SubchannelQ(ref q, true); disc.RawTOCEntries.Add(new RawTOCEntry { QData = q }); } //generate the toc from the entries var tocSynth = new DiscTOCRaw.SynthesizeFromRawTOCEntriesJob() { Entries = disc.RawTOCEntries }; tocSynth.Run(); disc.TOCRaw = tocSynth.Result; //synthesize DiscStructure var structureSynth = new DiscStructure.SynthesizeFromDiscTOCRawJob() { TOCRaw = disc.TOCRaw }; structureSynth.Run(); disc.Structure = structureSynth.Result; //I *think* implicitly there is an index 0.. at.. i dunno, 0 maybe, for track 1 { var dsi0 = new DiscStructure.Index(); dsi0.LBA = 0; dsi0.Number = 0; disc.Structure.Sessions[0].Tracks[0].Indexes.Add(dsi0); } //now, how to get the track types for the DiscStructure? //1. the CCD tells us (somehow the reader has judged) //2. scan it out of the Q subchannel //lets choose1. //TODO - better consider how to handle the situation where we have havent received all the [TRACK] items we need foreach (var st in disc.Structure.Sessions[0].Tracks) { var ccdt = ccdf.TracksByNumber[st.Number]; switch (ccdt.Mode) { case 0: st.TrackType = ETrackType.Audio; //for CCD, this means audio, apparently. break; case 1: st.TrackType = ETrackType.Mode1_2352; break; case 2: st.TrackType = ETrackType.Mode2_2352; break; default: throw new InvalidOperationException("Unsupported CCD mode"); } //add indexes for this track foreach (var ccdi in ccdt.Indexes) { var dsi = new DiscStructure.Index(); //if (ccdi.Key == 0) continue; dsi.LBA = ccdi.Value; dsi.Number = ccdi.Key; st.Indexes.Add(dsi); } } //add sectors for the lead-in, which isn't stored in the CCD file, I think //TODO - synthesize lead-in sectors from TOC, if the lead-in isn't available. //need a test case for that though. var leadin_sector_zero = new Sector_Zero(); var leadin_subcode_zero = new ZeroSubcodeSector(); for (int i = 0; i < 150; i++) { var se = new SectorEntry(leadin_sector_zero); disc.Sectors.Add(se); se.SubcodeSector = leadin_subcode_zero; } //build the sectors: //set up as many sectors as we have img/sub for, even if the TOC doesnt reference them (TOC is unreliable, although the tracks should have covered it all) for (int i = 0; i < loadResults.NumImgSectors; i++) { var isec = new Sector_RawBlob(); isec.Offset = ((long)i) * 2352; isec.Blob = imgBlob; var se = new SectorEntry(isec); disc.Sectors.Add(se); var scsec = new BlobSubcodeSectorPreDeinterleaved(); scsec.Offset = ((long)i) * 96; scsec.Blob = subBlob; se.SubcodeSector = scsec; } return(disc); }
public void Synth(SectorSynthJob job) { //be lazy, just generate the whole sector unconditionally //this is mostly based on mednafen's approach, which was probably finely tailored for PSX //heres the comments on the subject: // I'm not trusting that the "control" field for the TOC leadout entry will always be set properly, so | the control fields for the last track entry // and the leadout entry together before extracting the D2 bit. Audio track->data leadout is fairly benign though maybe noisy(especially if we ever implement // data scrambling properly), but data track->audio leadout could break things in an insidious manner for the more accurate drive emulation code). var ses = job.Disc.Structure.Sessions[SessionNumber]; int lba_relative = job.LBA - ses.LeadoutTrack.LBA; //data is zero int ts = lba_relative; int ats = job.LBA; const int ADR = 0x1; // Q channel data encodes position EControlQ control = ses.LeadoutTrack.Control; //ehhh? CDI? //if(toc.tracks[toc.last_track].valid) // control |= toc.tracks[toc.last_track].control & 0x4; //else if(toc.disc_type == DISC_TYPE_CD_I) // control |= 0x4; control |= (EControlQ)(((int)ses.LastInformationTrack.Control) & 4); SubchannelQ sq = new SubchannelQ(); sq.SetStatus(ADR, control); sq.q_tno.BCDValue = 0xAA; sq.q_index.BCDValue = 0x01; sq.Timestamp = ts; sq.AP_Timestamp = ats; sq.zero = 0; //finally, rely on a gap sector to do the heavy lifting to synthesize this CUE.CueTrackType TrackType = CUE.CueTrackType.Audio; if (ses.LeadoutTrack.IsData) { if (job.Disc.TOC.Session1Format == SessionFormat.Type20_CDXA || job.Disc.TOC.Session1Format == SessionFormat.Type10_CDI) TrackType = CUE.CueTrackType.Mode2_2352; else TrackType = CUE.CueTrackType.Mode1_2352; } CUE.SS_Gap ss_gap = new CUE.SS_Gap() { Policy = Policy, sq = sq, TrackType = TrackType, Pause = true //? }; ss_gap.Synth(job); }
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 }); }
/// <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 (Disc.Blob_ECM.IsECM(ecmPath)) { var ecm = new Disc.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 Disc.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 Disc.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() { Entries = 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); }
public void ReadLBA_SubchannelQ(int offset, ref SubchannelQ sq) { sq.q_status = buffer[offset + 0]; sq.q_tno = buffer[offset + 1]; sq.q_index = buffer[offset + 2]; sq.min.BCDValue = buffer[offset + 3]; sq.sec.BCDValue = buffer[offset + 4]; sq.frame.BCDValue = buffer[offset + 5]; //nothing in byte[6] sq.ap_min.BCDValue = buffer[offset + 7]; sq.ap_sec.BCDValue = buffer[offset + 8]; sq.ap_frame.BCDValue = buffer[offset + 9]; //CRC is stored inverted and big endian.. so... do the opposite byte hibyte = (byte)(~buffer[offset + 10]); byte lobyte = (byte)(~buffer[offset + 11]); sq.q_crc = (ushort)((hibyte << 8) | lobyte); }
/// <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); }
static bool NewTest(string path) { bool ret = false; Disc disc; if (Path.GetExtension(path).ToLower() == ".cue") disc = Disc.FromCuePath(path, new CueBinPrefs()); else disc = Disc.FromCCDPath(path); IntPtr mednadisc = mednadisc_LoadCD(path); //TODO - test leadout a bit, or determine length some superior way //TODO - check length against mednadisc int nSectors = (int)(disc.Structure.BinarySize / 2352) - 150; var subbuf = new byte[96]; var discbuf = new byte[2352 + 96]; var monkeybuf = new byte[2352 + 96]; var disc_qbuf = new byte[96]; var monkey_qbuf = new byte[96]; for (int i = 0; i < nSectors; i++) { mednadisc_ReadSector(mednadisc, i, monkeybuf); disc.ReadLBA_2352(i, discbuf, 0); disc.ReadLBA_SectorEntry(i).SubcodeSector.ReadSubcodeDeinterleaved(subbuf, 0); SubcodeUtils.Interleave(subbuf, 0, discbuf, 2352); //remove P for (int q = 2352; q < 2352 + 96; q++) { discbuf[q] &= 0x7F; monkeybuf[q] &= 0x7F; } for (int q = 0; q < 2352 + 96; q++) { if (discbuf[q] != monkeybuf[q]) { Console.WriteLine("MISMATCH: " + Path.GetFileName(path)); //decode Q subchannels for manual investigation SubcodeUtils.Deinterleave(discbuf, 2352, disc_qbuf, 0); var asr = new QuickSubcodeReader(disc_qbuf); SubchannelQ disc_q = new SubchannelQ(); asr.ReadLBA_SubchannelQ(12, ref disc_q); SubcodeUtils.Deinterleave(monkeybuf, 2352, monkey_qbuf, 0); asr = new QuickSubcodeReader(monkey_qbuf); SubchannelQ monkey_q = new SubchannelQ(); asr.ReadLBA_SubchannelQ(12, ref monkey_q); goto END; } } } ret = true; END: disc.Dispose(); mednadisc_CloseCD(mednadisc); return ret; }
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 }); }