/// <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; } }
/// <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; }
/// <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++; } }
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 }); }
/// <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++; } }
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; } }