import WIP discsys work from svn

This commit is contained in:
zeromus 2015-06-23 13:57:11 -05:00
parent fada87b3d3
commit 80164c1fba
52 changed files with 5256 additions and 849 deletions

View File

@ -237,12 +237,7 @@ namespace BizHawk.Client.Common
Disc disc = null;
string discPath = e.Path;
string discExt = Path.GetExtension(discPath).ToLower();
if (discExt == ".iso")
disc = Disc.FromIsoPath(discPath);
if (discExt == ".cue")
disc = Disc.FromCuePath(discPath, new CueBinPrefs());
if (discExt == ".ccd")
disc = Disc.FromCCDPath(discPath);
disc = Disc.LoadAutomagic(discPath);
if(disc == null)
throw new InvalidOperationException("Can't load one of the files specified in the M3U");
discNames.Add(Path.GetFileNameWithoutExtension(discPath));
@ -260,13 +255,7 @@ namespace BizHawk.Client.Common
throw new InvalidOperationException("Can't load CD files from archives!");
}
Disc disc = null;
if(ext == ".iso")
disc = Disc.FromIsoPath(path);
if(ext == ".cue")
disc = Disc.FromCuePath(path, new CueBinPrefs());
if (ext == ".ccd")
disc = Disc.FromCCDPath(path);
Disc disc = Disc.LoadAutomagic(path);
var hash = disc.GetHash();
game = Database.CheckDatabase(hash);

View File

@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}</ProjectGuid>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>BizHawk.Client.DBMan</RootNamespace>
<AssemblyName>BizHawk.Client.DBMan</AssemblyName>
@ -22,6 +25,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<UseVSHostingProcess>false</UseVSHostingProcess>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
@ -53,6 +57,7 @@
<Compile Include="DirectoryScan.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="DiscHash.cs" />
<Compile Include="RomHasher.cs" />
<EmbeddedResource Include="DBMan_MainForm.resx">
<DependentUpon>DBMan_MainForm.cs</DependentUpon>

View File

@ -0,0 +1,314 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using BizHawk.Common;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Client.DBMan
{
class DiscHash
{
public class CRC32
{
// Lookup table for speed.
private static readonly uint[] CRC32Table;
static CRC32()
{
CRC32Table = new uint[256];
for (uint i = 0; i < 256; ++i)
{
uint crc = i;
for (int j = 8; j > 0; --j)
{
if ((crc & 1) == 1)
crc = ((crc >> 1) ^ 0xEDB88320);
else
crc >>= 1;
}
CRC32Table[i] = crc;
}
}
uint current = 0xFFFFFFFF;
public void Add(byte[] data, int offset, int size)
{
for (int i = 0; i < size; i++)
{
byte b = data[offset + i];
current = (((Result) >> 8) ^ CRC32Table[b ^ ((Result) & 0xFF)]);
}
}
byte[] smallbuf = new byte[8];
public void Add(int data)
{
smallbuf[0] = (byte)((data) & 0xFF);
smallbuf[1] = (byte)((data >> 8) & 0xFF);
smallbuf[2] = (byte)((data >> 16) & 0xFF);
smallbuf[3] = (byte)((data >> 24) & 0xFF);
Add(smallbuf, 0, 4);
}
public uint Result { get { return current ^ 0xFFFFFFFF; } }
}
Job job;
public void Run(string[] args)
{
using (job = new Job())
{
MyRun(args);
}
}
void MyRun(string[] args)
{
string indir = null;
string dpTemp = null;
string fpOutfile = null;
for(int i=0;;)
{
if (i == args.Length) break;
var arg = args[i++];
if (arg == "--indir")
indir = args[i++];
if (arg == "--tempdir")
dpTemp = args[i++];
if (arg == "--outfile")
fpOutfile = args[i++];
}
//prepare input
var diInput = new DirectoryInfo(indir);
//prepare output
if(dpTemp == null) dpTemp = Path.GetTempFileName() + ".dir";
//delete existing files in output
foreach (var fi in new DirectoryInfo(dpTemp).GetFiles())
fi.Delete();
foreach (var di in new DirectoryInfo(dpTemp).GetDirectories())
di.Delete(true);
using(var outf = new StreamWriter(fpOutfile))
{
Dictionary<uint, string> FoundHashes = new Dictionary<uint, string>();
object olock = new object();
int ctr = 0;
//loop over games
var po = new ParallelOptions();
po.MaxDegreeOfParallelism = Environment.ProcessorCount - 1;
Parallel.ForEach(diInput.GetFiles("*.7z"), po, (fi) =>
{
CRC32 crc = new CRC32();
byte[] discbuf = new byte[2352*28];
int myctr;
lock (olock)
myctr = ctr++;
string mydpTemp = Path.Combine(dpTemp,myctr.ToString());
var mydiTemp = new DirectoryInfo(mydpTemp);
mydiTemp.Create();
var process = new System.Diagnostics.Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = "7z.exe";
process.StartInfo.Arguments = string.Format("x -o\"{1}\" \"{0}\"", fi.FullName, mydiTemp.FullName);
process.Start();
job.AddProcess(process.Handle);
//if we need it
//for (; ; )
//{
// int c = process.StandardOutput.Read();
// if (c == -1)
// break;
// Console.Write((char)c);
//}
process.WaitForExit(); //just in case
//now look for the cue file
var fiCue = mydiTemp.GetFiles("*.cue").FirstOrDefault();
if (fiCue == null)
{
Console.WriteLine("MISSING CUE FOR: " + fi.Name);
outf.WriteLine("MISSING CUE FOR: " + fi.Name);
Console.Out.Flush();
}
else
{
using (var disc = Disc.LoadAutomagic(fiCue.FullName))
{
//generate a hash with our own custom approach
//basically, the "TOC" and a few early sectors completely
crc.Add(disc.LBACount);
crc.Add(disc.Structure.Sessions[0].Tracks.Count);
foreach (var track in disc.Structure.Sessions[0].Tracks)
{
crc.Add(track.Start_ABA);
crc.Add(track.LengthInSectors);
}
//ZAMMO: change to disc sector reader, maybe a new class to read multiple
disc.ReadLBA_2352_Flat(0, discbuf, 0, discbuf.Length);
crc.Add(discbuf, 0, discbuf.Length);
lock (olock)
{
Console.WriteLine("[{0:X8}] {1}", crc.Result, fi.Name);
outf.WriteLine("[{0:X8}] {1}", crc.Result, fi.Name);
if (FoundHashes.ContainsKey(crc.Result))
{
Console.WriteLine("--> COLLISION WITH: ", FoundHashes[crc.Result]);
outf.WriteLine("--> COLLISION WITH: ", FoundHashes[crc.Result]);
}
Console.Out.Flush();
}
}
}
foreach (var fsi in mydiTemp.GetFileSystemInfos())
fsi.Delete();
mydiTemp.Delete();
}); //major loop
} //using(outfile)
} //MyRun()
} //class PsxRedump
public enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public Int64 PerProcessUserTimeLimit;
public Int64 PerJobUserTimeLimit;
public Int16 LimitFlags;
public UInt32 MinimumWorkingSetSize;
public UInt32 MaximumWorkingSetSize;
public Int16 ActiveProcessLimit;
public Int64 Affinity;
public Int16 PriorityClass;
public Int16 SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
public UInt64 ReadOperationCount;
public UInt64 WriteOperationCount;
public UInt64 OtherOperationCount;
public UInt64 ReadTransferCount;
public UInt64 WriteTransferCount;
public UInt64 OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UInt32 ProcessMemoryLimit;
public UInt32 JobMemoryLimit;
public UInt32 PeakProcessMemoryUsed;
public UInt32 PeakJobMemoryUsed;
}
public class Job : IDisposable
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern IntPtr CreateJobObject(object a, string lpName);
[DllImport("kernel32.dll")]
static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
private IntPtr m_handle;
private bool m_disposed = false;
public Job()
{
m_handle = CreateJobObject(null, null);
JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
info.LimitFlags = 0x2000;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
extendedInfo.BasicLimitInformation = info;
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
throw new Exception(string.Format("Unable to set information. Error: {0}", Marshal.GetLastWin32Error()));
}
#region IDisposable Members
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
private void Dispose(bool disposing)
{
if (m_disposed)
return;
if (disposing) { }
Close();
m_disposed = true;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
public void Close()
{
CloseHandle(m_handle);
m_handle = IntPtr.Zero;
}
public bool AddProcess(IntPtr handle)
{
return AssignProcessToJobObject(m_handle, handle);
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Windows.Forms;
using Community.CsharpSqlite.SQLiteClient;
@ -7,8 +8,18 @@ namespace BizHawk.Client.DBMan
internal static class Program
{
[STAThread]
static void Main()
static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--dischash")
{
new DiscHash().Run(args.Skip(1).ToArray());
return;
}
//if (args.Length > 0 && args[0] == "--disccmp")
//{
// new DiscCmp().Run(args.Skip(1).ToArray());
// return;
//}
try
{
InitDB();

View File

@ -178,7 +178,7 @@ namespace BizHawk.Client.DBMan
try
{
string ext = new FileInfo(file).Extension.ToLowerInvariant();
using (var disc = ext == ".iso" ? Disc.FromIsoPath(file) : Disc.FromCuePath(file, new CueBinPrefs()))
using (var disc = Disc.LoadAutomagic(file))
{
return disc.GetHash();
}

View File

@ -118,14 +118,14 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="richTextBox1.Text" xml:space="preserve">
<value>DiscoHawk converts bolloxed-up crusty disc images to totally tidy cue+bin.
<value>DiscoHawk converts bolloxed-up crusty disc images to totally tidy CCD.
DiscoHawk is part of the BizHawk project ( http://code.google.com/p/bizhawk ).
BizHawk is a .net-based multi-system emulator brought to you by some of the rerecording emulator principals. We wrote our own cue parsing/generating code to be able to handle any kind of junk we threw at it. Instead of trapping it in the emulator, we liberated it in the form of this tool, to be useful in other environments.
To use, drag a .cue into the MAGIC area. DiscoHawk will dump a newly cleaned up cue+bin pair to the same directory as the original .cue, and call it _hawked.
To use, drag a disc (.cue, .iso, .ccd) into the top area. DiscoHawk will dump a newly cleaned up CCD file set to the same directory as the original disc image, and call it _hawked.
This is beta software. You are invited to report problems to our bug tracker or IRC. Problems consist of: crusty disc images that crash DiscoHawk or that cause DiscoHawk to produce a _hawked.cue which fails to serve your particular purposes (which we will need to be informed of, in case we are outputting wrongly.)</value>
This is beta software. You are invited to report problems to our bug tracker or IRC. Problems consist of: crusty disc images that crash DiscoHawk or that cause DiscoHawk to produce a _hawked.ccd which fails to serve your particular purposes (which we will need to be informed of, in case we are outputting wrongly.)</value>
</data>
</root>

View File

@ -16,17 +16,19 @@ namespace BizHawk.Client.DiscoHawk
public static void Extract(Disc disc, string path, string filebase)
{
var dsr = new DiscSectorReader(disc);
bool confirmed = false;
var tracks = disc.Structure.Sessions[0].Tracks;
foreach (var track in tracks)
{
if (track.TrackType != ETrackType.Audio)
if (track.TrackType != DiscStructure.ETrackType.Audio)
continue;
var waveData = new byte[track.LengthInSectors * 2352];
int startLba = track.Indexes[1].LBA;
for (int sector = 0; sector < track.LengthInSectors; sector++)
disc.ReadLBA_2352(startLba + sector, waveData, sector * 2352);
dsr.ReadLBA_2352(startLba + sector, waveData, sector * 2352);
string mp3Path = string.Format("{0} - Track {1:D2}.mp3", Path.Combine(path, filebase), track.Number);
if (File.Exists(mp3Path))

View File

@ -44,6 +44,7 @@
<UseVSHostingProcess>false</UseVSHostingProcess>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<PlatformTarget>x86</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<DebugType>pdbonly</DebugType>
@ -87,20 +88,19 @@
<DependentUpon>About.cs</DependentUpon>
</Compile>
<Compile Include="AudioExtractor.cs" />
<Compile Include="DiscoHawk.cs" />
<Compile Include="DiscoHawkDialog.cs">
<Compile Include="ComparisonResults.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="DiscoHawkDialog.Designer.cs">
<DependentUpon>DiscoHawkDialog.cs</DependentUpon>
<Compile Include="ComparisonResults.Designer.cs">
<DependentUpon>ComparisonResults.cs</DependentUpon>
</Compile>
<Compile Include="DiscoHawk.cs" />
<Compile Include="MainDiscoForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="MainDiscoForm.Designer.cs">
<DependentUpon>MainDiscoForm.cs</DependentUpon>
</Compile>
<Compile Include="MednadiscTester.cs" />
<Compile Include="ProgressDialog.cs">
<SubType>Form</SubType>
</Compile>
@ -124,8 +124,8 @@
<EmbeddedResource Include="About.resx">
<DependentUpon>About.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="DiscoHawkDialog.resx">
<DependentUpon>DiscoHawkDialog.cs</DependentUpon>
<EmbeddedResource Include="ComparisonResults.resx">
<DependentUpon>ComparisonResults.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="MainDiscoForm.resx">
<DependentUpon>MainDiscoForm.cs</DependentUpon>

View File

@ -0,0 +1,119 @@
namespace BizHawk.Client.DiscoHawk
{
partial class ComparisonResults
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.RichTextBox();
this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPage1 = new System.Windows.Forms.TabPage();
this.tabPage2 = new System.Windows.Forms.TabPage();
this.tabPage3 = new System.Windows.Forms.TabPage();
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
this.SuspendLayout();
//
// textBox1
//
this.textBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.textBox1.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.textBox1.Location = new System.Drawing.Point(3, 3);
this.textBox1.Name = "textBox1";
this.textBox1.ReadOnly = true;
this.textBox1.Size = new System.Drawing.Size(757, 394);
this.textBox1.TabIndex = 1;
this.textBox1.Text = "";
//
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Controls.Add(this.tabPage2);
this.tabControl1.Controls.Add(this.tabPage3);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
this.tabControl1.Size = new System.Drawing.Size(771, 426);
this.tabControl1.TabIndex = 2;
//
// tabPage1
//
this.tabPage1.Controls.Add(this.textBox1);
this.tabPage1.Location = new System.Drawing.Point(4, 22);
this.tabPage1.Name = "tabPage1";
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
this.tabPage1.Size = new System.Drawing.Size(763, 400);
this.tabPage1.TabIndex = 0;
this.tabPage1.Text = "Log";
this.tabPage1.UseVisualStyleBackColor = true;
//
// tabPage2
//
this.tabPage2.Location = new System.Drawing.Point(4, 22);
this.tabPage2.Name = "tabPage2";
this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
this.tabPage2.Size = new System.Drawing.Size(763, 400);
this.tabPage2.TabIndex = 1;
this.tabPage2.Text = "SRC Context";
this.tabPage2.UseVisualStyleBackColor = true;
//
// tabPage3
//
this.tabPage3.Location = new System.Drawing.Point(4, 22);
this.tabPage3.Name = "tabPage3";
this.tabPage3.Padding = new System.Windows.Forms.Padding(3);
this.tabPage3.Size = new System.Drawing.Size(763, 400);
this.tabPage3.TabIndex = 2;
this.tabPage3.Text = "DST Context";
this.tabPage3.UseVisualStyleBackColor = true;
//
// ComparisonResults
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(771, 426);
this.Controls.Add(this.tabControl1);
this.Name = "ComparisonResults";
this.Text = "ComparisonResults";
this.tabControl1.ResumeLayout(false);
this.tabPage1.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
public System.Windows.Forms.RichTextBox textBox1;
private System.Windows.Forms.TabControl tabControl1;
private System.Windows.Forms.TabPage tabPage1;
private System.Windows.Forms.TabPage tabPage2;
private System.Windows.Forms.TabPage tabPage3;
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace BizHawk.Client.DiscoHawk
{
public partial class ComparisonResults : Form
{
public ComparisonResults()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -186,24 +186,178 @@ namespace BizHawk.Client.DiscoHawk
{
public void Run(string[] args)
{
bool gui = true;
foreach (var arg in args)
{
if (arg.ToUpper() == "COMMAND") gui = false;
}
if (gui)
if (args.Length == 0)
{
var dialog = new MainDiscoForm();
dialog.ShowDialog();
return;
}
else
string infile = null;
var loadDiscInterface = DiscInterface.BizHawk;
var compareDiscInterfaces = new List<DiscInterface> ();
bool hawk = false;
int idx = 0;
while (idx < args.Length)
{
//test stuff...
MednadiscTester tester = new MednadiscTester();
tester.TestDirectory(@"c:\isos\psx");
string a = args[idx++];
string au = a.ToUpperInvariant();
if (au == "LOAD")
loadDiscInterface = (DiscInterface)Enum.Parse(typeof(DiscInterface), args[idx++], true);
else if (au == "COMPARE")
compareDiscInterfaces.Add((DiscInterface)Enum.Parse(typeof(DiscInterface), args[idx++], true));
else if (au == "HAWK")
hawk = true;
else infile = a;
}
if (infile == null)
return;
var dmj = new DiscMountJob { IN_DiscInterface = loadDiscInterface, IN_FromPath = infile };
dmj.Run();
var disc = dmj.OUT_Disc;
if(hawk) {} //todo - write it out
StringWriter sw = new StringWriter();
foreach (var cmpif in compareDiscInterfaces)
{
sw.WriteLine("BEGIN COMPARE: SRC {0} vs DST {1}", loadDiscInterface, cmpif);
var src_disc = disc;
var dst_dmj = new DiscMountJob { IN_DiscInterface = cmpif, IN_FromPath = infile };
dst_dmj.Run();
var dst_disc = dst_dmj.OUT_Disc;
var src_dsr = new DiscSectorReader(src_disc);
var dst_dsr = new DiscSectorReader(dst_disc);
var src_toc = src_disc.TOCRaw;
var dst_toc = dst_disc.TOCRaw;
var src_databuf = new byte[2448];
var dst_databuf = new byte[2448];
Action<DiscTOCRaw.TOCItem> sw_dump_toc_one = (item) =>
{
if(!item.Exists)
sw.Write("(---missing---)");
else
sw.Write("({0:X2} - {1})",(byte)item.Control,item.LBATimestamp);
};
Action<int> sw_dump_toc = (index) =>
{
sw.Write("SRC TOC#{0,3} ",index); sw_dump_toc_one(src_toc.TOCItems[index]); sw.WriteLine();
sw.Write("DST TOC#{0,3} ",index); sw_dump_toc_one(dst_toc.TOCItems[index]); sw.WriteLine();
};
//verify sector count
if (src_disc.LBACount != dst_disc.LBACount)
{
sw.Write("LBACount count {0} vs {1}", src_disc.LBACount, dst_disc.LBACount);
goto SKIPPO;
}
//verify TOC match
if (src_disc.TOCRaw.FirstRecordedTrackNumber != dst_disc.TOCRaw.FirstRecordedTrackNumber
|| src_disc.TOCRaw.LastRecordedTrackNumber != dst_disc.TOCRaw.LastRecordedTrackNumber)
{
sw.WriteLine("Mismatch of RecordedTrackNumbers: {0}-{1} vs {2}-{3}",
src_disc.TOCRaw.FirstRecordedTrackNumber, src_disc.TOCRaw.LastRecordedTrackNumber,
dst_disc.TOCRaw.FirstRecordedTrackNumber, dst_disc.TOCRaw.LastRecordedTrackNumber
);
goto SKIPPO;
}
bool badToc = false;
for (int t = 0; t < 101; t++)
{
if (src_toc.TOCItems[t].Exists != dst_toc.TOCItems[t].Exists
|| src_toc.TOCItems[t].Control != dst_toc.TOCItems[t].Control
|| src_toc.TOCItems[t].LBATimestamp.Sector != dst_toc.TOCItems[t].LBATimestamp.Sector
)
{
sw.WriteLine("Mismatch in TOCItem");
sw_dump_toc(t);
badToc = true;
}
}
if(badToc)
goto SKIPPO;
Action<string, int, byte[], int, int> sw_dump_chunk_one = (comment, lba, buf, addr, count) =>
{
sw.Write("{0} - ", comment);
for (int i = 0; i < count; i++)
{
if (i + addr >= buf.Length) continue;
sw.Write("{0:X2}{1}", buf[addr + i], (i == count - 1) ? " " : " ");
}
sw.WriteLine();
};
Action<int, int, int, int> sw_dump_chunk = (lba, addr, count, offender) =>
{
sw.Write(" ");
for (int i = 0; i < count; i++) sw.Write((addr + i == offender) ? "vvv " : " ");
sw.WriteLine();
sw.Write(" ");
for(int i=0;i<count;i++) sw.Write("{0:X3} ", addr+i, (i == count - 1) ? " " : " ");
sw.WriteLine();
sw.Write(" ");
sw.Write(new string('-', count * 4));
sw.WriteLine();
sw_dump_chunk_one(string.Format("SRC #{0,6} ({1})", lba, new Timestamp(lba)), lba, src_databuf, addr, count);
sw_dump_chunk_one(string.Format("DST #{0,6} ({1})", lba, new Timestamp(lba)), lba, dst_databuf, addr, count);
};
//verify each sector contents (skip the pregap junk for now)
for (int lba = 0; lba < src_disc.LBACount; lba++)
{
if (lba % 1000 == 0)
Console.WriteLine("LBA {0} of {1}", lba, src_disc.LBACount);
src_dsr.ReadLBA_2442(lba, src_databuf, 0);
dst_dsr.ReadLBA_2442(lba, dst_databuf, 0);
//check the header
for (int b = 0; b < 16; b++)
{
if (src_databuf[b] != dst_databuf[b])
{
//cmp_sw.Write("SRC ABA {0,6}:000 : {1:X2} {2:X2} {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} {13:X2} {14:X2} {15:X2} ",i,disc_databuf[0],
sw.WriteLine("Mismatch in sector header at byte {0}",b);
sw_dump_chunk(lba, 0, 16, b);
goto SKIPPO;
}
}
for (int b = 16; b < 2352; b++)
{
if (src_databuf[b] != dst_databuf[b])
{
sw.Write("ABA {0} mismatch at byte {1}; terminating sector cmp\n", lba, b);
break;
}
}
}
SKIPPO:
sw.WriteLine("END COMPARE");
sw.WriteLine("-----------------------------");
}
sw.Flush();
var cr = new ComparisonResults();
string results = sw.ToString();
cr.textBox1.Text = results;
cr.ShowDialog();
}
}

View File

@ -98,16 +98,6 @@ namespace BizHawk.Client.DiscoHawk
txtCuePreview.Text = cueBin.cue.Replace("\n", "\r\n"); ;
}
CueBinPrefs GetCuePrefs()
{
var prefs = new CueBinPrefs();
prefs.AnnotateCue = checkCueProp_Annotations.Checked;
prefs.OneBlobPerTrack = checkCueProp_OneBlobPerTrack.Checked;
prefs.ReallyDumpBin = false;
prefs.SingleSession = true;
return prefs;
}
private void btnPresetCanonical_Click(object sender, EventArgs e)
{
PresetCanonical();
@ -185,30 +175,30 @@ namespace BizHawk.Client.DiscoHawk
return ret;
}
bool Dump(CueBin cueBin, string directoryTo, CueBinPrefs prefs)
{
ProgressReport pr = new ProgressReport();
Thread workThread = new Thread(() =>
{
cueBin.Dump(directoryTo, prefs, pr);
});
//bool Dump(CueBin cueBin, string directoryTo, CueBinPrefs prefs)
//{
// ProgressReport pr = new ProgressReport();
// Thread workThread = new Thread(() =>
// {
// cueBin.Dump(directoryTo, prefs, pr);
// });
ProgressDialog pd = new ProgressDialog(pr);
pd.Show(this);
this.Enabled = false;
workThread.Start();
for (; ; )
{
Application.DoEvents();
Thread.Sleep(10);
if (workThread.ThreadState != ThreadState.Running)
break;
pd.Update();
}
this.Enabled = true;
pd.Dispose();
return !pr.CancelSignal;
}
// ProgressDialog pd = new ProgressDialog(pr);
// pd.Show(this);
// this.Enabled = false;
// workThread.Start();
// for (; ; )
// {
// Application.DoEvents();
// Thread.Sleep(10);
// if (workThread.ThreadState != ThreadState.Running)
// break;
// pd.Update();
// }
// this.Enabled = true;
// pd.Dispose();
// return !pr.CancelSignal;
//}
private void btnExportCue_Click(object sender, EventArgs e)
{

View File

@ -28,6 +28,8 @@
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.ListViewItem listViewItem1 = new System.Windows.Forms.ListViewItem("BizHawk");
System.Windows.Forms.ListViewItem listViewItem2 = new System.Windows.Forms.ListViewItem("Mednafen");
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainDiscoForm));
this.ExitButton = new System.Windows.Forms.Button();
this.lblMagicDragArea = new System.Windows.Forms.Panel();
@ -35,13 +37,27 @@
this.lblMp3ExtractMagicArea = new System.Windows.Forms.Panel();
this.label2 = new System.Windows.Forms.Label();
this.btnAbout = new System.Windows.Forms.Button();
this.radioButton1 = new System.Windows.Forms.RadioButton();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.label4 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.radioButton2 = new System.Windows.Forms.RadioButton();
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.checkEnableOutput = new System.Windows.Forms.CheckBox();
this.radioButton4 = new System.Windows.Forms.RadioButton();
this.label6 = new System.Windows.Forms.Label();
this.label7 = new System.Windows.Forms.Label();
this.lvCompareTargets = new System.Windows.Forms.ListView();
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.lblMagicDragArea.SuspendLayout();
this.lblMp3ExtractMagicArea.SuspendLayout();
this.groupBox1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.SuspendLayout();
//
// ExitButton
//
this.ExitButton.Location = new System.Drawing.Point(146, 245);
this.ExitButton.Location = new System.Drawing.Point(411, 401);
this.ExitButton.Name = "ExitButton";
this.ExitButton.Size = new System.Drawing.Size(75, 23);
this.ExitButton.TabIndex = 0;
@ -54,7 +70,7 @@
this.lblMagicDragArea.AllowDrop = true;
this.lblMagicDragArea.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
this.lblMagicDragArea.Controls.Add(this.label1);
this.lblMagicDragArea.Location = new System.Drawing.Point(21, 12);
this.lblMagicDragArea.Location = new System.Drawing.Point(286, 31);
this.lblMagicDragArea.Name = "lblMagicDragArea";
this.lblMagicDragArea.Size = new System.Drawing.Size(200, 100);
this.lblMagicDragArea.TabIndex = 1;
@ -63,19 +79,18 @@
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(47, 10);
this.label1.Location = new System.Drawing.Point(17, 25);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(106, 13);
this.label1.Size = new System.Drawing.Size(166, 47);
this.label1.TabIndex = 0;
this.label1.Text = "Drag here for MAGIC";
this.label1.Text = "Drag here to HAWK your disc - dump it out as a clean CCD";
//
// lblMp3ExtractMagicArea
//
this.lblMp3ExtractMagicArea.AllowDrop = true;
this.lblMp3ExtractMagicArea.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
this.lblMp3ExtractMagicArea.Controls.Add(this.label2);
this.lblMp3ExtractMagicArea.Location = new System.Drawing.Point(21, 127);
this.lblMp3ExtractMagicArea.Location = new System.Drawing.Point(286, 146);
this.lblMp3ExtractMagicArea.Name = "lblMp3ExtractMagicArea";
this.lblMp3ExtractMagicArea.Size = new System.Drawing.Size(200, 100);
this.lblMp3ExtractMagicArea.TabIndex = 2;
@ -84,16 +99,15 @@
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(17, 25);
this.label2.Location = new System.Drawing.Point(20, 25);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(151, 39);
this.label2.Size = new System.Drawing.Size(163, 39);
this.label2.TabIndex = 0;
this.label2.Text = "Drag cue here to extract audio\ntracks to MP3 for listening\npleasure!";
this.label2.Text = "Drag a disc here to extract the audio tracks to MP3";
//
// btnAbout
//
this.btnAbout.Location = new System.Drawing.Point(21, 245);
this.btnAbout.Location = new System.Drawing.Point(319, 401);
this.btnAbout.Name = "btnAbout";
this.btnAbout.Size = new System.Drawing.Size(75, 23);
this.btnAbout.TabIndex = 3;
@ -101,11 +115,139 @@
this.btnAbout.UseVisualStyleBackColor = true;
this.btnAbout.Click += new System.EventHandler(this.btnAbout_Click);
//
// radioButton1
//
this.radioButton1.AutoSize = true;
this.radioButton1.Checked = true;
this.radioButton1.Location = new System.Drawing.Point(6, 19);
this.radioButton1.Name = "radioButton1";
this.radioButton1.Size = new System.Drawing.Size(67, 17);
this.radioButton1.TabIndex = 4;
this.radioButton1.TabStop = true;
this.radioButton1.Text = "BizHawk";
this.radioButton1.UseVisualStyleBackColor = true;
//
// groupBox1
//
this.groupBox1.Controls.Add(this.label4);
this.groupBox1.Controls.Add(this.label3);
this.groupBox1.Controls.Add(this.radioButton2);
this.groupBox1.Controls.Add(this.radioButton1);
this.groupBox1.Location = new System.Drawing.Point(12, 12);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(253, 206);
this.groupBox1.TabIndex = 5;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Disc Reading Engine";
//
// label4
//
this.label4.Location = new System.Drawing.Point(20, 95);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(216, 43);
this.label4.TabIndex = 8;
this.label4.Text = "- Doesn\'t support audio decoding yet\r\n(even though Mednafen proper can do it)\r\n- " +
"Loads ISO, CUE, and CCD";
//
// label3
//
this.label3.Location = new System.Drawing.Point(20, 39);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(216, 33);
this.label3.TabIndex = 7;
this.label3.Text = "- Uses FFMPEG for audio decoding\r\n- Loads ISO, CUE, and CCD";
//
// radioButton2
//
this.radioButton2.AutoSize = true;
this.radioButton2.Location = new System.Drawing.Point(6, 75);
this.radioButton2.Name = "radioButton2";
this.radioButton2.Size = new System.Drawing.Size(73, 17);
this.radioButton2.TabIndex = 5;
this.radioButton2.Text = "Mednafen";
this.radioButton2.UseVisualStyleBackColor = true;
//
// groupBox2
//
this.groupBox2.Controls.Add(this.checkEnableOutput);
this.groupBox2.Controls.Add(this.radioButton4);
this.groupBox2.Location = new System.Drawing.Point(12, 224);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(253, 69);
this.groupBox2.TabIndex = 6;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Output Format";
//
// checkEnableOutput
//
this.checkEnableOutput.AutoSize = true;
this.checkEnableOutput.Checked = true;
this.checkEnableOutput.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkEnableOutput.Location = new System.Drawing.Point(177, 19);
this.checkEnableOutput.Name = "checkEnableOutput";
this.checkEnableOutput.Size = new System.Drawing.Size(59, 17);
this.checkEnableOutput.TabIndex = 7;
this.checkEnableOutput.Text = "Enable";
this.checkEnableOutput.UseVisualStyleBackColor = true;
//
// radioButton4
//
this.radioButton4.AutoSize = true;
this.radioButton4.Checked = true;
this.radioButton4.Location = new System.Drawing.Point(12, 19);
this.radioButton4.Name = "radioButton4";
this.radioButton4.Size = new System.Drawing.Size(47, 17);
this.radioButton4.TabIndex = 5;
this.radioButton4.TabStop = true;
this.radioButton4.Text = "CCD";
this.radioButton4.UseVisualStyleBackColor = true;
//
// label6
//
this.label6.AutoSize = true;
this.label6.Location = new System.Drawing.Point(9, 305);
this.label6.Name = "label6";
this.label6.Size = new System.Drawing.Size(111, 13);
this.label6.TabIndex = 2;
this.label6.Text = "Compare Reading To:";
//
// label7
//
this.label7.AutoSize = true;
this.label7.Location = new System.Drawing.Point(343, 12);
this.label7.Name = "label7";
this.label7.Size = new System.Drawing.Size(70, 13);
this.label7.TabIndex = 10;
this.label7.Text = "- Operations -";
//
// lvCompareTargets
//
this.lvCompareTargets.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1});
this.lvCompareTargets.FullRowSelect = true;
this.lvCompareTargets.GridLines = true;
this.lvCompareTargets.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
this.lvCompareTargets.HideSelection = false;
this.lvCompareTargets.Items.AddRange(new System.Windows.Forms.ListViewItem[] {
listViewItem1,
listViewItem2});
this.lvCompareTargets.Location = new System.Drawing.Point(12, 321);
this.lvCompareTargets.Name = "lvCompareTargets";
this.lvCompareTargets.Size = new System.Drawing.Size(121, 97);
this.lvCompareTargets.TabIndex = 11;
this.lvCompareTargets.UseCompatibleStateImageBehavior = false;
this.lvCompareTargets.View = System.Windows.Forms.View.Details;
//
// MainDiscoForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(247, 285);
this.ClientSize = new System.Drawing.Size(510, 436);
this.Controls.Add(this.lvCompareTargets);
this.Controls.Add(this.label6);
this.Controls.Add(this.label7);
this.Controls.Add(this.groupBox2);
this.Controls.Add(this.groupBox1);
this.Controls.Add(this.btnAbout);
this.Controls.Add(this.lblMp3ExtractMagicArea);
this.Controls.Add(this.lblMagicDragArea);
@ -118,10 +260,13 @@
this.Text = "DiscoHawk";
this.Load += new System.EventHandler(this.MainDiscoForm_Load);
this.lblMagicDragArea.ResumeLayout(false);
this.lblMagicDragArea.PerformLayout();
this.lblMp3ExtractMagicArea.ResumeLayout(false);
this.lblMp3ExtractMagicArea.PerformLayout();
this.groupBox1.ResumeLayout(false);
this.groupBox1.PerformLayout();
this.groupBox2.ResumeLayout(false);
this.groupBox2.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
@ -133,5 +278,17 @@
private System.Windows.Forms.Panel lblMp3ExtractMagicArea;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button btnAbout;
private System.Windows.Forms.RadioButton radioButton1;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.RadioButton radioButton2;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.GroupBox groupBox2;
private System.Windows.Forms.Label label6;
private System.Windows.Forms.Label label7;
private System.Windows.Forms.RadioButton radioButton4;
private System.Windows.Forms.ListView lvCompareTargets;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.CheckBox checkEnableOutput;
}
}

View File

@ -30,7 +30,7 @@ namespace BizHawk.Client.DiscoHawk
private void MainDiscoForm_Load(object sender, EventArgs e)
{
lvCompareTargets.Columns[0].Width = lvCompareTargets.ClientSize.Width;
}
private void ExitButton_Click(object sender, EventArgs e)
@ -38,40 +38,23 @@ namespace BizHawk.Client.DiscoHawk
this.Close();
}
public static CueBinPrefs GetCuePrefs()
{
var prefs = new CueBinPrefs();
prefs.AnnotateCue = true; // TODO? checkCueProp_Annotations.Checked;
prefs.OneBlobPerTrack = false; //TODO? checkCueProp_OneBlobPerTrack.Checked;
prefs.ReallyDumpBin = false;
prefs.SingleSession = true;
prefs.ExtensionAware = true;
return prefs;
}
private void lblMagicDragArea_DragDrop(object sender, DragEventArgs e)
{
List<string> files = validateDrop(e.Data);
if (files.Count == 0) return;
try
{
this.Cursor = Cursors.WaitCursor;
foreach (var file in files)
{
var prefs = GetCuePrefs();
string ext = Path.GetExtension(file).ToUpper();
Disc disc = null;
if (ext == ".ISO")
disc = Disc.FromIsoPath(file);
else if (ext == ".CUE")
disc = Disc.FromCuePath(file, prefs);
else if (ext == ".CCD")
disc = Disc.FromCCDPath(file);
var disc = Disc.LoadAutomagic(file);
string baseName = Path.GetFileNameWithoutExtension(file);
baseName += "_hawked";
prefs.ReallyDumpBin = true;
var cueBin = disc.DumpCueBin(baseName, GetCuePrefs());
Dump(cueBin, Path.GetDirectoryName(file), prefs);
string outfile = Path.Combine(Path.GetDirectoryName(file), baseName) + ".ccd";
//TODO CCD
//CCD_Format.Dump(disc, outfile);
}
this.Cursor = Cursors.Default;
}
catch (Exception ex)
{
@ -80,30 +63,30 @@ namespace BizHawk.Client.DiscoHawk
}
}
bool Dump(CueBin cueBin, string directoryTo, CueBinPrefs prefs)
{
ProgressReport pr = new ProgressReport();
Thread workThread = new Thread(() =>
{
cueBin.Dump(directoryTo, prefs, pr);
});
//bool Dump(CueBin cueBin, string directoryTo, CueBinPrefs prefs)
//{
// ProgressReport pr = new ProgressReport();
// Thread workThread = new Thread(() =>
// {
// cueBin.Dump(directoryTo, prefs, pr);
// });
ProgressDialog pd = new ProgressDialog(pr);
pd.Show(this);
this.Enabled = false;
workThread.Start();
for (; ; )
{
Application.DoEvents();
Thread.Sleep(10);
if (workThread.ThreadState != ThreadState.Running)
break;
pd.Update();
}
this.Enabled = true;
pd.Dispose();
return !pr.CancelSignal;
}
// ProgressDialog pd = new ProgressDialog(pr);
// pd.Show(this);
// this.Enabled = false;
// workThread.Start();
// for (; ; )
// {
// Application.DoEvents();
// Thread.Sleep(10);
// if (workThread.ThreadState != ThreadState.Running)
// break;
// pd.Update();
// }
// this.Enabled = true;
// pd.Dispose();
// return !pr.CancelSignal;
//}
private void lblMagicDragArea_DragEnter(object sender, DragEventArgs e)
{
@ -132,22 +115,23 @@ namespace BizHawk.Client.DiscoHawk
private void lblMp3ExtractMagicArea_DragDrop(object sender, DragEventArgs e)
{
var files = validateDrop(e.Data);
if (files.Count == 0) return;
foreach (var file in files)
{
using (var disc = Disc.FromCuePath(file, new CueBinPrefs()))
{
var path = Path.GetDirectoryName(file);
var filename = Path.GetFileNameWithoutExtension(file);
AudioExtractor.Extract(disc, path, filename);
}
}
// var files = validateDrop(e.Data);
// if (files.Count == 0) return;
// foreach (var file in files)
// {
// using (var disc = Disc.FromCuePath(file, new CueBinPrefs()))
// {
// var path = Path.GetDirectoryName(file);
// var filename = Path.GetFileNameWithoutExtension(file);
// AudioExtractor.Extract(disc, path, filename);
// }
// }
}
private void btnAbout_Click(object sender, EventArgs e)
{
new About().ShowDialog();
}
}
}

View File

@ -1,21 +1,12 @@
using System;
using System.Threading.Tasks;
using System.IO;
using System.Runtime.InteropServices;
using BizHawk.Emulation.DiscSystem;
class MednadiscTester
unsafe class MednadiscTester
{
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr mednadisc_LoadCD(string path);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int mednadisc_ReadSector(IntPtr disc, int lba, byte[] buf2448);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void mednadisc_CloseCD(IntPtr disc);
public class QuickSubcodeReader
{
public QuickSubcodeReader(byte[] buffer)
@ -45,29 +36,103 @@ class MednadiscTester
byte[] buffer;
}
public void TestDirectory(string dpTarget)
public static void TestDirectory(string dpTarget)
{
foreach (var fi in new DirectoryInfo(dpTarget).GetFiles())
bool skip = true;
var po = new ParallelOptions();
po.MaxDegreeOfParallelism = 1;
var files = new DirectoryInfo(dpTarget).GetFiles();
//foreach (var fi in new DirectoryInfo(dpTarget).GetFiles())
Parallel.ForEach(files, po, (fi) =>
{
if (fi.Extension.ToLower() == ".cue") { }
else if (fi.Extension.ToLower() == ".ccd") { }
else continue;
else return;
NewTest(fi.FullName);
}
//if (skip)
//{
// //GOAL STORM!!!! (track flags)
// //Street Fighter Collection (USA)!!! (data track at end)
// //Wu-Tang Shaolin Style is a PS1 game that reads from the leadout area and will flip out and become unresponsive at the parental lock screen if you haven't got it at least somewhat right in regards to Q subchannel data.
// if (fi.FullName.Contains("Strike Point"))
// skip = false;
//}
//if (skip) return;
NewTest(fi.FullName,true);
});
foreach (var di in new DirectoryInfo(dpTarget).GetDirectories())
TestDirectory(di.FullName);
}
static bool NewTest(string path)
static void ReadBizTOC(Disc disc, ref MednadiscTOC read_target, ref MednadiscTOCTrack[] tracks101)
{
read_target.disc_type = (byte)disc.TOCRaw.Session1Format;
read_target.first_track = (byte)disc.TOCRaw.FirstRecordedTrackNumber; //i _think_ thats what is meant here
read_target.last_track = (byte)disc.TOCRaw.LastRecordedTrackNumber; //i _think_ thats what is meant here
tracks101[0].lba = tracks101[0].adr = tracks101[0].control = 0;
for (int i = 1; i < 100; i++)
{
var item = disc.TOCRaw.TOCItems[i];
tracks101[i].adr = (byte)(item.Exists ? 1 : 0);
tracks101[i].lba = (uint)item.LBATimestamp.Sector;
tracks101[i].control = (byte)item.Control;
}
//"Convenience leadout track duplication." for mednafen purposes so follow mednafen rules
tracks101[read_target.last_track + 1].adr = 1;
tracks101[read_target.last_track + 1].control = (byte)(tracks101[read_target.last_track].control & 0x04);
tracks101[read_target.last_track + 1].lba = (uint)disc.TOCRaw.LeadoutTimestamp.Sector;
//element 100 is to be copied as the lead-out track
tracks101[100] = tracks101[read_target.last_track + 1];
}
public static bool NewTest(string path, bool useNewCuew)
{
bool ret = false;
Disc disc;
if (Path.GetExtension(path).ToLower() == ".cue") disc = Disc.FromCuePath(path, new CueBinPrefs());
else disc = Disc.FromCCDPath(path);
IntPtr mednadisc = mednadisc_LoadCD(path);
Console.WriteLine(Path.GetFileNameWithoutExtension(path));
//TODO - test leadout a bit, or determine length some superior way
//TODO - check length against mednadisc
Disc disc;
if (Path.GetExtension(path).ToLower() == ".cue" || Path.GetExtension(path).ToLower() == ".ccd")
{
DiscMountJob dmj = new DiscMountJob();
dmj.IN_FromPath = path;
dmj.Run();
disc = dmj.OUT_Disc;
}
else return false;
IntPtr mednadisc = mednadisc_LoadCD(path);
if (mednadisc == IntPtr.Zero)
{
Console.WriteLine("MEDNADISC COULDNT LOAD FILE");
goto END;
}
//check tocs
MednadiscTOC medna_toc;
MednadiscTOCTrack[] medna_tracks = new MednadiscTOCTrack[101];
fixed (MednadiscTOCTrack* _tracks = &medna_tracks[0])
mednadisc_ReadTOC(mednadisc, &medna_toc, _tracks);
MednadiscTOC biz_toc = new MednadiscTOC();
MednadiscTOCTrack[] biz_tracks = new MednadiscTOCTrack[101];
ReadBizTOC(disc, ref biz_toc, ref biz_tracks);
if (medna_toc.first_track != biz_toc.first_track) System.Diagnostics.Debugger.Break();
if (medna_toc.last_track != biz_toc.last_track) System.Diagnostics.Debugger.Break();
if (medna_toc.disc_type != biz_toc.disc_type) System.Diagnostics.Debugger.Break();
for (int i = 0; i < 101; i++)
{
if(medna_tracks[i].adr != biz_tracks[i].adr) System.Diagnostics.Debugger.Break();
if (medna_tracks[i].control != biz_tracks[i].control) System.Diagnostics.Debugger.Break();
if (medna_tracks[i].lba != biz_tracks[i].lba) System.Diagnostics.Debugger.Break();
}
//TODO - determine length some superior way
int nSectors = (int)(disc.Structure.BinarySize / 2352) - 150;
var subbuf = new byte[96];
@ -76,7 +141,8 @@ class MednadiscTester
var disc_qbuf = new byte[96];
var monkey_qbuf = new byte[96];
for (int i = 0; i < nSectors; i++)
int startSector = 0;
for (int i = startSector; i < nSectors; i++)
{
mednadisc_ReadSector(mednadisc, i, monkeybuf);
disc.ReadLBA_2352(i, discbuf, 0);
@ -105,6 +171,7 @@ class MednadiscTester
SubchannelQ monkey_q = new SubchannelQ();
asr.ReadLBA_SubchannelQ(12, ref monkey_q);
System.Diagnostics.Debugger.Break();
goto END;
}
}
@ -114,6 +181,7 @@ class MednadiscTester
END:
disc.Dispose();
if(mednadisc != IntPtr.Zero)
mednadisc_CloseCD(mednadisc);
return ret;

View File

@ -0,0 +1,234 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Security.Cryptography;
namespace BizHawk.Common.BufferExtensions
{
public static class BufferExtensions
{
public static void SaveAsHex(this byte[] buffer, TextWriter writer)
{
foreach (var b in buffer)
{
writer.Write("{0:X2}", b);
}
writer.WriteLine();
}
public unsafe static void SaveAsHexFast(this byte[] buffer, TextWriter writer)
{
char* table = Util.HexConvPtr;
if (buffer.Length > 0)
{
int len = buffer.Length;
fixed (byte* src = &buffer[0])
for (int i = 0; i < len; i++)
{
writer.Write(table[src[i] >> 4]);
writer.Write(table[src[i] & 15]);
}
}
writer.WriteLine();
}
public static void SaveAsHex(this byte[] buffer, TextWriter writer, int length)
{
for (int i = 0; i < length; i++)
{
writer.Write("{0:X2}", buffer[i]);
}
writer.WriteLine();
}
public static void SaveAsHex(this short[] buffer, TextWriter writer)
{
foreach (var b in buffer)
{
writer.Write("{0:X4}", b);
}
writer.WriteLine();
}
public static void SaveAsHex(this ushort[] buffer, TextWriter writer)
{
foreach (var b in buffer)
{
writer.Write("{0:X4}", b);
}
writer.WriteLine();
}
public static void SaveAsHex(this int[] buffer, TextWriter writer)
{
foreach (int b in buffer)
{
writer.Write("{0:X8}", b);
}
writer.WriteLine();
}
public static void SaveAsHex(this uint[] buffer, TextWriter writer)
{
foreach (var b in buffer)
{
writer.Write("{0:X8}", b);
}
writer.WriteLine();
}
public static void ReadFromHex(this byte[] buffer, string hex)
{
if (hex.Length % 2 != 0)
{
throw new Exception("Hex value string does not appear to be properly formatted.");
}
for (int i = 0; i < buffer.Length && i * 2 < hex.Length; i++)
{
var bytehex = "" + hex[i * 2] + hex[i * 2 + 1];
buffer[i] = byte.Parse(bytehex, NumberStyles.HexNumber);
}
}
public static unsafe void ReadFromHexFast(this byte[] buffer, string hex)
{
if (buffer.Length * 2 != hex.Length)
{
throw new Exception("Data size mismatch");
}
int count = buffer.Length;
fixed (byte* _dst = buffer)
fixed (char* _src = hex)
{
byte* dst = _dst;
char* src = _src;
while (count > 0)
{
// in my tests, replacing Hex2Int() with a 256 entry LUT slowed things down slightly
*dst++ = (byte)(Hex2Int(*src++) << 4 | Hex2Int(*src++));
count--;
}
}
}
public static void ReadFromHex(this short[] buffer, string hex)
{
if (hex.Length % 4 != 0)
{
throw new Exception("Hex value string does not appear to be properly formatted.");
}
for (int i = 0; i < buffer.Length && i * 4 < hex.Length; i++)
{
var shorthex = "" + hex[i * 4] + hex[(i * 4) + 1] + hex[(i * 4) + 2] + hex[(i * 4) + 3];
buffer[i] = short.Parse(shorthex, NumberStyles.HexNumber);
}
}
public static void ReadFromHex(this ushort[] buffer, string hex)
{
if (hex.Length % 4 != 0)
{
throw new Exception("Hex value string does not appear to be properly formatted.");
}
for (int i = 0; i < buffer.Length && i * 4 < hex.Length; i++)
{
var ushorthex = "" + hex[i * 4] + hex[(i * 4) + 1] + hex[(i * 4) + 2] + hex[(i * 4) + 3];
buffer[i] = ushort.Parse(ushorthex, NumberStyles.HexNumber);
}
}
public static void ReadFromHex(this int[] buffer, string hex)
{
if (hex.Length % 8 != 0)
{
throw new Exception("Hex value string does not appear to be properly formatted.");
}
for (int i = 0; i < buffer.Length && i * 8 < hex.Length; i++)
{
//string inthex = "" + hex[i * 8] + hex[(i * 8) + 1] + hex[(i * 4) + 2] + hex[(i * 4) + 3] + hex[(i*4
var inthex = hex.Substring(i * 8, 8);
buffer[i] = int.Parse(inthex, NumberStyles.HexNumber);
}
}
/// <summary>
/// Converts bytes to an uppercase string of hex numbers in upper case without any spacing or anything
/// </summary>
public static string BytesToHexString(this byte[] bytes)
{
var sb = new StringBuilder();
foreach (var b in bytes)
{
sb.AppendFormat("{0:X2}", b);
}
return sb.ToString();
}
public static bool FindBytes(this byte[] array, byte[] pattern)
{
var fidx = 0;
int result = Array.FindIndex(array, 0, array.Length, (byte b) =>
{
fidx = (b == pattern[fidx]) ? fidx + 1 : 0;
return (fidx == pattern.Length);
});
return (result >= pattern.Length - 1);
}
public static string HashMD5(this byte[] data, int offset, int len)
{
using (var md5 = MD5.Create())
{
md5.ComputeHash(data, offset, len);
return md5.Hash.BytesToHexString();
}
}
public static string HashMD5(this byte[] data)
{
return HashMD5(data, 0, data.Length);
}
public static string HashSHA1(this byte[] data, int offset, int len)
{
using (var sha1 = SHA1.Create())
{
sha1.ComputeHash(data, offset, len);
return sha1.Hash.BytesToHexString();
}
}
public static string HashSHA1(this byte[] data)
{
return HashSHA1(data, 0, data.Length);
}
#region Helpers
private static int Hex2Int(char c)
{
if (c <= '9')
{
return c - '0';
}
if (c <= 'F')
{
return c - '7';
}
return c - 'W';
}
#endregion
}
}

View File

@ -442,7 +442,8 @@ namespace BizHawk.Emulation.Cores.Sega.Saturn
byte[] data = new byte[2352];
try
{
CD.ReadABA_2352(FAD, data, 0);
//CD.ReadABA_2352(FAD, data, 0);
CD.ReadLBA_2352(FAD-150, data, 0); //zero 21-jun-2015 - did I adapt this right?
}
catch (Exception e)
{

View File

@ -160,7 +160,7 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
int ShockDisc_ReadTOC(IntPtr opaque, OctoshockDll.ShockTOC* read_target, OctoshockDll.ShockTOCTrack* tracks101)
{
read_target->disc_type = 1; //hardcoded in octoshock
read_target->disc_type = (byte)Disc.TOCRaw.Session1Format;
read_target->first_track = (byte)Disc.TOCRaw.FirstRecordedTrackNumber; //i _think_ thats what is meant here
read_target->last_track = (byte)Disc.TOCRaw.LastRecordedTrackNumber; //i _think_ thats what is meant here
@ -169,7 +169,7 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
for (int i = 1; i < 100; i++)
{
var item = Disc.TOCRaw.TOCItems[i];
tracks101[i].adr = 1; //not sure what this is
tracks101[i].adr = (byte)(item.Exists ? 1 : 0);
tracks101[i].lba = (uint)item.LBATimestamp.Sector;
tracks101[i].control = (byte)item.Control;
}
@ -178,13 +178,6 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
tracks101[read_target->last_track + 1].adr = 1;
tracks101[read_target->last_track + 1].control = 0;
tracks101[read_target->last_track + 1].lba = (uint)Disc.TOCRaw.LeadoutTimestamp.Sector;
////laaaame
//tracks101[read_target->last_track + 1].lba =
// (uint)(
// Disc.Structure.Sessions[0].Tracks[read_target->last_track - 1].Start_ABA //AUGH. see comment in Start_ABA
// + Disc.Structure.Sessions[0].Tracks[read_target->last_track - 1].LengthInSectors
// - 150
// );
//element 100 is to be copied as the lead-out track
tracks101[100] = tracks101[read_target->last_track + 1];

View File

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using BizHawk.Common.BufferExtensions;
//some old junk
namespace BizHawk.Emulation.DiscSystem
{
[Serializable]
public class DiscReferenceException : Exception
{
public DiscReferenceException(string fname, Exception inner)
: base(string.Format("A disc attempted to reference a file which could not be accessed or loaded: {0}", fname), inner)
{
}
public DiscReferenceException(string fname, string extrainfo)
: base(string.Format("A disc attempted to reference a file which could not be accessed or loaded:\n\n{0}\n\n{1}", fname, extrainfo))
{
}
}
/// <summary>
/// Simplifies access to the subcode data in a disc
/// </summary>
public class SubcodeReader
{
public SubcodeReader(Disc disc)
{
this.disc = disc;
}
public void ReadLBA_SubchannelQ(int lba, ref SubchannelQ sq)
{
var se = disc.ReadLBA_SectorEntry(lba);
se.SubcodeSector.ReadSubcodeChannel(1, buffer, 0);
int offset = 0;
sq.q_status = buffer[offset + 0];
sq.q_tno.BCDValue = buffer[offset + 1];
sq.q_index.BCDValue = buffer[offset + 2];
sq.min.BCDValue = buffer[offset + 3];
sq.sec.BCDValue = buffer[offset + 4];
sq.frame.BCDValue = buffer[offset + 5];
//nothing in byte[6]
sq.ap_min.BCDValue = buffer[offset + 7];
sq.ap_sec.BCDValue = buffer[offset + 8];
sq.ap_frame.BCDValue = buffer[offset + 9];
//CRC is stored inverted and big endian.. so... do the opposite
byte hibyte = (byte)(~buffer[offset + 10]);
byte lobyte = (byte)(~buffer[offset + 11]);
sq.q_crc = (ushort)((hibyte << 8) | lobyte);
}
Disc disc;
byte[] buffer = new byte[96];
}
/// <summary>
/// this is junk
/// </summary>
public class ProgressReport
{
public string Message;
public bool InfoPresent;
public double ProgressEstimate;
public double ProgressCurrent;
public int TaskCount;
public int TaskCurrent;
public bool CancelSignal;
}
sealed public partial class Disc
{
/// <summary>
/// Returns a SectorEntry from which you can retrieve various interesting pieces of information about the sector.
/// The SectorEntry's interface is not likely to be stable, though, but it may be more convenient.
/// </summary>
public SectorEntry ReadLBA_SectorEntry(int lba)
{
return Sectors[lba + 150];
}
/// <summary>
/// Main API to determine how many LBAs are available on the disc.
/// This counts from LBA 0 to the final sector available.
/// </summary>
public int LBACount { get { return ABACount - 150; } }
/// <summary>
/// Main API to determine how many ABAs (sectors) are available on the disc.
/// This counts from ABA 0 to the final sector available.
/// </summary>
public int ABACount { get { return Sectors.Count; } }
/// <summary>
/// main api for reading the structure from a disc.
/// TODO - this is weak sauce. Why this one method to read something which is nothing but returning a structure? Lame.
/// Either get rid of this, or rethink the disc API wrapper concept
/// </summary>
public DiscStructure ReadStructure()
{
return Structure;
}
// converts LBA to minute:second:frame format.
//TODO - somewhat redundant with Timestamp, which is due for refactoring into something not cue-related
public static void ConvertLBAtoMSF(int lba, out byte m, out byte s, out byte f)
{
lba += 150;
m = (byte)(lba / 75 / 60);
s = (byte)((lba - (m * 75 * 60)) / 75);
f = (byte)(lba - (m * 75 * 60) - (s * 75));
}
// converts MSF to LBA offset
public static int ConvertMSFtoLBA(byte m, byte s, byte f)
{
return f + (s * 75) + (m * 75 * 60) - 150;
}
}
}

View File

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
//disc type identification logic
namespace BizHawk.Emulation.DiscSystem
{
public enum DiscType
{
/// <summary>
/// Nothing is known about this disc type
/// </summary>
UnknownFormat,
/// <summary>
/// This is definitely a CDFS disc, but we can't identify anything more about it
/// </summary>
UnknownCDFS,
/// <summary>
/// Sony PSX
/// </summary>
SonyPSX,
/// <summary>
/// Sony PSP
/// </summary>
SonyPSP,
/// <summary>
/// Sega Saturn
/// </summary>
SegaSaturn,
/// <summary>
/// Its not clear whether we can ever have enough info to ID a turboCD disc (we're using hashes)
/// </summary>
TurboCD,
/// <summary>
/// MegaDrive addon
/// </summary>
MegaCD
}
public class DiscIdentifier
{
public DiscIdentifier(Disc disc)
{
this.disc = disc;
dsr = new DiscSectorReader(disc);
}
Disc disc;
DiscSectorReader dsr;
Dictionary<int, byte[]> sectorCache = new Dictionary<int,byte[]>();
/// <summary>
/// Attempts to determine the type of the disc.
/// In the future, we might return a struct or a class with more detailed information
/// </summary>
public DiscType DetectDiscType()
{
//sega doesnt put anything identifying in the cdfs volume info. but its consistent about putting its own header here in sector 0
if (DetectSegaSaturn()) return DiscType.SegaSaturn;
// not fully tested yet
if (DetectMegaCD()) return DiscType.MegaCD;
// not fully tested yet
if (DetectPSX()) return DiscType.SonyPSX;
//we dont know how to detect TurboCD.
//an emulator frontend will likely just guess TurboCD if the disc is UnknownFormat
//(we can also have a gameDB!)
var iso = new ISOFile();
bool isIso = iso.Parse(new DiscStream(disc, EDiscStreamView.DiscStreamView_Mode1_2048, 0));
if (isIso)
{
var appId = System.Text.Encoding.ASCII.GetString(iso.VolumeDescriptors[0].ApplicationIdentifier).TrimEnd('\0', ' ');
//NOTE: PSX magical drop F (JP SLPS_02337) doesn't have the correct iso PVD fields
//if (appId == "PLAYSTATION")
// return DiscType.SonyPSX;
if(appId == "PSP GAME")
return DiscType.SonyPSP;
return DiscType.UnknownCDFS;
}
return DiscType.UnknownFormat;
}
/// <summary>
/// This is reasonable approach to ID saturn.
/// </summary>
bool DetectSegaSaturn()
{
return StringAt("SEGA SEGASATURN", 0);
}
/// <summary>
/// probably wrong
/// </summary>
bool DetectMegaCD()
{
return StringAt("SEGADISCSYSTEM", 0) || StringAt("SEGADISCSYSTEM", 16);
}
bool DetectPSX()
{
if (!StringAt(" Licensed by ",0, 4)) return false;
return (StringAt("Sony Computer Entertainment Euro", 32, 4)
|| StringAt("Sony Computer Entertainment Inc.", 32, 4)
|| StringAt("Sony Computer Entertainment Amer", 32, 4)
|| StringAt("Sony Computer Entertainment of A", 32, 4)
);
}
private bool StringAt(string s, int n, int lba = 0)
{
//read it if we dont have it cached
//we wont be caching very much here, it's no big deal
//identification is not something we want to take a long time
byte[] data;
if (!sectorCache.TryGetValue(lba, out data))
{
data = new byte[2048];
dsr.ReadLBA_2048(lba, data, 0);
sectorCache[lba] = data;
}
byte[] cmp = System.Text.Encoding.ASCII.GetBytes(s);
byte[] cmp2 = new byte[cmp.Length];
Buffer.BlockCopy(data, n, cmp2, 0, cmp.Length);
return System.Linq.Enumerable.SequenceEqual(cmp, cmp2);
}
}
}

View File

@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using BizHawk.Common.BufferExtensions;
//main apis for emulator core routine use
//(this could probably use a lot of re-considering. we need to open up views to the disc, not interrogating it directly)
namespace BizHawk.Emulation.DiscSystem
{
public class DiscSectorReaderPolicy
{
/// <summary>
/// Different methods that can be used to get 2048 byte sectors
/// </summary>
public enum EUserData2048Mode
{
/// <summary>
/// The contents of the sector should be inspected (mode and form) and 2048 bytes returned accordingly
/// </summary>
InspectSector,
/// <summary>
/// Read it as mode 1
/// </summary>
AssumeMode1,
/// <summary>
/// Read it as mode 2 (form 1)
/// </summary>
AssumeMode2_Form1,
}
/// <summary>
/// The method used to get 2048 byte sectors
/// </summary>
public EUserData2048Mode UserData2048Mode = EUserData2048Mode.InspectSector;
/// <summary>
/// Throw exceptions if 2048 byte data can't be read
/// </summary>
public bool ThrowExceptions2048 = true;
/// <summary>
/// Indicates whether subcode should be delivered deinterleaved. It isn't stored that way on actual discs.
/// How about .sub files? Can't remember right now.
/// </summary>
public bool DeinterleavedSubcode = true;
}
/// <summary>
/// Main entry point for reading sectors from a disc.
/// This is not a multi-thread capable interface.
/// </summary>
public class DiscSectorReader
{
public DiscSectorReaderPolicy Policy = new DiscSectorReaderPolicy();
Disc disc;
public DiscSectorReader(Disc disc)
{
this.disc = disc;
}
void PrepareJob(int lba)
{
job.LBA = lba;
job.Params = disc.SynthParams;
job.Disc = disc;
}
//todo - methods to read only subcode
/// <summary>
/// Reads a full 2352 bytes of user data from a sector
/// </summary>
public int ReadLBA_2352(int lba, byte[] buffer, int offset)
{
var sector = disc.Sectors[lba + 150];
PrepareJob(lba);
job.DestBuffer2448 = buf2442;
job.DestOffset = 0;
job.Parts = ESectorSynthPart.User2352;
job.Disc = disc;
sector.SectorSynth.Synth(job);
Buffer.BlockCopy(buf2442, 0, buffer, offset, 2352);
return 2352;
}
/// <summary>
/// Reads the absolutely complete 2442 byte sector including all the user data and subcode
/// </summary>
public int ReadLBA_2442(int lba, byte[] buffer, int offset)
{
var sector = disc.Sectors[lba + 150];
PrepareJob(lba);
job.DestBuffer2448 = buffer; //go straight to the caller's buffer
job.DestOffset = offset; //go straight to the caller's buffer
job.Parts = ESectorSynthPart.Complete2448;
if (Policy.DeinterleavedSubcode)
job.Parts |= ESectorSynthPart.SubcodeDeinterleave;
sector.SectorSynth.Synth(job);
//we went straight to the caller's buffer, so no need to copy
return 2442;
}
int ReadLBA_2048_Mode1(int lba, byte[] buffer, int offset)
{
//we can read the 2048 bytes directly
var sector = disc.Sectors[lba + 150];
PrepareJob(lba);
job.DestBuffer2448 = buf2442;
job.DestOffset = 0;
job.Parts = ESectorSynthPart.User2048;
sector.SectorSynth.Synth(job);
Buffer.BlockCopy(buf2442, 16, buffer, offset, 2048);
return 2048;
}
int ReadLBA_2048_Mode2_Form1(int lba, byte[] buffer, int offset)
{
//we can read the 2048 bytes directly but we have to get them from the mode 2 data
var sector = disc.Sectors[lba + 150];
PrepareJob(lba);
job.DestBuffer2448 = buf2442;
job.DestOffset = 0;
job.Parts = ESectorSynthPart.User2336;
sector.SectorSynth.Synth(job);
Buffer.BlockCopy(buf2442, 24, buffer, offset, 2048);
return 2048;
}
/// <summary>
/// reads 2048 bytes of user data from a sector.
/// </summary>
public int ReadLBA_2048(int lba, byte[] buffer, int offset)
{
if (Policy.UserData2048Mode == DiscSectorReaderPolicy.EUserData2048Mode.AssumeMode1)
return ReadLBA_2048_Mode1(lba, buffer, offset);
else if (Policy.UserData2048Mode == DiscSectorReaderPolicy.EUserData2048Mode.AssumeMode2_Form1)
return ReadLBA_2048_Mode2_Form1(lba, buffer, offset);
else
{
//we need to determine the type of the sector.
//in no case do we need the ECC so build special flags here
var sector = disc.Sectors[lba + 150];
PrepareJob(lba);
job.DestBuffer2448 = buf2442;
job.DestOffset = 0;
job.Parts = ESectorSynthPart.Header16 | ESectorSynthPart.User2048 | ESectorSynthPart.EDC12;
sector.SectorSynth.Synth(job);
//now the inspection, based on the mode
byte mode = buf2442[15];
if (mode == 1)
{
Buffer.BlockCopy(buf2442, 16, buffer, offset, 2048);
return 2048;
}
else if (mode == 2)
{
//greenbook pg II-22
//we're going to do a sanity check here.. we're not sure what happens if we try to read 2048 bytes from a form-2 2324 byte sector
//we could handle it by policy but for now the policy is exception
byte submodeByte = buf2442[18];
int form = ((submodeByte >> 5) & 1) + 1;
if (form == 2)
{
if (Policy.ThrowExceptions2048)
throw new InvalidOperationException("Unsupported scenario: reading 2048 bytes from a Mode2 Form 2 sector");
else return 0;
}
//otherwise it's OK
Buffer.BlockCopy(buf2442, 24, buffer, offset, 2048);
return 2048;
}
else
{
if (Policy.ThrowExceptions2048)
throw new InvalidOperationException("Unsupported scenario: reading 2048 bytes from an unhandled sector type");
else return 0;
}
}
}
//lets not try to use this as a sector cache. it gets too complicated. its just a temporary variable.
byte[] buf2442 = new byte[2448];
SectorSynthJob job = new SectorSynthJob();
}
}

View File

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using BizHawk.Common.BufferExtensions;
namespace BizHawk.Emulation.DiscSystem
{
public enum EDiscStreamView
{
/// <summary>
/// views the disc as Mode 0 (aka Audio)
/// </summary>
DiscStreamView_Mode0_2352,
/// <summary>
/// views the disc as audio (aka Mode 0)
/// </summary>
DiscStreamView_Audio_2352 = DiscStreamView_Mode0_2352,
/// <summary>
/// views the disc as Mode 1
/// </summary>
DiscStreamView_Mode1_2048,
/// <summary>
/// views the disc as Mode 2
/// </summary>
DiscStreamView_Mode2_2336,
/// <summary>
/// views the disc as Mode 2 Form 1
/// </summary>
DiscStreamView_Mode2_Form1_2048,
/// <summary>
/// views the disc as Mode 2 Form 2
/// </summary>
DiscStreamView_Mode2_Form2_2324,
}
/// <summary>
/// Allows you to stream data off a disc.
/// For future work: depending on the View you select, it may not be seekable (in other words, it would need to read sequentially)
///
/// OLD COMMENTS:
/// Allows you to stream data off a disc
/// NOTE - it's probably been commented elsewhere, but this is possibly a bad idea! Turn it into views instead,
/// and the exact behaviour depends on the requested access level (raw or logical) and then what type of sectors are getting used
/// NOTE - actually even THAT is probably a bad idea. sector types can change on the fly.
/// this class promises something it can't deliver. (it's only being used to scan an ISO disc)
/// Well, we could make code that is full of red flags and warnings like "if this ISNT a 2048 byte sector ISO disc, then this wont work"
/// </summary>
public class DiscStream : System.IO.Stream
{
int SectorSize;
int NumSectors;
Disc Disc;
long currPosition;
int cachedSector;
byte[] cachedSectorBuffer;
DiscSectorReader dsr;
public DiscStream(Disc disc, EDiscStreamView view, int from_lba)
{
if (view != EDiscStreamView.DiscStreamView_Mode1_2048)
throw new NotSupportedException("disc streams of not mode 1 are currently unsupported");
SectorSize = 2048;
Disc = disc;
NumSectors = disc.LBACount;
dsr = new DiscSectorReader(disc);
//following the provided view
dsr.Policy.UserData2048Mode = DiscSectorReaderPolicy.EUserData2048Mode.AssumeMode1;
currPosition = from_lba * SectorSize;
cachedSector = -1;
cachedSectorBuffer = new byte[SectorSize];
}
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return true; } }
public override bool CanWrite { get { return false; } }
public override void Flush() { throw new NotImplementedException(); }
public override long Length { get { return NumSectors * SectorSize; } }
public override long Position
{
get { return currPosition; }
set
{
currPosition = value;
//invalidate the cached sector..
//as a later optimization, we could actually intelligently decide if this is necessary
cachedSector = -1;
}
}
internal void READLBA_Flat_Implementation(long disc_offset, byte[] buffer, int offset, int length, Action<int, byte[], int> sectorReader, int sectorSize, byte[] sectorBuf, ref int sectorBufferHint)
{
//hint is the sector number which is already read. to avoid repeatedly reading the sector from the disc in case of several small reads, so that sectorBuf can be used as a sector cache
}
public override int Read(byte[] buffer, int offset, int count)
{
long remainInDisc = Length - currPosition;
if (count > remainInDisc)
count = (int)Math.Min(remainInDisc, int.MaxValue);
int sectorBufferHint = -1;
int remain = count;
int readed = 0;
while (remain > 0)
{
int lba = (int)(currPosition / SectorSize);
int lba_within = (int)(currPosition % SectorSize);
int todo = remain;
int remains_in_lba = SectorSize - lba_within;
if (remains_in_lba < todo)
todo = remains_in_lba;
if (sectorBufferHint != lba)
{
//todo - read sector to cachedSectorBuffer
dsr.ReadLBA_2048(lba, cachedSectorBuffer, 0);
}
sectorBufferHint = lba;
Array.Copy(cachedSectorBuffer, lba_within, buffer, offset, todo);
offset += todo;
remain -= todo;
currPosition += todo;
readed += todo;
}
return readed;
}
public override long Seek(long offset, System.IO.SeekOrigin origin)
{
switch (origin)
{
case System.IO.SeekOrigin.Begin: Position = offset; break;
case System.IO.SeekOrigin.Current: Position += offset; break;
case System.IO.SeekOrigin.End: Position = Length - offset; break;
}
return Position;
}
public override void SetLength(long value) { throw new NotImplementedException(); }
public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
}
}

View File

View File

@ -0,0 +1,37 @@
using System;
using BizHawk.Common.BufferExtensions;
namespace BizHawk.Emulation.DiscSystem
{
public class OldHasher
{
public OldHasher(Disc disc)
{
this.disc = disc;
}
Disc disc;
// gets an identifying hash. hashes the first 512 sectors of
// the first data track on the disc.
//TODO - this is a very platform-specific thing. hashing the TOC may be faster and be just as effective. so, rename it appropriately
public string GetHash()
{
byte[] buffer = new byte[512 * 2352];
DiscSectorReader dsr = new DiscSectorReader(disc);
foreach (var track in disc.Structure.Sessions[0].Tracks)
{
if (track.TrackType == DiscStructure.ETrackType.Audio)
continue;
int lba_len = Math.Min(track.LengthInSectors, 512);
for (int s = 0; s < 512 && s < track.LengthInSectors; s++)
dsr.ReadLBA_2352(track.Indexes[1].LBA + s, buffer, s * 2352);
return buffer.HashMD5(0, lba_len * 2352);
}
return "no data track found";
}
}
}

View File

@ -50,6 +50,12 @@
<Compile Include="..\Version\VersionInfo.cs">
<Link>VersionInfo.cs</Link>
</Compile>
<Compile Include="API\Disc.API.cs" />
<Compile Include="API\Disc.ID.cs" />
<Compile Include="API\DiscSectorReader.cs" />
<Compile Include="API\DiscStream.cs" />
<Compile Include="API\Junk.cs" />
<Compile Include="API\OldHash.cs" />
<Compile Include="Blobs\Blob_ECM.cs" />
<Compile Include="Blobs\Blob_RawFile.cs" />
<Compile Include="Blobs\Blob_WaveFile.cs" />
@ -65,16 +71,22 @@
<Compile Include="cdfs\ISONode.cs" />
<Compile Include="cdfs\ISONodeRecord.cs" />
<Compile Include="cdfs\ISOVolumeDescriptor.cs" />
<Compile Include="CUE_format.cs" />
<Compile Include="CUE\CUE_Analyze.cs" />
<Compile Include="CUE\CUE_Format.cs" />
<Compile Include="CUE\CUE_Load.cs" />
<Compile Include="CUE\CUE_Parse.cs" />
<Compile Include="Decoding.cs" />
<Compile Include="Disc.API.cs" />
<Compile Include="Disc.cs" />
<Compile Include="Disc.ID.cs" />
<Compile Include="DiscTypes.cs" />
<Compile Include="DiscUtils.cs" />
<Compile Include="ECM.cs" />
<Compile Include="GPL_ECM.cs" />
<Compile Include="Jobs\DiscMountJob.cs" />
<Compile Include="Jobs\DiscMountJob.MednaDisc.cs" />
<Compile Include="Jobs\LoadSBIJob.cs" />
<Compile Include="Jobs\LoggedJob.cs" />
<Compile Include="M3U_file.cs" />
<Compile Include="MednaDiscAPI.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SBI_format.cs" />
<Compile Include="SectorInterfaces.cs" />
@ -96,7 +108,9 @@
<ItemGroup>
<Content Include="docs\notes.txt" />
<Content Include="docs\todo.txt" />
<Content Include="~notes-2015.txt" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>

View File

@ -34,7 +34,7 @@ namespace BizHawk.Emulation.DiscSystem
{
sealed partial class Disc
{
private class Blob_ECM : IBlob
internal class Blob_ECM : IBlob
{
FileStream stream;
@ -66,7 +66,7 @@ namespace BizHawk.Emulation.DiscSystem
public long Length;
public void Parse(string path)
public void Load(string path)
{
stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
@ -136,7 +136,8 @@ namespace BizHawk.Emulation.DiscSystem
else MisformedException();
}
//TODO - endian bug. need endian-independent binary reader with good license
//TODO - endian bug. need an endian-independent binary reader with good license (miscutils is apache license)
//extension methods on binary reader wont suffice, we need something that lets you control the endianness used for reading. a complete replacement.
var br = new BinaryReader(stream);
EDC = br.ReadInt32();

View File

@ -7,9 +7,10 @@ namespace BizHawk.Emulation.DiscSystem
partial class Disc
{
/// <summary>
/// TODO - dont we need to modify RiffMaster to be able to stream from the disk instead of loading everything at parse time?
/// TODO - doublecheck that riffmaster is not filling memory at load-time but reading through to the disk
/// TODO - clarify stream disposing semantics
/// </summary>
class Blob_WaveFile : IBlob
internal class Blob_WaveFile : IBlob
{
[Serializable]
public class Blob_WaveFile_Exception : Exception
@ -22,9 +23,12 @@ namespace BizHawk.Emulation.DiscSystem
public Blob_WaveFile()
{
} private class Blob_RawFile : IBlob
}
private class Blob_RawFile : IBlob
{
public string PhysicalPath
{
public string PhysicalPath {
get
{
return physicalPath;
@ -123,7 +127,7 @@ namespace BizHawk.Emulation.DiscSystem
waveDataStreamPos = dataChunk.Position;
mDataLength = dataChunk.Length;
}
catch
catch(Exception ex)
{
Dispose();
throw;

View File

@ -5,6 +5,40 @@ namespace BizHawk.Emulation.DiscSystem
{
public partial class Disc : IDisposable
{
/// <summary>
/// For use with blobs which are prematurely ended: buffers what there is, and zero-pads the rest
/// </summary>
public sealed class Blob_ZeroPadBuffer : IBlob
{
public static Blob_ZeroPadBuffer MakeBufferFrom(IBlob baseBlob, long start, int bufferLength)
{
var ret = new Blob_ZeroPadBuffer();
//allocate the entire buffer we'll need, and it will already be zero-padded
ret.buffer = new byte[bufferLength];
//read as much as is left
baseBlob.Read(start, ret.buffer, 0, bufferLength);
//if any less got read, well, there were already zeroes there
return ret;
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
int have = buffer.Length;
int todo = count;
if (have < todo)
todo = have;
Buffer.BlockCopy(this.buffer, 0, buffer, offset, todo);
return todo;
}
byte[] buffer;
public void Dispose()
{
buffer = null;
}
}
public sealed class Blob_ZeroPadAdapter : IBlob
{
public Blob_ZeroPadAdapter(IBlob baseBlob, long padFrom, long padLen)

View File

@ -11,6 +11,7 @@ namespace BizHawk.Emulation.DiscSystem
/// References to large blobs remain mostly on disk in the file which RiffMaster keeps a reference too. Dispose it to close the file.
/// You can modify blobs however you want and write the file back out to a new path, if youre careful (that was the original point of this)
/// Please be sure to test round-tripping when you make any changes. This architecture is a bit tricky to use, but it works if youre careful.
/// TODO - clarify stream disposing semantics
/// </summary>
class RiffMaster : IDisposable
{
@ -311,6 +312,7 @@ namespace BizHawk.Emulation.DiscSystem
Length = size
};
readCounter += size;
br.BaseStream.Position += size;
ret = rsc.Morph();
}
if (size % 2 != 0)

View File

@ -80,16 +80,17 @@ namespace BizHawk.Emulation.DiscSystem
public int Session;
/// <summary>
/// this seems just to be the LBA corresponding to AMIN:ASEC:AFRAME. It's not stored on the disc, and it's redundant.
/// this seems just to be the LBA corresponding to AMIN:ASEC:AFRAME (give or take 150). It's not stored on the disc, and it's redundant.
/// </summary>
public int ALBA;
/// <summary>
/// this seems just to be the LBA corresponding to PMIN:PSEC:PFRAME. It's not stored on the disc, and it's redundant.
/// this seems just to be the LBA corresponding to PMIN:PSEC:PFRAME (give or take 150). It's not stored on the disc, and it's redundant.
/// </summary>
public int PLBA;
//these correspond pretty directly to values in the Q subchannel fields
//NOTE: they're specified as absolute MSF. That means, they're 2 seconds off from what they should be when viewed as final TOC values
public int Control;
public int ADR;
public int TrackNo;
@ -119,7 +120,7 @@ namespace BizHawk.Emulation.DiscSystem
public int Number;
/// <summary>
/// The specified data mode
/// The specified data mode.
/// </summary>
public int Mode;
@ -285,6 +286,7 @@ namespace BizHawk.Emulation.DiscSystem
entry.PFrame = section.FetchOrFail("PFRAME");
entry.PLBA = section.FetchOrFail("PLBA");
//note: LBA 0 is Ansolute MSF 00:02:00
if (new Timestamp(entry.AMin, entry.ASec, entry.AFrame).Sector != entry.ALBA + 150)
throw new CCDParseException("Warning: inconsistency in CCD ALBA vs computed A MSF");
if (new Timestamp(entry.PMin, entry.PSec, entry.PFrame).Sector != entry.PLBA + 150)
@ -362,6 +364,111 @@ namespace BizHawk.Emulation.DiscSystem
return ret;
}
public static void Dump(Disc disc, string path)
{
using (var sw = new StreamWriter(path))
{
sw.WriteLine("[CloneCD]");
sw.WriteLine("Version=3");
sw.WriteLine("[Disc]");
sw.WriteLine("TocEntries={0}", disc.RawTOCEntries.Count);
sw.WriteLine("Sessions=1");
sw.WriteLine("DataTracksScrambled=0");
sw.WriteLine("CDTextLength=0"); //not supported anyway
sw.WriteLine("[Session 1]");
sw.WriteLine("PreGapMode=2");
sw.WriteLine("PreGapSubC=1");
for (int i = 0; i < disc.RawTOCEntries.Count; i++)
{
var entry = disc.RawTOCEntries[i];
sw.WriteLine("[Entry {0}]", i);
sw.WriteLine("Session=1");
sw.WriteLine("Point=0x{0:x2}", entry.QData.q_index);
sw.WriteLine("ADR=0x{0:x2}", entry.QData.ADR);
sw.WriteLine("Control=0x{0:x2}", (int)entry.QData.CONTROL);
sw.WriteLine("TrackNo={0}", entry.QData.q_tno);
sw.WriteLine("AMin={0}", entry.QData.min.DecimalValue);
sw.WriteLine("ASec={0}", entry.QData.sec.DecimalValue);
sw.WriteLine("AFrame={0}", entry.QData.frame.DecimalValue);
sw.WriteLine("ALBA={0}", entry.QData.Timestamp.Sector - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
sw.WriteLine("Zero={0}", entry.QData.zero);
sw.WriteLine("PMin={0}", entry.QData.ap_min.DecimalValue);
sw.WriteLine("PSec={0}", entry.QData.ap_sec.DecimalValue);
sw.WriteLine("PFrame={0}", entry.QData.ap_frame.DecimalValue);
sw.WriteLine("PLBA={0}", entry.QData.AP_Timestamp.Sector - 150); //remember to adapt the absolute MSF to an LBA (this field is redundant...)
}
for (int i = 0; i < disc.Structure.Sessions[0].Tracks.Count; i++)
{
var st = disc.Structure.Sessions[0].Tracks[i];
sw.WriteLine("[TRACK {0}]", st.Number);
sw.WriteLine("MODE={0}", st.ModeHeuristic); //MAYBE A BAD PLAN!
//dont write an index=0 identical to an index=1. It might work, or it might not.
int idx = 0;
if (st.Indexes[0].LBA == st.Indexes[1].LBA)
idx = 1;
for (; idx < st.Indexes.Count; idx++)
{
sw.WriteLine("INDEX {0}={1}", st.Indexes[idx].Number, st.Indexes[idx].LBA);
}
}
}
//dump the img and sub
//TODO - acquire disk size first
string imgPath = Path.ChangeExtension(path, ".img");
string subPath = Path.ChangeExtension(path, ".sub");
var buffer = new byte[2352];
using (var s = File.OpenWrite(imgPath))
{
DiscSectorReader dsr = new DiscSectorReader(disc);
//TODO - dont write leadout sectors, if they exist!
for (int aba = 150; aba < disc.Sectors.Count; aba++)
{
dsr.ReadLBA_2352(aba - 150, buffer, 0);
s.Write(buffer, 0, 2352);
}
}
using (var s = File.OpenWrite(subPath))
{
//TODO - dont write leadout sectors, if they exist!
for (int aba = 150; aba < disc.Sectors.Count; aba++)
{
disc.ReadLBA_SectorEntry(aba - 150).SubcodeSector.ReadSubcodeDeinterleaved(buffer, 0);
s.Write(buffer, 0, 96);
}
}
}
class SS_CCD : ISectorSynthJob2448
{
public void Synth(SectorSynthJob job)
{
//CCD is always containing everything we'd need (unless a .sub is missing?) so don't about flags
var imgBlob = job.Disc.DisposableResources[0] as Disc.Blob_RawFile;
var subBlob = job.Disc.DisposableResources[1] as Disc.Blob_RawFile;
//Read_2442(job.LBA, job.DestBuffer2448, job.DestOffset);
//read the IMG data
long ofs = job.LBA * 2352;
imgBlob.Read(ofs, job.DestBuffer2448, 0, 2352);
//if subcode is needed, read it
if ((job.Parts & (ESectorSynthPart.SubcodeAny)) != 0)
{
ofs = job.LBA * 96;
imgBlob.Read(ofs, job.DestBuffer2448, 2352, 96);
//we may still need to deinterleave it
if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave)) != 0)
{
SubcodeUtils.DeinterleaveInplace(job.DestBuffer2448, 2352);
}
}
}
}
/// <summary>
/// Loads a CCD at the specified path to a Disc object
/// </summary>
@ -373,23 +480,41 @@ namespace BizHawk.Emulation.DiscSystem
Disc disc = new Disc();
//mount the IMG and SUB files
var ccdf = loadResults.ParsedCCDFile;
var imgBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.ImgPath };
var subBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.SubPath };
disc.Blobs.Add(imgBlob);
disc.Blobs.Add(subBlob);
disc.DisposableResources.Add(imgBlob);
disc.DisposableResources.Add(subBlob);
//the only instance of a sector synthesizer we'll need
SS_CCD synth = new SS_CCD();
//generate DiscTOCRaw items from the ones specified in the CCD file
//TODO - range validate these (too many truncations to byte)
disc.RawTOCEntries = new List<RawTOCEntry>();
BufferedSubcodeSector bss = new BufferedSubcodeSector();
BufferedSubcodeSector bss = new BufferedSubcodeSector(); //TODO - its hacky that we need this..
foreach (var entry in ccdf.TOCEntries)
{
BCD2 tno, ino;
//this should actually be zero. im not sure if this is stored as BCD2 or not
tno = BCD2.FromDecimal(entry.TrackNo);
//these are special values.. I think, taken from this:
//http://www.staff.uni-mainz.de/tacke/scsi/SCSI2-14.html
//the CCD will contain Points as decimal values except for these specially converted decimal values which should stay as BCD.
//Why couldn't they all be BCD? I don't know. I guess because BCD is inconvenient, but only A0 and friends have special meaning. It's confusing.
ino = BCD2.FromDecimal(entry.Point);
if (entry.Point == 0xA0) ino.BCDValue = 0xA0;
else if (entry.Point == 0xA1) ino.BCDValue = 0xA1;
else if (entry.Point == 0xA2) ino.BCDValue = 0xA2;
var q = new SubchannelQ
{
q_status = SubchannelQ.ComputeStatus(entry.ADR, (EControlQ)(entry.Control & 0xF)),
q_tno = (byte)entry.TrackNo,
q_index = (byte)entry.Point,
q_tno = tno,
q_index = ino,
min = BCD2.FromDecimal(entry.AMin),
sec = BCD2.FromDecimal(entry.ASec),
frame = BCD2.FromDecimal(entry.AFrame),
@ -410,56 +535,47 @@ namespace BizHawk.Emulation.DiscSystem
tocSynth.Run();
disc.TOCRaw = tocSynth.Result;
//synthesize DiscStructure
var structureSynth = new DiscStructure.SynthesizeFromDiscTOCRawJob() { TOCRaw = disc.TOCRaw };
structureSynth.Run();
disc.Structure = structureSynth.Result;
disc.Structure = new DiscStructure();
var ses = new DiscStructure.Session();
disc.Structure.Sessions.Add(ses);
//I *think* implicitly there is an index 0.. at.. i dunno, 0 maybe, for track 1
for(int i=1;i<=99;i++)
{
var dsi0 = new DiscStructure.Index();
dsi0.LBA = 0;
dsi0.Number = 0;
disc.Structure.Sessions[0].Tracks[0].Indexes.Add(dsi0);
}
if(!ccdf.TracksByNumber.ContainsKey(i))
continue;
var ccdt = ccdf.TracksByNumber[i];
//now, how to get the track types for the DiscStructure?
//1. the CCD tells us (somehow the reader has judged)
//2. scan it out of the Q subchannel
//lets choose1.
//TODO - better consider how to handle the situation where we have havent received all the [TRACK] items we need
foreach (var st in disc.Structure.Sessions[0].Tracks)
{
var ccdt = ccdf.TracksByNumber[st.Number];
DiscStructure.Track track = new DiscStructure.Track() { Number = i };
ses.Tracks.Add(track);
//if index 0 is missing, add it
if (!ccdt.Indexes.ContainsKey(0))
track.Indexes.Add(new DiscStructure.Index { Number = 0, LBA = ccdt.Indexes[1] });
for(int j=1;j<=99;j++)
if (ccdt.Indexes.ContainsKey(j))
track.Indexes.Add(new DiscStructure.Index { Number = j, LBA = ccdt.Indexes[j] });
//TODO - this should only be used in case the .sub needs reconstructing
//determination should be done from heuristics.
//if we keep this, it should just be as a memo that later heuristics can use. For example: 'use guidance from original disc image'
track.ModeHeuristic = ccdt.Mode;
//TODO - this should be deleted anyway (
switch (ccdt.Mode)
{
case 0:
st.TrackType = ETrackType.Audio; //for CCD, this means audio, apparently.
track.TrackType = DiscStructure.ETrackType.Audio; //for CCD, this means audio, apparently.
break;
case 1:
st.TrackType = ETrackType.Mode1_2352;
break;
case 2:
st.TrackType = ETrackType.Mode2_2352;
track.TrackType = DiscStructure.ETrackType.Data;
break;
default:
throw new InvalidOperationException("Unsupported CCD mode");
}
//add indexes for this track
foreach (var ccdi in ccdt.Indexes)
{
var dsi = new DiscStructure.Index();
//if (ccdi.Key == 0) continue;
dsi.LBA = ccdi.Value;
dsi.Number = ccdi.Key;
st.Indexes.Add(dsi);
}
}
//add sectors for the lead-in, which isn't stored in the CCD file, I think
//TODO - synthesize lead-in sectors from TOC, if the lead-in isn't available.
//need a test case for that though.
//add sectors for the "mandatory track 1 pregap", which isn't stored in the CCD file
var leadin_sector_zero = new Sector_Zero();
var leadin_subcode_zero = new ZeroSubcodeSector();
for (int i = 0; i < 150; i++)
@ -484,24 +600,13 @@ namespace BizHawk.Emulation.DiscSystem
scsec.Offset = ((long)i) * 96;
scsec.Blob = subBlob;
se.SubcodeSector = scsec;
se.SectorSynth = synth;
}
return disc;
}
public void Dump(Disc disc, string ccdPath)
{
//TODO!!!!!!!
StringWriter sw = new StringWriter();
sw.WriteLine("[CloneCD]");
sw.WriteLine("Version=3");
sw.WriteLine("[Disc]");
//sw.WriteLine("TocEntries={0}",disc.TOCRaw.TOCItems
sw.WriteLine("Sessions=1");
sw.WriteLine("DataTracksScrambled=0");
sw.WriteLine("CDTextLength=0");
}
} //class CCD_Format
}

View File

@ -24,6 +24,7 @@ namespace BizHawk.Emulation.DiscSystem
public Action CallbackAction = delegate { };
public Disc Disc;
public DiscSectorReader DiscSectorReader;
public byte Mode = CDAudioMode_Stopped;
public byte PlayMode = PlaybackMode_LoopOnCompletion;
@ -43,6 +44,7 @@ namespace BizHawk.Emulation.DiscSystem
public CDAudio(Disc disc, int maxVolume = short.MaxValue)
{
Disc = disc;
DiscSectorReader = new DiscSectorReader(disc);
MaxVolume = maxVolume;
}
@ -124,7 +126,7 @@ namespace BizHawk.Emulation.DiscSystem
if (CurrentSector >= Disc.LBACount)
Array.Clear(SectorCache, 0, 2352); // request reading past end of available disc
else
Disc.ReadLBA_2352(CurrentSector, SectorCache, 0);
DiscSectorReader.ReadLBA_2352(CurrentSector, SectorCache, 0);
CachedSector = CurrentSector;
}
}

View File

@ -0,0 +1,310 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
partial class CUE_Format2
{
public class AnalyzeCueJob : LoggedJob
{
/// <summary>
/// input: the CueFile to analyze
/// </summary>
public CueFile IN_CueFile;
/// <summary>
/// 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.
/// </summary>
public int OUT_LoadTime;
/// <summary>
/// Analyzed-information about a file member of a cue
/// </summary>
public class CueFileInfo
{
public string FullPath;
public CueFileType Type;
public override string ToString()
{
return string.Format("{0}: {1}", Type, Path.GetFileName(FullPath));
}
}
/// <summary>
/// What type of file we're looking at.. each one would require a different ingestion handler
/// </summary>
public enum CueFileType
{
Unknown,
/// <summary>
/// a raw BIN that can be mounted directly
/// </summary>
BIN,
/// <summary>
/// a raw WAV that can be mounted directly
/// </summary>
WAVE,
/// <summary>
/// an ECM file that can be mounted directly (once the index is generated)
/// </summary>
ECM,
/// <summary>
/// An encoded audio file which can be seeked on the fly, therefore roughly mounted on the fly
/// THIS ISN'T SUPPORTED YET
/// </summary>
SeekAudio,
/// <summary>
/// An encoded audio file which can't be seeked on the fly. It must be decoded to a temp buffer, or pre-discohawked
/// </summary>
DecodeAudio,
}
/// <summary>
/// For each file referenced by the cue file, info about it
/// </summary>
public List<CueFileInfo> OUT_FileInfos;
}
public class CueFileResolver
{
public bool caseSensitive = false;
public bool IsHardcodedResolve { get; private set; }
string baseDir;
/// <summary>
/// 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?
/// </summary>
struct MyFileInfo
{
public string FullName;
public FileInfo FileInfo;
}
DirectoryInfo diBasedir;
MyFileInfo[] fisBaseDir;
/// <summary>
/// sets the base directory and caches the list of files in the directory
/// </summary>
public void SetBaseDirectory(string baseDir)
{
this.baseDir = baseDir;
diBasedir = new DirectoryInfo(baseDir);
//list all files, so we dont scan repeatedly.
fisBaseDir = MyFileInfosFromFileInfos(diBasedir.GetFiles());
}
/// <summary>
/// TODO - doesnt seem like we're using this...
/// </summary>
public void SetHardcodeResolve(IDictionary<string, string> 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;
}
/// <summary>
/// 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
/// </summary>
public List<string> 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<FileInfo>();
foreach (var fi in fileInfos)
{
var ext = Path.GetExtension(fi.FullName).ToLowerInvariant();
//SBI and CUE are always bad choices
if (ext == ".cue" || ext == ".sbi")
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<string>();
foreach (var fi in results)
ret.Add(fi.FullName);
return ret;
}
}
/// <summary>
/// Analyzes a cue file and its dependencies.
/// This should run as fast as possible.. no deep inspection of files
/// </summary>
public void AnalyzeCueFile(AnalyzeCueJob job)
{
//TODO - handle CD text file
job.OUT_FileInfos = new List<AnalyzeCueJob.CueFileInfo>();
var cue = job.IN_CueFile;
//first, collect information about all the input files
foreach (var cmd in cue.Commands)
{
var f = cmd as CueFile.Command.FILE;
if (f == null) continue;
//TODO TODO TODO TODO
//TODO TODO TODO TODO
//TODO TODO TODO 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)
{
job.Error("Couldn't resolve referenced cue file: " + f.Path);
continue;
}
else
{
choice = options[0];
if (options.Count > 1)
job.Warn("Multiple options resolving referenced cue file; choosing: " + Path.GetFileName(choice));
}
var cfi = new AnalyzeCueJob.CueFileInfo();
job.OUT_FileInfos.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") cfi.Type = AnalyzeCueJob.CueFileType.BIN;
else if (blobPathExt == ".ISO") cfi.Type = AnalyzeCueJob.CueFileType.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 = AnalyzeCueJob.CueFileType.WAVE;
}
catch
{
cfi.Type = AnalyzeCueJob.CueFileType.DecodeAudio;
}
}
}
else if (blobPathExt == ".APE") cfi.Type = AnalyzeCueJob.CueFileType.DecodeAudio;
else if (blobPathExt == ".MP3") cfi.Type = AnalyzeCueJob.CueFileType.DecodeAudio;
else if (blobPathExt == ".MPC") cfi.Type = AnalyzeCueJob.CueFileType.DecodeAudio;
else if (blobPathExt == ".FLAC") cfi.Type = AnalyzeCueJob.CueFileType.DecodeAudio;
else if (blobPathExt == ".ECM")
{
cfi.Type = AnalyzeCueJob.CueFileType.ECM;
if (!Disc.Blob_ECM.IsECM(choice))
{
job.Error("an ECM file was specified or detected, but it isn't a valid ECM file: " + Path.GetFileName(choice));
cfi.Type = AnalyzeCueJob.CueFileType.Unknown;
}
}
else
{
job.Error("Unknown cue file type. Since it's likely an unsupported compression, this is an error: ", Path.GetFileName(choice));
cfi.Type = AnalyzeCueJob.CueFileType.Unknown;
}
}
//TODO - check for mismatches between track types and file types, or is that best done when interpreting the commands?
//some quick checks:
if (job.OUT_FileInfos.Count == 0)
job.Error("Cue file doesn't specify any input files!");
//we can't readily analyze the length of files here, because we'd have to be interpreting the commands to know the track types. Not really worth the trouble
//we could check the format of the wav file here, though
//score the cost of loading the file
bool needsCodec = false;
job.OUT_LoadTime = 0;
foreach (var cfi in job.OUT_FileInfos)
{
if (cfi.Type == AnalyzeCueJob.CueFileType.DecodeAudio)
{
needsCodec = true;
job.OUT_LoadTime = Math.Max(job.OUT_LoadTime, 10);
}
if (cfi.Type == AnalyzeCueJob.CueFileType.SeekAudio)
needsCodec = true;
if (cfi.Type == AnalyzeCueJob.CueFileType.ECM)
job.OUT_LoadTime = Math.Max(job.OUT_LoadTime, 1);
}
//check whether processing was available
if (needsCodec)
{
FFMpeg ffmpeg = new FFMpeg();
if (!ffmpeg.QueryServiceAvailable())
job.Warn("Decoding service will be required for further processing, but is not available");
}
job.FinishLog();
}
} //partial class
} //namespace

View File

@ -0,0 +1,20 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
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
{
public partial class CUE_Format2
{
/// <summary>
/// The CueFileResolver to be used by this instance
/// </summary>
public CueFileResolver Resolver;
}
}

View File

@ -0,0 +1,888 @@
//TODO:
//"The first index of a file must start at 00:00:00" - if this isnt the case, we'll be doing nonsense for sure. so catch it
//Recover the idea of TOCPoints maybe, as it's a more flexible way of generating the structure.
//TODO
//check for flags changing after a PREGAP is processed. the PREGAP can't correctly run if the flags aren't set
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
partial class CUE_Format2
{
/// <summary>
/// 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
/// </summary>
public class LoadCueJob : LoggedJob
{
/// <summary>
/// The results of the analysis job, a prerequisite for this
/// </summary>
public AnalyzeCueJob IN_AnalyzeJob;
/// <summary>
/// The resulting disc
/// </summary>
public Disc OUT_Disc;
}
private enum SectorWriteType
{
Normal, Pregap, Postgap
}
public class CDTextData
{
public string Songwriter;
public string Performer;
public string Title;
}
/// <summary>
/// Represents a significant event in the structure of the disc which you might encounter while reading it sequentially.
/// </summary>
public class DiscPoint
{
/// <summary>
/// The Absolute LBA of this event
/// </summary>
public int AbsoluteLBA;
/// <summary>
/// The relative LBA of this event -- in other words, relative to Index 1.
/// This would be used for starting the pregap relative addressing.
/// </summary>
public int RelativeLBA;
/// <summary>
/// Track number at this point
/// </summary>
public int TrackNumber;
/// <summary>
/// Index number at this point.
/// If it's 0, we'll be in a pregap
/// </summary>
public int IndexNumber;
/// <summary>
/// Q status at this point
/// </summary>
public BCD2 Q_Status;
/// <summary>
/// The CD Text information at this point
/// </summary>
public CDTextData CDText = new CDTextData();
/// <summary>
/// The ISRC code (if present) at this point. null if not present.
/// </summary>
public string ISRC;
}
class CueIndexInfo
{
public int Number;
public Timestamp FileTimestamp;
public override string ToString()
{
return string.Format("I#{0:D2} {1}", Number, FileTimestamp);
}
}
class CueTrackInfo
{
public int BlobIndex;
public int Number;
public CDTextData CDTextData = new CDTextData();
public Timestamp Pregap, Postgap;
public CueFile.TrackFlags Flags = CueFile.TrackFlags.None;
public CueFile.TrackType TrackType = CueFile.TrackType.Unknown;
public List<CueIndexInfo> Indexes = new List<CueIndexInfo>();
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 indexlist = string.Join("|",Indexes);
return string.Format("T#{0:D2} {1}:{2} ({3})", Number, BlobIndex, idx.FileTimestamp,indexlist);
}
}
}
enum CuePointType
{
ZeroPregap, NormalPregap, Normal, Postgap,
}
class CuePoint
{
public DiscPoint DiscPoint = new DiscPoint();
public CuePointType Type;
public int BlobIndex;
public int BlobTimestampLBA;
}
void AnalyzeTracks(LoadCueJob job)
{
var a = job.IN_AnalyzeJob;
var cue = job.IN_AnalyzeJob.IN_CueFile;
List<CueTrackInfo> tracks = new List<CueTrackInfo>();
//current file tracking
int blob_index = -1;
//current cdtext and ISRC state
string cdtext_songwriter = null, cdtext_performer = null, cdtext_title = null;
string isrc = null;
//current track/index state
bool trackHasFlags = false;
CueTrackInfo track_curr = null;
//first, collate information into high level description of each track
for (int i = 0; i < cue.Commands.Count; i++)
{
var cmd = cue.Commands[i];
//these commands get dealt with globally. nothing to be done here yet
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;
//handle cdtext and ISRC state updates, theyre kind of like little registers
if (cmd is CueFile.Command.PERFORMER)
cdtext_performer = (cmd as CueFile.Command.PERFORMER).Value;
if (cmd is CueFile.Command.SONGWRITER)
cdtext_songwriter = (cmd as CueFile.Command.SONGWRITER).Value;
if (cmd is CueFile.Command.TITLE)
cdtext_title = (cmd as CueFile.Command.TITLE).Value;
if (cmd is CueFile.Command.ISRC)
isrc = (cmd as CueFile.Command.ISRC).Value;
//flags are also a kind of a register. but the flags value is reset by the track command
if (cmd is CueFile.Command.FLAGS)
{
if (track_curr == null)
job.Warn("FLAG command received before a TRACK command; ignoring");
else if (trackHasFlags)
job.Warn("Multiple FLAGS commands in track {0}; subsequent commands are ignored", track_curr.Number);
else
{
track_curr.Flags = (cmd as CueFile.Command.FLAGS).Flags;
trackHasFlags = true;
}
}
if (cmd is CueFile.Command.TRACK)
{
if(blob_index == -1)
job.Error("TRACK command received before FILE command; ignoring");
else
{
var track = cmd as CueFile.Command.TRACK;
//setup new track
track_curr = new CueTrackInfo();
track_curr.BlobIndex = blob_index;
track_curr.Number = track.Number;
track_curr.TrackType = track.Type;
tracks.Add(track_curr);
//setup default flags for the track
if (track.Type != CueFile.TrackType.Audio)
track_curr.Flags = CueFile.TrackFlags.DATA;
trackHasFlags = false;
}
}
if (cmd is CueFile.Command.FILE)
{
blob_index++;
track_curr = null;
}
if (cmd is CueFile.Command.INDEX)
{
var cfindex = cmd as CueFile.Command.INDEX;
if(track_curr == null)
job.Error("INDEX command received before TRACK command; ignoring");
else if (track_curr.Postgap.Valid)
job.Warn("INDEX command received after POSTGAP; ignoring");
else if (cfindex.Number != track_curr.Indexes.Count && (cfindex.Number != 1 && track_curr.Indexes.Count==0))
job.Warn("non-sequential INDEX command received; ignoring");
else {
var index = new CueIndexInfo { Number = cfindex.Number, FileTimestamp = cfindex.Timestamp };
track_curr.Indexes.Add(index);
}
}
if (cmd is CueFile.Command.POSTGAP)
{
if(track_curr == null)
job.Warn("POSTGAP command received before TRACK command; ignoring");
else if(track_curr.Postgap.Valid)
job.Warn("Multiple POSTGAP commands specified for a track; ignoring");
else
track_curr.Postgap = (cmd as CueFile.Command.POSTGAP).Length;
}
if (cmd is CueFile.Command.PREGAP)
{
if (track_curr == null)
job.Warn("PREGAP command received before TRACK command; ignoring");
else if (track_curr.Pregap.Valid)
job.Warn("Multiple PREGAP commands specified for a track; ignoring");
else
track_curr.Pregap = (cmd as CueFile.Command.PREGAP).Length;
}
} //commands loop
//check for tracks with no indexes, which will wreck our processing later and is an error anyway
{
RETRY:
foreach (var t in tracks)
if(t.Indexes.Count == 0)
{
job.Error("TRACK {0} is missing an INDEX",t.Number);
tracks.Remove(t);
goto RETRY;
}
}
//now, create a todo list
List<CuePoint> todo = new List<CuePoint>();
int lba = 0;
foreach (var t in tracks)
{
//find total length of pregap, so we can figure out how negative the relative timestamp goes
int lbaPregap0to1 = 0;
if (t.Indexes.Count > 1)
lbaPregap0to1 = t.Indexes[1].FileTimestamp.Sector - t.Indexes[0].FileTimestamp.Sector;
//if(t.Pregap.Valid)
//{
// var cpPregap = new CuePoint();
// cpPregap.DiscPoint.AbsoluteLBA = lba;
// cpPregap.DiscPoint
}
}
/// <summary>
/// runs a LoadCueJob
/// </summary>
public void LoadCueFile(LoadCueJob job)
{
AnalyzeTracks(job);
//params
var a = job.IN_AnalyzeJob;
var cue = job.IN_AnalyzeJob.IN_CueFile;
var disc = new Disc();
//utils
var zero_sector = new Sector_Zero();
var zero_subSector = new ZeroSubcodeSector();
//generate lead-in gap
//TODO - shouldnt this have proper subcode? dunno
//mednafen will probably be testing this in the next release
for(int i=0;i<150;i++)
{
var se_leadin = new SectorEntry(zero_sector);
se_leadin.SubcodeSector = zero_subSector;
disc.Sectors.Add(se_leadin);
}
//current cdtext and ISRC state
string cdtext_songwriter = null, cdtext_performer = null, cdtext_title = null;
string isrc = null;
//current track state
CueFile.TrackFlags track_pendingFlags = CueFile.TrackFlags.None;
CueFile.TrackFlags track_flags = CueFile.TrackFlags.None;
bool track_hasPendingFlags = false;
int track_num = 0;
//int track_index0MSF = -1; //NOT NEED ANYMORE
int track_index1MSF = -1;
bool track_readyForPregapCommand = false;
CueFile.Command.TRACK track_pendingCommand = null;
CueFile.Command.TRACK track_currentCommand = null;
Timestamp postgap_pending = new Timestamp();
Timestamp pregap_pending = new Timestamp();
Timestamp pregap_current = new Timestamp();
//current index state
int index_num = -1;
int index_msf = -1; //file-relative, remember
//current file state
CueFile.Command.FILE file_command = null;
int file_cfi_index = -1;
long file_ofs = 0, file_len = 0;
int file_ownmsf = -1;
IBlob file_blob = null;
//current output state
SubchannelQ priorSubchannelQ = new SubchannelQ();
int LBA = 0;
List<IDisposable> resources = disc.DisposableResources;
var TOCInfo = new Synthesize_A0A1A2_Job { FirstRecordedTrackNumber = -1, LastRecordedTrackNumber = -1, LeadoutLBA = -1};
//lets start with this. we'll change it if we ever find a track of a different format (this is based on mednafen's behaviour)
TOCInfo.Session1Format = DiscTOCRaw.SessionFormat.Type00_CDROM_CDDA;
//a little subroutine to wrap a sector if the blob is out of space
Func<int,Sector_RawBlob> eatBlobAndWrap = (required) =>
{
IBlob blob = file_blob;
var ret = new Sector_RawBlob { Blob = file_blob, Offset = file_ofs };
if (file_ofs + required > file_len)
{
job.Warn("Zero-padding mis-sized file: " + Path.GetFileName(file_command.Path));
blob = Disc.Blob_ZeroPadBuffer.MakeBufferFrom(file_blob,file_ofs,required);
resources.Add(blob);
ret.Blob = blob;
ret.Offset = 0;
}
file_ofs += required;
return ret;
};
Action emitRawTocEntry = () =>
{
//NOT TOO SURE ABOUT ALL THIS YET. NEED TO VALIDATE MORE DEEPLY.
SubchannelQ sq = new SubchannelQ();
//absent some kind of policy for how to set it, this is a safe assumption:
byte ADR = 1;
sq.SetStatus(ADR, (EControlQ)(int)track_flags);
sq.q_tno.BCDValue = 0; //kind of a little weird here.. the track number becomes the 'point' and put in the index instead. 0 is the track number here.
sq.q_index = BCD2.FromDecimal(track_num);
//not too sure about these yet
sq.min = BCD2.FromDecimal(0);
sq.sec = BCD2.FromDecimal(0);
sq.frame = BCD2.FromDecimal(0);
sq.AP_Timestamp = new Timestamp(LBA); //off by 150?
disc.RawTOCEntries.Add(new RawTOCEntry { QData = sq });
};
//a little subroutine to write a sector
//TODO - I intend to rethink how the sector interfaces work, but not yet. It works well enough now.
Action<SectorWriteType> writeSector = (SectorWriteType type) =>
{
ISector siface = null;
if (type == SectorWriteType.Normal)
{
switch (track_currentCommand.Type)
{
default:
case CueFile.TrackType.Unknown:
throw new InvalidOperationException("Internal error processing unknown track type which should have been caught earlier");
case CueFile.TrackType.CDI_2352:
case CueFile.TrackType.Mode1_2352:
case CueFile.TrackType.Mode2_2352:
case CueFile.TrackType.Audio:
//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_RawBlob
siface = eatBlobAndWrap(2352);
break;
case CueFile.TrackType.Mode1_2048:
{
//2048 bytes are present. ECM needs to be generated to create a full raw sector
var raw = eatBlobAndWrap(2048);
siface = new Sector_Mode1_2048(LBA + 150) //pass the ABA I guess
{
Blob = new ECMCacheBlob(raw.Blob), //archaic
Offset = raw.Offset
};
break;
}
case CueFile.TrackType.CDG: //2448
case CueFile.TrackType.Mode2_2336:
case CueFile.TrackType.CDI_2336:
throw new InvalidOperationException("Track types not supported yet");
}
}
else if (type == SectorWriteType.Postgap)
{
//TODO - does subchannel need to get written differently?
siface = zero_sector;
}
else if (type == SectorWriteType.Pregap)
{
//TODO - does subchannel need to get written differently?
siface = zero_sector;
}
//make the subcode
//TODO - according to policies, or better yet, defer this til it's needed (user delivers a policies object to disc reader apis)
//at any rate, we'd paste this logic into there so let's go ahead and write it here
var subcode = new BufferedSubcodeSector();
SubchannelQ sq = new SubchannelQ();
//absent some kind of policy for how to set it, this is a safe assumption:
byte ADR = 1;
sq.SetStatus(ADR, (EControlQ)(int)track_flags);
sq.q_tno = BCD2.FromDecimal(track_num);
sq.q_index = BCD2.FromDecimal(index_num);
int ABA = LBA + 150;
sq.ap_min = BCD2.FromDecimal(new Timestamp(ABA).MIN);
sq.ap_sec = BCD2.FromDecimal(new Timestamp(ABA).SEC);
sq.ap_frame = BCD2.FromDecimal(new Timestamp(ABA).FRAC);
int track_relative_msf = file_ownmsf - track_index1MSF;
//adjust the track_relative_msf here, since it's inserted before index 0
if (type == SectorWriteType.Pregap)
{
track_relative_msf -= pregap_current.Sector;
}
if (index_num == 0 || type == SectorWriteType.Pregap)
{
//PREGAP:
//things are negative here.
if (track_relative_msf > 0) throw new InvalidOperationException("Perplexing internal error with non-negative pregap MSF");
track_relative_msf = -track_relative_msf;
//now for something special.
//yellow-book says:
//pre-gap for "first part of a digital data track not containing user data and encoded as a pause"
//first interval: at least 75 sectors coded as preceding track
//second interval: at least 150 sectors coded as user data track.
//so... we ASSUME the 150 sector pregap is more important. so if thats all there is, theres no 75 sector pregap like the old track
//if theres a longer pregap, then we generate weird old track pregap to contain the rest.
//TODO - GENERATE P SUBCHANNEL
if (track_relative_msf > 150)
{
//only if we're burning a data track now
if((track_flags & CueFile.TrackFlags.DATA)!=0)
sq.q_status = priorSubchannelQ.q_status;
}
}
sq.min = BCD2.FromDecimal(new Timestamp(track_relative_msf).MIN);
sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC);
sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC);
//finally we're done: synthesize subchannel
subcode.Synthesize_SubchannelQ(ref sq, true);
priorSubchannelQ = sq;
//now we have the ISector and subcode; make the SectorEntry
var se = new SectorEntry(siface);
se.SubcodeSector = subcode;
disc.Sectors.Add(se);
LBA++;
file_ownmsf++;
};
//generates sectors until the file is exhausted
Action finishFile = () =>
{
while (file_ofs < file_len)
writeSector(SectorWriteType.Normal);
};
Action writePostgap = () =>
{
if (postgap_pending.Valid)
for (int i = 0; i < postgap_pending.Sector; i++)
{
writeSector(SectorWriteType.Postgap);
}
};
Action writePregap = () =>
{
if (pregap_current.Valid)
{
if (pregap_current.Sector > 150)
{
int zzz = 9;
}
for (int i = 0; i < pregap_current.Sector; i++)
{
writeSector(SectorWriteType.Pregap);
}
}
};
//prepare disc structure
disc.Structure = new DiscStructure();
disc.Structure.Sessions.Add(new DiscStructure.Session());
//now for the magic. Process commands in order
for (int i = 0; i < cue.Commands.Count; i++)
{
var cmd = cue.Commands[i];
//these commands get dealt with globally. nothing to be done here
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;
//handle cdtext and ISRC state updates, theyre kind of like little registers
if (cmd is CueFile.Command.PERFORMER)
cdtext_performer = (cmd as CueFile.Command.PERFORMER).Value;
if (cmd is CueFile.Command.SONGWRITER)
cdtext_songwriter = (cmd as CueFile.Command.SONGWRITER).Value;
if (cmd is CueFile.Command.TITLE)
cdtext_title = (cmd as CueFile.Command.TITLE).Value;
if(cmd is CueFile.Command.ISRC)
isrc = (cmd as CueFile.Command.ISRC).Value;
//flags are also a kind of a register. but the flags value is reset by the track command
if (cmd is CueFile.Command.FLAGS)
{
if (track_hasPendingFlags)
job.Warn("Multiple FLAGS commands in track {0}; subsequent commands are ignored", track_num);
else
{
track_pendingFlags = (cmd as CueFile.Command.FLAGS).Flags;
track_hasPendingFlags = true;
}
}
if (cmd is CueFile.Command.TRACK)
{
var track = cmd as CueFile.Command.TRACK;
//HOW TO HANDLE TRACKS:
//Tracks don't have timestamps. Therefore, they're always pending until something that does have a timestamp
track_pendingCommand = track;
track_pendingFlags = CueFile.TrackFlags.None;
track_readyForPregapCommand = true;
}
if (cmd is CueFile.Command.FILE)
{
var file = cmd as CueFile.Command.FILE;
//HOW TO HANDLE FILES:
//1. flush the current file with all current register settings
//2. mount the next file
//3. clear some register settings
//1. do the flush, if needed
if (file_cfi_index != -1)
finishFile();
//2a. clean up by nulling before starting next file...
file_command = null;
file_ofs = 0;
file_len = 0;
file_blob = null;
//2b. mount the next file
file_command = file;
file_ownmsf = 0;
var cfi = a.OUT_FileInfos[++file_cfi_index];
//TODO - a lot of redundant code here, maybe Blob should know his length in the IBlob interface since every freaking thing depends on it
if (cfi.Type == AnalyzeCueJob.CueFileType.BIN || cfi.Type == AnalyzeCueJob.CueFileType.Unknown)
{
//raw files:
var blob = new Disc.Blob_RawFile { PhysicalPath = cfi.FullPath };
resources.Add(file_blob = blob);
file_len = blob.Length;
}
else if (cfi.Type == AnalyzeCueJob.CueFileType.ECM)
{
var blob = new Disc.Blob_ECM();
resources.Add(file_blob = blob);
blob.Load(cfi.FullPath);
file_len = blob.Length;
}
else if (cfi.Type == AnalyzeCueJob.CueFileType.WAVE)
{
var blob = new Disc.Blob_WaveFile();
resources.Add(file_blob = blob);
blob.Load(cfi.FullPath);
file_len = blob.Length;
}
else if (cfi.Type == AnalyzeCueJob.CueFileType.DecodeAudio)
{
FFMpeg ffmpeg = new FFMpeg();
if (!ffmpeg.QueryServiceAvailable())
{
throw new DiscReferenceException(cfi.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(cfi.FullPath);
var blob = new Disc.Blob_WaveFile();
resources.Add(file_blob = blob);
blob.Load(new MemoryStream(buf));
}
//3. reset track and index registers
track_num = 0;
//TODO - do I need to reset the track extra stuff here? nothing can get generated
//track_pendingCommand = null;
//track_currentCommand = null;
//track_flags = CueFile.TrackFlags.None;
//track_hasFlags = false;
//track_index0MSF = -1;
//track_index1MSF = -1;
//and how about this? it should get reset when the track begins
//index_num = -1;
//index_msf = -1;
} //FILE command
if (cmd is CueFile.Command.INDEX)
{
var index = cmd as CueFile.Command.INDEX;
var timestamp = index.Timestamp;
var i_num = index.Number;
//cant get a pregap command anymore
track_readyForPregapCommand = false;
//ok, now for some truly arcane stuff.
//first, a warning if this isn't sequential
if (i_num != 0 && i_num != 1 && i_num != index_num + 1)
{
job.Error("Invalid index number {0}: must be one greater than previous. Fixing that for you.", i_num);
i_num = index_num + 1;
}
//now, an error if this is the first index and isnt 0 or 1
if (index_num == -1 && i_num != 0 && i_num != 1)
{
job.Error("Invalid index {0}: 1st IDX of track must be 0 or 1. Pretend it's 1.", i_num);
//how to recover? assume it's 1
i_num = 1;
}
//now, an error if this is not at 00:00:00 for the first index in a file
if (file_ofs == 0 && timestamp.Sector != 0 && index_num == -1)
{
job.Error("Invalid IDX {0}: 1st IDX in file must be at 00:00:00 but it's at {1}", i_num, index.Timestamp);
timestamp = new Timestamp(0);
}
if (i_num == 0)
{
//if this is index 0, we're gonna have a pregap.
//lets just record that we got this index, and go to the next command
index_num = 0;
//DONT NEED THIS ANYMORE?
//track_index0MSF = timestamp.Sector;
}
else if (i_num == 1)
{
//if we got an index 1:
track_index1MSF = timestamp.Sector;
//DONT NEED THIS ANYMORE?
////if we didnt get an index 0 LBA, give up and assume it's the same as this
//if (track_index0MSF == -1)
// track_index0MSF = timestamp.Sector;
//we can now execute a pending track change command
if (track_pendingCommand == null)
throw new InvalidOperationException("Unrecoverable error processing CUE: index without track context");
//before we begin a track, write any postgap that we may have queued...
writePostgap();
//...and also generate sectors up til the current index (as the last index)
while (file_ownmsf < timestamp.Sector)
writeSector(SectorWriteType.Normal);
//begin the track:
track_currentCommand = track_pendingCommand;
track_pendingCommand = null;
track_hasPendingFlags = false;
track_flags = track_pendingFlags;
track_num = track_currentCommand.Number;
pregap_current = pregap_pending;
pregap_pending = new Timestamp();
//default flags:
if (track_currentCommand.Type == CueFile.TrackType.Audio) { }
else track_flags |= CueFile.TrackFlags.DATA;
postgap_pending = new Timestamp();
//account for it in structure
var st = new DiscStructure.Track { Number = track_num };
//TODO - move out of here into analysis stage
switch (track_currentCommand.Type)
{
default:
case CueFile.TrackType.CDG:
case CueFile.TrackType.Unknown: throw new InvalidOperationException("UNEXPECTED PANDA MAYOR");
case CueFile.TrackType.Audio:
st.TrackType = DiscStructure.ETrackType.Audio;
st.ModeHeuristic = 0;
break;
case CueFile.TrackType.Mode1_2048:
case CueFile.TrackType.Mode1_2352:
st.TrackType = DiscStructure.ETrackType.Data;
st.ModeHeuristic = 1;
break;
case CueFile.TrackType.Mode2_2352:
case CueFile.TrackType.Mode2_2336:
case CueFile.TrackType.CDI_2336:
case CueFile.TrackType.CDI_2352:
st.TrackType = DiscStructure.ETrackType.Data;
st.ModeHeuristic = 1;
break;
}
disc.Structure.Sessions[0].Tracks.Add(st);
//maintain some memos
if (TOCInfo.FirstRecordedTrackNumber == -1)
TOCInfo.FirstRecordedTrackNumber = track_num;
TOCInfo.LastRecordedTrackNumber = track_num;
//update the disc type... do we need to do any double checks here to check for regressions? doubt it.
if (TOCInfo.Session1Format == DiscTOCRaw.SessionFormat.Type00_CDROM_CDDA)
{
switch (track_currentCommand.Type)
{
case CueFile.TrackType.Mode2_2336:
case CueFile.TrackType.Mode2_2352:
TOCInfo.Session1Format = DiscTOCRaw.SessionFormat.Type20_CDXA;
break;
case CueFile.TrackType.CDI_2336:
case CueFile.TrackType.CDI_2352:
TOCInfo.Session1Format = DiscTOCRaw.SessionFormat.Type10_CDI;
break;
default:
//no changes here
break;
}
}
{
var _currTrack = disc.Structure.Sessions[0].Tracks[disc.Structure.Sessions[0].Tracks.Count - 1];
if (_currTrack.Indexes.Count == 0)
_currTrack.Indexes.Add(new DiscStructure.Index { Number = 0, LBA = LBA });
}
//write a special pregap if a command was pending
writePregap();
}
//emit it to TOC if needed
if (i_num == 1)
emitRawTocEntry();
//update sense of current index
index_num = i_num;
index_msf = timestamp.Sector;
{
var _currTrack = disc.Structure.Sessions[0].Tracks[disc.Structure.Sessions[0].Tracks.Count - 1];
_currTrack.Indexes.Add(new DiscStructure.Index { Number = index_num, LBA = LBA });
}
}
if (cmd is CueFile.Command.POSTGAP)
{
var postgap = cmd as CueFile.Command.POSTGAP;
if(index_num == -1)
{
job.Warn("Ignoring POSTGAP before any indexes");
goto NO_POSTGAP;
}
if(postgap_pending.Valid)
{
job.Warn("Ignoring multiple POSTGAPs for track");
goto NO_POSTGAP;
}
postgap_pending = postgap.Length;
NO_POSTGAP: ;
}
if (cmd is CueFile.Command.PREGAP)
{
//see particularly http://digitalx.org/cue-sheet/examples/index.html#example05 for the most annoying example
var pregap = cmd as CueFile.Command.PREGAP;
if (!track_readyForPregapCommand)
{
job.Warn("Not ready for PREGAP command; ignoring");
goto NO_PREGAP;
}
pregap_pending = pregap.Length;
track_readyForPregapCommand = false;
NO_PREGAP:
;
}
}
//do a final flush
finishFile();
writePostgap();
//do some stupid crap for testing. maybe it isnt so stupid, but its what we have for now
disc.Structure.LengthInSectors = disc.Sectors.Count;
//add RawTOCEntries A0 A1 A2 to round out the TOC
TOCInfo.LeadoutLBA = LBA;
TOCInfo.Run(disc.RawTOCEntries);
//generate the TOCRaw from the RawTocEntries
var tocSynth = new DiscTOCRaw.SynthesizeFromRawTOCEntriesJob() { Entries = disc.RawTOCEntries };
tocSynth.Run();
disc.TOCRaw = tocSynth.Result;
//generate lead-out track with some canned number of sectors
//TODO - move this somewhere else and make it controllable depending on which console is loading up the disc
//TODO - we're not doing this yet
//var synthLeadoutJob = new Disc.SynthesizeLeadoutJob { Disc = disc, Length = 150 };
//synthLeadoutJob.Run();
//blech, old crap, maybe
disc.Structure.Synthesize_TOCPointsFromSessions();
job.OUT_Disc = disc;
job.FinishLog();
} //LoadCueFile
}
}

View File

@ -0,0 +1,452 @@
//TODO - object initialization syntax cleanup
using System;
using System.Linq;
using System.Text;
using System.IO;
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"
//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
{
partial class CUE_Format2
{
public class ParseCueJob : LoggedJob
{
/// <summary>
/// input: the cue string to parse
/// </summary>
public string IN_CueString;
/// <summary>
/// output: the resulting minimally-processed cue file
/// </summary>
public CueFile OUT_CueFile;
}
/// <summary>
/// Represents the contents of a cue file
/// This is relatively unstructured because in a lot (maybe every) way a cue file is meant to be processed one command at a time
/// </summary>
public class CueFile
{
// (here are all the commands we can encounter)
//TODO - record line number origin of command?
public static class Command
{
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); } }
}
/// <summary>
/// Stuff other than the commands, global for the whole disc
/// </summary>
public class DiscInfo
{
public Command.CATALOG Catalog;
public Command.ISRC ISRC;
public Command.CDTEXTFILE CDTextFile;
}
/// <summary>
/// The sequential list of commands parsed out of the cue file
/// </summary>
public List<object> Commands = new List<object>();
/// <summary>
/// Stuff other than the commands, global for the whole disc
/// </summary>
public DiscInfo GlobalDiscInfo = new DiscInfo();
[Flags]
public enum TrackFlags
{
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
//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
public enum FileType
{
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 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 (raw--whats the reason to distinguish this from the other 2352?)
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);
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;
Commands.Add(new Command.COMMENT() { Value = line });
}
else switch (key)
{
default:
job.Warn("Unknown command: " + key);
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;
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;
case "FILE":
{
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 });
}
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())
{
default:
job.Error("Unknown TRACK type: " + str_trackType);
tt = TrackType.Unknown;
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;
}
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 });
}
else job.Warn("Unknown text at end of line after processing command: " + key);
}
} //end cue parsing loop
job.FinishLog();
} //LoadFromString
}
/// <summary>
/// Performs minimum parse processing on a cue file
/// </summary>
public void ParseCueFile(ParseCueJob job)
{
job.OUT_CueFile = new CueFile();
job.OUT_CueFile.LoadFromString(job);
}
} //partial class
} //namespace

View File

@ -168,7 +168,7 @@ namespace BizHawk.Emulation.DiscSystem
}
Blob_ECM blob = new Blob_ECM();
Blobs.Add(blob);
blob.Parse(blobPath);
blob.Load(blobPath);
cue_blob = blob;
blob_length_aba = (int)(blob.Length / blob_sectorsize);
blob_length_bytes = blob.Length;
@ -205,7 +205,6 @@ namespace BizHawk.Emulation.DiscSystem
AudioDecoder dec = new AudioDecoder();
byte[] buf = dec.AcquireWaveData(blobPath);
blob.Load(new MemoryStream(buf));
WasSlowLoad = true;
}
}
catch (Exception ex)
@ -287,8 +286,8 @@ namespace BizHawk.Emulation.DiscSystem
//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;
toc_track.Control |= EControlQ.None;
else toc_track.Control |= EControlQ.DATA;
if (curr_track == 1)
{
@ -690,7 +689,7 @@ namespace BizHawk.Emulation.DiscSystem
var flags = clp.ReadToken();
if (flags == "DCP")
{
currTrack.Control |= EControlQ.CopyPermittedMask;
currTrack.Control |= EControlQ.DCP;
} else throw new CueBrokenException("Unknown flags: " + flags);
}
break;

View File

@ -4,6 +4,15 @@ using System.Text;
using System.IO;
using System.Collections.Generic;
//ARCHITECTURE NOTE:
//no provisions are made for caching synthesized data for later accelerated use.
//in the worst case that might result in synthesizing an entire disc in memory.
//instead, users should be advised to `hawk` the disc first for most rapid access
//this will result in a completely flattened CCD where everything comes right off the hard drive
//This might be an unwise decision for disc ID and miscellaneous purposes but it's best for gaming and stream-converting (hawking and hashing)
//http://www.staff.uni-mainz.de/tacke/scsi/SCSI2-14.html
//http://www.pctechguide.com/iso-9660-data-format-for-cds-cd-roms-cd-rs-and-cd-rws
//http://linux.die.net/man/1/cue2toc
@ -30,6 +39,9 @@ using System.Collections.Generic;
//http://www.cyberciti.biz/faq/getting-volume-information-from-cds-iso-images/
//http://www.cims.nyu.edu/cgi-systems/man.cgi?section=7I&topic=cdio
//some other docs
//http://www.emutalk.net/threads/54428-Reference-for-8-byte-sub-header-used-in-CDROM-XA references http://ccsun.nchu.edu.tw/~imtech/cou...act%20Disc.pdf which is pretty cool
//ideas:
/*
* do some stuff asynchronously. for example, decoding mp3 sectors.
@ -64,6 +76,7 @@ using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
public partial class Disc : IDisposable
{
/// <summary>
@ -73,11 +86,16 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// The raw TOC entries found in the lead-in track.
/// NOTE: it seems unlikey that we'll ever get these exactly.
/// The cd reader is supposed to read the multiple copies and pick the best-of-3 and turn them into a TOCRaw
/// So really this only needs to stick around so we can make the TOCRaw from it.
/// Not much of a different view, but.. different
/// </summary>
public List<RawTOCEntry> RawTOCEntries = new List<RawTOCEntry>();
/// <summary>
/// The DiscTOCRaw corresponding to the RawTOCEntries
/// The DiscTOCRaw corresponding to the RawTOCEntries.
/// Note: these should be retrieved differently, through a view accessor
/// </summary>
public DiscTOCRaw TOCRaw;
@ -87,162 +105,94 @@ namespace BizHawk.Emulation.DiscSystem
public DiscStructure Structure;
/// <summary>
/// The blobs mounted by this disc for supplying binary content
/// Disposable resources (blobs, mostly) referenced by this disc
/// </summary>
public List<IBlob> Blobs = new List<IBlob>();
internal List<IDisposable> DisposableResources = new List<IDisposable>();
/// <summary>
/// The sectors on the disc
/// </summary>
public List<SectorEntry> Sectors = new List<SectorEntry>();
internal SectorSynthParams SynthParams = new SectorSynthParams();
public Disc()
{
}
public void Dispose()
{
foreach (var blob in Blobs)
foreach (var res in DisposableResources)
{
blob.Dispose();
res.Dispose();
}
}
void FromIsoPathInternal(string isoPath)
/// <summary>
/// generates lead-out sectors according to very crude approximations
/// </summary>
public class SynthesizeLeadoutJob
{
//make a fake cue file to represent this iso file
const string isoCueWrapper = @"
FILE ""xarp.barp.marp.farp"" BINARY
TRACK 01 MODE1/2048
INDEX 01 00:00:00
";
public int Length;
public Disc Disc;
string cueDir = String.Empty;
var cue = new Cue();
CueFileResolver["xarp.barp.marp.farp"] = isoPath;
cue.LoadFromString(isoCueWrapper);
FromCueInternal(cue, cueDir, new CueBinPrefs());
}
public CueBin DumpCueBin(string baseName, CueBinPrefs prefs)
public void Run()
{
if (Structure.Sessions.Count > 1)
throw new NotSupportedException("can't dump cue+bin with more than 1 session yet");
//TODO: encode_mode2_form2_sector
var sz = new Sector_Zero();
CueBin ret = new CueBin();
ret.baseName = baseName;
ret.disc = this;
var leadoutTs = Disc.TOCRaw.LeadoutTimestamp;
var lastTrackTOCItem = Disc.TOCRaw.TOCItems[Disc.TOCRaw.LastRecordedTrackNumber]; //NOTE: in case LastRecordedTrackNumber is al ie, this will malfunction
if (!prefs.OneBlobPerTrack)
//leadout flags.. let's set them the same as the last track.
//THIS IS NOT EXACTLY THE SAME WAY MEDNAFEN DOES IT
EControlQ leadoutFlags = lastTrackTOCItem.Control;
//TODO - needs to be encoded as a certain mode (mode 2 form 2 for psx... i guess...)
for (int i = 0; i < Length; i++)
{
//this is the preferred mode of dumping things. we will always write full sectors.
string cue = new CUE_Format().GenerateCUE_OneBin(Structure,prefs);
var bfd = new CueBin.BinFileDescriptor {name = baseName + ".bin"};
ret.cue = string.Format("FILE \"{0}\" BINARY\n", bfd.name) + cue;
ret.bins.Add(bfd);
bfd.SectorSize = 2352;
var se = new SectorEntry(sz);
Disc.Sectors.Add(se);
SubchannelQ sq = new SubchannelQ();
//skip the mandatory track 1 pregap! cue+bin files do not contain it
for (int i = 150; i < Structure.LengthInSectors; i++)
{
bfd.abas.Add(i);
bfd.aba_zeros.Add(false);
int track_relative_msf = i;
sq.min = BCD2.FromDecimal(new Timestamp(track_relative_msf).MIN);
sq.sec = BCD2.FromDecimal(new Timestamp(track_relative_msf).SEC);
sq.frame = BCD2.FromDecimal(new Timestamp(track_relative_msf).FRAC);
int absolute_msf = i + leadoutTs.Sector;
sq.ap_min = BCD2.FromDecimal(new Timestamp(absolute_msf+150).MIN);
sq.ap_sec = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).SEC);
sq.ap_frame = BCD2.FromDecimal(new Timestamp(absolute_msf + 150).FRAC);
sq.q_tno.DecimalValue = 0xAA; //special value for leadout
sq.q_index.DecimalValue = 1;
byte ADR = 1;
sq.SetStatus(ADR, leadoutFlags);
var subcode = new BufferedSubcodeSector();
subcode.Synthesize_SubchannelQ(ref sq, true);
se.SubcodeSector = subcode;
}
}
else
{
//we build our own cue here (unlike above) because we need to build the cue and the output data at the same time
StringBuilder sbCue = new StringBuilder();
for (int i = 0; i < Structure.Sessions[0].Tracks.Count; i++)
{
var track = Structure.Sessions[0].Tracks[i];
var bfd = new CueBin.BinFileDescriptor
{
name = baseName + string.Format(" (Track {0:D2}).bin", track.Number),
SectorSize = Cue.BINSectorSizeForTrackType(track.TrackType)
};
ret.bins.Add(bfd);
int aba = 0;
//skip the mandatory track 1 pregap! cue+bin files do not contain it
if (i == 0) aba = 150;
for (; aba < track.LengthInSectors; aba++)
{
int thisaba = track.Indexes[0].aba + aba;
bfd.abas.Add(thisaba);
bfd.aba_zeros.Add(false);
}
sbCue.AppendFormat("FILE \"{0}\" BINARY\n", bfd.name);
sbCue.AppendFormat(" TRACK {0:D2} {1}\n", track.Number, Cue.TrackTypeStringForTrackType(track.TrackType));
foreach (var index in track.Indexes)
{
int x = index.aba - track.Indexes[0].aba;
if (index.Number == 0 && index.aba == track.Indexes[1].aba)
{
//dont emit index 0 when it is the same as index 1, it is illegal for some reason
}
//else if (i==0 && index.num == 0)
//{
// //don't generate the first index, it is illogical
//}
else
{
//track 1 included the lead-in at the beginning of it. sneak past that.
//if (i == 0) x -= 150;
sbCue.AppendFormat(" INDEX {0:D2} {1}\n", index.Number, new Timestamp(x).Value);
}
}
}
ret.cue = sbCue.ToString();
}
return ret;
}
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs)
{
var ret = new Disc();
ret.FromCuePathInternal(cuePath, prefs);
ret.Structure.Synthesize_TOCPointsFromSessions();
ret.Synthesize_SubcodeFromStructure();
ret.Synthesize_TOCRawFromStructure();
//try loading SBI. make sure its done after the subcode is synthesized!
string sbiPath = Path.ChangeExtension(cuePath, "sbi");
if (File.Exists(sbiPath) && SBI_Format.QuickCheckISSBI(sbiPath))
{
var sbi = new SBI_Format().LoadSBIPath(sbiPath);
ret.ApplySBI(sbi);
}
return ret;
}
public static Disc FromCCDPath(string ccdPath)
{
CCD_Format ccdLoader = new CCD_Format();
return ccdLoader.LoadCCDToDisc(ccdPath);
}
/// <summary>
/// THIS HASNT BEEN TESTED IN A LONG TIME. DOES IT WORK?
/// Automagically loads a disc, without any fine-tuned control at all
/// </summary>
public static Disc FromIsoPath(string isoPath)
public static Disc LoadAutomagic(string path)
{
var ret = new Disc();
ret.FromIsoPathInternal(isoPath);
ret.Structure.Synthesize_TOCPointsFromSessions();
ret.Synthesize_SubcodeFromStructure();
return ret;
var job = new DiscMountJob { IN_FromPath = path };
job.IN_DiscInterface = DiscInterface.MednaDisc; //TEST
job.Run();
return job.OUT_Disc;
}
/// <summary>
/// Synthesizes a crudely estimated TOCRaw from the disc structure.
/// </summary>
@ -270,7 +220,7 @@ FILE ""xarp.barp.marp.farp"" BINARY
/// <summary>
/// applies an SBI file to the disc
/// </summary>
public void ApplySBI(SBI_Format.SBIFile sbi)
public void ApplySBI(SBI.SubQPatchData sbi, bool asMednafen)
{
//save this, it's small, and we'll want it for disc processing a/b checks
Memos["sbi"] = sbi;
@ -289,8 +239,16 @@ FILE ""xarp.barp.marp.farp"" BINARY
if (patch == -1) continue;
else subcode[12 + j] = (byte)patch;
}
var bss = new BufferedSubcodeSector();
Sectors[aba].SubcodeSector = BufferedSubcodeSector.CloneFromBytesDeinterleaved(subcode);
var bss = BufferedSubcodeSector.CloneFromBytesDeinterleaved(subcode);
Sectors[aba].SubcodeSector = bss;
//not fully sure what the basis is for this, but here we go
if (asMednafen)
{
bss.Synthesize_SunchannelQ_Checksum();
bss.SubcodeDeinterleaved[12 + 10] ^= 0xFF;
bss.SubcodeDeinterleaved[12 + 11] ^= 0xFF;
}
}
}
@ -333,15 +291,15 @@ FILE ""xarp.barp.marp.farp"" BINARY
bool pause = true;
if (dp.Num != 0) //TODO - shouldnt this be IndexNum?
pause = false;
if ((dp.Control & EControlQ.DataUninterrupted)!=0)
if ((dp.Control & EControlQ.DATA)!=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;
sq.q_tno = BCD2.FromDecimal(dp.TrackNum);
sq.q_index = BCD2.FromDecimal(dp.IndexNum);
int track_relative_aba = aba - dp.Track.Indexes[1].aba;
track_relative_aba = Math.Abs(track_relative_aba);
@ -403,6 +361,11 @@ FILE ""xarp.barp.marp.farp"" BINARY
return new BCD2 {DecimalValue = d};
}
public static BCD2 FromBCD(byte b)
{
return new BCD2 { BCDValue = b };
}
public static int BCDToInt(byte n)
{
var bcd = new BCD2();
@ -416,45 +379,81 @@ FILE ""xarp.barp.marp.farp"" BINARY
int tens = Math.DivRem(n, 10, out ones);
return (byte)((tens << 4) | ones);
}
public override string ToString()
{
return BCDValue.ToString("X2");
}
}
/// <summary>
/// todo - rename to MSF? It can specify durations, so maybe it should be not suggestive of timestamp
/// TODO - can we maybe use BCD2 in here
/// </summary>
public struct Timestamp
{
/// <summary>
/// Checks if the string is a legit MSF. It's strict.
/// </summary>
public static bool IsMatch(string str)
{
return new Timestamp(str).Valid;
}
/// <summary>
/// creates a timestamp from a string in the form mm:ss:ff
/// </summary>
public Timestamp(string value)
public Timestamp(string str)
{
//TODO - could be performance-improved
MIN = int.Parse(value.Substring(0, 2));
SEC = int.Parse(value.Substring(3, 2));
FRAC = int.Parse(value.Substring(6, 2));
Sector = MIN * 60 * 75 + SEC * 75 + FRAC;
_value = null;
if (str.Length != 8) goto BOGUS;
if (str[0] < '0' || str[0] > '9') goto BOGUS;
if (str[1] < '0' || str[1] > '9') goto BOGUS;
if (str[2] != ':') goto BOGUS;
if (str[3] < '0' || str[3] > '9') goto BOGUS;
if (str[4] < '0' || str[4] > '9') goto BOGUS;
if (str[5] != ':') goto BOGUS;
if (str[6] < '0' || str[6] > '9') goto BOGUS;
if (str[7] < '0' || str[7] > '9') goto BOGUS;
MIN = (byte)((str[0] - '0') * 10 + (str[1] - '0'));
SEC = (byte)((str[3] - '0') * 10 + (str[4] - '0'));
FRAC = (byte)((str[6] - '0') * 10 + (str[7] - '0'));
Valid = true;
return;
BOGUS:
MIN = SEC = FRAC = 0;
Valid = false;
return;
}
public readonly int MIN, SEC, FRAC, Sector;
/// <summary>
/// The string representation of the MSF
/// </summary>
public string Value
{
get
{
if (_value != null) return _value;
return _value = string.Format("{0:D2}:{1:D2}:{2:D2}", MIN, SEC, FRAC);
if (!Valid) return "--:--:--";
return string.Format("{0:D2}:{1:D2}:{2:D2}", MIN, SEC, FRAC);
}
}
string _value;
public readonly byte MIN, SEC, FRAC;
public readonly bool Valid;
/// <summary>
/// creates timestamp from supplies MSF
/// The fully multiplied out flat-address Sector number
/// </summary>
public int Sector { get { return MIN * 60 * 75 + SEC * 75 + FRAC; } }
/// <summary>
/// creates timestamp from the supplied MSF
/// </summary>
public Timestamp(int m, int s, int f)
{
MIN = m;
SEC = s;
FRAC = f;
Sector = MIN * 60 * 75 + SEC * 75 + FRAC;
_value = null;
MIN = (byte)m;
SEC = (byte)s;
FRAC = (byte)f;
Valid = true;
}
/// <summary>
@ -462,255 +461,23 @@ FILE ""xarp.barp.marp.farp"" BINARY
/// </summary>
public Timestamp(int SectorNumber)
{
this.Sector = SectorNumber;
MIN = SectorNumber / (60 * 75);
SEC = (SectorNumber / 75) % 60;
FRAC = SectorNumber % 75;
_value = null;
}
MIN = (byte)(SectorNumber / (60 * 75));
SEC = (byte)((SectorNumber / 75) % 60);
FRAC = (byte)(SectorNumber % 75);
Valid = true;
}
/// <summary>
/// The type of a Track, not strictly (for now) adhering to the realistic values, but also including information for ourselves about what source the data came from.
/// We should make that not the case.
/// TODO - let CUE have its own "track type" enum, since cue tracktypes arent strictly corresponding to "real" track types, whatever those are.
/// </summary>
public enum ETrackType
public override string ToString()
{
/// <summary>
/// The track type isn't always known.. it can take this value til its populated
/// </summary>
Unknown,
/// <summary>
/// CD-ROM (yellowbook) specification - it is a Mode1 track, and we have all 2352 bytes for the sector
/// </summary>
Mode1_2352,
/// <summary>
/// CD-ROM (yellowbook) specification - it is a Mode1 track, but originally we only had 2048 bytes for the sector.
/// This means, for various purposes, we need to synthesize additional data
/// </summary>
Mode1_2048,
/// <summary>
/// CD-ROM (yellowbook) specification - it is a Mode2 track.
/// </summary>
Mode2_2352,
/// <summary>
/// CD-DA (redbook) specification.. nominally. In fact, it's just 2352 raw PCM bytes per sector, and that concept isnt directly spelled out in redbook.
/// </summary>
Audio
return Value;
}
}
/// <summary>
/// TODO - this is garbage. It's half input related, and half output related. This needs to be split up.
/// </summary>
public class CueBinPrefs
//not being used yet
class DiscPreferences
{
/// <summary>
/// Controls general operations: should the output be split into several blobs, or just use one?
/// </summary>
public bool OneBlobPerTrack;
/// <summary>
/// NOT SUPPORTED YET (just here as a reminder) If choosing OneBinPerTrack, you may wish to write wave files for audio tracks.
/// </summary>
//public bool DumpWaveFiles;
/// <summary>
/// turn this on to dump bins instead of just cues
/// </summary>
public bool ReallyDumpBin;
/// <summary>
/// Dump bins to bitbucket instead of disk
/// </summary>
public bool DumpToBitbucket;
/// <summary>
/// dump a .sub.q along with bins. one day we'll want to dump the entire subcode but really Q is all thats important for debugging most things
/// </summary>
public bool DumpSubchannelQ;
/// <summary>
/// generate remarks and other annotations to help humans understand whats going on, but which will confuse many cue parsers
/// </summary>
public bool AnnotateCue;
/// <summary>
/// EVIL: in theory this would attempt to generate pregap commands to save disc space, but I think this is a bad idea.
/// it would also be useful for OneBinPerTrack mode in making wave files.
/// HOWEVER - by the time we've loaded things up into our canonical format, we don't know which 'pregaps' are safe for turning back into pregaps
/// Because they might sometimes contain data (gapless audio discs). So we would have to inspect a series of sectors to look for silence.
/// And even still, the ECC information might be important. So, forget it.
/// NEVER USE OR IMPLEMENT THIS
/// </summary>
//public bool PreferPregapCommand = false;
/// <summary>
/// some cue parsers cant handle sessions. better not emit a session command then. multi-session discs will then be broken
/// </summary>
public bool SingleSession;
/// <summary>
/// enables various extension-aware behaviours.
/// enables auto-search for files with the same name but differing extension.
/// enables auto-detection of situations where cue blobfiles are indicating the wrong type in the cuefile
/// </summary>
public bool ExtensionAware = false;
/// <summary>
/// whenever we have a choice, use case sensitivity in searching for files
/// </summary>
public bool CaseSensitive = false;
/// <summary>
/// DO NOT CHANGE THIS! All sectors will be written with ECM data. It's a waste of space, but it is exact. (not completely supported yet)
/// </summary>
public bool DumpECM = true;
}
/// <summary>
/// Encapsulates an in-memory cue+bin (complete cuesheet and a little registry of files)
/// it will be based on a disc (fro mwhich it can read sectors to avoid burning through extra memory)
/// TODO - we must merge this with whatever reads in cue+bin
/// </summary>
public class CueBin
{
public string cue;
public string baseName;
public Disc disc;
public class BinFileDescriptor
{
public string name;
public List<int> abas = new List<int>();
//todo - do we really need this? i dont think so...
public List<bool> aba_zeros = new List<bool>();
public int SectorSize;
}
public List<BinFileDescriptor> bins = new List<BinFileDescriptor>();
//NOT SUPPORTED RIGHT NOW
//public string CreateRedumpReport()
//{
// if (disc.TOC.Sessions[0].Tracks.Count != bins.Count)
// throw new InvalidOperationException("Cannot generate redump report on CueBin lacking OneBinPerTrack property");
// StringBuilder sb = new StringBuilder();
// for (int i = 0; i < disc.TOC.Sessions[0].Tracks.Count; i++)
// {
// var track = disc.TOC.Sessions[0].Tracks[i];
// var bfd = bins[i];
// //dump the track
// byte[] dump = new byte[track.length_aba * 2352];
// //TODO ????????? post-ABA unknown
// //for (int aba = 0; aba < track.length_aba; aba++)
// // disc.ReadLBA_2352(bfd.lbas[lba],dump,lba*2352);
// string crc32 = string.Format("{0:X8}", CRC32.Calculate(dump));
// string md5 = Util.Hash_MD5(dump, 0, dump.Length);
// string sha1 = Util.Hash_SHA1(dump, 0, dump.Length);
// int pregap = track.Indexes[1].lba - track.Indexes[0].lba;
// Timestamp pregap_ts = new Timestamp(pregap);
// Timestamp len_ts = new Timestamp(track.length_lba);
// sb.AppendFormat("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}\n",
// i,
// Cue.RedumpTypeStringForTrackType(track.TrackType),
// pregap_ts.Value,
// len_ts.Value,
// track.length_lba,
// track.length_lba*Cue.BINSectorSizeForTrackType(track.TrackType),
// crc32,
// md5,
// sha1
// );
// }
// return sb.ToString();
//}
public void Dump(string directory, CueBinPrefs prefs)
{
ProgressReport pr = new ProgressReport();
Dump(directory, prefs, pr);
}
public void Dump(string directory, CueBinPrefs prefs, ProgressReport progress)
{
byte[] subcodeTemp = new byte[96];
progress.TaskCount = 2;
progress.Message = "Generating Cue";
progress.ProgressEstimate = 1;
progress.ProgressCurrent = 0;
progress.InfoPresent = true;
string cuePath = Path.Combine(directory, baseName + ".cue");
if (prefs.DumpToBitbucket) { }
else File.WriteAllText(cuePath, cue);
progress.Message = "Writing bin(s)";
progress.TaskCurrent = 1;
progress.ProgressEstimate = bins.Sum(bfd => bfd.abas.Count);
progress.ProgressCurrent = 0;
if(!prefs.ReallyDumpBin) return;
foreach (var bfd in bins)
{
int sectorSize = bfd.SectorSize;
byte[] temp = new byte[2352];
byte[] empty = new byte[2352];
string trackBinFile = bfd.name;
string trackBinPath = Path.Combine(directory, trackBinFile);
string subQPath = Path.ChangeExtension(trackBinPath, ".sub.q");
Stream fsSubQ = null;
Stream fs;
if(prefs.DumpToBitbucket)
fs = Stream.Null;
else fs = new FileStream(trackBinPath, FileMode.Create, FileAccess.Write, FileShare.None);
try
{
if (prefs.DumpSubchannelQ)
if (prefs.DumpToBitbucket)
fsSubQ = Stream.Null;
else fsSubQ = new FileStream(subQPath, FileMode.Create, FileAccess.Write, FileShare.None);
for (int i = 0; i < bfd.abas.Count; i++)
{
if (progress.CancelSignal) return;
progress.ProgressCurrent++;
int aba = bfd.abas[i];
if (bfd.aba_zeros[i])
{
fs.Write(empty, 0, sectorSize);
}
else
{
if (sectorSize == 2352)
disc.ReadABA_2352(aba, temp, 0);
else if (sectorSize == 2048) disc.ReadABA_2048(aba, temp, 0);
else throw new InvalidOperationException();
fs.Write(temp, 0, sectorSize);
//write subQ if necessary
if (fsSubQ != null)
{
disc.Sectors[aba].SubcodeSector.ReadSubcodeDeinterleaved(subcodeTemp, 0);
fsSubQ.Write(subcodeTemp, 12, 12);
}
}
}
}
finally
{
fs.Dispose();
if (fsSubQ != null) fsSubQ.Dispose();
}
}
}
}
}

View File

@ -7,7 +7,7 @@ using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Represents a TOC entry discovered in the Q subchannel data of the lead-in track.
/// Represents a TOC entry discovered in the Q subchannel data of the lead-in track by the reader. These are stored redundantly.
/// It isn't clear whether we need anything other than the SubchannelQ data, so I abstracted this in case we need it.
/// </summary>
public class RawTOCEntry
@ -15,6 +15,66 @@ namespace BizHawk.Emulation.DiscSystem
public SubchannelQ QData;
}
public enum DiscInterface
{
BizHawk, MednaDisc, LibMirage
}
/// <summary>
/// Synthesizes RawTCOEntry A0 A1 A2 from the provided information
/// TODO - dont these need to have timestamps too?
/// </summary>
public class Synthesize_A0A1A2_Job
{
//heres a thing
//https://books.google.com/books?id=caF_AAAAQBAJ&lpg=PA124&ots=OA9Ttj9CHZ&dq=disc%20TOC%20point%20A2&pg=PA124
public int FirstRecordedTrackNumber;
public int LastRecordedTrackNumber;
public int LeadoutLBA;
/// <summary>
/// TODO - this hasnt been set for CCD, has it?
/// </summary>
public DiscTOCRaw.SessionFormat Session1Format;
/// <summary>
/// Appends the new entries to the provided list
/// </summary>
public void Run(List<RawTOCEntry> entries)
{
SubchannelQ sq = new SubchannelQ();
//first recorded track number:
sq.q_index.DecimalValue = 0xA0;
sq.ap_min.DecimalValue = FirstRecordedTrackNumber;
switch(Session1Format)
{
case DiscTOCRaw.SessionFormat.Type00_CDROM_CDDA: sq.ap_sec.DecimalValue = 0x00; break;
case DiscTOCRaw.SessionFormat.Type10_CDI: sq.ap_sec.DecimalValue = 0x10; break;
case DiscTOCRaw.SessionFormat.Type20_CDXA: sq.ap_sec.DecimalValue = 0x20; break;
default: throw new InvalidOperationException("Invalid Session1Format");
}
sq.ap_frame.DecimalValue = 0;
entries.Add(new RawTOCEntry { QData = sq });
//last recorded track number:
sq.q_index.DecimalValue = 0xA1;
sq.ap_min.DecimalValue = LastRecordedTrackNumber;
sq.ap_sec.DecimalValue = 0;
sq.ap_frame.DecimalValue = 0;
entries.Add(new RawTOCEntry { QData = sq });
//leadout:
sq.q_index.DecimalValue = 0xA2;
sq.AP_Timestamp = new Timestamp(LeadoutLBA);
entries.Add(new RawTOCEntry { QData = sq });
}
}
/// <summary>
/// Main unit of organization for reading data from the disc. Represents one physical disc sector.
/// </summary>
@ -22,6 +82,8 @@ namespace BizHawk.Emulation.DiscSystem
{
public SectorEntry(ISector sec) { Sector = sec; }
internal ISectorSynthJob2448 SectorSynth;
/// <summary>
/// Access the --whatsitcalled-- normal data for the sector with this
/// </summary>

View File

@ -27,6 +27,7 @@
//it turns out mame's cdrom.c uses pretty much this exact thing, naming the tables mul2tab->ecclow and div3tab->ecchigh
//Corlett's ECM uses our same fundamental approach as well.
//I can't figure out what winUAE is doing.
//Martin writes these algorithms in his own inimitable format. We should try transcribing them here for testing.
using BizHawk.Common;

View File

@ -0,0 +1,171 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
partial class DiscMountJob
{
class SS_MednaDisc : ISectorSynthJob2448
{
public void Synth(SectorSynthJob job)
{
//mednafen is always synthesizing everything, no need to worry about flags.. mostly./
job.Params.MednaDisc.Read_2442(job.LBA, job.DestBuffer2448, job.DestOffset);
//we may still need to deinterleave it if subcode was requested and it needs deinterleaving
if ((job.Parts & (ESectorSynthPart.SubcodeDeinterleave | ESectorSynthPart.SubcodeAny)) != 0)
{
SubcodeUtils.DeinterleaveInplace(job.DestBuffer2448, 2352);
}
}
}
void RunMednaDisc()
{
var disc = new 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();
//make sector interfaces:
//1. right now for dumb reasons we have 150 lead-in sectors. I want to get rid of this crap.
var leadin_sector_zero = new Sector_Zero();
var leadin_subcode_zero = new ZeroSubcodeSector();
for (int i = 0; i < 150; i++)
{
var se = new SectorEntry(leadin_sector_zero);
disc.Sectors.Add(se);
se.SubcodeSector = leadin_subcode_zero;
}
//2. actual sectors
for (int i = 0; i < nSectors; i++)
{
//var sectorInterface = new MednaDiscSectorInterface() { LBA = i, md = md };
var se = new SectorEntry(null);
//se.SubcodeSector = new MednaDiscSubcodeSectorInterface() { LBA = i, md = md };
se.SectorSynth = synth;
disc.Sectors.Add(se);
}
BufferedSubcodeSector bss = new BufferedSubcodeSector(); //TODO - its hacky that we need this..
//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 = 0, //unknown with mednadisc
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),
};
//a special fixup: mednafen's entry 100 is the lead-out track
if (i == 100)
q.q_index.BCDValue = 0xA2;
//CRC cant be calculated til we've got all the fields setup
q.q_crc = bss.Synthesize_SubchannelQ(ref q, true);
disc.RawTOCEntries.Add(new RawTOCEntry { QData = q });
}
//synth A1 and A2 entries instead of setting FirstRecordedTrackNumber and LastRecordedTrackNumber below
var qA1 = new SubchannelQ
{
q_status = 0, //unknown with mednadisc
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(0),
ap_frame = BCD2.FromDecimal(0),
};
qA1.q_crc = bss.Synthesize_SubchannelQ(ref qA1, true);
disc.RawTOCEntries.Add(new RawTOCEntry { QData = qA1 });
var qA2 = new SubchannelQ
{
q_status = 0, //unknown with mednadisc
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),
};
qA2.q_crc = bss.Synthesize_SubchannelQ(ref qA2, true);
disc.RawTOCEntries.Add(new RawTOCEntry { QData = qA2 });
//generate the toc from the entries. still not sure we're liking this idea
var tocSynth = new DiscTOCRaw.SynthesizeFromRawTOCEntriesJob() { Entries = disc.RawTOCEntries };
tocSynth.Run();
disc.TOCRaw = tocSynth.Result;
//setup the DiscStructure
disc.Structure = new DiscStructure();
var ses = new DiscStructure.Session();
disc.Structure.Sessions.Add(ses);
for (int i = 1; i < 100; i++)
{
var m_te = md.TOCTracks[i];
if (!m_te.Valid) continue;
DiscStructure.Track track = new DiscStructure.Track() { Number = i };
ses.Tracks.Add(track);
if ((m_te.control & (int)EControlQ.DATA) == 0)
track.TrackType = DiscStructure.ETrackType.Audio;
else
track.TrackType = DiscStructure.ETrackType.Data;
//from mednafen, we couldnt build the index 0, and that's OK, since that's not really a sensible thing in CD terms anyway.
//I need to refactor this thing to oblivion
track.Indexes.Add(new DiscStructure.Index { Number = 0, LBA = (int)m_te.lba }); //<-- not accurate, but due for deletion
track.Indexes.Add(new DiscStructure.Index { Number = 1, LBA = (int)m_te.lba });
}
//NOT FULLY COMPLETE
OUT_Disc = disc;
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
public partial class DiscMountJob : LoggedJob
{
/// <summary>
/// The filename to be loaded
/// </summary>
public string IN_FromPath;
/// <summary>
/// Slow-loading cues won't finish loading if this threshold is exceeded.
/// Set to 10 to always load a cue
/// </summary>
public int IN_SlowLoadAbortThreshold = 10;
/// <summary>
/// The interface to be used for loading the disc.
/// Usually you'll want DiscInterface.BizHawk, but others can be used for A/B testing
/// </summary>
public DiscInterface IN_DiscInterface = DiscInterface.BizHawk;
/// <summary>
/// The resulting disc
/// </summary>
public Disc OUT_Disc;
public void Run()
{
switch (IN_DiscInterface)
{
case DiscInterface.LibMirage:
throw new NotSupportedException("LibMirage not supported yet");
case DiscInterface.BizHawk:
RunBizHawk();
break;
case DiscInterface.MednaDisc:
RunMednaDisc();
break;
}
FinishLog();
}
void RunBizHawk()
{
string infile = IN_FromPath;
string cue_content = null;
var cfr = new CUE_Format2.CueFileResolver();
RERUN:
var ext = Path.GetExtension(infile).ToLowerInvariant();
if (ext == ".iso")
{
//make a fake cue file to represent this iso file and rerun it as a cue
string filebase = Path.GetFileName(infile);
cue_content = string.Format(@"
FILE ""{0}"" BINARY
TRACK 01 MODE1/2048
INDEX 01 00:00:00",
filebase);
infile = Path.ChangeExtension(infile, ".cue");
goto RERUN;
}
if (ext == ".cue")
{
var cuePath = IN_FromPath;
var cue2 = new CUE_Format2();
cue2.Resolver = cfr;
if (!cfr.IsHardcodedResolve) cfr.SetBaseDirectory(Path.GetDirectoryName(infile));
//parse the cue file
var parseJob = new CUE_Format2.ParseCueJob();
if (cue_content == null)
cue_content = File.ReadAllText(cuePath);
parseJob.IN_CueString = cue_content;
cue2.ParseCueFile(parseJob);
if (parseJob.OUT_Log != "") Console.WriteLine(parseJob.OUT_Log);
ConcatenateJobLog(parseJob);
//resolve required bin files and find out what it's gonna take to load the cue
var analyzeJob = new CUE_Format2.AnalyzeCueJob();
analyzeJob.IN_CueFile = parseJob.OUT_CueFile;
cue2.AnalyzeCueFile(analyzeJob);
if (analyzeJob.OUT_Log != "") Console.WriteLine(analyzeJob.OUT_Log);
ConcatenateJobLog(analyzeJob);
//check slow loading threshold
if (analyzeJob.OUT_LoadTime >= IN_SlowLoadAbortThreshold)
{
Warn("Loading terminated due to slow load threshold");
goto DONE;
}
//actually load it all up
var loadJob = new CUE_Format2.LoadCueJob();
loadJob.IN_AnalyzeJob = analyzeJob;
cue2.LoadCueFile(loadJob);
if (loadJob.OUT_Log != "") Console.WriteLine(loadJob.OUT_Log);
ConcatenateJobLog(analyzeJob);
OUT_Disc = loadJob.OUT_Disc;
//apply SBI if it exists (TODO - for formats other than cue?)
var sbiPath = Path.ChangeExtension(IN_FromPath, ".sbi");
if (File.Exists(sbiPath) && SBI.SBIFormat.QuickCheckISSBI(sbiPath))
{
var sbiJob = new SBI.LoadSBIJob();
sbiJob.IN_Path = sbiPath;
sbiJob.Run();
OUT_Disc.ApplySBI(sbiJob.OUT_Data, true);
}
}
else if (ext == ".ccd")
{
CCD_Format ccdLoader = new CCD_Format();
OUT_Disc = ccdLoader.LoadCCDToDisc(IN_FromPath);
}
DONE:
;
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
using BizHawk.Common.IOExtensions;
namespace BizHawk.Emulation.DiscSystem.SBI
{
/// <summary>
/// Loads SBI files into an internal representation.
/// </summary>
public class LoadSBIJob : LoggedJob
{
/// <summary>
/// The file to be loaded
/// </summary>
public string IN_Path;
/// <summary>
/// The resulting interpreted data
/// </summary>
public SubQPatchData OUT_Data;
public void Run()
{
using (var fs = File.OpenRead(IN_Path))
{
BinaryReader br = new BinaryReader(fs);
string sig = br.ReadStringFixedAscii(4);
if (sig != "SBI\0")
throw new SBIParseException("Missing magic number");
SubQPatchData ret = new SubQPatchData();
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();
OUT_Data = ret;
}
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Returns ERRORS for things which will can't be processed any further. Processing of this job will continue though to try to collect the maximum amount of feedback.
/// Returns WARNINGS for things which will are irregular or erroneous but later jobs might be able to handle, or which can be worked around by configuration assumptions.
/// TODO - make IDisposable so I don't have to remember to Finish() it?
/// </summary>
public class LoggedJob
{
internal int CurrentLine = -1;
internal StringWriter swLog = new StringWriter();
internal void Warn(string format, params object[] args) { Log("WARN ", format, args); }
internal void Error(string format, params object[] args) { OUT_ErrorLevel = true; Log("ERROR", format, args); }
internal void Log(string level, string format, params object[] args)
{
var msg = string.Format(format, args);
if (CurrentLine == -1)
swLog.WriteLine("{0}: {1}", level, msg);
else
swLog.WriteLine("[{0,3}] {1}: {2}", CurrentLine, level, msg);
}
/// <summary>
/// Whether there were any errors
/// </summary>
public bool OUT_ErrorLevel = false;
/// <summary>
/// output: log transcript of the job
/// </summary>
public string OUT_Log;
/// <summary>
/// Finishes logging. Flushes the output and closes the logging mechanism
/// </summary>
internal void FinishLog()
{
OUT_Log = swLog.ToString();
swLog.Close();
}
/// <summary>
/// Concatenates the log results from the provided job into this log
/// </summary>
public void ConcatenateJobLog(LoggedJob job)
{
OUT_ErrorLevel |= job.OUT_ErrorLevel;
swLog.Write(job.OUT_Log);
}
}
}

View File

@ -0,0 +1,164 @@
using System;
using System.Text;
using System.IO;
using System.Globalization;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// A resource representing a disc opened and mounted through the MednaDisc component.
/// Does not attempt to virtually present the disc as a BizHawk disc - that will be the
/// responsibility of the user of this code
/// </summary>
public unsafe class MednaDisc : IDisposable
{
public MednaDisc(string pathToDisc)
{
if (!IsLibraryAvailable)
throw new InvalidOperationException("MednaDisc library is not available!");
handle = mednadisc_LoadCD(pathToDisc);
if (handle == IntPtr.Zero)
throw new InvalidOperationException("Failed to load MednaDisc: " + pathToDisc);
//read the mednafen toc
TOCTracks = new MednadiscTOCTrack[101];
fixed (MednadiscTOCTrack* _tracks = &TOCTracks[0])
fixed(MednadiscTOC* _toc = &TOC)
mednadisc_ReadTOC(handle, _toc, _tracks);
//leave the disc open until this is disposed so we can read sectors from it
}
IntPtr handle;
public MednadiscTOC TOC;
public MednadiscTOCTrack[] TOCTracks;
[ThreadStatic] static byte[] buf2442 = new byte[2448];
[ThreadStatic] static byte[] buf96 = new byte[96];
public void Read_2442(int LBA, byte[] buffer, int offset)
{
//read directly into the target buffer
fixed(byte* pBuffer = &buffer[0])
mednadisc_ReadSector(handle, LBA, pBuffer + offset);
}
//public void ReadSubcodeDeinterleaved(int LBA, byte[] buffer, int offset)
//{
// fixed (byte* pBuffer = buf2442)
// mednadisc_ReadSector(handle, LBA, pBuffer);
// SubcodeUtils.Deinterleave(buf2442, 2352, buffer, offset);
//}
//public void ReadSubcodeChannel(int LBA, int number, byte[] buffer, int offset)
//{
// fixed (byte* pBuffer = buf2442)
// mednadisc_ReadSector(handle, LBA, pBuffer);
// SubcodeUtils.Deinterleave(buf2442, 2352, buf96, 0);
// for (int i = 0; i < 12; i++)
// buffer[offset + i] = buf96[number * 12 + i];
//}
//public void Read_2352(int LBA, byte[] buffer, int offset)
//{
// fixed (byte* pBuffer = buf2442)
// mednadisc_ReadSector(handle, LBA, pBuffer);
// Buffer.BlockCopy(buf2442, 0, buffer, offset, 2352);
//}
//public void Read_2048(int LBA, byte[] buffer, int offset)
//{
// //this depends on CD-XA mode and such. so we need to read the mode bytes
// //HEY!!!!!! SHOULD THIS BE DONE BASED ON THE CLAIMED TRACK TYPE, OR ON WHATS IN THE SECTOR?
// //this is kind of a function of the CD reader.. it's not clear how this function should work.
// //YIKES!!!!!!!!!!!!!!
// //well, we need to scrutinize it for CCD files anyway, so...
// //this sucks.
// fixed (byte* pBuffer = buf2442)
// mednadisc_ReadSector(handle, LBA, pBuffer);
// byte mode = buf2442[15];
// if (mode == 1)
// Buffer.BlockCopy(buf2442, 16, buffer, offset, 2048);
// else
// Buffer.BlockCopy(buf2442, 24, buffer, offset, 2048); //PSX assumptions about CD-XA.. BAD BAD BAD
//}
static void CheckLibrary()
{
IntPtr lib = LoadLibrary("mednadisc.dll");
if (lib == IntPtr.Zero)
{
_IsLibraryAvailable = false;
return;
}
IntPtr addr = GetProcAddress(lib, "mednadisc_LoadCD");
FreeLibrary(lib);
if (addr == IntPtr.Zero)
{
_IsLibraryAvailable = false;
}
_IsLibraryAvailable = true;
}
static MednaDisc()
{
CheckLibrary();
}
static bool _IsLibraryAvailable;
public static bool IsLibraryAvailable { get { return _IsLibraryAvailable; } }
public void Dispose()
{
if(handle == IntPtr.Zero) return;
mednadisc_CloseCD(handle);
}
[StructLayout(LayoutKind.Sequential)]
public struct MednadiscTOC
{
public byte first_track;
public byte last_track;
public byte disc_type;
};
[StructLayout(LayoutKind.Explicit)]
public struct MednadiscTOCTrack
{
[FieldOffset(0)] public byte adr;
[FieldOffset(1)] public byte control;
[FieldOffset(4)] public uint lba;
//can't be a bool due to marshalling...
[FieldOffset(8)] public byte _validByte;
public bool Valid { get { return _validByte != 0; } }
};
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string dllToLoad);
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
[DllImport("kernel32.dll")]
static extern bool FreeLibrary(IntPtr hModule);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr mednadisc_LoadCD(string path);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int mednadisc_ReadSector(IntPtr disc, int lba, byte* buf2448);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void mednadisc_CloseCD(IntPtr disc);
[DllImport("mednadisc.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void mednadisc_ReadTOC(IntPtr disc, MednadiscTOC* toc, MednadiscTOCTrack* tracks101);
}
}

View File

@ -5,16 +5,17 @@ using System.Collections.Generic;
using BizHawk.Common.IOExtensions;
namespace BizHawk.Emulation.DiscSystem
{
public class SBI_Format
namespace BizHawk.Emulation.DiscSystem.SBI
{
public class SBIParseException : Exception
{
public SBIParseException(string message) : base(message) { }
}
public class SBIFile
/// <summary>
/// The interpreted contents of an SBI file
/// </summary>
public class SubQPatchData
{
/// <summary>
/// a list of patched ABAs
@ -27,6 +28,8 @@ namespace BizHawk.Emulation.DiscSystem
public short[] subq;
}
public static class SBIFormat
{
/// <summary>
/// Does a cursory check to see if the file looks like an SBI
/// </summary>
@ -41,64 +44,7 @@ namespace BizHawk.Emulation.DiscSystem
}
return true;
}
/// <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;
}
}
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
//TODO - most of these sector interfaces are really only useful for CUEs, I guess. most other disc formats arent nearly as lame.. I think
namespace BizHawk.Emulation.DiscSystem
{
public interface ISector
@ -13,13 +16,125 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// reads 2048 bytes of userdata.. precisely what this means isnt always 100% certain (for instance mode2 form 0 has 2336 bytes of userdata instead of 2048)..
/// ..but its certain enough for this to be useful
/// THIS IS SO ANNOYING!!!! UGH!!!!!!!!!
/// </summary>
int Read_2048(byte[] buffer, int offset);
}
/// <summary>
/// Indicates which part of a sector are needing to be synthesized.
/// Sector synthesis may create too much data, but this is a hint as to what's needed
/// </summary>
[Flags] enum ESectorSynthPart
{
/// <summary>
/// The header is required
/// </summary>
Header16 = 1,
/// <summary>
/// The main 2048 user data bytes are required
/// </summary>
User2048 = 2,
/// <summary>
/// The 276 bytes of error correction are required
/// </summary>
ECC276 = 4,
/// <summary>
/// The 12 bytes preceding the ECC section are required (usually EDC and zero but also userdata sometimes)
/// </summary>
EDC12 = 8,
/// <summary>
/// A mode2 userdata section is required: the main 2048 user bytes AND the ECC and EDC areas
/// </summary>
User2336 = (User2048|ECC276|EDC12),
/// <summary>
/// The entirety of the sector userdata is required
/// </summary>
User2352 = 15,
/// <summary>
/// SubP is required
/// </summary>
SubchannelP = 16,
/// <summary>
/// SubQ is required
/// </summary>
SubchannelQ = 32,
/// <summary>
/// Complete subcode is required
/// </summary>
SubcodeComplete = (16|32|64|128|256|512|1024|2048),
/// <summary>
/// Any of the subcode might be required (just another way of writing SubcodeComplete)
/// </summary>
SubcodeAny = SubcodeComplete,
/// <summary>
/// The subcode should be deinterleaved
/// </summary>
SubcodeDeinterleave = 4096,
/// <summary>
/// The 100% complete sector is required including 2352 bytes of userdata and 96 bytes of subcode
/// </summary>
Complete2448 = SubcodeComplete | User2352,
}
interface ISectorSynthJob2448
{
void Synth(SectorSynthJob job);
}
/// <summary>
/// Not a proper job? maybe with additional flags, it could be
/// </summary>
class SectorSynthJob
{
public int LBA;
public ESectorSynthPart Parts;
public byte[] DestBuffer2448;
public int DestOffset;
public SectorSynthParams Params;
public Disc Disc;
}
/// <summary>
/// Generic parameters for sector synthesis.
/// To cut down on resource utilization, these can be stored in a disc and are tightly coupled to
/// the SectorSynths that have been setup for it
/// </summary>
struct SectorSynthParams
{
public long[] BlobOffsets;
public MednaDisc MednaDisc;
}
class SS_Multi : ISectorSynthJob2448
{
public List<ISectorSynthJob2448> Agenda = new List<ISectorSynthJob2448>();
public void Synth(SectorSynthJob job)
{
foreach (var a in Agenda)
{
a.Synth(job);
}
}
}
/// <summary>
/// this ISector is dumb and only knows how to drag chunks off a source blob
/// TODO - actually make it that way!
/// </summary>
public class Sector_RawBlob : ISector
{
@ -37,17 +152,20 @@ namespace BizHawk.Emulation.DiscSystem
//YIKES!!!!!!!!!!!!!!
//well, we need to scrutinize it for CCD files anyway, so...
//this sucks.
Blob.Read(Offset + 16, buffer, 0, 1);
//read mode byte, use that to determine what kind of sector this is
Blob.Read(Offset + 15, buffer, 0, 1);
byte mode = buffer[0];
if(mode == 1)
return Blob.Read(Offset + 16, buffer, offset, 2048);
else
return Blob.Read(Offset + 24, buffer, offset, 2048);
return Blob.Read(Offset + 24, buffer, offset, 2048); //PSX assumptions about CD-XA.. BAD BAD BAD
}
}
/// <summary>
/// this ISector always returns zeroes
/// (not even SYNC stuff is set.... pretty bogus and useless, this)
/// </summary>
class Sector_Zero : ISector
{
@ -119,6 +237,7 @@ namespace BizHawk.Emulation.DiscSystem
}
//a blob that also has an ECM cache associated with it. maybe one day.
//UHHH this is kind of redundant right now... see how Sector_Mode1_2048 manages its own cache
class ECMCacheBlob
{
public ECMCacheBlob(IBlob blob)
@ -129,7 +248,7 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <summary>
/// this ISector is a MODE1 sector that is generating itself from an underlying MODE1/2048 userdata piece
/// transforms Mode1/2048 -> Mode1/2352
/// </summary>
class Sector_Mode1_2048 : ISector
{
@ -197,7 +316,7 @@ namespace BizHawk.Emulation.DiscSystem
//if we read the 2048 physical bytes OK, then return the complete sector
if (read == 2048)
{
extra_data = new byte[16 + 4 + 8 + 172 + 104];
extra_data = new byte[16 + 4 + 8 + 172 + 104]; //aka 2048
Buffer.BlockCopy(buffer, 0, extra_data, 0, 16);
Buffer.BlockCopy(buffer, 2064, extra_data, 16, 4 + 8 + 172 + 104);
has_extra_data = true;

View File

@ -1,10 +1,23 @@
using System;
//TODO - call on unmanaged code in mednadisc if available to do deinterleaving faster. be sure to benchmark it though..
//a decent little subcode reference
//http://www.jbum.com/cdg_revealed.html
//NOTES: the 'subchannel Q' stuff here has a lot to do with the q-Mode 1. q-Mode 2 is different,
//and q-Mode 1 technically is defined a little differently in the lead-in area, although the fields align so the datastructures can be reused
//Q subchannel basic structure: (quick ref: https://en.wikipedia.org/wiki/Compact_Disc_subcode)
//Byte 1: (aka `status`)
// q-Control: 4 bits (i.e. flags)
// q-Mode: 4 bits (aka ADR; WHY is this called ADR?)
//q-Data: other stuff depending on q-Mode and type of track
//q-CRC: CRC of preceding
namespace BizHawk.Emulation.DiscSystem
{
//YET ANOTHER BAD IDEA
public interface ISubcodeSector
{
/// <summary>
@ -20,6 +33,10 @@ namespace BizHawk.Emulation.DiscSystem
public static class SubcodeUtils
{
/// <summary>
/// Converts the useful (but unrealistic) deinterleaved data into the useless (but realistic) interleaved subchannel format.
/// in_buf and out_buf should not overlap
/// </summary>
public static void Interleave(byte[] in_buf, int in_buf_index, byte[] out_buf, int out_buf_index)
{
for (int d = 0; d < 12; d++)
@ -37,6 +54,10 @@ namespace BizHawk.Emulation.DiscSystem
}
}
/// <summary>
/// Converts the useless (but realistic) interleaved subchannel data into a useful (but unrealistic) deinterleaved format.
/// in_buf and out_buf should not overlap
/// </summary>
public static void Deinterleave(byte[] in_buf, int in_buf_index, byte[] out_buf, int out_buf_index)
{
for (int i = 0; i < 96; i++)
@ -49,11 +70,30 @@ namespace BizHawk.Emulation.DiscSystem
out_buf[(ch * 12) + (i >> 3) + out_buf_index] |= (byte)(((in_buf[i + in_buf_index] >> (7 - ch)) & 0x1) << (7 - (i & 0x7)));
}
}
}
/// <summary>
/// Converts the useless (but realistic) interleaved subchannel data into a useful (but unrealistic) deinterleaved format.
/// </summary>
public unsafe static void DeinterleaveInplace(byte[] buf, int buf_index)
{
byte* out_buf = stackalloc byte[96];
for (int i = 0; i < 96; i++)
out_buf[i] = 0;
for (int ch = 0; ch < 8; ch++)
{
for (int i = 0; i < 96; i++)
{
out_buf[(ch * 12) + (i >> 3)] |= (byte)(((buf[i + buf_index] >> (7 - ch)) & 0x1) << (7 - (i & 0x7)));
}
}
for (int i = 0; i < 96; i++)
buf[i + buf_index] = out_buf[i];
}
}
/// <summary>
@ -76,8 +116,8 @@ namespace BizHawk.Emulation.DiscSystem
{
int offset = 12; //Q subchannel begins after P, 12 bytes in
SubcodeDeinterleaved[offset + 0] = sq.q_status;
SubcodeDeinterleaved[offset + 1] = sq.q_tno;
SubcodeDeinterleaved[offset + 2] = sq.q_index;
SubcodeDeinterleaved[offset + 1] = sq.q_tno.BCDValue;
SubcodeDeinterleaved[offset + 2] = sq.q_index.BCDValue;
SubcodeDeinterleaved[offset + 3] = sq.min.BCDValue;
SubcodeDeinterleaved[offset + 4] = sq.sec.BCDValue;
SubcodeDeinterleaved[offset + 5] = sq.frame.BCDValue;
@ -98,6 +138,17 @@ namespace BizHawk.Emulation.DiscSystem
return crc16;
}
public void Synthesize_SunchannelQ_Checksum()
{
int offset = 12; //Q subchannel begins after P, 12 bytes in
ushort crc16 = CRC16_CCITT.Calculate(SubcodeDeinterleaved, offset, 10);
//CRC is stored inverted and big endian
SubcodeDeinterleaved[offset + 10] = (byte)(~(crc16 >> 8));
SubcodeDeinterleaved[offset + 11] = (byte)(~(crc16));
}
public void ReadSubcodeDeinterleaved(byte[] buffer, int offset)
{
Buffer.BlockCopy(SubcodeDeinterleaved, 0, buffer, offset, 96);
@ -157,22 +208,55 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <summary>
/// Control bit flags for the Q Subchannel
/// Control bit flags for the Q Subchannel.
/// </summary>
[Flags]
public enum EControlQ
{
None = 0,
StereoNoPreEmph = 0,
StereoPreEmph = 1,
MonoNoPreemph = 8,
MonoPreEmph = 9,
DataUninterrupted = 4,
DataIncremental = 5,
PRE = 1, //Pre-emphasis enabled (audio tracks only)
DCP = 2, //Digital copy permitted
DATA = 4, //set for data tracks, clear for audio tracks
_4CH = 8, //Four channel audio
}
CopyProhibitedMask = 0,
CopyPermittedMask = 2,
/// <summary>
/// the q-mode 1 Q subchannel data. These contain TOC entries.
/// design of this struct is from [IEC10149].
/// Nothing in here is BCD
/// </summary>
public class Q_Mode_1
{
/// <summary>
/// Supposed to be zero, because it was in the lead-in track, but you never know
/// </summary>
public byte TNO;
/// <summary>
/// The track number (or a special A0 A1 A2 value) of the track being described
/// </summary>
public byte POINTER;
/// <summary>
/// Supposedly the timestamp within the lead-in track. but it's useless
/// </summary>
public byte MIN, SEC, FRAC;
/// <summary>
/// Supposed to be zero
/// </summary>
public byte ZERO;
/// <summary>
/// A track number of the first (for A0) or final (A1) information track; or the _absolute_ time position of an information track
/// </summary>
public byte P_MIN;
/// <summary>
/// ZERO for an A0 or A1 entry (where P_MIN was the track number); or the _absolute_ time position of an information track
/// </summary>
public byte P_SEC, P_FRAC;
}
/// <summary>
@ -191,21 +275,23 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// normal track: BCD indications of the current track number
/// leadin track: should be 0
/// TODO - make BCD2?
/// </summary>
public byte q_tno;
public BCD2 q_tno;
/// <summary>
/// normal track: BCD indications of the current index
/// leadin track: 'POINT' field used to ID the TOC entry #
/// </summary>
public byte q_index;
public BCD2 q_index;
/// <summary>
/// These are the initial set of timestamps. Meaning varies:
/// check yellowbook 22.3.3 and 22.3.4
/// normal track: relative timestamp
/// leadin track: unknown
/// leadout: relative timestamp
/// TODO - why are these BCD2? having things in BCD2 is freaking annoying, I should only make them BCD2 when serializing
/// EDIT - elsewhere I rambled "why not BCD2?". geh. need to make a final organized approach
/// </summary>
public BCD2 min, sec, frame;
@ -218,7 +304,8 @@ namespace BizHawk.Emulation.DiscSystem
/// These are the second set of timestamps. Meaning varies:
/// check yellowbook 22.3.3 and 22.3.4
/// normal track: absolute timestamp
/// leadin track: timestamp of toc entry
/// leadin track q-mode 1: TOC entry, absolute MSF of track
/// leadout: absolute timestamp
/// </summary>
public BCD2 ap_min, ap_sec, ap_frame;
@ -237,9 +324,12 @@ namespace BizHawk.Emulation.DiscSystem
public Timestamp Timestamp { get { return new Timestamp(min.DecimalValue, sec.DecimalValue, frame.DecimalValue); } }
/// <summary>
/// Retrieves the second set of timestamps (ap_min, ap_sec, ap_frac) as a convenient Timestamp
/// Retrieves the second set of timestamps (ap_min, ap_sec, ap_frac) as a convenient Timestamp.
/// </summary>
public Timestamp AP_Timestamp { get { return new Timestamp(ap_min.DecimalValue, ap_sec.DecimalValue, ap_frame.DecimalValue); } }
public Timestamp AP_Timestamp {
get { return new Timestamp(ap_min.DecimalValue, ap_sec.DecimalValue, ap_frame.DecimalValue); }
set { ap_min.DecimalValue = value.MIN; ap_sec.DecimalValue = value.SEC; ap_frame.DecimalValue = value.FRAC; }
}
/// <summary>
/// sets the status byte from the provided adr and control values

View File

@ -8,6 +8,8 @@ namespace BizHawk.Emulation.DiscSystem
/// Contains structural information for the disc broken down into c# data structures for easy interrogation.
/// This represents a best-effort interpretation of the raw disc image.
/// You cannot assume a disc can be round-tripped into its original format through this (especially if it came from a format more detailed)
/// TODO - index 0s arent populated
/// IDEA: Make this be generated by a module which is aware of the 'firmware' being emulated, so firmware-specific rules can be applied.
/// </summary>
public class DiscStructure
{
@ -18,20 +20,26 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// List of Points described by the TOC.
/// TODO - this is kind of garbage, but... I was using it for Synthesize_SubcodeFromStructure() :/
/// Really, what it is, is a series of points where the tno/index change. Kind of an agenda. And thats how that function uses it.
/// TODO - this is kind of garbage.
/// TODO - rename this
/// TODO - generate it during loading of ccd/cue
/// TODO - is this 98% redundant with the Tracks list? I think so. Maybe it can encode more detail, but the DiscStructure is lossy anyway
/// Really, what it is, is a series of points where the tno/index change. Kind of an agenda.
/// Maybe I should rename it something different, or at least comment it
/// Or, you could look at this as a kind of compressed disc map
/// NOTE: While this class is actually pretty useless for robust stuff, this list of TOCPoints could be considered robust once fully evaluated
/// </summary>
public List<TOCPoint> Points;
/// <summary>
/// How many sectors in the disc, including the 150 lead-in sectors
/// How many sectors in the disc, including the 150 lead-in sectors, up to the end of the last track (before the lead-out track)
/// TODO - does anyone ever need this as the ABA Count? Rename it LBACount or ABACount
/// </summary>
public int LengthInSectors;
/// <summary>
/// Length (including lead-in) of the disc as a timestamp
/// TODO - does anyone ever need this as the ABA Count? Rename it LBACount or ABACount
/// </summary>
public Timestamp FriendlyLength { get { return new Timestamp(LengthInSectors); } }
@ -44,39 +52,33 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <summary>
/// Synthesizes the DiscStructure from a DiscTOCRaw
/// Synthesizes the DiscStructure from RawTOCEntriesJob
/// TODO - move to attic, not being used
/// </summary>
public class SynthesizeFromDiscTOCRawJob
public class SynthesizeFromRawTOCEntriesJob
{
public DiscTOCRaw TOCRaw;
public IEnumerable<RawTOCEntry> Entries;
public DiscStructure Result;
public void Run()
{
Result = new DiscStructure();
Result.Sessions.Add(new Session());
Track lastTrack = null;
for (int tnum = TOCRaw.FirstRecordedTrackNumber; tnum <= TOCRaw.LastRecordedTrackNumber; tnum++)
{
var ti = TOCRaw.TOCItems[tnum];
var track = new Track();
Result.Sessions[0].Tracks.Add(track);
track.Number = tnum;
track.Control = ti.Control;
track.TrackType = ETrackType.Unknown; //not known yet
track.Start_ABA = ti.LBATimestamp.Sector + 150;
if (lastTrack != null)
{
lastTrack.LengthInSectors = track.Start_ABA - lastTrack.Start_ABA;
}
lastTrack = track;
}
if(lastTrack != null)
lastTrack.LengthInSectors = (TOCRaw.LeadoutTimestamp.Sector + 150) - lastTrack.Start_ABA;
var session = new Session();
Result.Sessions.Add(session);
//total length of the disc is counted up to the leadout, including the lead-in.
//i guess supporting storing the leadout is future work.
Result.LengthInSectors = TOCRaw.LeadoutTimestamp.Sector;
//TODO - are these necessarily in order?
foreach (var te in Entries)
{
int pt = te.QData.q_index.DecimalValue;
int lba = te.QData.Timestamp.Sector;
var bcd2 = new BCD2 { BCDValue = (byte)pt };
if (bcd2.DecimalValue > 99) //A0 A1 A2 leadout and crap
continue;
var track = new Track { Start_ABA = lba, Number = pt };
track.Indexes.Add(new Index()); //dummy index 0
track.Indexes.Add(new Index() { Number = 1, LBA = lba });
session.Tracks.Add(track);
}
}
}
@ -97,7 +99,8 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
///
/// Uhm... I'm back to thinking this is a good idea. It's a pretty lean log of the shape of the disc and a good thing to generate a DiscStructure from (and maybe avoid using the DiscStructure altogether)
/// Rename it to DiscMapEntry?
/// </summary>
public class TOCPoint
{
@ -105,7 +108,7 @@ namespace BizHawk.Emulation.DiscSystem
public int ABA, TrackNum, IndexNum;
public Track Track;
public int ADR;
public int ADR; //meh...
public EControlQ Control;
public int LBA
@ -141,7 +144,7 @@ namespace BizHawk.Emulation.DiscSystem
TrackNum = track.Number,
IndexNum = index.Number,
Track = track,
ADR = track.ADR,
//ADR = 1, //It's hard to understand what other value could go here
Control = track.Control
};
@ -196,6 +199,7 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// All the tracks in the session.. but... Tracks[0] should be "Track 1". So beware of this.
/// SO SUCKY!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
/// We might should keep this organized as a dictionary as well.
/// </summary>
public List<Track> Tracks = new List<Track>();
@ -205,6 +209,28 @@ namespace BizHawk.Emulation.DiscSystem
public Timestamp FriendlyLength { get { return new Timestamp(length_aba); } }
}
/// <summary>
/// The Type of a track as specified in the TOC Q-Subchannel data from the control flags.
/// Could also be 4-Channel Audio, but we'll handle that later if needed
/// </summary>
public enum ETrackType
{
/// <summary>
/// The track type isn't always known.. it can take this value til its populated
/// </summary>
Unknown,
/// <summary>
/// Data track( TOC Q control 0x04 flag set )
/// </summary>
Data,
/// <summary>
/// Audio track( TOC Q control 0x04 flag clear )
/// </summary>
Audio
}
public class Track
{
/// <summary>
@ -213,10 +239,20 @@ namespace BizHawk.Emulation.DiscSystem
public int Number;
/// <summary>
/// Conceptual track type, not necessarily stored this way anywhere
/// Is this track audio or data?
/// </summary>
public ETrackType TrackType;
/// <summary>
/// The mode of a track.
/// 0 Will be audio, 1 and 2 will be data.
/// XA sub-forms are expected to be variable within a track
/// This is named as a Heuristic because it is a very ill-defined concept.
/// There's virtually nothing that tells us what the Mode of a track is. You just have to guess.
/// By contrast, the Control field is implied (and maybe specified) to be consistent and match the TOC
/// </summary>
public int ModeHeuristic;
/// <summary>
/// The 'control' properties of the track indicated by the subchannel Q.
/// While in principle these could vary during the track, illegally (or maybe legally according to weird redbook rules)
@ -227,7 +263,7 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// Well, it seems a track can have an ADR property (used to fill the subchannel Q). This is delivered from a CCD file but may have to be guessed from
/// </summary>
public int ADR = 1;
//public int ADR = 1; //??
/// <summary>
/// All the indexes related to the track. These will be 0-Indexed, but they could be non-consecutive.
@ -237,7 +273,7 @@ namespace BizHawk.Emulation.DiscSystem
/// <summary>
/// a track logically starts at index 1.
/// so this is the length from this index 1 to the next index 1 (or the end of the disc)
/// the time before track 1 index 1 is the lead-in and isn't accounted for in any track...
/// the time before track 1 index 1 is the lead-in [edit: IS IT?] and isn't accounted for in any track...
/// </summary>
public int LengthInSectors;

View File

@ -11,9 +11,10 @@ namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Synthesizes the TOC from a set of raw entries.
/// When a disc drive firmware reads the lead-in area, it builds this TOC from finding ADR=1 (mode=1) sectors in the Q subchannel of the lead-in area.
/// I don't think this lead-in area Q subchannel is stored in a CCD .sub file.
/// The disc drive firmware will discover other mode sectors in the lead-in area, and it will register those in separate data structures.
/// When a disc drive firmware reads the lead-in area, it builds this TOC from finding q-mode 1 sectors in the Q subchannel of the lead-in area.
/// Question: does it ignore q-mode != 1?
/// Once upon a time, I said: "The disc drive firmware will discover other mode sectors in the lead-in area, and it will register those in separate data structures."
/// What do I mean by this?
/// </summary>
public class SynthesizeFromRawTOCEntriesJob
{
@ -26,6 +27,11 @@ namespace BizHawk.Emulation.DiscSystem
SynthesizeFromRawTOCEntriesJob job = this;
DiscTOCRaw ret = new DiscTOCRaw();
//this is a dummy, for convenience
ret.TOCItems[0].LBATimestamp = new Timestamp(0);
ret.TOCItems[0].Control = 0;
ret.TOCItems[0].Exists = false;
//just in case this doesnt get set...
ret.FirstRecordedTrackNumber = 0;
ret.LastRecordedTrackNumber = 0;
@ -35,20 +41,22 @@ namespace BizHawk.Emulation.DiscSystem
foreach (var te in job.Entries)
{
var q = te.QData;
int point = q.q_index;
var point = q.q_index.DecimalValue;
//see ECMD-394 page 5-14 for info about point = 0xA0, 0xA1, 0xA2
if (point == 0x00)
job.Log.Add("unexpected POINT=00 in lead-in Q-channel");
else if (point == 255)
throw new InvalidOperationException("point == 255");
else if (point <= 99)
{
maxFoundTrack = Math.Max(maxFoundTrack, point);
ret.TOCItems[point].LBATimestamp = q.AP_Timestamp;
ret.TOCItems[point].LBATimestamp = new Timestamp(q.AP_Timestamp.Sector - 150); //RawTOCEntries contained an absolute time
ret.TOCItems[point].Control = q.CONTROL;
ret.TOCItems[point].Exists = true;
}
else if (point == 0xA0)
else if (point == 100) //0xA0 bcd
{
ret.FirstRecordedTrackNumber = q.ap_min.DecimalValue;
if (q.ap_frame.DecimalValue != 0) job.Log.Add("PFRAME should be 0 for POINT=0xA0");
@ -57,13 +65,13 @@ namespace BizHawk.Emulation.DiscSystem
else if (q.ap_sec.DecimalValue == 0x20) ret.Session1Format = DiscTOCRaw.SessionFormat.Type20_CDXA;
else job.Log.Add("Unrecognized session format: PSEC should be one of {0x00,0x10,0x20} for POINT=0xA0");
}
else if (point == 0xA1)
else if (point == 101) //0xA1 bcd
{
ret.LastRecordedTrackNumber = q.ap_min.DecimalValue;
if (q.ap_sec.DecimalValue != 0) job.Log.Add("PSEC should be 0 for POINT=0xA1");
if (q.ap_frame.DecimalValue != 0) job.Log.Add("PFRAME should be 0 for POINT=0xA1");
}
else if (point == 0xA2)
else if (point == 102) //0xA2 bcd
{
ret.LeadoutTimestamp = q.AP_Timestamp;
}
@ -71,50 +79,60 @@ namespace BizHawk.Emulation.DiscSystem
//this is speculative:
//well, nothing to be done here..
if (ret.FirstRecordedTrackNumber == 0) { }
if (ret.LastRecordedTrackNumber == 0) { ret.LastRecordedTrackNumber = maxFoundTrack; }
if (ret.FirstRecordedTrackNumber == -1) { }
if (ret.LastRecordedTrackNumber == -1) { ret.LastRecordedTrackNumber = maxFoundTrack; }
if (ret.Session1Format == SessionFormat.None) ret.Session1Format = SessionFormat.Type00_CDROM_CDDA;
if (!ret.LeadoutTimestamp.Valid) {
//we're DOOMED. we cant know the length of the last track without this....
}
job.Result = ret;
}
}
public enum SessionFormat
{
Type00_CDROM_CDDA,
Type10_CDI,
Type20_CDXA
None = -1,
Type00_CDROM_CDDA = 0x00,
Type10_CDI = 0x10,
Type20_CDXA = 0x20
}
/// <summary>
/// The TOC specifies the first recorded track number, independently of whatever may actually be recorded (its unclear what happens if theres a mismatch)
/// The TOC specifies the first recorded track number, independently of whatever may actually be recorded
/// </summary>
public int FirstRecordedTrackNumber;
public int FirstRecordedTrackNumber = -1;
/// <summary>
/// The TOC specifies the last recorded track number, independently of whatever may actually be recorded (its unclear what happens if theres a mismatch)
/// The TOC specifies the last recorded track number, independently of whatever may actually be recorded
/// </summary>
public int LastRecordedTrackNumber;
public int LastRecordedTrackNumber = -1;
/// <summary>
/// The TOC specifies the format of the session, so here it is.
/// </summary>
public SessionFormat Session1Format = SessionFormat.Type00_CDROM_CDDA;
public SessionFormat Session1Format = SessionFormat.None;
public struct TOCItem
{
public EControlQ Control;
public Timestamp LBATimestamp;
/// <summary>
/// TODO - this is used for setting ADR to 1 or 0 for mednafen. maybe we need to track ADR separately? (CCD specifies it)
/// </summary>
public bool Exists;
}
/// <summary>
/// I think it likely that a firmware would just have a buffer for 100 of these. There can never be more than 100 and it might not match the 0xA0 and 0xA1 -specified values
/// Also #0 is illegal and is always empty.
/// This is a convenient format for storing the TOC (taken from mednafen)
/// Index 0 is empty, so that track 1 is in index 1.
/// Index 100 is the Lead-out track
/// </summary>
public TOCItem[] TOCItems = new TOCItem[100];
public TOCItem[] TOCItems = new TOCItem[101];
/// <summary>
/// POINT=0xA2 specifies this
/// </summary>
public Timestamp LeadoutTimestamp;
public Timestamp LeadoutTimestamp = new Timestamp();
}
}

View File

@ -1,5 +1,5 @@

Microsoft Visual Studio Solution File, Format Version 11.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Version", "Version\Version.csproj", "{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}"
EndProject
@ -56,85 +56,187 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizHawk.Bizware.BizwareGL.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizHawk.Client.MultiHawk", "BizHawk.Client.MultiHawk\BizHawk.Client.MultiHawk.csproj", "{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mednadisc", "psx\mednadisc\bizhawk\mednadisc.vcxproj", "{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Mixed Platforms = Debug|Mixed Platforms
Debug|Win32 = Debug|Win32
Debug|x86 = Debug|x86
Release|Mixed Platforms = Release|Mixed Platforms
Release|Win32 = Release|Win32
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Debug|Mixed Platforms.Build.0 = Debug|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Debug|Win32.ActiveCfg = Debug|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Debug|x86.ActiveCfg = Debug|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Debug|x86.Build.0 = Debug|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Release|Mixed Platforms.ActiveCfg = Release|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Release|Mixed Platforms.Build.0 = Release|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Release|Win32.ActiveCfg = Release|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Release|x86.ActiveCfg = Release|x86
{0CE8B337-08E3-4602-BF10-C4D4C75D2F13}.Release|x86.Build.0 = Release|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|Mixed Platforms.Build.0 = Debug|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|Win32.ActiveCfg = Debug|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x86.ActiveCfg = Debug|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x86.Build.0 = Debug|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|Mixed Platforms.ActiveCfg = Release|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|Mixed Platforms.Build.0 = Release|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|Win32.ActiveCfg = Release|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x86.ActiveCfg = Release|x86
{24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x86.Build.0 = Release|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|Mixed Platforms.Build.0 = Debug|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|Win32.ActiveCfg = Debug|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x86.ActiveCfg = Debug|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x86.Build.0 = Debug|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|Mixed Platforms.ActiveCfg = Release|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|Mixed Platforms.Build.0 = Release|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|Win32.ActiveCfg = Release|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x86.ActiveCfg = Release|x86
{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x86.Build.0 = Release|x86
{DD448B37-BA3F-4544-9754-5406E8094723}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Debug|Win32.ActiveCfg = Debug|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x86.ActiveCfg = Debug|x86
{DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x86.Build.0 = Debug|x86
{DD448B37-BA3F-4544-9754-5406E8094723}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Release|Win32.ActiveCfg = Release|Any CPU
{DD448B37-BA3F-4544-9754-5406E8094723}.Release|x86.ActiveCfg = Release|x86
{DD448B37-BA3F-4544-9754-5406E8094723}.Release|x86.Build.0 = Release|x86
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|Win32.ActiveCfg = Debug|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x86.ActiveCfg = Debug|x86
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x86.Build.0 = Debug|x86
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|Win32.ActiveCfg = Release|Any CPU
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x86.ActiveCfg = Release|x86
{C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x86.Build.0 = Release|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|Mixed Platforms.Build.0 = Debug|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|Win32.ActiveCfg = Debug|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x86.ActiveCfg = Debug|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x86.Build.0 = Debug|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Release|Mixed Platforms.ActiveCfg = Release|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Release|Mixed Platforms.Build.0 = Release|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Release|Win32.ActiveCfg = Release|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x86.ActiveCfg = Release|x86
{F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x86.Build.0 = Release|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Debug|Mixed Platforms.Build.0 = Debug|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Debug|Win32.ActiveCfg = Debug|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x86.ActiveCfg = Debug|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x86.Build.0 = Debug|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Release|Mixed Platforms.ActiveCfg = Release|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Release|Mixed Platforms.Build.0 = Release|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Release|Win32.ActiveCfg = Release|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Release|x86.ActiveCfg = Release|x86
{E1A23168-B571-411C-B360-2229E7225E0E}.Release|x86.Build.0 = Release|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|Mixed Platforms.Build.0 = Debug|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|Win32.ActiveCfg = Debug|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x86.ActiveCfg = Debug|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x86.Build.0 = Debug|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|Mixed Platforms.ActiveCfg = Release|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|Mixed Platforms.Build.0 = Release|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|Win32.ActiveCfg = Release|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x86.ActiveCfg = Release|x86
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x86.Build.0 = Release|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|Mixed Platforms.Build.0 = Debug|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|Win32.ActiveCfg = Debug|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x86.ActiveCfg = Debug|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x86.Build.0 = Debug|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|Mixed Platforms.ActiveCfg = Release|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|Mixed Platforms.Build.0 = Release|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|Win32.ActiveCfg = Release|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x86.ActiveCfg = Release|x86
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x86.Build.0 = Release|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Debug|Mixed Platforms.Build.0 = Debug|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Debug|Win32.ActiveCfg = Debug|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Debug|x86.ActiveCfg = Debug|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Debug|x86.Build.0 = Debug|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Release|Mixed Platforms.ActiveCfg = Release|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Release|Mixed Platforms.Build.0 = Release|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Release|Win32.ActiveCfg = Release|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Release|x86.ActiveCfg = Release|x86
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE}.Release|x86.Build.0 = Release|x86
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Debug|Win32.ActiveCfg = Debug|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Debug|x86.ActiveCfg = Debug|x86
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Debug|x86.Build.0 = Debug|x86
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Release|Win32.ActiveCfg = Release|Any CPU
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Release|x86.ActiveCfg = Release|x86
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9}.Release|x86.Build.0 = Release|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Debug|Mixed Platforms.Build.0 = Debug|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Debug|Win32.ActiveCfg = Debug|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Debug|x86.ActiveCfg = Debug|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Debug|x86.Build.0 = Debug|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Release|Mixed Platforms.ActiveCfg = Release|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Release|Mixed Platforms.Build.0 = Release|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Release|Win32.ActiveCfg = Release|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Release|x86.ActiveCfg = Release|x86
{337CA23E-65E7-44E1-9411-97EE08BB8116}.Release|x86.Build.0 = Release|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Debug|Mixed Platforms.Build.0 = Debug|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Debug|Win32.ActiveCfg = Debug|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Debug|x86.ActiveCfg = Debug|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Debug|x86.Build.0 = Debug|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Release|Mixed Platforms.ActiveCfg = Release|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Release|Mixed Platforms.Build.0 = Release|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Release|Win32.ActiveCfg = Release|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Release|x86.ActiveCfg = Release|x86
{E6B436B1-A3CD-4C9A-8F76-5D7154726884}.Release|x86.Build.0 = Release|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Debug|Mixed Platforms.Build.0 = Debug|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Debug|Win32.ActiveCfg = Debug|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Debug|x86.ActiveCfg = Debug|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Debug|x86.Build.0 = Debug|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Release|Mixed Platforms.ActiveCfg = Release|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Release|Mixed Platforms.Build.0 = Release|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Release|Win32.ActiveCfg = Release|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Release|x86.ActiveCfg = Release|x86
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18}.Release|x86.Build.0 = Release|x86
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|Mixed Platforms.Build.0 = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|Win32.ActiveCfg = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|Win32.Build.0 = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|x86.ActiveCfg = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Debug|x86.Build.0 = Debug|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Release|Mixed Platforms.ActiveCfg = Release|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Release|Mixed Platforms.Build.0 = Release|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Release|Win32.ActiveCfg = Release|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Release|Win32.Build.0 = Release|Win32
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC}.Release|x86.ActiveCfg = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{24A0AA3C-B25F-4197-B23D-476D6462DBA0} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{DD448B37-BA3F-4544-9754-5406E8094723} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{C4366030-6D03-424B-AE53-F4F43BB217C3} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{F51946EA-827F-4D82-B841-1F2F6D060312} = {3627C08B-3E43-4224-9DA4-40BD69495FBC}
{24A0AA3C-B25F-4197-B23D-476D6462DBA0} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{E1A23168-B571-411C-B360-2229E7225E0E} = {3627C08B-3E43-4224-9DA4-40BD69495FBC}
{F51946EA-827F-4D82-B841-1F2F6D060312} = {3627C08B-3E43-4224-9DA4-40BD69495FBC}
{197D4314-8A9F-49BA-977D-54ACEFAEB6BA} = {3627C08B-3E43-4224-9DA4-40BD69495FBC}
{5F35CAFC-6208-4FBE-AD17-0E69BA3F70EC} = {3627C08B-3E43-4224-9DA4-40BD69495FBC}
{9F84A0B2-861E-4EF4-B89B-5E2A3F38A465} = {0540A9A6-977E-466D-8BD3-1D8590BD5282}
{5160CFB1-5389-47C1-B7F6-8A0DC97641EE} = {0540A9A6-977E-466D-8BD3-1D8590BD5282}
{2D2890A8-C338-4439-AD8B-CB9EE85A94F9} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
{337CA23E-65E7-44E1-9411-97EE08BB8116} = {0540A9A6-977E-466D-8BD3-1D8590BD5282}
{E6B436B1-A3CD-4C9A-8F76-5D7154726884} = {0540A9A6-977E-466D-8BD3-1D8590BD5282}
{B95649F5-A0AE-41EB-B62B-578A2AFF5E18} = {B51F1139-3D2C-41BE-A762-EF1F9B41EACA}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
StartupItem = BizHawk.Client.EmuHawk\BizHawk.Client.EmuHawk.csproj