From b5d3ff43970665e421f7f7b904b3ca4efcf8a3e9 Mon Sep 17 00:00:00 2001 From: zeromus Date: Sun, 12 Jul 2015 17:45:20 -0500 Subject: [PATCH] refactor cue code to be less weirdly structured and partial-classy --- .../BizHawk.Emulation.DiscSystem.csproj | 2 + .../DiscFormats/CUE/CUE_Compile.cs | 816 +++++++++--------- .../DiscFormats/CUE/CUE_Context.cs | 4 +- .../DiscFormats/CUE/CUE_File.cs | 52 ++ .../DiscFormats/CUE/CUE_Load.cs | 693 ++++++++------- .../DiscFormats/CUE/CUE_Parse.cs | 703 +++++++-------- .../DiscFormats/CUE/CUE_Synths.cs | 265 +++--- .../DiscFormats/CUE/CUE_Types.cs | 46 + .../DiscFormats/CUE/CueFileResolver.cs | 205 +++-- BizHawk.Emulation.DiscSystem/DiscMountJob.cs | 12 +- 10 files changed, 1400 insertions(+), 1398 deletions(-) create mode 100644 BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_File.cs create mode 100644 BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Types.cs diff --git a/BizHawk.Emulation.DiscSystem/BizHawk.Emulation.DiscSystem.csproj b/BizHawk.Emulation.DiscSystem/BizHawk.Emulation.DiscSystem.csproj index ebccdb8ab6..1da8e0a609 100644 --- a/BizHawk.Emulation.DiscSystem/BizHawk.Emulation.DiscSystem.csproj +++ b/BizHawk.Emulation.DiscSystem/BizHawk.Emulation.DiscSystem.csproj @@ -71,9 +71,11 @@ + + diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Compile.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Compile.cs index 0ca2c873b4..2034f0ce03 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Compile.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Compile.cs @@ -7,483 +7,479 @@ using System.Collections.Generic; //this would be a good place for structural validation //after this step, we won't want to have to do stuff like that (it will gunk up already sticky code) -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - partial class CUE_Context + internal class CompiledCDText { - internal class CompiledCDText - { - public string Songwriter; - public string Performer; - public string Title; - public string ISRC; - } + public string Songwriter; + public string Performer; + public string Title; + public string ISRC; + } - internal class CompiledCueIndex - { - public int Number; - - /// - /// this is annoying, it should just be an integer - /// - public Timestamp FileMSF; - - public override string ToString() - { - return string.Format("I#{0:D2} {1}", Number, FileMSF); - } - } + internal class CompiledCueIndex + { + public int Number; /// - /// What type of file we're looking at.. each one would require a different ingestion handler + /// this is annoying, it should just be an integer /// - public enum CompiledCueFileType + public Timestamp FileMSF; + + public override string ToString() { - Unknown, - - /// - /// a raw BIN that can be mounted directly - /// - BIN, - - /// - /// a raw WAV that can be mounted directly - /// - WAVE, - - /// - /// an ECM file that can be mounted directly (once the index is generated) - /// - ECM, - - /// - /// An encoded audio file which can be seeked on the fly, therefore roughly mounted on the fly - /// THIS ISN'T SUPPORTED YET - /// - SeekAudio, - - /// - /// An encoded audio file which can't be seeked on the fly. It must be decoded to a temp buffer, or pre-discohawked - /// - DecodeAudio, + return string.Format("I#{0:D2} {1}", Number, FileMSF); } + } - internal class CompiledCueFile + /// + /// What type of file we're looking at.. each one would require a different ingestion handler + /// + public enum CompiledCueFileType + { + Unknown, + + /// + /// a raw BIN that can be mounted directly + /// + BIN, + + /// + /// a raw WAV that can be mounted directly + /// + WAVE, + + /// + /// an ECM file that can be mounted directly (once the index is generated) + /// + ECM, + + /// + /// An encoded audio file which can be seeked on the fly, therefore roughly mounted on the fly + /// THIS ISN'T SUPPORTED YET + /// + SeekAudio, + + /// + /// An encoded audio file which can't be seeked on the fly. It must be decoded to a temp buffer, or pre-discohawked + /// + DecodeAudio, + } + + internal class CompiledCueFile + { + public string FullPath; + public CompiledCueFileType Type; + public override string ToString() { - public string FullPath; - public CompiledCueFileType Type; - public override string ToString() - { - return string.Format("{0}: {1}", Type, Path.GetFileName(FullPath)); - } + return string.Format("{0}: {1}", Type, Path.GetFileName(FullPath)); } + } - internal class CompiledDiscInfo - { - public int FirstRecordedTrackNumber, LastRecordedTrackNumber; - public SessionFormat SessionFormat; - } + internal class CompiledDiscInfo + { + public int FirstRecordedTrackNumber, LastRecordedTrackNumber; + public SessionFormat SessionFormat; + } - internal class CompiledCueTrack - { - public int BlobIndex; - public int Number; + internal class CompiledCueTrack + { + public int BlobIndex; + public int Number; - /// - /// A track that's final in a file gets its length from the length of the file; other tracks lengths are determined from the succeeding track - /// - public bool IsFinalInFile; + /// + /// A track that's final in a file gets its length from the length of the file; other tracks lengths are determined from the succeeding track + /// + public bool IsFinalInFile; - /// - /// A track that's first in a file has an implicit index 0 at 00:00:00 - /// Otherwise it has an implicit index 0 at the placement of the index 1 - /// - public bool IsFirstInFile; + /// + /// A track that's first in a file has an implicit index 0 at 00:00:00 + /// Otherwise it has an implicit index 0 at the placement of the index 1 + /// + public bool IsFirstInFile; - public CompiledCDText CDTextData = new CompiledCDText(); - public Timestamp PregapLength, PostgapLength; - public CueFile.TrackFlags Flags = CueFile.TrackFlags.None; - public CueFile.TrackType TrackType = CueFile.TrackType.Unknown; + public CompiledCDText CDTextData = new CompiledCDText(); + public Timestamp PregapLength, PostgapLength; + public CueTrackFlags Flags = CueTrackFlags.None; + public CueTrackType TrackType = CueTrackType.Unknown; - public List Indexes = new List(); + public List Indexes = new List(); - public override string ToString() + public override string ToString() + { + var idx = Indexes.Find((i) => i.Number == 1); + if (idx == null) + return string.Format("T#{0:D2} NO INDEX 1", Number); + else { - var idx = Indexes.Find((i) => i.Number == 1); - if (idx == null) - return string.Format("T#{0:D2} NO INDEX 1", Number); - else + var indexlist = string.Join("|", Indexes); + return string.Format("T#{0:D2} {1}:{2} ({3})", Number, BlobIndex, idx.FileMSF, indexlist); + } + } + } + + internal class CompileCueJob : DiscJob + { + /// + /// input: the CueFile to analyze + /// + public CUE_File IN_CueFile; + + /// + /// The context used for this compiling job + /// TODO - rename something like context + /// + public CUE_Context IN_CueFormat; + + /// + /// output: high level disc info + /// + public CompiledDiscInfo OUT_CompiledDiscInfo; + + /// + /// output: CD-Text set at the global level (before any track commands) + /// + public CompiledCDText OUT_GlobalCDText; + + /// + /// output: The compiled file info + /// + public List OUT_CompiledCueFiles; + + /// + /// output: The compiled track info + /// + public List OUT_CompiledCueTracks; + + /// + /// output: An integer between 0 and 10 indicating how costly it will be to load this disc completely. + /// Activites like decoding non-seekable media will increase the load time. + /// 0 - Requires no noticeable time + /// 1 - Requires minimal processing (indexing ECM) + /// 10 - Requires ages, decoding audio data, etc. + /// + public int OUT_LoadTime; + + //----------------------------------------------------------------- + + CompiledCDText curr_cdtext; + int curr_blobIndex = -1; + CompiledCueTrack curr_track = null; + CompiledCueFile curr_file = null; + bool discinfo_session1Format_determined = false; + bool curr_fileHasTrack = false; + + void UpdateDiscInfo(CUE_File.Command.TRACK trackCommand) + { + if (OUT_CompiledDiscInfo.FirstRecordedTrackNumber == 0) + OUT_CompiledDiscInfo.FirstRecordedTrackNumber = trackCommand.Number; + OUT_CompiledDiscInfo.LastRecordedTrackNumber = trackCommand.Number; + if (!discinfo_session1Format_determined) + { + switch (trackCommand.Type) { - var indexlist = string.Join("|", Indexes); - return string.Format("T#{0:D2} {1}:{2} ({3})", Number, BlobIndex, idx.FileMSF, indexlist); + case CueTrackType.Mode2_2336: + case CueTrackType.Mode2_2352: + OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type20_CDXA; + discinfo_session1Format_determined = true; + break; + + case CueTrackType.CDI_2336: + case CueTrackType.CDI_2352: + OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type10_CDI; + discinfo_session1Format_determined = true; + break; + + default: + break; } } } - internal class CompileCueJob : DiscJob + void CloseFile() { - /// - /// input: the CueFile to analyze - /// - public CueFile IN_CueFile; - - /// - /// The context used for this compiling job - /// TODO - rename something like context - /// - public CUE_Context IN_CueFormat; - - /// - /// output: high level disc info - /// - public CompiledDiscInfo OUT_CompiledDiscInfo; - - /// - /// output: CD-Text set at the global level (before any track commands) - /// - public CompiledCDText OUT_GlobalCDText; - - /// - /// output: The compiled file info - /// - public List OUT_CompiledCueFiles; - - /// - /// output: The compiled track info - /// - public List OUT_CompiledCueTracks; - - /// - /// output: An integer between 0 and 10 indicating how costly it will be to load this disc completely. - /// Activites like decoding non-seekable media will increase the load time. - /// 0 - Requires no noticeable time - /// 1 - Requires minimal processing (indexing ECM) - /// 10 - Requires ages, decoding audio data, etc. - /// - public int OUT_LoadTime; - - //----------------------------------------------------------------- - - CompiledCDText curr_cdtext; - int curr_blobIndex = -1; - CompiledCueTrack curr_track = null; - CompiledCueFile curr_file = null; - bool discinfo_session1Format_determined = false; - bool curr_fileHasTrack = false; - - void UpdateDiscInfo(CueFile.Command.TRACK trackCommand) + if (curr_track != null) { - if (OUT_CompiledDiscInfo.FirstRecordedTrackNumber == 0) - OUT_CompiledDiscInfo.FirstRecordedTrackNumber = trackCommand.Number; - OUT_CompiledDiscInfo.LastRecordedTrackNumber = trackCommand.Number; - if (!discinfo_session1Format_determined) + //flag this track as the final one in the file + curr_track.IsFinalInFile = true; + } + + curr_file = null; + } + + void OpenFile(CUE_File.Command.FILE f) + { + if (curr_file != null) + CloseFile(); + + curr_blobIndex++; + curr_fileHasTrack = false; + + var Resolver = IN_CueFormat.Resolver; + + //TODO - smart audio file resolving only for AUDIO types. not BINARY or MOTOROLA or AIFF or ECM or what have you + + var options = Resolver.Resolve(f.Path); + string choice = null; + if (options.Count == 0) + { + Error("Couldn't resolve referenced cue file: " + f.Path); + return; + } + else + { + choice = options[0]; + if (options.Count > 1) + Warn("Multiple options resolving referenced cue file; choosing: " + Path.GetFileName(choice)); + } + + var cfi = new CompiledCueFile(); + OUT_CompiledCueFiles.Add(cfi); + + cfi.FullPath = choice; + + //determine the CueFileInfo's type, based on extension and extra checking + //TODO - once we reorganize the file ID stuff, do legit checks here (this is completely redundant with the fileID system + //TODO - decode vs stream vs unpossible policies in input policies object (including ffmpeg availability-checking callback (results can be cached)) + string blobPathExt = Path.GetExtension(choice).ToUpperInvariant(); + if (blobPathExt == ".BIN" || blobPathExt == ".IMG") cfi.Type = CompiledCueFileType.BIN; + else if (blobPathExt == ".ISO") cfi.Type = CompiledCueFileType.BIN; + else if (blobPathExt == ".WAV") + { + //quickly, check the format. turn it to DecodeAudio if it can't be supported + //TODO - fix exception-throwing inside + //TODO - verify stream-disposing semantics + var fs = File.OpenRead(choice); + using (var blob = new Disc.Blob_WaveFile()) { - switch (trackCommand.Type) + try { - case CueFile.TrackType.Mode2_2336: - case CueFile.TrackType.Mode2_2352: - OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type20_CDXA; - discinfo_session1Format_determined = true; - break; - - case CueFile.TrackType.CDI_2336: - case CueFile.TrackType.CDI_2352: - OUT_CompiledDiscInfo.SessionFormat = SessionFormat.Type10_CDI; - discinfo_session1Format_determined = true; - break; - - default: - break; + blob.Load(fs); + cfi.Type = CompiledCueFileType.WAVE; + } + catch + { + cfi.Type = CompiledCueFileType.DecodeAudio; } } } - - void CloseFile() + else if (blobPathExt == ".APE") cfi.Type = CompiledCueFileType.DecodeAudio; + else if (blobPathExt == ".MP3") cfi.Type = CompiledCueFileType.DecodeAudio; + else if (blobPathExt == ".MPC") cfi.Type = CompiledCueFileType.DecodeAudio; + else if (blobPathExt == ".FLAC") cfi.Type = CompiledCueFileType.DecodeAudio; + else if (blobPathExt == ".ECM") { - if (curr_track != null) + cfi.Type = CompiledCueFileType.ECM; + if (!Disc.Blob_ECM.IsECM(choice)) { - //flag this track as the final one in the file - curr_track.IsFinalInFile = true; - } - - curr_file = null; - } - - void OpenFile(CueFile.Command.FILE f) - { - if (curr_file != null) - CloseFile(); - - curr_blobIndex++; - curr_fileHasTrack = false; - - var Resolver = IN_CueFormat.Resolver; - - //TODO - smart audio file resolving only for AUDIO types. not BINARY or MOTOROLA or AIFF or ECM or what have you - - var options = Resolver.Resolve(f.Path); - string choice = null; - if (options.Count == 0) - { - Error("Couldn't resolve referenced cue file: " + f.Path); - return; - } - else - { - choice = options[0]; - if (options.Count > 1) - Warn("Multiple options resolving referenced cue file; choosing: " + Path.GetFileName(choice)); - } - - var cfi = new CompiledCueFile(); - OUT_CompiledCueFiles.Add(cfi); - - cfi.FullPath = choice; - - //determine the CueFileInfo's type, based on extension and extra checking - //TODO - once we reorganize the file ID stuff, do legit checks here (this is completely redundant with the fileID system - //TODO - decode vs stream vs unpossible policies in input policies object (including ffmpeg availability-checking callback (results can be cached)) - string blobPathExt = Path.GetExtension(choice).ToUpperInvariant(); - if (blobPathExt == ".BIN" || blobPathExt == ".IMG") cfi.Type = CompiledCueFileType.BIN; - else if (blobPathExt == ".ISO") cfi.Type = CompiledCueFileType.BIN; - else if (blobPathExt == ".WAV") - { - //quickly, check the format. turn it to DecodeAudio if it can't be supported - //TODO - fix exception-throwing inside - //TODO - verify stream-disposing semantics - var fs = File.OpenRead(choice); - using (var blob = new Disc.Blob_WaveFile()) - { - try - { - blob.Load(fs); - cfi.Type = CompiledCueFileType.WAVE; - } - catch - { - cfi.Type = CompiledCueFileType.DecodeAudio; - } - } - } - else if (blobPathExt == ".APE") cfi.Type = CompiledCueFileType.DecodeAudio; - else if (blobPathExt == ".MP3") cfi.Type = CompiledCueFileType.DecodeAudio; - else if (blobPathExt == ".MPC") cfi.Type = CompiledCueFileType.DecodeAudio; - else if (blobPathExt == ".FLAC") cfi.Type = CompiledCueFileType.DecodeAudio; - else if (blobPathExt == ".ECM") - { - cfi.Type = CompiledCueFileType.ECM; - if (!Disc.Blob_ECM.IsECM(choice)) - { - Error("an ECM file was specified or detected, but it isn't a valid ECM file: " + Path.GetFileName(choice)); - cfi.Type = CompiledCueFileType.Unknown; - } - } - else - { - Error("Unknown cue file type. Since it's likely an unsupported compression, this is an error: ", Path.GetFileName(choice)); + Error("an ECM file was specified or detected, but it isn't a valid ECM file: " + Path.GetFileName(choice)); cfi.Type = CompiledCueFileType.Unknown; } - - //TODO - check for mismatches between track types and file types, or is that best done when interpreting the commands? + } + else + { + Error("Unknown cue file type. Since it's likely an unsupported compression, this is an error: ", Path.GetFileName(choice)); + cfi.Type = CompiledCueFileType.Unknown; } - void CreateTrack1Pregap() + //TODO - check for mismatches between track types and file types, or is that best done when interpreting the commands? + } + + void CreateTrack1Pregap() + { + if (OUT_CompiledCueTracks[1].PregapLength.Sector == 0) { } + else if (OUT_CompiledCueTracks[1].PregapLength.Sector == 150) { } + else { - if (OUT_CompiledCueTracks[1].PregapLength.Sector == 0) { } - else if (OUT_CompiledCueTracks[1].PregapLength.Sector == 150) { } - else + Error("Track 1 specified an illegal pregap. It's being ignored and replaced with a 00:02:00 pregap"); + } + OUT_CompiledCueTracks[1].PregapLength = new Timestamp(150); + } + + void FinalAnalysis() + { + //some quick checks: + if (OUT_CompiledCueFiles.Count == 0) + Error("Cue file doesn't specify any input files!"); + + //we can't reliably analyze the length of files here, because we might have to be decoding to get lengths (VBR mp3s) + //So, it's not really worth the trouble. We'll cope with lengths later + //we could check the format of the wav file here, though + + //score the cost of loading the file + bool needsCodec = false; + OUT_LoadTime = 0; + foreach (var cfi in OUT_CompiledCueFiles) + { + if (cfi.Type == CompiledCueFileType.DecodeAudio) { - Error("Track 1 specified an illegal pregap. It's being ignored and replaced with a 00:02:00 pregap"); + needsCodec = true; + OUT_LoadTime = Math.Max(OUT_LoadTime, 10); } - OUT_CompiledCueTracks[1].PregapLength = new Timestamp(150); + if (cfi.Type == CompiledCueFileType.SeekAudio) + needsCodec = true; + if (cfi.Type == CompiledCueFileType.ECM) + OUT_LoadTime = Math.Max(OUT_LoadTime, 1); } - void FinalAnalysis() + //check whether processing was available + if (needsCodec) { - //some quick checks: - if (OUT_CompiledCueFiles.Count == 0) - Error("Cue file doesn't specify any input files!"); - - //we can't reliably analyze the length of files here, because we might have to be decoding to get lengths (VBR mp3s) - //So, it's not really worth the trouble. We'll cope with lengths later - //we could check the format of the wav file here, though - - //score the cost of loading the file - bool needsCodec = false; - OUT_LoadTime = 0; - foreach (var cfi in OUT_CompiledCueFiles) - { - if (cfi.Type == CompiledCueFileType.DecodeAudio) - { - needsCodec = true; - OUT_LoadTime = Math.Max(OUT_LoadTime, 10); - } - if (cfi.Type == CompiledCueFileType.SeekAudio) - needsCodec = true; - if (cfi.Type == CompiledCueFileType.ECM) - OUT_LoadTime = Math.Max(OUT_LoadTime, 1); - } - - //check whether processing was available - if (needsCodec) - { - FFMpeg ffmpeg = new FFMpeg(); - if (!ffmpeg.QueryServiceAvailable()) - Warn("Decoding service will be required for further processing, but is not available"); - } + FFMpeg ffmpeg = new FFMpeg(); + if (!ffmpeg.QueryServiceAvailable()) + Warn("Decoding service will be required for further processing, but is not available"); } + } - void CloseTrack() - { - if (curr_track == null) - return; + void CloseTrack() + { + if (curr_track == null) + return; - //normalize: if an index 0 is missing, add it here - if (curr_track.Indexes[0].Number != 0) - { - var index0 = new CompiledCueIndex(); - var index1 = curr_track.Indexes[0]; - index0.Number = 0; - index0.FileMSF = index1.FileMSF; //same MSF as index 1 will make it effectively nonexistent + //normalize: if an index 0 is missing, add it here + if (curr_track.Indexes[0].Number != 0) + { + var index0 = new CompiledCueIndex(); + var index1 = curr_track.Indexes[0]; + index0.Number = 0; + index0.FileMSF = index1.FileMSF; //same MSF as index 1 will make it effectively nonexistent - //well now, if it's the first in the file, an implicit index will take its value from 00:00:00 in the file - //this is the kind of thing I sought to solve originally by 'interpreting' the file, but it seems easy enough to handle this way - //my carlin.cue tests this but test cases shouldnt be hard to find - if (curr_track.IsFirstInFile) - index0.FileMSF = new Timestamp(0); + //well now, if it's the first in the file, an implicit index will take its value from 00:00:00 in the file + //this is the kind of thing I sought to solve originally by 'interpreting' the file, but it seems easy enough to handle this way + //my carlin.cue tests this but test cases shouldnt be hard to find + if (curr_track.IsFirstInFile) + index0.FileMSF = new Timestamp(0); - curr_track.Indexes.Insert(0, index0); - } - - OUT_CompiledCueTracks.Add(curr_track); - curr_track = null; + curr_track.Indexes.Insert(0, index0); } - void OpenTrack(CueFile.Command.TRACK trackCommand) - { - curr_track = new CompiledCueTrack(); + OUT_CompiledCueTracks.Add(curr_track); + curr_track = null; + } - //spill cdtext data into this track - curr_cdtext = curr_track.CDTextData; + void OpenTrack(CUE_File.Command.TRACK trackCommand) + { + curr_track = new CompiledCueTrack(); + + //spill cdtext data into this track + curr_cdtext = curr_track.CDTextData; - curr_track.BlobIndex = curr_blobIndex; - curr_track.Number = trackCommand.Number; - curr_track.TrackType = trackCommand.Type; + curr_track.BlobIndex = curr_blobIndex; + curr_track.Number = trackCommand.Number; + curr_track.TrackType = trackCommand.Type; - //default flags - if (curr_track.TrackType != CueFile.TrackType.Audio) - curr_track.Flags = CueFile.TrackFlags.DATA; + //default flags + if (curr_track.TrackType != CueTrackType.Audio) + curr_track.Flags = CueTrackFlags.DATA; - if (!curr_fileHasTrack) - { - curr_fileHasTrack = curr_track.IsFirstInFile = true; - } - - UpdateDiscInfo(trackCommand); + if (!curr_fileHasTrack) + { + curr_fileHasTrack = curr_track.IsFirstInFile = true; } - void AddIndex(CueFile.Command.INDEX indexCommand) + UpdateDiscInfo(trackCommand); + } + + void AddIndex(CUE_File.Command.INDEX indexCommand) + { + var newindex = new CompiledCueIndex(); + newindex.FileMSF = indexCommand.Timestamp; + newindex.Number = indexCommand.Number; + curr_track.Indexes.Add(newindex); + } + + public void Run() + { + //in params + var cue = IN_CueFile; + + //output state + OUT_GlobalCDText = new CompiledCDText(); + OUT_CompiledDiscInfo = new CompiledDiscInfo(); + OUT_CompiledCueFiles = new List(); + OUT_CompiledCueTracks = new List(); + + //add a track 0, for addressing convenience. + //note: for future work, track 0 may need emulation (accessible by very negative LBA--the TOC is stored there) + var track0 = new CompiledCueTrack() { + Number = 0, + }; + OUT_CompiledCueTracks.Add(track0); + + //global cd text will acquire the cdtext commands set before track commands + curr_cdtext = OUT_GlobalCDText; + + for (int i = 0; i < cue.Commands.Count; i++) { - var newindex = new CompiledCueIndex(); - newindex.FileMSF = indexCommand.Timestamp; - newindex.Number = indexCommand.Number; - curr_track.Indexes.Add(newindex); - } + var cmd = cue.Commands[i]; - public void Run() - { - //in params - var cue = IN_CueFile; + //these commands get dealt with globally. nothing to be done here + //(but in the future we need to accumulate them into the compile pass output) + if (cmd is CUE_File.Command.CATALOG || cmd is CUE_File.Command.CDTEXTFILE) continue; - //output state - OUT_GlobalCDText = new CompiledCDText(); - OUT_CompiledDiscInfo = new CompiledDiscInfo(); - OUT_CompiledCueFiles = new List(); - OUT_CompiledCueTracks = new List(); + //nothing to be done for comments + if (cmd is CUE_File.Command.REM) continue; + if (cmd is CUE_File.Command.COMMENT) continue; - //add a track 0, for addressing convenience. - //note: for future work, track 0 may need emulation (accessible by very negative LBA--the TOC is stored there) - var track0 = new CompiledCueTrack() { - Number = 0, - }; - OUT_CompiledCueTracks.Add(track0); + //CD-text and related + if (cmd is CUE_File.Command.PERFORMER) curr_cdtext.Performer = (cmd as CUE_File.Command.PERFORMER).Value; + if (cmd is CUE_File.Command.SONGWRITER) curr_cdtext.Songwriter = (cmd as CUE_File.Command.SONGWRITER).Value; + if (cmd is CUE_File.Command.TITLE) curr_cdtext.Title = (cmd as CUE_File.Command.TITLE).Value; + if (cmd is CUE_File.Command.ISRC) curr_cdtext.ISRC = (cmd as CUE_File.Command.ISRC).Value; - //global cd text will acquire the cdtext commands set before track commands - curr_cdtext = OUT_GlobalCDText; - - for (int i = 0; i < cue.Commands.Count; i++) + //flags can only be set when a track command is running + if (cmd is CUE_File.Command.FLAGS) { - var cmd = cue.Commands[i]; - - //these commands get dealt with globally. nothing to be done here - //(but in the future we need to accumulate them into the compile pass output) - if (cmd is CueFile.Command.CATALOG || cmd is CueFile.Command.CDTEXTFILE) continue; - - //nothing to be done for comments - if (cmd is CueFile.Command.REM) continue; - if (cmd is CueFile.Command.COMMENT) continue; - - //CD-text and related - if (cmd is CueFile.Command.PERFORMER) curr_cdtext.Performer = (cmd as CueFile.Command.PERFORMER).Value; - if (cmd is CueFile.Command.SONGWRITER) curr_cdtext.Songwriter = (cmd as CueFile.Command.SONGWRITER).Value; - if (cmd is CueFile.Command.TITLE) curr_cdtext.Title = (cmd as CueFile.Command.TITLE).Value; - if (cmd is CueFile.Command.ISRC) curr_cdtext.ISRC = (cmd as CueFile.Command.ISRC).Value; - - //flags can only be set when a track command is running - if (cmd is CueFile.Command.FLAGS) - { - if (curr_track == null) - Warn("Ignoring invalid flag commands outside of a track command"); - else - //take care to |= it here, so the data flag doesn't get cleared - curr_track.Flags |= (cmd as CueFile.Command.FLAGS).Flags; - } - - if (cmd is CueFile.Command.TRACK) - { - CloseTrack(); - OpenTrack(cmd as CueFile.Command.TRACK); - } - - if (cmd is CueFile.Command.FILE) - { - CloseFile(); - OpenFile(cmd as CueFile.Command.FILE); - } - - if (cmd is CueFile.Command.INDEX) - { - //todo - validate no postgap specified - AddIndex(cmd as CueFile.Command.INDEX); - } - - if (cmd is CueFile.Command.PREGAP) - { - //validate track open - //validate no indexes - curr_track.PregapLength = (cmd as CueFile.Command.PREGAP).Length; - } - - if (cmd is CueFile.Command.POSTGAP) - { - curr_track.PostgapLength = (cmd as CueFile.Command.POSTGAP).Length; - } - + if (curr_track == null) + Warn("Ignoring invalid flag commands outside of a track command"); + else + //take care to |= it here, so the data flag doesn't get cleared + curr_track.Flags |= (cmd as CUE_File.Command.FLAGS).Flags; } - //it's a bit odd to close the file before closing the track, but... - //we need to be sure to CloseFile first to make sure the track is marked as the final one in the file - CloseFile(); - CloseTrack(); + if (cmd is CUE_File.Command.TRACK) + { + CloseTrack(); + OpenTrack(cmd as CUE_File.Command.TRACK); + } - CreateTrack1Pregap(); - FinalAnalysis(); + if (cmd is CUE_File.Command.FILE) + { + CloseFile(); + OpenFile(cmd as CUE_File.Command.FILE); + } + + if (cmd is CUE_File.Command.INDEX) + { + //todo - validate no postgap specified + AddIndex(cmd as CUE_File.Command.INDEX); + } + + if (cmd is CUE_File.Command.PREGAP) + { + //validate track open + //validate no indexes + curr_track.PregapLength = (cmd as CUE_File.Command.PREGAP).Length; + } + + if (cmd is CUE_File.Command.POSTGAP) + { + curr_track.PostgapLength = (cmd as CUE_File.Command.POSTGAP).Length; + } + + } + + //it's a bit odd to close the file before closing the track, but... + //we need to be sure to CloseFile first to make sure the track is marked as the final one in the file + CloseFile(); + CloseTrack(); + + CreateTrack1Pregap(); + FinalAnalysis(); - } //Run() + } //Run() - } //class CompileCueJob - - } //partial class CUE_Format2 + } //class CompileCueJob } //namespace BizHawk.Emulation.DiscSystem \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Context.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Context.cs index f98da48aee..4a6529c5be 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Context.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Context.cs @@ -6,9 +6,9 @@ using System.Collections.Generic; //http://digitalx.org/cue-sheet/index.html "all cue sheet information is a straight 1:1 copy from the cdrwin helpfile" -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - public partial class CUE_Context + public class CUE_Context { /// /// The CueFileResolver to be used by this instance diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_File.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_File.cs new file mode 100644 index 0000000000..7a55fead7b --- /dev/null +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_File.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.DiscSystem.CUE +{ + /// + /// Represents the contents of a cue file + /// + class CUE_File + { + // (here are all the commands we can encounter) + public static class Command + { + //TODO - record line number origin of command? Kind of nice but inessential + public class CATALOG { public string Value; public override string ToString() { return string.Format("CATALOG: {0}", Value); } } + public class CDTEXTFILE { public string Path; public override string ToString() { return string.Format("CDTEXTFILE: {0}", Path); } } + public class FILE { public string Path; public CueFileType Type; public override string ToString() { return string.Format("FILE ({0}): {1}", Type, Path); } } + public class FLAGS { public CueTrackFlags Flags; public override string ToString() { return string.Format("FLAGS {0}", Flags); } } + public class INDEX { public int Number; public Timestamp Timestamp; public override string ToString() { return string.Format("INDEX {0,2} {1}", Number, Timestamp); } } + public class ISRC { public string Value; public override string ToString() { return string.Format("ISRC: {0}", Value); } } + public class PERFORMER { public string Value; public override string ToString() { return string.Format("PERFORMER: {0}", Value); } } + public class POSTGAP { public Timestamp Length; public override string ToString() { return string.Format("POSTGAP: {0}", Length); } } + public class PREGAP { public Timestamp Length; public override string ToString() { return string.Format("PREGAP: {0}", Length); } } + public class REM { public string Value; public override string ToString() { return string.Format("REM: {0}", Value); } } + public class COMMENT { public string Value; public override string ToString() { return string.Format("COMMENT: {0}", Value); } } + public class SONGWRITER { public string Value; public override string ToString() { return string.Format("SONGWRITER: {0}", Value); } } + public class TITLE { public string Value; public override string ToString() { return string.Format("TITLE: {0}", Value); } } + public class TRACK { public int Number; public CueTrackType Type; public override string ToString() { return string.Format("TRACK {0,2} ({1})", Number, Type); } } + } + + + /// + /// Stuff other than the commands, global for the whole disc + /// + public class DiscInfo + { + public Command.CATALOG Catalog; + public Command.ISRC ISRC; + public Command.CDTEXTFILE CDTextFile; + } + + /// + /// The sequential list of commands parsed out of the cue file + /// + public List Commands = new List(); + + /// + /// Stuff other than the commands, global for the whole disc + /// + public DiscInfo GlobalDiscInfo = new DiscInfo(); + } +} \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Load.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Load.cs index c07d3e95a3..04fe00ba8c 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Load.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Load.cs @@ -23,381 +23,378 @@ using System.Text; using System.IO; using System.Collections.Generic; -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - partial class CUE_Context + /// + /// Loads a cue file into a Disc. + /// For this job, virtually all nonsense input is treated as errors, but the process will try to recover as best it can. + /// The user should still reject any jobs which generated errors + /// + internal class LoadCueJob : DiscJob { /// - /// Loads a cue file into a Disc. - /// For this job, virtually all nonsense input is treated as errors, but the process will try to recover as best it can. - /// The user should still reject any jobs which generated errors + /// The results of the compile job, a prerequisite for this /// - internal class LoadCueJob : DiscJob + public CompileCueJob IN_CompileJob; + + /// + /// The resulting disc + /// + public Disc OUT_Disc; + + private enum BurnType { - /// - /// The results of the compile job, a prerequisite for this - /// - public CompileCueJob IN_CompileJob; + Normal, Pregap, Postgap + } - /// - /// The resulting disc - /// - public Disc OUT_Disc; + class BlobInfo + { + public IBlob Blob; + public long Length; + } - private enum BurnType + //not sure if we need this... + class TrackInfo + { + public int Length; + + public CompiledCueTrack CompiledCueTrack; + } + + List BlobInfos; + List TrackInfos = new List(); + + + void MountBlobs() + { + IBlob file_blob = null; + + BlobInfos = new List(); + foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles) { - Normal, Pregap, Postgap - } + var bi = new BlobInfo(); + BlobInfos.Add(bi); - class BlobInfo - { - public IBlob Blob; - public long Length; - } - - //not sure if we need this... - class TrackInfo - { - public int Length; - - public CompiledCueTrack CompiledCueTrack; - } - - List BlobInfos; - List TrackInfos = new List(); - - - void MountBlobs() - { - IBlob file_blob = null; - - BlobInfos = new List(); - foreach (var ccf in IN_CompileJob.OUT_CompiledCueFiles) + switch (ccf.Type) { - var bi = new BlobInfo(); - BlobInfos.Add(bi); - - switch (ccf.Type) - { - case CompiledCueFileType.BIN: - case CompiledCueFileType.Unknown: - { - //raw files: - var blob = new Disc.Blob_RawFile { PhysicalPath = ccf.FullPath }; - OUT_Disc.DisposableResources.Add(file_blob = blob); - bi.Length = blob.Length; - break; - } - case CompiledCueFileType.ECM: - { - var blob = new Disc.Blob_ECM(); - OUT_Disc.DisposableResources.Add(file_blob = blob); - blob.Load(ccf.FullPath); - bi.Length = blob.Length; - break; - } - case CompiledCueFileType.WAVE: - { - var blob = new Disc.Blob_WaveFile(); - OUT_Disc.DisposableResources.Add(file_blob = blob); - blob.Load(ccf.FullPath); - bi.Length = blob.Length; - break; - } - case CompiledCueFileType.DecodeAudio: - { - FFMpeg ffmpeg = new FFMpeg(); - if (!ffmpeg.QueryServiceAvailable()) - { - throw new DiscReferenceException(ccf.FullPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)"); - } - AudioDecoder dec = new AudioDecoder(); - byte[] buf = dec.AcquireWaveData(ccf.FullPath); - var blob = new Disc.Blob_WaveFile(); - OUT_Disc.DisposableResources.Add(file_blob = blob); - blob.Load(new MemoryStream(buf)); - bi.Length = buf.Length; - break; - } - default: - throw new InvalidOperationException(); - } //switch(file type) - - //wrap all the blobs with zero padding - bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length); - } - } - - - void AnalyzeTracks() - { - var compiledTracks = IN_CompileJob.OUT_CompiledCueTracks; - - for(int t=0;t 0) + case CompiledCueFileType.BIN: + case CompiledCueFileType.Unknown: { - //if burning through a specified pregap, count it down - generateGap = true; - specifiedPregapLength--; - } - else - { - //if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it - for (; ; ) - { - if (curr_index == cct.Indexes.Count - 1) - break; - if (curr_blobMSF >= cct.Indexes[curr_index + 1].FileMSF.Sector) - { - curr_index++; - if (curr_index == 1) - { - //WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track - EmitRawTOCEntry(cct); - } - } - else break; - } - } - - //select the track type for the subQ - //it's obviously the same as the main track type usually, but during a pregap it can be different - TrackInfo qTrack = ti; - int qRelMSF = relMSF; - if (curr_index == 0) - { - //tweak relMSF due to ambiguity/contradiction in yellowbook docs - if (!context.DiscMountPolicy.CUE_PregapContradictionModeA) - qRelMSF++; - - //[IEC10149] says there's two "intervals" of a pregap. - //mednafen's pseudocode interpretation of this: - //if this is a data track and the previous track was not data, the last 150 sectors of the pregap match this track and the earlier sectors (at least 75) math the previous track - //I agree, so let's do it that way - if (t != 1 && cct.TrackType != CueFile.TrackType.Audio && TrackInfos[t - 1].CompiledCueTrack.TrackType == CueFile.TrackType.Audio) - { - if (relMSF < -150) - { - qTrack = TrackInfos[t - 1]; - } - } - } - - //generate the right kind of sector synth for this track - SS_Base ss = null; - if (generateGap) - { - var ss_gap = new SS_Gap(); - ss_gap.TrackType = qTrack.CompiledCueTrack.TrackType; - ss = ss_gap; - } - else - { - int sectorSize = int.MaxValue; - switch (qTrack.CompiledCueTrack.TrackType) - { - case CueFile.TrackType.Audio: - case CueFile.TrackType.CDI_2352: - case CueFile.TrackType.Mode1_2352: - case CueFile.TrackType.Mode2_2352: - ss = new SS_2352(); - sectorSize = 2352; - break; - - case CueFile.TrackType.Mode1_2048: - ss = new SS_Mode1_2048(); - sectorSize = 2048; - break; - - default: - case CueFile.TrackType.Mode2_2336: - throw new InvalidOperationException("Not supported: " + cct.TrackType); - } - - ss.Blob = curr_blobInfo.Blob; - ss.BlobOffset = curr_blobOffset; - curr_blobOffset += sectorSize; - curr_blobMSF++; - } - - ss.Policy = context.DiscMountPolicy; - - //setup subQ - byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption: - ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags); - ss.sq.q_tno = BCD2.FromDecimal(cct.Number); - ss.sq.q_index = BCD2.FromDecimal(curr_index); - ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count); - ss.sq.Timestamp = new Timestamp(qRelMSF); - - //setup subP - if (curr_index == 0) - ss.Pause = true; - - OUT_Disc.Sectors.Add(ss); - relMSF++; - - if (cct.IsFinalInFile) - { - //sometimes, break when the file is exhausted - if (curr_blobOffset >= curr_blobInfo.Length) - trackDone = true; - } - else - { - //other times, break when the track is done - //(this check is safe because it's not the final track overall if it's not the final track in a file) - if (curr_blobMSF >= TrackInfos[t + 1].CompiledCueTrack.Indexes[0].FileMSF.Sector) - trackDone = true; - } - - if (trackDone) + //raw files: + var blob = new Disc.Blob_RawFile { PhysicalPath = ccf.FullPath }; + OUT_Disc.DisposableResources.Add(file_blob = blob); + bi.Length = blob.Length; break; + } + case CompiledCueFileType.ECM: + { + var blob = new Disc.Blob_ECM(); + OUT_Disc.DisposableResources.Add(file_blob = blob); + blob.Load(ccf.FullPath); + bi.Length = blob.Length; + break; + } + case CompiledCueFileType.WAVE: + { + var blob = new Disc.Blob_WaveFile(); + OUT_Disc.DisposableResources.Add(file_blob = blob); + blob.Load(ccf.FullPath); + bi.Length = blob.Length; + break; + } + case CompiledCueFileType.DecodeAudio: + { + FFMpeg ffmpeg = new FFMpeg(); + if (!ffmpeg.QueryServiceAvailable()) + { + throw new DiscReferenceException(ccf.FullPath, "No decoding service was available (make sure ffmpeg.exe is available. even though this may be a wav, ffmpeg is used to load oddly formatted wave files. If you object to this, please send us a note and we'll see what we can do. It shouldn't be too hard.)"); + } + AudioDecoder dec = new AudioDecoder(); + byte[] buf = dec.AcquireWaveData(ccf.FullPath); + var blob = new Disc.Blob_WaveFile(); + OUT_Disc.DisposableResources.Add(file_blob = blob); + blob.Load(new MemoryStream(buf)); + bi.Length = buf.Length; + break; + } + default: + throw new InvalidOperationException(); + } //switch(file type) + + //wrap all the blobs with zero padding + bi.Blob = new Disc.Blob_ZeroPadAdapter(file_blob, bi.Length); + } + } + + + void AnalyzeTracks() + { + var compiledTracks = IN_CompileJob.OUT_CompiledCueTracks; + + for(int t=0;t 0) + { + //if burning through a specified pregap, count it down + generateGap = true; + specifiedPregapLength--; + } + else + { + //if burning through the file, select the appropriate index by inspecting the next index and seeing if we've reached it + for (; ; ) + { + if (curr_index == cct.Indexes.Count - 1) + break; + if (curr_blobMSF >= cct.Indexes[curr_index + 1].FileMSF.Sector) + { + curr_index++; + if (curr_index == 1) + { + //WE ARE NOW AT INDEX 1: generate the RawTOCEntry for this track + EmitRawTOCEntry(cct); + } + } + else break; + } } - //--------------------------------- - //gen postgap sectors - int specifiedPostgapLength = cct.PostgapLength.Sector; - for (int s = 0; s < specifiedPostgapLength; s++) + //select the track type for the subQ + //it's obviously the same as the main track type usually, but during a pregap it can be different + TrackInfo qTrack = ti; + int qRelMSF = relMSF; + if (curr_index == 0) { - var ss = new SS_Gap(); - ss.TrackType = cct.TrackType; //TODO - old track type in some < -150 cases? + //tweak relMSF due to ambiguity/contradiction in yellowbook docs + if (!context.DiscMountPolicy.CUE_PregapContradictionModeA) + qRelMSF++; - //-subq- - byte ADR = 1; - ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags); - ss.sq.q_tno = BCD2.FromDecimal(cct.Number); - ss.sq.q_index = BCD2.FromDecimal(curr_index); - ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count); - ss.sq.Timestamp = new Timestamp(relMSF); + //[IEC10149] says there's two "intervals" of a pregap. + //mednafen's pseudocode interpretation of this: + //if this is a data track and the previous track was not data, the last 150 sectors of the pregap match this track and the earlier sectors (at least 75) math the previous track + //I agree, so let's do it that way + if (t != 1 && cct.TrackType != CueTrackType.Audio && TrackInfos[t - 1].CompiledCueTrack.TrackType == CueTrackType.Audio) + { + if (relMSF < -150) + { + qTrack = TrackInfos[t - 1]; + } + } + } - //-subP- - //always paused--is this good enough? + //generate the right kind of sector synth for this track + SS_Base ss = null; + if (generateGap) + { + var ss_gap = new SS_Gap(); + ss_gap.TrackType = qTrack.CompiledCueTrack.TrackType; + ss = ss_gap; + } + else + { + int sectorSize = int.MaxValue; + switch (qTrack.CompiledCueTrack.TrackType) + { + case CueTrackType.Audio: + case CueTrackType.CDI_2352: + case CueTrackType.Mode1_2352: + case CueTrackType.Mode2_2352: + ss = new SS_2352(); + sectorSize = 2352; + break; + + case CueTrackType.Mode1_2048: + ss = new SS_Mode1_2048(); + sectorSize = 2048; + break; + + default: + case CueTrackType.Mode2_2336: + throw new InvalidOperationException("Not supported: " + cct.TrackType); + } + + ss.Blob = curr_blobInfo.Blob; + ss.BlobOffset = curr_blobOffset; + curr_blobOffset += sectorSize; + curr_blobMSF++; + } + + ss.Policy = context.DiscMountPolicy; + + //setup subQ + byte ADR = 1; //absent some kind of policy for how to set it, this is a safe assumption: + ss.sq.SetStatus(ADR, (EControlQ)(int)qTrack.CompiledCueTrack.Flags); + ss.sq.q_tno = BCD2.FromDecimal(cct.Number); + ss.sq.q_index = BCD2.FromDecimal(curr_index); + ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count); + ss.sq.Timestamp = new Timestamp(qRelMSF); + + //setup subP + if (curr_index == 0) ss.Pause = true; - OUT_Disc.Sectors.Add(ss); - relMSF++; + OUT_Disc.Sectors.Add(ss); + relMSF++; + + if (cct.IsFinalInFile) + { + //sometimes, break when the file is exhausted + if (curr_blobOffset >= curr_blobInfo.Length) + trackDone = true; + } + else + { + //other times, break when the track is done + //(this check is safe because it's not the final track overall if it's not the final track in a file) + if (curr_blobMSF >= TrackInfos[t + 1].CompiledCueTrack.Indexes[0].FileMSF.Sector) + trackDone = true; } + if (trackDone) + break; + } - } //end track loop + //--------------------------------- + //gen postgap sectors + int specifiedPostgapLength = cct.PostgapLength.Sector; + for (int s = 0; s < specifiedPostgapLength; s++) + { + var ss = new SS_Gap(); + ss.TrackType = cct.TrackType; //TODO - old track type in some < -150 cases? + + //-subq- + byte ADR = 1; + ss.sq.SetStatus(ADR, (EControlQ)(int)cct.Flags); + ss.sq.q_tno = BCD2.FromDecimal(cct.Number); + ss.sq.q_index = BCD2.FromDecimal(curr_index); + ss.sq.AP_Timestamp = new Timestamp(OUT_Disc.Sectors.Count); + ss.sq.Timestamp = new Timestamp(relMSF); + + //-subP- + //always paused--is this good enough? + ss.Pause = true; + + OUT_Disc.Sectors.Add(ss); + relMSF++; + } - //add RawTOCEntries A0 A1 A2 to round out the TOC - var TOCMiscInfo = new Synthesize_A0A1A2_Job { - IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber, - IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber, - IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat, - IN_LeadoutTimestamp = new Timestamp(OUT_Disc.Sectors.Count) //do we need a +150? - }; - TOCMiscInfo.Run(OUT_Disc.RawTOCEntries); + } //end track loop - //TODO - generate leadout, or delegates at least - //blech, old crap, maybe - //OUT_Disc.Structure.Synthesize_TOCPointsFromSessions(); + //add RawTOCEntries A0 A1 A2 to round out the TOC + var TOCMiscInfo = new Synthesize_A0A1A2_Job { + IN_FirstRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.FirstRecordedTrackNumber, + IN_LastRecordedTrackNumber = IN_CompileJob.OUT_CompiledDiscInfo.LastRecordedTrackNumber, + IN_Session1Format = IN_CompileJob.OUT_CompiledDiscInfo.SessionFormat, + IN_LeadoutTimestamp = new Timestamp(OUT_Disc.Sectors.Count) //do we need a +150? + }; + TOCMiscInfo.Run(OUT_Disc.RawTOCEntries); - //FinishLog(); + //TODO - generate leadout, or delegates at least - } //Run() - } //class LoadCueJob - } //partial class CUE_Format2 + //blech, old crap, maybe + //OUT_Disc.Structure.Synthesize_TOCPointsFromSessions(); + + //FinishLog(); + + } //Run() + } //class LoadCueJob } //namespace BizHawk.Emulation.DiscSystem diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Parse.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Parse.cs index ee29c8414a..bc517dd8a2 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Parse.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Parse.cs @@ -10,444 +10,355 @@ using System.Collections.Generic; //http://www.gnu.org/software/libcdio/libcdio.html#Sectors //this is actually a great reference. they use LSN instead of LBA.. maybe a good idea for us -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - partial class CUE_Context + /// + /// Performs minimum parse processing on a cue file + /// + class ParseCueJob : DiscJob { - public class ParseCueJob : DiscJob - { - /// - /// input: the cue string to parse - /// - public string IN_CueString; - - /// - /// output: the resulting minimally-processed cue file - /// - public CueFile OUT_CueFile; - } + /// + /// input: the cue string to parse + /// + public string IN_CueString; /// - /// Represents the contents of a cue file + /// output: the resulting minimally-processed cue file /// - public class CueFile + public CUE_File OUT_CueFile; + + + class CueLineParser { - - // (here are all the commands we can encounter) - public static class Command + int index; + string str; + public bool EOF; + public CueLineParser(string line) { - //TODO - record line number origin of command? Kind of nice but inessential - public class CATALOG { public string Value; public override string ToString() { return string.Format("CATALOG: {0}", Value); } } - public class CDTEXTFILE { public string Path; public override string ToString() { return string.Format("CDTEXTFILE: {0}", Path); } } - public class FILE { public string Path; public FileType Type; public override string ToString() { return string.Format("FILE ({0}): {1}", Type, Path); } } - public class FLAGS { public TrackFlags Flags; public override string ToString() { return string.Format("FLAGS {0}", Flags); } } - public class INDEX { public int Number; public Timestamp Timestamp; public override string ToString() { return string.Format("INDEX {0,2} {1}", Number, Timestamp); } } - public class ISRC { public string Value; public override string ToString() { return string.Format("ISRC: {0}", Value); } } - public class PERFORMER { public string Value; public override string ToString() { return string.Format("PERFORMER: {0}", Value); } } - public class POSTGAP { public Timestamp Length; public override string ToString() { return string.Format("POSTGAP: {0}", Length); } } - public class PREGAP { public Timestamp Length; public override string ToString() { return string.Format("PREGAP: {0}", Length); } } - public class REM { public string Value; public override string ToString() { return string.Format("REM: {0}", Value); } } - public class COMMENT { public string Value; public override string ToString() { return string.Format("COMMENT: {0}", Value); } } - public class SONGWRITER { public string Value; public override string ToString() { return string.Format("SONGWRITER: {0}", Value); } } - public class TITLE { public string Value; public override string ToString() { return string.Format("TITLE: {0}", Value); } } - public class TRACK { public int Number; public TrackType Type; public override string ToString() { return string.Format("TRACK {0,2} ({1})", Number, Type); } } + str = line; } - /// - /// Stuff other than the commands, global for the whole disc - /// - public class DiscInfo + public string ReadPath() { return ReadToken(Mode.Quotable); } + public string ReadToken() { return ReadToken(Mode.Normal); } + public string ReadLine() { - public Command.CATALOG Catalog; - public Command.ISRC ISRC; - public Command.CDTEXTFILE CDTextFile; + int len = str.Length; + string ret = str.Substring(index, len - index); + index = len; + EOF = true; + return ret; } - /// - /// The sequential list of commands parsed out of the cue file - /// - public List Commands = new List(); - - /// - /// Stuff other than the commands, global for the whole disc - /// - public DiscInfo GlobalDiscInfo = new DiscInfo(); - - [Flags] - public enum TrackFlags + enum Mode { - None = 0, - PRE = 1, //Pre-emphasis enabled (audio tracks only) - DCP = 2, //Digital copy permitted - DATA = 4, //Set automatically by cue-processing equipment, here for completeness - _4CH = 8, //Four channel audio - SCMS = 64, //Serial copy management system (not supported by all recorders) (??) + Normal, Quotable } - //All audio files (WAVE, AIFF, and MP3) must be in 44.1KHz 16-bit stereo format. - //BUT NOTE: MP3 can be VBR and the length can't be known without decoding the whole thing. - //But, some ideas: - //1. we could operate ffmpeg differently to retrieve the length, which maybe it can do without having to decode the entire thing - //2. we could retrieve it from an ID3 if present. - //3. as a last resort, since MP3 is the annoying case usually, we could include my c# mp3 parser and sum the length (test the performance, this might be reasonably fast on par with ECM parsing) - //NOTE: once deciding the length, we would have to stick with it! samples would have to be discarded or inserted to make the track work out - //but we COULD effectively achieve stream-loading mp3 discs, with enough work. - public enum FileType + string ReadToken(Mode mode) { - Unspecified, - BINARY, //Intel binary file (least significant byte first) - MOTOROLA, //Motorola binary file (most significant byte first) - AIFF, //Audio AIFF file - WAVE, //Audio WAVE file - MP3, //Audio MP3 file - } + if (EOF) return null; - public enum TrackType - { - Unknown, - Audio, //Audio/Music (2352) - CDG, //Karaoke CD+G (2448) - Mode1_2048, //CDROM Mode1 Data (cooked) - Mode1_2352, //CDROM Mode1 Data (raw) - Mode2_2336, //CDROM-XA Mode2 Data (could contain form 1 or form 2) - Mode2_2352, //CDROM-XA Mode2 Data (but there's no reason to distinguish this from Mode1_2352 other than to alert us that the entire session should be XA - CDI_2336, //CDI Mode2 Data - CDI_2352 //CDI Mode2 Data - } - - class CueLineParser - { - int index; - string str; - public bool EOF; - public CueLineParser(string line) - { - str = line; - } - - public string ReadPath() { return ReadToken(Mode.Quotable); } - public string ReadToken() { return ReadToken(Mode.Normal); } - public string ReadLine() - { - int len = str.Length; - string ret = str.Substring(index, len - index); - index = len; - EOF = true; - return ret; - } - - enum Mode - { - Normal, Quotable - } - - string ReadToken(Mode mode) - { - if (EOF) return null; - - bool isPath = mode == Mode.Quotable; - - int startIndex = index; - bool inToken = false; - bool inQuote = false; - for (; ; ) - { - bool done = false; - char c = str[index]; - bool isWhiteSpace = (c == ' ' || c == '\t'); - - if (isWhiteSpace) - { - if (inQuote) - index++; - else - { - if (inToken) - done = true; - else - index++; - } - } - else - { - bool startedQuote = false; - if (!inToken) - { - startIndex = index; - if (isPath && c == '"') - startedQuote = inQuote = true; - inToken = true; - } - switch (str[index]) - { - case '"': - index++; - if (inQuote && !startedQuote) - { - done = true; - } - break; - case '\\': - index++; - break; - - default: - index++; - break; - } - } - if (index == str.Length) - { - EOF = true; - done = true; - } - if (done) break; - } - - string ret = str.Substring(startIndex, index - startIndex); - - if (mode == Mode.Quotable) - ret = ret.Trim('"'); - - return ret; - } - } - - internal void LoadFromString(ParseCueJob job) - { - string cueString = job.IN_CueString; - TextReader tr = new StringReader(cueString); + bool isPath = mode == Mode.Quotable; + int startIndex = index; + bool inToken = false; + bool inQuote = false; for (; ; ) { - job.CurrentLine++; - string line = tr.ReadLine(); - if (line == null) break; - line = line.Trim(); - if (line == "") continue; - var clp = new CueLineParser(line); + bool done = false; + char c = str[index]; + bool isWhiteSpace = (c == ' ' || c == '\t'); - string key = clp.ReadToken().ToUpperInvariant(); - if (key.StartsWith(";")) + if (isWhiteSpace) { - clp.EOF = true; - Commands.Add(new Command.COMMENT() { Value = line }); + if (inQuote) + index++; + else + { + if (inToken) + done = true; + else + index++; + } } - else switch (key) + else { - default: - job.Warn("Unknown command: " + key); - break; + bool startedQuote = false; + if (!inToken) + { + startIndex = index; + if (isPath && c == '"') + startedQuote = inQuote = true; + inToken = true; + } + switch (str[index]) + { + case '"': + index++; + if (inQuote && !startedQuote) + { + done = true; + } + break; + case '\\': + index++; + break; - case "CATALOG": - if (GlobalDiscInfo.Catalog != null) - job.Warn("Multiple CATALOG commands detected. Subsequent ones are ignored."); - else if (clp.EOF) - job.Warn("Ignoring empty CATALOG command"); - else Commands.Add(GlobalDiscInfo.Catalog = new Command.CATALOG() { Value = clp.ReadToken() }); - break; + default: + index++; + break; + } + } + if (index == str.Length) + { + EOF = true; + done = true; + } + if (done) break; + } - case "CDTEXTFILE": - if (GlobalDiscInfo.CDTextFile != null) - job.Warn("Multiple CDTEXTFILE commands detected. Subsequent ones are ignored."); - else if (clp.EOF) - job.Warn("Ignoring empty CDTEXTFILE command"); - else Commands.Add(GlobalDiscInfo.CDTextFile = new Command.CDTEXTFILE() { Path = clp.ReadPath() }); - break; + string ret = str.Substring(startIndex, index - startIndex); - case "FILE": + if (mode == Mode.Quotable) + ret = ret.Trim('"'); + + return ret; + } + } + + void LoadFromString(ParseCueJob job) + { + string cueString = job.IN_CueString; + TextReader tr = new StringReader(cueString); + + for (; ; ) + { + job.CurrentLine++; + string line = tr.ReadLine(); + if (line == null) break; + line = line.Trim(); + if (line == "") continue; + var clp = new CueLineParser(line); + + string key = clp.ReadToken().ToUpperInvariant(); + if (key.StartsWith(";")) + { + clp.EOF = true; + OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = line }); + } + else switch (key) + { + default: + job.Warn("Unknown command: " + key); + break; + + case "CATALOG": + if (OUT_CueFile.GlobalDiscInfo.Catalog != null) + job.Warn("Multiple CATALOG commands detected. Subsequent ones are ignored."); + else if (clp.EOF) + job.Warn("Ignoring empty CATALOG command"); + else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.Catalog = new CUE_File.Command.CATALOG() { Value = clp.ReadToken() }); + break; + + case "CDTEXTFILE": + if (OUT_CueFile.GlobalDiscInfo.CDTextFile != null) + job.Warn("Multiple CDTEXTFILE commands detected. Subsequent ones are ignored."); + else if (clp.EOF) + job.Warn("Ignoring empty CDTEXTFILE command"); + else OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.CDTextFile = new CUE_File.Command.CDTEXTFILE() { Path = clp.ReadPath() }); + break; + + case "FILE": + { + var path = clp.ReadPath(); + CueFileType ft; + if (clp.EOF) { - var path = clp.ReadPath(); - FileType ft; - if (clp.EOF) - { - job.Error("FILE command is missing file type."); - ft = FileType.Unspecified; - } - else - { - var strType = clp.ReadToken().ToUpperInvariant(); - switch (strType) - { - default: - job.Error("Unknown FILE type: " + strType); - ft = FileType.Unspecified; - break; - case "BINARY": ft = FileType.BINARY; break; - case "MOTOROLA": ft = FileType.MOTOROLA; break; - case "BINARAIFF": ft = FileType.AIFF; break; - case "WAVE": ft = FileType.WAVE; break; - case "MP3": ft = FileType.MP3; break; - } - } - Commands.Add(new Command.FILE() { Path = path, Type = ft }); + job.Error("FILE command is missing file type."); + ft = CueFileType.Unspecified; } - break; - - case "FLAGS": - { - var cmd = new Command.FLAGS(); - Commands.Add(cmd); - while (!clp.EOF) - { - var flag = clp.ReadToken().ToUpperInvariant(); - switch (flag) - { - case "DATA": - default: - job.Warn("Unknown FLAG: " + flag); - break; - case "DCP": cmd.Flags |= TrackFlags.DCP; break; - case "4CH": cmd.Flags |= TrackFlags._4CH; break; - case "PRE": cmd.Flags |= TrackFlags.PRE; break; - case "SCMS": cmd.Flags |= TrackFlags.SCMS; break; - } - } - if (cmd.Flags == TrackFlags.None) - job.Warn("Empty FLAG command"); - } - break; - - case "INDEX": - { - if (clp.EOF) - { - job.Error("Incomplete INDEX command"); - break; - } - string strindexnum = clp.ReadToken(); - int indexnum; - if (!int.TryParse(strindexnum, out indexnum) || indexnum < 0 || indexnum > 99) - { - job.Error("Invalid INDEX number: " + strindexnum); - break; - } - string str_timestamp = clp.ReadToken(); - var ts = new Timestamp(str_timestamp); - if (!ts.Valid) - { - job.Error("Invalid INDEX timestamp: " + str_timestamp); - break; - } - Commands.Add(new Command.INDEX() { Number = indexnum, Timestamp = ts }); - } - break; - - case "ISRC": - if (GlobalDiscInfo.ISRC != null) - job.Warn("Multiple ISRC commands detected. Subsequent ones are ignored."); - else if (clp.EOF) - job.Warn("Ignoring empty ISRC command"); else { - var isrc = clp.ReadToken(); - if (isrc.Length != 12) - job.Warn("Invalid ISRC code ignored: " + isrc); - else - { - Commands.Add(new Command.ISRC() { Value = isrc }); - } - } - break; - - case "PERFORMER": - Commands.Add(new Command.PERFORMER() { Value = clp.ReadPath() ?? "" }); - break; - - case "POSTGAP": - case "PREGAP": - { - var str_msf = clp.ReadToken(); - var msf = new Timestamp(str_msf); - if (!msf.Valid) - job.Error("Ignoring {0} with invalid length MSF: " + str_msf, key); - else - { - if (key == "POSTGAP") - Commands.Add(new Command.POSTGAP() { Length = msf }); - else - Commands.Add(new Command.PREGAP() { Length = msf }); - } - } - break; - - case "REM": - Commands.Add(new Command.REM() { Value = clp.ReadLine() }); - break; - - case "SONGWRITER": - Commands.Add(new Command.SONGWRITER() { Value = clp.ReadPath() ?? "" }); - break; - - case "TITLE": - Commands.Add(new Command.TITLE() { Value = clp.ReadPath() ?? "" }); - break; - - case "TRACK": - { - if (clp.EOF) - { - job.Error("Incomplete TRACK command"); - break; - } - string str_tracknum = clp.ReadToken(); - int tracknum; - if (!int.TryParse(str_tracknum, out tracknum) || tracknum < 1 || tracknum > 99) - { - job.Error("Invalid TRACK number: " + str_tracknum); - break; - } - - //TODO - check sequentiality? maybe as a warning - - TrackType tt; - var str_trackType = clp.ReadToken(); - switch (str_trackType.ToUpperInvariant()) + var strType = clp.ReadToken().ToUpperInvariant(); + switch (strType) { default: - job.Error("Unknown TRACK type: " + str_trackType); - tt = TrackType.Unknown; + job.Error("Unknown FILE type: " + strType); + ft = CueFileType.Unspecified; break; - case "AUDIO": tt = TrackType.Audio; break; - case "CDG": tt = TrackType.CDG; break; - case "MODE1/2048": tt = TrackType.Mode1_2048; break; - case "MODE1/2352": tt = TrackType.Mode1_2352; break; - case "MODE2/2336": tt = TrackType.Mode2_2336; break; - case "MODE2/2352": tt = TrackType.Mode2_2352; break; - case "CDI/2336": tt = TrackType.CDI_2336; break; - case "CDI/2352": tt = TrackType.CDI_2352; break; + case "BINARY": ft = CueFileType.BINARY; break; + case "MOTOROLA": ft = CueFileType.MOTOROLA; break; + case "BINARAIFF": ft = CueFileType.AIFF; break; + case "WAVE": ft = CueFileType.WAVE; break; + case "MP3": ft = CueFileType.MP3; break; } - - Commands.Add(new Command.TRACK() { Number = tracknum, Type = tt }); } - break; - } - - if (!clp.EOF) - { - var remainder = clp.ReadLine(); - if (remainder.TrimStart().StartsWith(";")) - { - //add a comment - Commands.Add(new Command.COMMENT() { Value = remainder }); + OUT_CueFile.Commands.Add(new CUE_File.Command.FILE() { Path = path, Type = ft }); } - else job.Warn("Unknown text at end of line after processing command: " + key); + break; + + case "FLAGS": + { + var cmd = new CUE_File.Command.FLAGS(); + OUT_CueFile.Commands.Add(cmd); + while (!clp.EOF) + { + var flag = clp.ReadToken().ToUpperInvariant(); + switch (flag) + { + case "DATA": + default: + job.Warn("Unknown FLAG: " + flag); + break; + case "DCP": cmd.Flags |= CueTrackFlags.DCP; break; + case "4CH": cmd.Flags |= CueTrackFlags._4CH; break; + case "PRE": cmd.Flags |= CueTrackFlags.PRE; break; + case "SCMS": cmd.Flags |= CueTrackFlags.SCMS; break; + } + } + if (cmd.Flags == CueTrackFlags.None) + job.Warn("Empty FLAG command"); + } + break; + + case "INDEX": + { + if (clp.EOF) + { + job.Error("Incomplete INDEX command"); + break; + } + string strindexnum = clp.ReadToken(); + int indexnum; + if (!int.TryParse(strindexnum, out indexnum) || indexnum < 0 || indexnum > 99) + { + job.Error("Invalid INDEX number: " + strindexnum); + break; + } + string str_timestamp = clp.ReadToken(); + var ts = new Timestamp(str_timestamp); + if (!ts.Valid) + { + job.Error("Invalid INDEX timestamp: " + str_timestamp); + break; + } + OUT_CueFile.Commands.Add(new CUE_File.Command.INDEX() { Number = indexnum, Timestamp = ts }); + } + break; + + case "ISRC": + if (OUT_CueFile.GlobalDiscInfo.ISRC != null) + job.Warn("Multiple ISRC commands detected. Subsequent ones are ignored."); + else if (clp.EOF) + job.Warn("Ignoring empty ISRC command"); + else + { + var isrc = clp.ReadToken(); + if (isrc.Length != 12) + job.Warn("Invalid ISRC code ignored: " + isrc); + else + { + OUT_CueFile.Commands.Add(OUT_CueFile.GlobalDiscInfo.ISRC = new CUE_File.Command.ISRC() { Value = isrc }); + } + } + break; + + case "PERFORMER": + OUT_CueFile.Commands.Add(new CUE_File.Command.PERFORMER() { Value = clp.ReadPath() ?? "" }); + break; + + case "POSTGAP": + case "PREGAP": + { + var str_msf = clp.ReadToken(); + var msf = new Timestamp(str_msf); + if (!msf.Valid) + job.Error("Ignoring {0} with invalid length MSF: " + str_msf, key); + else + { + if (key == "POSTGAP") + OUT_CueFile.Commands.Add(new CUE_File.Command.POSTGAP() { Length = msf }); + else + OUT_CueFile.Commands.Add(new CUE_File.Command.PREGAP() { Length = msf }); + } + } + break; + + case "REM": + OUT_CueFile.Commands.Add(new CUE_File.Command.REM() { Value = clp.ReadLine() }); + break; + + case "SONGWRITER": + OUT_CueFile.Commands.Add(new CUE_File.Command.SONGWRITER() { Value = clp.ReadPath() ?? "" }); + break; + + case "TITLE": + OUT_CueFile.Commands.Add(new CUE_File.Command.TITLE() { Value = clp.ReadPath() ?? "" }); + break; + + case "TRACK": + { + if (clp.EOF) + { + job.Error("Incomplete TRACK command"); + break; + } + string str_tracknum = clp.ReadToken(); + int tracknum; + if (!int.TryParse(str_tracknum, out tracknum) || tracknum < 1 || tracknum > 99) + { + job.Error("Invalid TRACK number: " + str_tracknum); + break; + } + + //TODO - check sequentiality? maybe as a warning + + CueTrackType tt; + var str_trackType = clp.ReadToken(); + switch (str_trackType.ToUpperInvariant()) + { + default: + job.Error("Unknown TRACK type: " + str_trackType); + tt = CueTrackType.Unknown; + break; + case "AUDIO": tt = CueTrackType.Audio; break; + case "CDG": tt = CueTrackType.CDG; break; + case "MODE1/2048": tt = CueTrackType.Mode1_2048; break; + case "MODE1/2352": tt = CueTrackType.Mode1_2352; break; + case "MODE2/2336": tt = CueTrackType.Mode2_2336; break; + case "MODE2/2352": tt = CueTrackType.Mode2_2352; break; + case "CDI/2336": tt = CueTrackType.CDI_2336; break; + case "CDI/2352": tt = CueTrackType.CDI_2352; break; + } + + OUT_CueFile.Commands.Add(new CUE_File.Command.TRACK() { Number = tracknum, Type = tt }); + } + break; + } + + if (!clp.EOF) + { + var remainder = clp.ReadLine(); + if (remainder.TrimStart().StartsWith(";")) + { + //add a comment + OUT_CueFile.Commands.Add(new CUE_File.Command.COMMENT() { Value = remainder }); } + else job.Warn("Unknown text at end of line after processing command: " + key); + } - } //end cue parsing loop + } //end cue parsing loop - job.FinishLog(); - } //LoadFromString - } + job.FinishLog(); + } //LoadFromString - /// - /// Performs minimum parse processing on a cue file - /// - public void ParseCueFile(ParseCueJob job) + public void Run(ParseCueJob job) { - job.OUT_CueFile = new CueFile(); - job.OUT_CueFile.LoadFromString(job); + job.OUT_CueFile = new CUE_File(); + job.LoadFromString(job); } + } + + - } //partial class } //namespace \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Synths.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Synths.cs index cc66e70972..558f084827 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Synths.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Synths.cs @@ -2,155 +2,154 @@ using System; using System.IO; using System.Collections.Generic; -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - partial class CUE_Context + internal abstract class SS_Base : ISectorSynthJob2448 { + public IBlob Blob; + public long BlobOffset; - abstract class SS_Base : ISectorSynthJob2448 + public DiscMountPolicy Policy; + + //subQ data + public SubchannelQ sq; + + //subP data + public bool Pause; + + public abstract void Synth(SectorSynthJob job); + + protected void SynthSubchannelAsNeed(SectorSynthJob job) { - public IBlob Blob; - public long BlobOffset; - - public DiscMountPolicy Policy; - - //subQ data - public SubchannelQ sq; - - //subP data - public bool Pause; - - public abstract void Synth(SectorSynthJob job); - - protected void SynthSubchannelAsNeed(SectorSynthJob job) + //synth P if needed + if ((job.Parts & ESectorSynthPart.SubchannelP) != 0) { - //synth P if needed - if ((job.Parts & ESectorSynthPart.SubchannelP) != 0) - { - SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause); - } - - //synth Q if needed - //TODO - why not already have it serialized? Into a disc resource, even. - if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0) - { - SynthUtils.SubQ_Serialize(job.DestBuffer2448, job.DestOffset + 2352 + 12, ref sq); - } - - //clear R-W if needed - if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0) - { - Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, (12 * 6)); - } - - //subcode has been generated deinterleaved; we may still need to interleave it - if((job.Parts & ESectorSynthPart.SubcodeAny) != 0) - if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave)) == 0) - { - SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352); - } + SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause); } - } - /// - /// Represents a Mode1 2048-byte sector - /// - class SS_Mode1_2048 : SS_Base - { - public override void Synth(SectorSynthJob job) + //synth Q if needed + //TODO - why not already have it serialized? Into a disc resource, even. + if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0) { - //read the sector user data - if((job.Parts & ESectorSynthPart.User2048) != 0) - Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 16, 2048); + SynthUtils.SubQ_Serialize(job.DestBuffer2448, job.DestOffset + 2352 + 12, ref sq); + } + //clear R-W if needed + if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0) + { + Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, (12 * 6)); + } + + //subcode has been generated deinterleaved; we may still need to interleave it + if((job.Parts & ESectorSynthPart.SubcodeAny) != 0) + if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave)) == 0) + { + SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352); + } + } + } + + /// + /// Represents a Mode1 2048-byte sector + /// + class SS_Mode1_2048 : SS_Base + { + public override void Synth(SectorSynthJob job) + { + //read the sector user data + if((job.Parts & ESectorSynthPart.User2048) != 0) + Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 16, 2048); + + if ((job.Parts & ESectorSynthPart.Header16) != 0) + SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, 1); + + if ((job.Parts & ESectorSynthPart.ECMAny) != 0) + SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA); + + SynthSubchannelAsNeed(job); + } + } + + /// + /// Represents a 2352-byte sector of any sort + /// + class SS_2352 : SS_Base + { + public override void Synth(SectorSynthJob job) + { + //read the sector user data + Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352); + + //if subcode is needed, synthesize it + SynthSubchannelAsNeed(job); + } + } + + /// + /// Encodes a pre-gap sector + /// + class SS_Gap : SS_Base + { + public CueTrackType TrackType; + + public override void Synth(SectorSynthJob job) + { + //this isn't fully analyzed/optimized + Array.Clear(job.DestBuffer2448, job.DestOffset, 2352); + + byte mode = 255; + int form = -1; + switch (TrackType) + { + case CueTrackType.Audio: + mode = 0; + break; + + case CueTrackType.CDI_2352: + case CueTrackType.Mode1_2352: + mode = 1; + break; + + case CueTrackType.Mode2_2352: + mode = 2; + if (Policy.CUE_PregapMode2_As_XA_Form2) + { + job.DestBuffer2448[job.DestOffset + 12 + 6] = 0x20; + job.DestBuffer2448[job.DestOffset + 12 + 10] = 0x20; + } + form = 2; //no other choice right now really + break; + + case CueTrackType.Mode1_2048: + mode = 1; + Pause = true; + break; + + case CueTrackType.Mode2_2336: + default: + throw new InvalidOperationException("Not supported: " + TrackType); + } + + //audio has no sector header but the others do + if (mode != 0) + { if ((job.Parts & ESectorSynthPart.Header16) != 0) - SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, 1); + SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, mode); + } + if (mode == 1) + { if ((job.Parts & ESectorSynthPart.ECMAny) != 0) SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA); - - SynthSubchannelAsNeed(job); } - } - - /// - /// Represents a 2352-byte sector of any sort - /// - class SS_2352 : SS_Base - { - public override void Synth(SectorSynthJob job) + if (mode == 2 && form == 2) { - //read the sector user data - Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352); - - //if subcode is needed, synthesize it - SynthSubchannelAsNeed(job); + SynthUtils.EDC_Mode2_Form2(job.DestBuffer2448, job.DestOffset); } + + SynthSubchannelAsNeed(job); } - - class SS_Gap : SS_Base - { - public CueFile.TrackType TrackType; - - public override void Synth(SectorSynthJob job) - { - //this isn't fully analyzed/optimized - Array.Clear(job.DestBuffer2448, job.DestOffset, 2352); - - byte mode = 255; - int form = -1; - switch (TrackType) - { - case CueFile.TrackType.Audio: - mode = 0; - break; - - case CueFile.TrackType.CDI_2352: - case CueFile.TrackType.Mode1_2352: - mode = 1; - break; - - case CueFile.TrackType.Mode2_2352: - mode = 2; - if (Policy.CUE_PregapMode2_As_XA_Form2) - { - job.DestBuffer2448[job.DestOffset + 12 + 6] = 0x20; - job.DestBuffer2448[job.DestOffset + 12 + 10] = 0x20; - } - form = 2; //no other choice right now really - break; - - case CueFile.TrackType.Mode1_2048: - mode = 1; - Pause = true; - break; - - case CueFile.TrackType.Mode2_2336: - default: - throw new InvalidOperationException("Not supported: " + TrackType); - } - - //audio has no sector header but the others do - if (mode != 0) - { - if ((job.Parts & ESectorSynthPart.Header16) != 0) - SynthUtils.SectorHeader(job.DestBuffer2448, job.DestOffset + 0, job.LBA, mode); - } - - if (mode == 1) - { - if ((job.Parts & ESectorSynthPart.ECMAny) != 0) - SynthUtils.ECM_Mode1(job.DestBuffer2448, job.DestOffset + 0, job.LBA); - } - if (mode == 2 && form == 2) - { - SynthUtils.EDC_Mode2_Form2(job.DestBuffer2448, job.DestOffset); - } - - SynthSubchannelAsNeed(job); - } - } - - } + + } \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Types.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Types.cs new file mode 100644 index 0000000000..1402705e2a --- /dev/null +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CUE_Types.cs @@ -0,0 +1,46 @@ +using System; + +namespace BizHawk.Emulation.DiscSystem.CUE +{ + [Flags] + public enum CueTrackFlags + { + None = 0, + PRE = 1, //Pre-emphasis enabled (audio tracks only) + DCP = 2, //Digital copy permitted + DATA = 4, //Set automatically by cue-processing equipment, here for completeness + _4CH = 8, //Four channel audio + SCMS = 64, //Serial copy management system (not supported by all recorders) (??) + } + + //All audio files (WAVE, AIFF, and MP3) must be in 44.1KHz 16-bit stereo format. + //BUT NOTE: MP3 can be VBR and the length can't be known without decoding the whole thing. + //But, some ideas: + //1. we could operate ffmpeg differently to retrieve the length, which maybe it can do without having to decode the entire thing + //2. we could retrieve it from an ID3 if present. + //3. as a last resort, since MP3 is the annoying case usually, we could include my c# mp3 parser and sum the length (test the performance, this might be reasonably fast on par with ECM parsing) + //NOTE: once deciding the length, we would have to stick with it! samples would have to be discarded or inserted to make the track work out + //but we COULD effectively achieve stream-loading mp3 discs, with enough work. + public enum CueFileType + { + Unspecified, + BINARY, //Intel binary file (least significant byte first) + MOTOROLA, //Motorola binary file (most significant byte first) + AIFF, //Audio AIFF file + WAVE, //Audio WAVE file + MP3, //Audio MP3 file + } + + public enum CueTrackType + { + Unknown, + Audio, //Audio/Music (2352) + CDG, //Karaoke CD+G (2448) + Mode1_2048, //CDROM Mode1 Data (cooked) + Mode1_2352, //CDROM Mode1 Data (raw) + Mode2_2336, //CDROM-XA Mode2 Data (could contain form 1 or form 2) + Mode2_2352, //CDROM-XA Mode2 Data (but there's no reason to distinguish this from Mode1_2352 other than to alert us that the entire session should be XA + CDI_2336, //CDI Mode2 Data + CDI_2352 //CDI Mode2 Data + } +} \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CueFileResolver.cs b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CueFileResolver.cs index bc195170b9..55727a144d 100644 --- a/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CueFileResolver.cs +++ b/BizHawk.Emulation.DiscSystem/DiscFormats/CUE/CueFileResolver.cs @@ -4,121 +4,118 @@ using System.Text; using System.IO; using System.Collections.Generic; -namespace BizHawk.Emulation.DiscSystem +namespace BizHawk.Emulation.DiscSystem.CUE { - partial class CUE_Context + /// + /// The CUE module user's hook for controlling how cue member file paths get resolved + /// + public class CueFileResolver { + public bool caseSensitive = false; + public bool IsHardcodedResolve { get; private set; } + string baseDir; + /// - /// The CUE module user's hook for controlling how cue member file paths get resolved + /// Retrieving the FullName from a FileInfo can be slow (and probably other operations), so this will cache all the needed values + /// TODO - could we treat it like an actual cache and only fill the FullName if it's null? /// - public class CueFileResolver + struct MyFileInfo { - public bool caseSensitive = false; - public bool IsHardcodedResolve { get; private set; } - string baseDir; + public string FullName; + public FileInfo FileInfo; + } - /// - /// Retrieving the FullName from a FileInfo can be slow (and probably other operations), so this will cache all the needed values - /// TODO - could we treat it like an actual cache and only fill the FullName if it's null? - /// - struct MyFileInfo + DirectoryInfo diBasedir; + MyFileInfo[] fisBaseDir; + + /// + /// sets the base directory and caches the list of files in the directory + /// + public void SetBaseDirectory(string baseDir) + { + this.baseDir = baseDir; + diBasedir = new DirectoryInfo(baseDir); + //list all files, so we dont scan repeatedly. + fisBaseDir = MyFileInfosFromFileInfos(diBasedir.GetFiles()); + } + + /// + /// TODO - doesnt seem like we're using this... + /// + public void SetHardcodeResolve(IDictionary hardcodes) + { + IsHardcodedResolve = true; + fisBaseDir = new MyFileInfo[hardcodes.Count]; + int i = 0; + foreach (var kvp in hardcodes) { - public string FullName; - public FileInfo FileInfo; + fisBaseDir[i++] = new MyFileInfo { FullName = kvp.Key, FileInfo = new FileInfo(kvp.Value) }; + } + } + + MyFileInfo[] MyFileInfosFromFileInfos(FileInfo[] fis) + { + var myfis = new MyFileInfo[fis.Length]; + for (int i = 0; i < fis.Length; i++) + { + myfis[i].FileInfo = fis[i]; + myfis[i].FullName = fis[i].FullName; + } + return myfis; + } + + /// + /// Performs cue-intelligent logic to acquire a file requested by the cue. + /// Returns the resulting full path(s). + /// If there are multiple options, it returns them all + /// + public List Resolve(string path) + { + string targetFile = Path.GetFileName(path); + string targetFragment = Path.GetFileNameWithoutExtension(path); + + DirectoryInfo di = null; + MyFileInfo[] fileInfos; + if (!string.IsNullOrEmpty(Path.GetDirectoryName(path))) + { + di = new FileInfo(path).Directory; + //fileInfos = di.GetFiles(Path.GetFileNameWithoutExtension(path)); //does this work? + fileInfos = MyFileInfosFromFileInfos(di.GetFiles()); //we (probably) have to enumerate all the files to do a search anyway, so might as well do this + //TODO - dont do the search until a resolve fails + } + else + { + di = diBasedir; + fileInfos = fisBaseDir; } - DirectoryInfo diBasedir; - MyFileInfo[] fisBaseDir; - - /// - /// sets the base directory and caches the list of files in the directory - /// - public void SetBaseDirectory(string baseDir) + var results = new List(); + foreach (var fi in fileInfos) { - this.baseDir = baseDir; - diBasedir = new DirectoryInfo(baseDir); - //list all files, so we dont scan repeatedly. - fisBaseDir = MyFileInfosFromFileInfos(diBasedir.GetFiles()); - } - - /// - /// TODO - doesnt seem like we're using this... - /// - public void SetHardcodeResolve(IDictionary hardcodes) - { - IsHardcodedResolve = true; - fisBaseDir = new MyFileInfo[hardcodes.Count]; - int i = 0; - foreach (var kvp in hardcodes) - { - fisBaseDir[i++] = new MyFileInfo { FullName = kvp.Key, FileInfo = new FileInfo(kvp.Value) }; - } - } - - MyFileInfo[] MyFileInfosFromFileInfos(FileInfo[] fis) - { - var myfis = new MyFileInfo[fis.Length]; - for (int i = 0; i < fis.Length; i++) - { - myfis[i].FileInfo = fis[i]; - myfis[i].FullName = fis[i].FullName; - } - return myfis; - } - - /// - /// Performs cue-intelligent logic to acquire a file requested by the cue. - /// Returns the resulting full path(s). - /// If there are multiple options, it returns them all - /// - public List Resolve(string path) - { - string targetFile = Path.GetFileName(path); - string targetFragment = Path.GetFileNameWithoutExtension(path); - - DirectoryInfo di = null; - MyFileInfo[] fileInfos; - if (!string.IsNullOrEmpty(Path.GetDirectoryName(path))) - { - di = new FileInfo(path).Directory; - //fileInfos = di.GetFiles(Path.GetFileNameWithoutExtension(path)); //does this work? - fileInfos = MyFileInfosFromFileInfos(di.GetFiles()); //we (probably) have to enumerate all the files to do a search anyway, so might as well do this - //TODO - dont do the search until a resolve fails - } - else - { - di = diBasedir; - fileInfos = fisBaseDir; - } - - var results = new List(); - foreach (var fi in fileInfos) - { - var ext = Path.GetExtension(fi.FullName).ToLowerInvariant(); - - //some choices are always bad: (we're looking for things like .bin and .wav) - //it's a little unclear whether we should go for a whitelist or a blacklist here. - //there's similar numbers of cases either way. - //perhaps we could code both (and prefer choices from the whitelist) - if (ext == ".cue" || ext == ".sbi" || ext == ".ccd" || ext == ".sub") - continue; - - - string fragment = Path.GetFileNameWithoutExtension(fi.FullName); - //match files with differing extensions - int cmp = string.Compare(fragment, targetFragment, !caseSensitive); - if (cmp != 0) - //match files with another extension added on (likely to be mygame.bin.ecm) - cmp = string.Compare(fragment, targetFile, !caseSensitive); - if (cmp == 0) - results.Add(fi.FileInfo); - - } - var ret = new List(); - foreach (var fi in results) - ret.Add(fi.FullName); - return ret; + var ext = Path.GetExtension(fi.FullName).ToLowerInvariant(); + + //some choices are always bad: (we're looking for things like .bin and .wav) + //it's a little unclear whether we should go for a whitelist or a blacklist here. + //there's similar numbers of cases either way. + //perhaps we could code both (and prefer choices from the whitelist) + if (ext == ".cue" || ext == ".sbi" || ext == ".ccd" || ext == ".sub") + continue; + + + string fragment = Path.GetFileNameWithoutExtension(fi.FullName); + //match files with differing extensions + int cmp = string.Compare(fragment, targetFragment, !caseSensitive); + if (cmp != 0) + //match files with another extension added on (likely to be mygame.bin.ecm) + cmp = string.Compare(fragment, targetFile, !caseSensitive); + if (cmp == 0) + results.Add(fi.FileInfo); + } + var ret = new List(); + foreach (var fi in results) + ret.Add(fi.FullName); + return ret; } } } \ No newline at end of file diff --git a/BizHawk.Emulation.DiscSystem/DiscMountJob.cs b/BizHawk.Emulation.DiscSystem/DiscMountJob.cs index 4663db7a9d..fd48e835b4 100644 --- a/BizHawk.Emulation.DiscSystem/DiscMountJob.cs +++ b/BizHawk.Emulation.DiscSystem/DiscMountJob.cs @@ -4,6 +4,8 @@ using System.Text; using System.IO; using System.Collections.Generic; +using BizHawk.Emulation.DiscSystem.CUE; + namespace BizHawk.Emulation.DiscSystem { /// @@ -80,7 +82,7 @@ namespace BizHawk.Emulation.DiscSystem string infile = IN_FromPath; string cue_content = null; - var cfr = new CUE_Context.CueFileResolver(); + var cfr = new CueFileResolver(); RERUN: var ext = Path.GetExtension(infile).ToLowerInvariant(); @@ -109,18 +111,18 @@ namespace BizHawk.Emulation.DiscSystem if (!cfr.IsHardcodedResolve) cfr.SetBaseDirectory(Path.GetDirectoryName(infile)); //parse the cue file - var parseJob = new CUE_Context.ParseCueJob(); + var parseJob = new ParseCueJob(); if (cue_content == null) cue_content = File.ReadAllText(cuePath); parseJob.IN_CueString = cue_content; - cue2.ParseCueFile(parseJob); + parseJob.Run(parseJob); //TODO - need better handling of log output if (!string.IsNullOrEmpty(parseJob.OUT_Log)) Console.WriteLine(parseJob.OUT_Log); ConcatenateJobLog(parseJob); //compile the cue file: //includes this work: resolve required bin files and find out what it's gonna take to load the cue - var compileJob = new CUE_Context.CompileCueJob(); + var compileJob = new CompileCueJob(); compileJob.IN_CueFormat = cue2; compileJob.IN_CueFile = parseJob.OUT_CueFile; compileJob.Run(); @@ -137,7 +139,7 @@ namespace BizHawk.Emulation.DiscSystem } //actually load it all up - var loadJob = new CUE_Context.LoadCueJob(); + var loadJob = new LoadCueJob(); loadJob.IN_CompileJob = compileJob; loadJob.Run(); //TODO - need better handling of log output