big discsystem refactors and preliminary CCD handling. not to be considered stable, but ill start supporting it.

This commit is contained in:
zeromus 2014-12-04 05:40:10 +00:00
parent d8a204572d
commit 29b217b587
31 changed files with 1914 additions and 629 deletions

View File

@ -203,9 +203,16 @@ namespace BizHawk.Client.Common
try
{
var ext = file.Extension.ToLower();
if (ext == ".iso" || ext == ".cue")
if (ext == ".iso" || ext == ".cue" || ext == ".ccd")
{
var disc = ext == ".iso" ? Disc.FromIsoPath(path) : Disc.FromCuePath(path, new CueBinPrefs());
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);
var hash = disc.GetHash();
game = Database.CheckDatabase(hash);
if (game == null)

View File

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

View File

@ -6,7 +6,7 @@
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{C4366030-6D03-424B-AE53-F4F43BB217C3}</ProjectGuid>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>BizHawk.Client.DiscoHawk</RootNamespace>
<AssemblyName>DiscoHawk</AssemblyName>
@ -153,6 +153,10 @@
<Content Include="discohawk.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BizHawk.Common\BizHawk.Common.csproj">
<Project>{866F8D13-0678-4FF9-80A4-A3993FD4D8A3}</Project>
<Name>BizHawk.Common</Name>
</ProjectReference>
<ProjectReference Include="..\BizHawk.Emulation.DiscSystem\BizHawk.Emulation.DiscSystem.csproj">
<Project>{f51946ea-827f-4d82-b841-1f2f6d060312}</Project>
<Name>BizHawk.Emulation.DiscSystem</Name>

View File

@ -178,6 +178,10 @@ namespace BizHawk.Client.DiscoHawk
dialog.ShowDialog();
return;
}
else
{
//test stuff...
}
}
}

View File

@ -76,11 +76,11 @@ namespace BizHawk.Client.DiscoHawk
Disc disc = discRecord.Disc;
boundDiscRecord = discRecord;
DiscTOC toc = disc.ReadTOC();
DiscStructure toc = disc.ReadStructure();
boundDisc = disc;
lblSessions.Text = toc.Sessions.Count.ToString();
lblTracks.Text = toc.Sessions.Sum((ses) => ses.Tracks.Count).ToString();
lblSectors.Text = string.Format("{0} ({1})", toc.length_aba, toc.FriendlyLength.Value);
lblSectors.Text = string.Format("{0} ({1})", toc.LengthInSectors, toc.FriendlyLength.Value);
lblSize.Text = string.Format("{0:0.00} MB", toc.BinarySize / 1024.0 / 1024.0);
btnExportCue.Enabled = true;
UpdateCue();

View File

@ -9,6 +9,8 @@ using System.Windows.Forms;
using System.IO;
using System.Threading;
using BizHawk.Common;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Client.DiscoHawk
@ -60,8 +62,10 @@ namespace BizHawk.Client.DiscoHawk
Disc disc = null;
if (ext == ".ISO")
disc = Disc.FromIsoPath(file);
else if(ext == ".CUE")
else if (ext == ".CUE")
disc = Disc.FromCuePath(file, prefs);
else if (ext == ".CCD")
disc = Disc.FromCCDPath(file);
string baseName = Path.GetFileNameWithoutExtension(file);
baseName += "_hawked";
prefs.ReallyDumpBin = true;
@ -116,7 +120,8 @@ namespace BizHawk.Client.DiscoHawk
if (files == null) return new List<string>();
foreach (string str in files)
{
if (Path.GetExtension(str).ToUpper() != ".CUE" && Path.GetExtension(str).ToUpper() != ".ISO")
string ext = Path.GetExtension(str).ToUpper();
if(!ext.In(new string[]{".CUE",".ISO",".CCD"}))
{
return new List<string>();
}

View File

@ -1875,20 +1875,20 @@ namespace BizHawk.Client.EmuHawk
if (VersionInfo.DeveloperBuild)
{
ofd.Filter = FormatFilter(
"Rom Files", "*.nes;*.fds;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.cue;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.col;.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;%ARCH%",
"Rom Files", "*.nes;*.fds;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.cue;*.ccd;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.col;.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;%ARCH%",
"Music Files", "*.psf;*.sid",
"Disc Images", "*.cue",
"Disc Images", "*.cue;*.ccd",
"NES", "*.nes;*.fds;%ARCH%",
"Super NES", "*.smc;*.sfc;*.xml;%ARCH%",
"Master System", "*.sms;*.gg;*.sg;%ARCH%",
"PC Engine", "*.pce;*.sgx;*.cue;%ARCH%",
"PC Engine", "*.pce;*.sgx;*.cue;*.ccd;%ARCH%",
"TI-83", "*.rom;%ARCH%",
"Archive Files", "%ARCH%",
"Savestate", "*.state",
"Atari 2600", "*.a26;*.bin;%ARCH%",
"Atari 7800", "*.a78;*.bin;%ARCH%",
"Atari Lynx", "*.lnx;%ARCH%",
"Genesis", "*.gen;*.smd;*.bin;*.md;*.cue;%ARCH%",
"Genesis", "*.gen;*.smd;*.bin;*.md;*.cue;*.ccd;%ARCH%",
"Gameboy", "*.gb;*.gbc;*.sgb;%ARCH%",
"Gameboy Advance", "*.gba;%ARCH%",
"Colecovision", "*.col;%ARCH%",
@ -1904,15 +1904,15 @@ namespace BizHawk.Client.EmuHawk
else
{
ofd.Filter = FormatFilter(
"Rom Files", "*.nes;*.fds;*.sms;*.gg;*.sg;*.gb;*.gbc;*.gba;*.pce;*.sgx;*.bin;*.smd;*.gen;*.md;*.smc;*.sfc;*.a26;*.a78;*.lnx;*.col;*.rom;*.cue;*.sgb;*.z64;*.v64;*.n64;*.ws;*.wsc;*.xml;%ARCH%",
"Disc Images", "*.cue",
"Rom Files", "*.nes;*.fds;*.sms;*.gg;*.sg;*.gb;*.gbc;*.gba;*.pce;*.sgx;*.bin;*.smd;*.gen;*.md;*.smc;*.sfc;*.a26;*.a78;*.lnx;*.col;*.rom;*.cue;*.ccd;*.sgb;*.z64;*.v64;*.n64;*.ws;*.wsc;*.xml;%ARCH%",
"Disc Images", "*.cue;*.ccd",
"NES", "*.nes;*.fds;%ARCH%",
"Super NES", "*.smc;*.sfc;*.xml;%ARCH%",
"Nintendo 64", "*.z64;*.v64;*.n64",
"Gameboy", "*.gb;*.gbc;*.sgb;%ARCH%",
"Gameboy Advance", "*.gba;%ARCH%",
"Master System", "*.sms;*.gg;*.sg;%ARCH%",
"PC Engine", "*.pce;*.sgx;*.cue;%ARCH%",
"PC Engine", "*.pce;*.sgx;*.cue;*.ccd;%ARCH%",
"Atari 2600", "*.a26;%ARCH%",
"Atari 7800", "*.a78;%ARCH%",
"Atari Lynx", "*.lnx;%ARCH%",
@ -1920,7 +1920,7 @@ namespace BizHawk.Client.EmuHawk
"TI-83", "*.rom;%ARCH%",
"Archive Files", "%ARCH%",
"Savestate", "*.state",
"Genesis", "*.gen;*.md;*.smd;*.bin;*.cue;%ARCH%",
"Genesis", "*.gen;*.md;*.smd;*.bin;*.cue;*.ccd;%ARCH%",
"WonderSwan", "*.ws;*.wsc;%ARCH%",
"All Files", "*.*");
}

View File

@ -65,6 +65,10 @@
<Reference Include="Newtonsoft.Json">
<HintPath>..\References\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="OpenTK, Version=1.1.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\References\OpenTK.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>

View File

@ -147,6 +147,8 @@ namespace BizHawk.Emulation.Cores.PCEngine
PCEngine pce;
public Disc disc;
SubcodeReader subcodeReader;
SubchannelQ subchannelQ;
int audioStartLBA;
int audioEndLBA;
@ -154,6 +156,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
{
this.pce = pce;
this.disc = disc;
subcodeReader = new SubcodeReader(disc);
}
public void Think()
@ -418,7 +421,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
case 0x80: // Set start offset in track units
byte trackNo = CommandBuffer[2].BCDtoBin();
audioStartLBA = disc.TOC.Sessions[0].Tracks[trackNo - 1].Indexes[1].aba - 150;
audioStartLBA = disc.Structure.Sessions[0].Tracks[trackNo - 1].Indexes[1].aba - 150;
break;
}
@ -453,10 +456,10 @@ namespace BizHawk.Emulation.Cores.PCEngine
case 0x80: // Set end offset in track units
byte trackNo = CommandBuffer[2].BCDtoBin();
if (trackNo - 1 >= disc.TOC.Sessions[0].Tracks.Count)
if (trackNo - 1 >= disc.Structure.Sessions[0].Tracks.Count)
audioEndLBA = disc.LBACount;
else
audioEndLBA = disc.TOC.Sessions[0].Tracks[trackNo - 1].Indexes[1].aba - 150;
audioEndLBA = disc.Structure.Sessions[0].Tracks[trackNo - 1].Indexes[1].aba - 150;
break;
}
@ -493,7 +496,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
void CommandReadSubcodeQ()
{
bool playing = pce.CDAudio.Mode != CDAudio.CDAudioMode_Stopped;
var sectorEntry = disc.ReadLBA_SectorEntry(playing ? pce.CDAudio.CurrentSector : CurrentReadingSector);
int sectorNum = playing ? pce.CDAudio.CurrentSector : CurrentReadingSector;
DataIn.Clear();
@ -504,15 +507,16 @@ namespace BizHawk.Emulation.Cores.PCEngine
case CDAudio.CDAudioMode_Stopped: DataIn.Enqueue(3); break;
}
DataIn.Enqueue(sectorEntry.q_status); // I do not know what status is
DataIn.Enqueue(sectorEntry.q_tno.BCDValue); // track
DataIn.Enqueue(sectorEntry.q_index.BCDValue); // index
DataIn.Enqueue(sectorEntry.q_min.BCDValue); // M(rel)
DataIn.Enqueue(sectorEntry.q_sec.BCDValue); // S(rel)
DataIn.Enqueue(sectorEntry.q_frame.BCDValue); // F(rel)
DataIn.Enqueue(sectorEntry.q_amin.BCDValue); // M(abs)
DataIn.Enqueue(sectorEntry.q_asec.BCDValue); // S(abs)
DataIn.Enqueue(sectorEntry.q_aframe.BCDValue); // F(abs)
subcodeReader.ReadLBA_SubchannelQ(sectorNum, ref subchannelQ);
DataIn.Enqueue(subchannelQ.q_status); // I do not know what status is
DataIn.Enqueue(subchannelQ.q_tno); // track
DataIn.Enqueue(subchannelQ.q_index); // index
DataIn.Enqueue(subchannelQ.min.BCDValue); // M(rel)
DataIn.Enqueue(subchannelQ.sec.BCDValue); // S(rel)
DataIn.Enqueue(subchannelQ.frame.BCDValue); // F(rel)
DataIn.Enqueue(subchannelQ.ap_min.BCDValue); // M(abs)
DataIn.Enqueue(subchannelQ.ap_sec.BCDValue); // S(abs)
DataIn.Enqueue(subchannelQ.ap_frame.BCDValue); // F(abs)
SetPhase(BusPhase_DataIn);
}
@ -525,7 +529,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
{
DataIn.Clear();
DataIn.Enqueue(0x01);
DataIn.Enqueue(((byte)disc.TOC.Sessions[0].Tracks.Count).BinToBCD());
DataIn.Enqueue(((byte)disc.Structure.Sessions[0].Tracks.Count).BinToBCD());
SetPhase(BusPhase_DataIn);
break;
}
@ -546,7 +550,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
case 2: // Return starting position of specified track in MSF format
{
int track = CommandBuffer[2].BCDtoBin();
var tracks = disc.TOC.Sessions[0].Tracks;
var tracks = disc.Structure.Sessions[0].Tracks;
if (CommandBuffer[2] > 0x99)
throw new Exception("invalid track number BCD request... is something I need to handle?");
if (track == 0) track = 1;
@ -556,7 +560,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
int lbaPos;
if (track > tracks.Count)
lbaPos = disc.TOC.Sessions[0].length_aba - 150;
lbaPos = disc.Structure.Sessions[0].length_aba - 150;
else
lbaPos = tracks[track].Indexes[1].aba - 150;
@ -568,7 +572,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
DataIn.Enqueue(s.BinToBCD());
DataIn.Enqueue(f.BinToBCD());
if (track > tracks.Count || disc.TOC.Sessions[0].Tracks[track].TrackType == ETrackType.Audio)
if (track > tracks.Count || disc.Structure.Sessions[0].Tracks[track].TrackType == ETrackType.Audio)
DataIn.Enqueue(0);
else
DataIn.Enqueue(4);

View File

@ -627,7 +627,7 @@ namespace BizHawk.Emulation.Cores.Sega.Saturn
{
// this stuff from yabause's cdbase.c. don't ask me to explain it
var TOC = CD.ReadTOC();
var TOC = CD.ReadStructure();
int[] rTOC = new int[102];
var ses = TOC.Sessions[0];
int ntrk = ses.Tracks.Count;

View File

@ -316,7 +316,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx
ret.readcallback = cd_callback_handle = new LibGPGX.cd_read_cb(CDRead);
var ses = CD.TOC.Sessions[0];
var ses = CD.Structure.Sessions[0];
int ntrack = ses.Tracks.Count;
// bet you a dollar this is all wrong
@ -325,7 +325,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx
if (i < ntrack)
{
ret.tracks[i].start = ses.Tracks[i].Indexes[1].aba - 150;
ret.tracks[i].end = ses.Tracks[i].length_aba + ret.tracks[i].start;
ret.tracks[i].end = ses.Tracks[i].LengthInSectors + ret.tracks[i].start;
if (i == ntrack - 1)
{
ret.end = ret.tracks[i].end;

View File

@ -46,7 +46,10 @@
<Link>VersionInfo.cs</Link>
</Compile>
<Compile Include="Blobs\Blob_ECM.cs" />
<Compile Include="Blobs\Blob_RawFile.cs" />
<Compile Include="Blobs\Blob_WaveFile.cs" />
<Compile Include="Blobs\Blob_ZeroPadAdapter.cs" />
<Compile Include="Blobs\IBlob.cs" />
<Compile Include="Blobs\RiffMaster.cs" />
<Compile Include="CCD_format.cs" />
<Compile Include="CDAudio.cs" />
@ -62,12 +65,16 @@
<Compile Include="Disc.API.cs" />
<Compile Include="Disc.cs" />
<Compile Include="Disc.ID.cs" />
<Compile Include="DiscTOC.cs" />
<Compile Include="DiscTypes.cs" />
<Compile Include="DiscUtils.cs" />
<Compile Include="ECM.cs" />
<Compile Include="GPL_ECM.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\svnrev.cs" />
<Compile Include="SectorInterfaces.cs" />
<Compile Include="Subcode.cs" />
<Compile Include="TOC\DiscStructure.cs" />
<Compile Include="TOC\TOCRaw.cs" />
<Compile Include="TOC_format.cs" />
</ItemGroup>
<ItemGroup>
@ -80,6 +87,10 @@
<Name>BizHawk.Emulation.Common</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="docs\notes.txt" />
<Content Include="docs\todo.txt" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>"$(SolutionDir)subwcrev.bat" "$(ProjectDir)"</PreBuildEvent>

View File

@ -0,0 +1,62 @@
using System;
using System.Linq;
using System.IO;
namespace BizHawk.Emulation.DiscSystem
{
partial class Disc
{
internal class Blob_RawFile : IBlob
{
public string PhysicalPath
{
get
{
return physicalPath;
}
set
{
physicalPath = value;
length = new FileInfo(physicalPath).Length;
}
}
string physicalPath;
long length;
public long Offset = 0;
BufferedStream fs;
public void Dispose()
{
if (fs != null)
{
fs.Dispose();
fs = null;
}
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
//use quite a large buffer, because normally we will be reading these sequentially but in small chunks.
//this enhances performance considerably
//NOTE: wouldnt very large buffering create stuttering? this would depend on how it's implemented.
//really, we need a smarter asynchronous read-ahead buffer. that requires substantially more engineering, some kind of 'DiscUniverse' of carefully managed threads and such.
const int buffersize = 2352 * 75 * 2;
if (fs == null)
fs = new BufferedStream(new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read), buffersize);
long target = byte_pos + Offset;
if (fs.Position != target)
fs.Position = target;
return fs.Read(buffer, offset, count);
}
public long Length
{
get
{
return length;
}
}
}
}
}

View File

@ -6,6 +6,9 @@ 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?
/// </summary>
class Blob_WaveFile : IBlob
{
[Serializable]
@ -19,7 +22,53 @@ namespace BizHawk.Emulation.DiscSystem
public Blob_WaveFile()
{
} private class Blob_RawFile : IBlob
{
public string PhysicalPath {
get
{
return physicalPath;
}
set
{
physicalPath = value;
length = new FileInfo(physicalPath).Length;
}
}
string physicalPath;
long length;
public long Offset = 0;
BufferedStream fs;
public void Dispose()
{
if (fs != null)
{
fs.Dispose();
fs = null;
}
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
//use quite a large buffer, because normally we will be reading these sequentially but in small chunks.
//this enhances performance considerably
const int buffersize = 2352 * 75 * 2;
if (fs == null)
fs = new BufferedStream(new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read), buffersize);
long target = byte_pos + Offset;
if(fs.Position != target)
fs.Position = target;
return fs.Read(buffer, offset, count);
}
public long Length
{
get
{
return length;
}
}
}
public void Load(byte[] waveData)
{

View File

@ -0,0 +1,70 @@
using System;
using System.IO;
namespace BizHawk.Emulation.DiscSystem
{
public partial class Disc : IDisposable
{
public sealed class Blob_ZeroPadAdapter : IBlob
{
public Blob_ZeroPadAdapter(IBlob baseBlob, long padFrom, long padLen)
{
this.baseBlob = baseBlob;
this.padFrom = padFrom;
this.padLen = padLen;
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
//I have an ff9 disc which can demo this
throw new NotImplementedException("Blob_ZeroPadAdapter hasnt been tested yet! please report this!");
//something about this seems unnecessarily complex, but... i dunno.
/*
//figure out how much remains until the zero-padding begins
long remain = byte_pos - padFrom;
int todo;
if (remain < count)
todo = (int)remain;
else todo = count;
//read up until the zero-padding
int totalRead = 0;
int readed = baseBlob.Read(byte_pos, buffer, offset, todo);
totalRead += readed;
offset += todo;
//if we didnt read enough, we certainly shouldnt try to read any more
if (readed < todo)
return readed;
//if that was all we needed, then we're done
count -= todo;
if (count == 0)
return totalRead;
//if we need more, it must come from zero-padding
remain = padLen;
if (remain < count)
todo = (int)remain;
else todo = count;
Array.Clear(buffer, offset, todo);
totalRead += todo;
return totalRead;
*/
}
public void Dispose()
{
baseBlob.Dispose();
}
private readonly IBlob baseBlob;
private long padFrom;
private long padLen;
}
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Presently, an IBlob doesn't need to work multithreadedly. It's quite an onerous demand.
/// This should probably be managed by the Disc class somehow, or by the user making another Disc.
/// </summary>
public interface IBlob : IDisposable
{
/// <summary>
/// what a weird parameter order. normally the dest buffer would be first. weird.
/// </summary>
/// <param name="byte_pos">location in the blob to read from</param>
/// <param name="buffer">destination buffer for read data</param>
/// <param name="offset">offset into destination buffer</param>
/// <param name="count">amount to read</param>
int Read(long byte_pos, byte[] buffer, int offset, int count);
}
}

View File

@ -1,9 +1,509 @@
namespace BizHawk.Emulation.DiscSystem
using System;
using System.Text;
using System.IO;
using System.Globalization;
using System.Collections.Generic;
//check out ccd2iso linux program?
//https://wiki.archlinux.org/index.php/CD_Burning#TOC.2FCUE.2FBIN_for_mixed-mode_disks advice here
//also referencing mednafen sources
//TODO - copy mednafen sanity checking
//TODO - check subcode vs TOC (warnings if different)
//TODO - check TOC vs tracks (warnings if different)
namespace BizHawk.Emulation.DiscSystem
{
//TBD CCD format
//check out ccd2iso linux program?
//https://wiki.archlinux.org/index.php/CD_Burning#TOC.2FCUE.2FBIN_for_mixed-mode_disks advice here
public class CCDFormat
public class CCD_Format
{
}
}
/// <summary>
/// Represents a CCD file, faithfully. Minimal interpretation of the data happens.
/// Currently the [TRACK] sections aren't parsed, though.
/// </summary>
public class CCDFile
{
/// <summary>
/// which version CCD file this came from. We hope it shouldn't affect the semantics of anything else in here, but just in case..
/// </summary>
public int Version;
/// <summary>
/// this is probably a 0 or 1 bool
/// </summary>
public int DataTracksScrambled;
/// <summary>
/// ???
/// </summary>
public int CDTextLength;
/// <summary>
/// The [Session] sections
/// </summary>
public List<CCDSession> Sessions = new List<CCDSession>();
/// <summary>
/// The [Entry] sctions
/// </summary>
public List<CCDTocEntry> TOCEntries = new List<CCDTocEntry>();
/// <summary>
/// The [TRACK] sections
/// </summary>
public List<CCDTrack> Tracks = new List<CCDTrack>();
/// <summary>
/// The [TRACK] sections, indexed by number
/// </summary>
public Dictionary<int, CCDTrack> TracksByNumber = new Dictionary<int, CCDTrack>();
}
/// <summary>
/// Represents an [Entry] section from a CCD file
/// </summary>
public class CCDTocEntry
{
public CCDTocEntry(int entryNum)
{
EntryNum = entryNum;
}
/// <summary>
/// these should be 0-indexed
/// </summary>
public int EntryNum;
/// <summary>
/// the CCD specifies this, but it isnt in the actual disc data as such, it is encoded some other (likely difficult to extract) way and thats why CCD puts it here
/// </summary>
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.
/// </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.
/// </summary>
public int PLBA;
//these correspond pretty directly to values in the Q subchannel fields
public int Control;
public int ADR;
public int TrackNo;
public int Point;
public int AMin;
public int ASec;
public int AFrame;
public int Zero;
public int PMin;
public int PSec;
public int PFrame;
}
/// <summary>
/// Represents a [Track] section from a CCD file
/// </summary>
public class CCDTrack
{
public CCDTrack(int number)
{
this.Number = number;
}
/// <summary>
/// note: this is 1-indexed
/// </summary>
public int Number;
/// <summary>
/// The specified data mode
/// </summary>
public int Mode;
/// <summary>
/// The indexes specified for the track (these are 0-indexed)
/// </summary>
public Dictionary<int, int> Indexes = new Dictionary<int, int>();
}
/// <summary>
/// Represents a [Session] section from a CCD file
/// </summary>
public class CCDSession
{
public CCDSession(int number)
{
this.Number = number;
}
/// <summary>
/// note: this is 1-indexed.
/// </summary>
public int Number;
//Not sure what the default should be.. ive only seen mode=2
public int PregapMode;
/// <summary>
/// this is probably a 0 or 1 bool
/// </summary>
public int PregapSubcode;
}
public class CCDParseException : Exception
{
public CCDParseException(string message) : base(message) { }
}
class CCDSection : Dictionary<string,int>
{
public string Name;
public int FetchOrDefault(int def, string key)
{
TryGetValue(key, out def);
return def;
}
public int FetchOrFail(string key)
{
int ret;
if(!TryGetValue(key, out ret))
throw new CCDParseException("Malformed or unexpected CCD format: missing required [Entry] key: " + key);
return ret;
}
}
List<CCDSection> ParseSections(Stream stream)
{
List<CCDSection> sections = new List<CCDSection>();
//TODO - do we need to attempt to parse out the version tag in a first pass?
//im doing this from a version 3 example
StreamReader sr = new StreamReader(stream);
CCDSection currSection = null;
for (; ; )
{
var line = sr.ReadLine();
if (line == null) break;
if (line == "") continue;
if (line.StartsWith("["))
{
currSection = new CCDSection();
currSection.Name = line.Trim('[', ']').ToUpper();
sections.Add(currSection);
}
else
{
var parts = line.Split('=');
if (parts.Length != 2)
throw new CCDParseException("Malformed or unexpected CCD format: parsing item into two parts");
int val;
if (parts[1].StartsWith("0x") || parts[1].StartsWith("0X"))
val = int.Parse(parts[1].Substring(2), NumberStyles.HexNumber);
else val = int.Parse(parts[1]);
currSection[parts[0].ToUpper()] = val;
}
} //loop until lines exhausted
return sections;
}
int PreParseIntegrityCheck(List<CCDSection> sections)
{
if (sections.Count == 0) throw new CCDParseException("Malformed CCD format: no sections");
//we need at least a CloneCD and Disc section
if (sections.Count < 2) throw new CCDParseException("Malformed CCD format: insufficient sections");
var ccdSection = sections[0];
if (ccdSection.Name != "CLONECD")
throw new CCDParseException("Malformed CCD format: confusing first section name");
if (!ccdSection.ContainsKey("VERSION"))
throw new CCDParseException("Malformed CCD format: missing version in CloneCD section");
if(sections[1].Name != "DISC")
throw new CCDParseException("Malformed CCD format: section[1] isn't [Disc]");
int version = ccdSection["VERSION"];
return version;
}
/// <summary>
/// Parses a CCD file contained in the provided stream
/// </summary>
public CCDFile ParseFrom(Stream stream)
{
CCDFile ccdf = new CCDFile();
var sections = ParseSections(stream);
ccdf.Version = PreParseIntegrityCheck(sections);
var discSection = sections[1];
int nTocEntries = discSection["TOCENTRIES"]; //its conceivable that this could be missing
int nSessions = discSection["SESSIONS"]; //its conceivable that this could be missing
ccdf.DataTracksScrambled = discSection.FetchOrDefault(0, "DATATRACKSSCRAMBLED");
ccdf.CDTextLength = discSection.FetchOrDefault(0, "CDTEXTLENGTH");
if (ccdf.DataTracksScrambled==1) throw new CCDParseException("Malformed CCD format: DataTracksScrambled=1 not supported. Please report this, so we can understand what it means.");
for (int i = 2; i < sections.Count; i++)
{
var section = sections[i];
if (section.Name.StartsWith("SESSION"))
{
int sesnum = int.Parse(section.Name.Split(' ')[1]);
CCDSession session = new CCDSession(sesnum);
ccdf.Sessions.Add(session);
if (sesnum != ccdf.Sessions.Count)
throw new CCDParseException("Malformed CCD format: wrong session number in sequence");
session.PregapMode = section.FetchOrDefault(0, "PREGAPMODE");
session.PregapSubcode = section.FetchOrDefault(0, "PREGAPSUBC");
}
else if (section.Name.StartsWith("ENTRY"))
{
int entryNum = int.Parse(section.Name.Split(' ')[1]);
CCDTocEntry entry = new CCDTocEntry(entryNum);
ccdf.TOCEntries.Add(entry);
entry.Session = section.FetchOrFail("SESSION");
entry.Point = section.FetchOrFail("POINT");
entry.ADR = section.FetchOrFail("ADR");
entry.Control = section.FetchOrFail("CONTROL");
entry.TrackNo = section.FetchOrFail("TRACKNO");
entry.AMin = section.FetchOrFail("AMIN");
entry.ASec = section.FetchOrFail("ASEC");
entry.AFrame = section.FetchOrFail("AFRAME");
entry.ALBA = section.FetchOrFail("ALBA");
entry.Zero = section.FetchOrFail("ZERO");
entry.PMin = section.FetchOrFail("PMIN");
entry.PSec = section.FetchOrFail("PSEC");
entry.PFrame = section.FetchOrFail("PFRAME");
entry.PLBA = section.FetchOrFail("PLBA");
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)
throw new CCDParseException("Warning: inconsistency in CCD PLBA vs computed P MSF");
if(entry.Session != 1)
throw new CCDParseException("Malformed CCD format: not yet supporting multi-session files");
}
else if (section.Name.StartsWith("TRACK"))
{
int entryNum = int.Parse(section.Name.Split(' ')[1]);
CCDTrack track = new CCDTrack(entryNum);
ccdf.Tracks.Add(track);
ccdf.TracksByNumber[entryNum] = track;
foreach (var kvp in section)
{
if (kvp.Key == "MODE")
track.Mode = kvp.Value;
if (kvp.Key.StartsWith("INDEX"))
{
int inum = int.Parse(kvp.Key.Split(' ')[1]);
track.Indexes[inum] = kvp.Value;
}
}
}
} //sections loop
return ccdf;
}
public class LoadResults
{
public List<RawTOCEntry> RawTOCEntries;
public CCDFile ParsedCCDFile;
public bool Valid;
public Exception FailureException;
public string ImgPath;
public string SubPath;
public string CcdPath;
public int NumImgSectors;
}
public static LoadResults LoadCCDPath(string path)
{
LoadResults ret = new LoadResults();
ret.CcdPath = path;
ret.ImgPath = Path.ChangeExtension(path, ".img");
ret.SubPath = Path.ChangeExtension(path, ".sub");
try
{
if(!File.Exists(path)) throw new CCDParseException("Malformed CCD format: nonexistent CCD file!");
if (!File.Exists(ret.ImgPath)) throw new CCDParseException("Malformed CCD format: nonexistent IMG file!");
if (!File.Exists(ret.SubPath)) throw new CCDParseException("Malformed CCD format: nonexistent SUB file!");
//quick check of .img and .sub sizes
long imgLen = new FileInfo(ret.ImgPath).Length;
long subLen = new FileInfo(ret.SubPath).Length;
if(imgLen % 2352 != 0) throw new CCDParseException("Malformed CCD format: IMG file length not multiple of 2352");
ret.NumImgSectors = (int)(imgLen / 2352);
if (subLen != ret.NumImgSectors * 96) throw new CCDParseException("Malformed CCD format: SUB file length not matching IMG");
CCDFile ccdf;
using (var infCCD = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
ccdf = new CCD_Format().ParseFrom(infCCD);
ret.ParsedCCDFile = ccdf;
ret.Valid = true;
}
catch (CCDParseException ex)
{
ret.FailureException = ex;
}
return ret;
}
/// <summary>
/// Loads a CCD at the specified path to a Disc object
/// </summary>
public Disc LoadCCDToDisc(string ccdPath)
{
var loadResults = LoadCCDPath(ccdPath);
if (!loadResults.Valid)
throw loadResults.FailureException;
Disc disc = new Disc();
var ccdf = loadResults.ParsedCCDFile;
var imgBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.ImgPath };
var subBlob = new Disc.Blob_RawFile() { PhysicalPath = loadResults.SubPath };
disc.Blobs.Add(imgBlob);
disc.Blobs.Add(subBlob);
//generate DiscTOCRaw items from the ones specified in the CCD file
//TODO - range validate these (too many truncations to byte)
disc.RawTOCEntries = new List<RawTOCEntry>();
BufferedSubcodeSector bss = new BufferedSubcodeSector();
foreach (var entry in ccdf.TOCEntries)
{
var q = new SubchannelQ
{
q_status = SubchannelQ.ComputeStatus(entry.ADR, (EControlQ)(entry.Control & 0xF)),
q_tno = (byte)entry.TrackNo,
q_index = (byte)entry.Point,
min = BCD2.FromDecimal(entry.AMin),
sec = BCD2.FromDecimal(entry.ASec),
frame = BCD2.FromDecimal(entry.AFrame),
zero = (byte)entry.Zero,
ap_min = BCD2.FromDecimal(entry.PMin),
ap_sec = BCD2.FromDecimal(entry.PSec),
ap_frame = BCD2.FromDecimal(entry.PFrame),
};
//CRC cant be calculated til we've got all the fields setup
q.q_crc = bss.Synthesize_SubchannelQ(ref q, true);
disc.RawTOCEntries.Add(new RawTOCEntry { QData = q });
}
//generate the toc from the entries
var tocSynth = new DiscTOCRaw.SynthesizeFromRawTOCEntriesJob() { Entries = disc.RawTOCEntries };
tocSynth.Run();
disc.TOCRaw = tocSynth.Result;
//synthesize DiscStructure
var structureSynth = new DiscStructure.SynthesizeFromDiscTOCRawJob() { TOCRaw = disc.TOCRaw };
structureSynth.Run();
disc.Structure = structureSynth.Result;
//I *think* implicitly there is an index 0.. at.. i dunno, 0 maybe, for track 1
{
var dsi0 = new DiscStructure.Index();
dsi0.LBA = 0;
dsi0.Number = 0;
disc.Structure.Sessions[0].Tracks[0].Indexes.Add(dsi0);
}
//now, how to get the track types for the DiscStructure?
//1. the CCD tells us (somehow the reader has judged)
//2. scan it out of the Q subchannel
//lets choose1.
//TODO - better consider how to handle the situation where we have havent received all the [TRACK] items we need
foreach (var st in disc.Structure.Sessions[0].Tracks)
{
var ccdt = ccdf.TracksByNumber[st.Number];
switch (ccdt.Mode)
{
case 0:
st.TrackType = ETrackType.Audio; //for CCD, this means audio, apparently.
break;
case 1:
st.TrackType = ETrackType.Mode1_2352;
break;
case 2:
st.TrackType = ETrackType.Mode2_2352;
break;
default:
throw new InvalidOperationException("Unsupported CCD mode");
}
//add indexes for this track
foreach (var ccdi in ccdt.Indexes)
{
var dsi = new DiscStructure.Index();
//if (ccdi.Key == 0) continue;
dsi.LBA = ccdi.Value;
dsi.Number = ccdi.Key;
st.Indexes.Add(dsi);
}
}
//add sectors for the lead-in, which isn't stored in the CCD file, I think
//TODO - synthesize lead-in sectors from TOC, if the lead-in isn't available.
//need a test case for that though.
var leadin_sector_zero = new Sector_Zero();
var leadin_subcode_zero = new ZeroSubcodeSector();
for (int i = 0; i < 150; i++)
{
var se = new SectorEntry(leadin_sector_zero);
disc.Sectors.Add(se);
se.SubcodeSector = leadin_subcode_zero;
}
//build the sectors:
//set up as many sectors as we have img/sub for, even if the TOC doesnt reference them (TOC is unreliable, although the tracks should have covered it all)
for (int i = 0; i < loadResults.NumImgSectors; i++)
{
var isec = new Sector_RawBlob();
isec.Offset = ((long)i) * 2352;
isec.Blob = imgBlob;
var se = new SectorEntry(isec);
disc.Sectors.Add(se);
var scsec = new BlobSubcodeSectorPreDeinterleaved();
scsec.Offset = ((long)i) * 96;
scsec.Blob = subBlob;
se.SubcodeSector = scsec;
}
return disc;
}
public void 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

@ -48,11 +48,11 @@ namespace BizHawk.Emulation.DiscSystem
public void PlayTrack(int track)
{
if (track < 1 || track > Disc.TOC.Sessions[0].Tracks.Count)
if (track < 1 || track > Disc.Structure.Sessions[0].Tracks.Count)
return;
StartLBA = Disc.TOC.Sessions[0].Tracks[track - 1].Indexes[1].aba - 150;
EndLBA = StartLBA + Disc.TOC.Sessions[0].Tracks[track - 1].length_aba;
StartLBA = Disc.Structure.Sessions[0].Tracks[track - 1].Indexes[1].aba - 150;
EndLBA = StartLBA + Disc.Structure.Sessions[0].Tracks[track - 1].LengthInSectors;
PlayingTrack = track;
CurrentSector = StartLBA;
SectorOffset = 0;
@ -64,12 +64,12 @@ namespace BizHawk.Emulation.DiscSystem
public void PlayStartingAtLba(int lba)
{
var point = Disc.TOC.SeekPoint(lba);
var point = Disc.Structure.SeekPoint(lba);
if (point == null || point.Track == null) return;
PlayingTrack = point.TrackNum;
StartLBA = lba;
EndLBA = point.Track.Indexes[1].aba + point.Track.length_aba - 150;
EndLBA = point.Track.Indexes[1].aba + point.Track.LengthInSectors - 150;
CurrentSector = StartLBA;
SectorOffset = 0;

View File

@ -7,6 +7,64 @@ using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
public class CUE_Format
{
/// <summary>
/// Generates the CUE file for the provided DiscStructure
/// </summary>
public string GenerateCUE_OneBin(DiscStructure structure, CueBinPrefs prefs)
{
if (prefs.OneBlobPerTrack) throw new InvalidOperationException("OneBinPerTrack passed to GenerateCUE_OneBin");
//this generates a single-file cue!!!!!!! dont expect it to generate bin-per-track!
StringBuilder sb = new StringBuilder();
foreach (var session in structure.Sessions)
{
if (!prefs.SingleSession)
{
//dont want to screw around with sessions for now
sb.AppendFormat("SESSION {0:D2}\n", session.num);
if (prefs.AnnotateCue) sb.AppendFormat("REM ; session (length={0})\n", session.length_aba);
}
foreach (var track in session.Tracks)
{
ETrackType trackType = track.TrackType;
//mutate track type according to our principle of canonicalization
if (trackType == ETrackType.Mode1_2048 && prefs.DumpECM)
trackType = ETrackType.Mode1_2352;
sb.AppendFormat(" TRACK {0:D2} {1}\n", track.Number, Cue.TrackTypeStringForTrackType(trackType));
if (prefs.AnnotateCue) sb.AppendFormat(" REM ; track (length={0})\n", track.LengthInSectors);
foreach (var index in track.Indexes)
{
//cue+bin has an implicit 150 sector pregap which neither the cue nor the bin has any awareness of
//except for the baked-in sector addressing.
//but, if there is an extra-long pregap, we want to reflect it this way
int lba = index.aba - 150;
if (lba <= 0 && index.Number == 0 && track.Number == 1)
{
}
//dont emit index 0 when it is the same as index 1, it is illegal for some reason
else if (index.Number == 0 && index.aba == track.Indexes[1].aba)
{
//dont emit index 0 when it is the same as index 1, it confuses some cue parsers
}
else
{
sb.AppendFormat(" INDEX {0:D2} {1}\n", index.Number, new Timestamp(lba).Value);
}
}
}
}
return sb.ToString();
}
}
partial class Disc
{
/// <summary>
@ -53,8 +111,9 @@ namespace BizHawk.Emulation.DiscSystem
{
//TODO - add cue directory to CueBinPrefs???? could make things cleaner...
var session = new DiscTOC.Session {num = 1};
TOC.Sessions.Add(session);
Structure = new DiscStructure();
var session = new DiscStructure.Session {num = 1};
Structure.Sessions.Add(session);
var pregap_sector = new Sector_Zero();
int curr_track = 1;
@ -190,7 +249,7 @@ namespace BizHawk.Emulation.DiscSystem
}
//validate that the first index in the file is 00:00:00
if (cue_file.Tracks[0].Indexes[0].Timestamp.ABA != 0) throw new Cue.CueBrokenException("`The first index of a blob must start at 00:00:00.`");
if (cue_file.Tracks[0].Indexes[0].Timestamp.Sector != 0) throw new Cue.CueBrokenException("`The first index of a blob must start at 00:00:00.`");
//for each track within the file:
@ -211,14 +270,19 @@ namespace BizHawk.Emulation.DiscSystem
//check integrity of track sequence and setup data structures
//TODO - check for skipped tracks in cue parser instead
if (cue_track.TrackNum != curr_track) throw new Cue.CueBrokenException("Found a cue with skipped tracks");
var toc_track = new DiscTOC.Track();
toc_track.num = curr_track;
toc_track.TrackType = cue_track.TrackType;
var toc_track = new DiscStructure.Track();
session.Tracks.Add(toc_track);
toc_track.Number = curr_track;
toc_track.TrackType = cue_track.TrackType;
//choose a Control value based on
if (toc_track.TrackType == ETrackType.Audio)
toc_track.Control = EControlQ.StereoNoPreEmph;
else toc_track.Control = EControlQ.DataUninterrupted;
if (curr_track == 1)
{
if (cue_track.PreGap.ABA != 0)
if (cue_track.PreGap.Sector != 0)
throw new InvalidOperationException("not supported (yet): cue files with track 1 pregaps");
//but now we add one anyway, because every existing cue+bin seems to implicitly specify this
cue_track.PreGap = new Timestamp(150);
@ -226,9 +290,9 @@ namespace BizHawk.Emulation.DiscSystem
//check whether a pregap is requested.
//this causes empty sectors to get generated without consuming data from the blob
if (cue_track.PreGap.ABA > 0)
if (cue_track.PreGap.Sector > 0)
{
for (int i = 0; i < cue_track.PreGap.ABA; i++)
for (int i = 0; i < cue_track.PreGap.Sector; i++)
{
Sectors.Add(new SectorEntry(pregap_sector));
}
@ -239,7 +303,7 @@ namespace BizHawk.Emulation.DiscSystem
int track_length_aba;
if (t == cue_file.Tracks.Count - 1)
track_length_aba = blob_length_aba - blob_timestamp;
else track_length_aba = cue_file.Tracks[t + 1].Indexes[1].Timestamp.ABA - blob_timestamp;
else track_length_aba = cue_file.Tracks[t + 1].Indexes[1].Timestamp.Sector - blob_timestamp;
//toc_track.length_aba = track_length_aba; //xxx
//find out how many indexes we have
@ -253,11 +317,11 @@ namespace BizHawk.Emulation.DiscSystem
bool is_last_index = index == num_indexes - 1;
//install index into hierarchy
var toc_index = new DiscTOC.Index {num = index};
var toc_index = new DiscStructure.Index {Number = index};
toc_track.Indexes.Add(toc_index);
if (index == 0)
{
toc_index.aba = track_disc_pregap_aba - (cue_track.Indexes[1].Timestamp.ABA - cue_track.Indexes[0].Timestamp.ABA);
toc_index.aba = track_disc_pregap_aba - (cue_track.Indexes[1].Timestamp.Sector - cue_track.Indexes[0].Timestamp.Sector);
}
else toc_index.aba = Sectors.Count;
@ -266,7 +330,7 @@ namespace BizHawk.Emulation.DiscSystem
int index_length_aba;
if (is_last_index)
index_length_aba = track_length_aba - (blob_timestamp - blob_track_start);
else index_length_aba = cue_track.Indexes[index + 1].Timestamp.ABA - blob_timestamp;
else index_length_aba = cue_track.Indexes[index + 1].Timestamp.Sector - blob_timestamp;
//emit sectors
for (int aba = 0; aba < index_length_aba; aba++)
@ -327,14 +391,14 @@ namespace BizHawk.Emulation.DiscSystem
} //index loop
//check whether a postgap is requested. if it is, we need to generate silent sectors
for (int i = 0; i < cue_track.PostGap.ABA; i++)
for (int i = 0; i < cue_track.PostGap.Sector; i++)
{
Sectors.Add(new SectorEntry(pregap_sector));
}
//we're done with the track now.
//record its length:
toc_track.length_aba = Sectors.Count - toc_track.Indexes[1].aba;
toc_track.LengthInSectors = Sectors.Count - toc_track.Indexes[1].aba;
curr_track++;
//if we ran off the end of the blob, pad it with zeroes, I guess
@ -350,8 +414,8 @@ namespace BizHawk.Emulation.DiscSystem
//finally, analyze the length of the sessions and the entire disc by summing the lengths of the tracks
//this is a little more complex than it looks, because the length of a thing is not determined by summing it
//but rather by the difference in abas between start and end
TOC.length_aba = 0;
foreach (var toc_session in TOC.Sessions)
Structure.LengthInSectors = 0;
foreach (var toc_session in Structure.Sessions)
{
var firstTrack = toc_session.Tracks[0];
@ -359,8 +423,8 @@ namespace BizHawk.Emulation.DiscSystem
//firstTrack.Indexes[0].aba -= 150;
var lastTrack = toc_session.Tracks[toc_session.Tracks.Count - 1];
session.length_aba = lastTrack.Indexes[1].aba + lastTrack.length_aba - firstTrack.Indexes[0].aba;
TOC.length_aba += toc_session.length_aba;
session.length_aba = lastTrack.Indexes[1].aba + lastTrack.LengthInSectors - firstTrack.Indexes[0].aba;
Structure.LengthInSectors += toc_session.length_aba;
}
}
@ -469,6 +533,10 @@ namespace BizHawk.Emulation.DiscSystem
{
public CueTrackIndex(int num) { IndexNum = num; }
public int IndexNum;
/// <summary>
/// Is this an ABA or a LBA? please say.
/// </summary>
public Timestamp Timestamp;
}

View File

@ -63,6 +63,44 @@ namespace BizHawk.Emulation.DiscSystem
}
}
/// <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 = buffer[offset + 1];
sq.q_index = buffer[offset + 2];
sq.min.BCDValue = buffer[offset + 3];
sq.sec.BCDValue = buffer[offset + 4];
sq.frame.BCDValue = buffer[offset + 5];
//nothing in byte[6]
sq.ap_min.BCDValue = buffer[offset + 7];
sq.ap_sec.BCDValue = buffer[offset + 8];
sq.ap_frame.BCDValue = buffer[offset + 9];
//CRC is stored inverted and big endian.. so... do the opposite
byte hibyte = (byte)(~buffer[offset + 10]);
byte lobyte = (byte)(~buffer[offset + 11]);
sq.q_crc = (ushort)((hibyte << 8) | lobyte);
}
Disc disc;
byte[] buffer = new byte[96];
}
/// <summary>
/// Allows you to stream data off a disc
/// </summary>
@ -140,8 +178,7 @@ namespace BizHawk.Emulation.DiscSystem
sealed public partial class Disc
{
/// <summary>
/// Main API to read a 2352-byte sector from a disc.
/// This starts after the mandatory pregap of 2 seconds (but what happens if there is more more?).
/// Main API to read a 2352-byte sector from a disc, identified by LBA.
/// </summary>
public void ReadLBA_2352(int lba, byte[] buffer, int offset)
{
@ -149,26 +186,31 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <summary>
/// Main API to read a 2048-byte sector from a disc.
/// This starts after the mandatory pregap of 2 seconds (but what happens if there is more more?).
/// Main API to read a 2048-byte sector from a disc, identified by LBA.
/// </summary>
public void ReadLBA_2048(int lba, byte[] buffer, int offset)
{
ReadABA_2048(lba + 150, buffer, offset);
}
/// <summary>
/// Main API to read a 2352-byte sector from a disc, identified by ABA
/// </summary>
public void ReadABA_2352(int aba, byte[] buffer, int offset)
{
Sectors[aba].Sector.Read_2352(buffer, offset);
}
/// <summary>
/// Main API to read a 2048-byte sector from a disc, identified by ABA
/// </summary>
public void ReadABA_2048(int aba, byte[] buffer, int offset)
{
Sectors[aba].Sector.Read_2048(buffer, offset);
}
/// <summary>
/// reads logical data from a flat disc address space
/// reads logical data from a flat (logical) disc address space. disc_offset=0 represents LBA=0.
/// useful for plucking data from a known location on the disc
/// </summary>
public void ReadLBA_2352_Flat(long disc_offset, byte[] buffer, int offset, int length)
@ -180,7 +222,7 @@ namespace BizHawk.Emulation.DiscSystem
}
/// <summary>
/// reads logical data from a flat disc address space
/// reads logical data from a flat (absolute) disc address space. disc_offset=0 represents LBA=0.
/// useful for plucking data from a known location on the disc
/// </summary>
public void ReadLBA_2048_Flat(long disc_offset, byte[] buffer, int offset, int length)
@ -191,7 +233,7 @@ namespace BizHawk.Emulation.DiscSystem
READLBA_Flat_Implementation(disc_offset, buffer, offset, length, (a, b, c) => ReadLBA_2048(a, b, c), secsize, lba_buf, ref sectorHint);
}
public 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)
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
while (length > 0)
@ -221,25 +263,6 @@ namespace BizHawk.Emulation.DiscSystem
return Sectors[lba + 150];
}
/// <summary>
/// Reads the specified LBA's subcode (96 bytes) deinterleaved into the provided buffer.
/// P is first 12 bytes, followed by 12 Q bytes, etc.
/// I'm not sure what format scsi commands generally return it in.
/// It could be this, or RAW (interleaved) which I could also supply when we need it
/// </summary>
public void ReadLBA_Subcode_Deinterleaved(int lba, byte[] buffer, int offset)
{
Array.Clear(buffer, offset, 96);
Sectors[lba + 150].Read_SubchannelQ(buffer, offset + 12);
}
/// <summary>
/// Reads the specified LBA's subchannel Q (12 bytes) into the provided buffer
/// </summary>
public void ReadLBA_Subchannel_Q(int lba, byte[] buffer, int offset)
{
Sectors[lba + 150].Read_SubchannelQ(buffer, offset);
}
/// <summary>
/// Main API to determine how many LBAs are available on the disc.
@ -260,11 +283,13 @@ namespace BizHawk.Emulation.DiscSystem
public bool WasSlowLoad { get; private set; }
/// <summary>
/// main api for reading the TOC from a disc
/// 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 DiscTOC ReadTOC()
public DiscStructure ReadStructure()
{
return TOC;
return Structure;
}
// converts LBA to minute:second:frame format.
@ -285,16 +310,17 @@ namespace BizHawk.Emulation.DiscSystem
// 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];
foreach (var track in TOC.Sessions[0].Tracks)
foreach (var track in Structure.Sessions[0].Tracks)
{
if (track.TrackType == ETrackType.Audio)
continue;
int lba_len = Math.Min(track.length_aba, 512);
for (int s = 0; s < 512 && s < track.length_aba; s++)
int lba_len = Math.Min(track.LengthInSectors, 512);
for (int s = 0; s < 512 && s < track.LengthInSectors; s++)
ReadABA_2352(track.Indexes[1].aba + s, buffer, s * 2352);
return buffer.HashMD5(0, lba_len * 2352);

View File

@ -66,411 +66,34 @@ namespace BizHawk.Emulation.DiscSystem
{
public partial class Disc : IDisposable
{
public interface ISector
{
/// <summary>
/// reads the entire sector, raw
/// </summary>
int Read_2352(byte[] buffer, int offset);
/// <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
/// </summary>
int Read_2048(byte[] buffer, int offset);
}
/// <summary>
/// The raw TOC entries found in the lead-in track.
/// </summary>
public List<RawTOCEntry> RawTOCEntries = new List<RawTOCEntry>();
/// <summary>
/// Presently, an IBlob doesn't need to work multithreadedly. It's quite an onerous demand.
/// This should probably be managed by the Disc class somehow, or by the user making another Disc.
/// The DiscTOCRaw corresponding to the RawTOCEntries
/// </summary>
public interface IBlob : IDisposable
{
/// <summary>
/// what a weird parameter order. normally the dest buffer would be first. weird.
/// </summary>
/// <param name="byte_pos">location in the blob to read from</param>
/// <param name="buffer">destination buffer for read data</param>
/// <param name="offset">offset into destination buffer</param>
/// <param name="count">amount to read</param>
int Read(long byte_pos, byte[] buffer, int offset, int count);
}
public sealed class Blob_ZeroPadAdapter : IBlob
{
public Blob_ZeroPadAdapter(IBlob baseBlob, long padFrom, long padLen)
{
this.baseBlob = baseBlob;
this.padFrom = padFrom;
this.padLen = padLen;
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
throw new NotImplementedException("Blob_ZeroPadAdapter hasnt been tested yet! please report this!");
//something about this seems unnecessarily complex, but... i dunno.
/*
//figure out how much remains until the zero-padding begins
long remain = byte_pos - padFrom;
int todo;
if (remain < count)
todo = (int)remain;
else todo = count;
//read up until the zero-padding
int totalRead = 0;
int readed = baseBlob.Read(byte_pos, buffer, offset, todo);
totalRead += readed;
offset += todo;
//if we didnt read enough, we certainly shouldnt try to read any more
if (readed < todo)
return readed;
//if that was all we needed, then we're done
count -= todo;
if (count == 0)
return totalRead;
//if we need more, it must come from zero-padding
remain = padLen;
if (remain < count)
todo = (int)remain;
else todo = count;
Array.Clear(buffer, offset, todo);
totalRead += todo;
return totalRead;
*/
}
public void Dispose()
{
baseBlob.Dispose();
}
private readonly IBlob baseBlob;
private long padFrom;
private long padLen;
}
private class Blob_RawFile : IBlob
{
public string PhysicalPath {
get
{
return physicalPath;
}
set
{
physicalPath = value;
length = new FileInfo(physicalPath).Length;
}
}
string physicalPath;
long length;
public long Offset = 0;
BufferedStream fs;
public void Dispose()
{
if (fs != null)
{
fs.Dispose();
fs = null;
}
}
public int Read(long byte_pos, byte[] buffer, int offset, int count)
{
//use quite a large buffer, because normally we will be reading these sequentially but in small chunks.
//this enhances performance considerably
const int buffersize = 2352 * 75 * 2;
if (fs == null)
fs = new BufferedStream(new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read), buffersize);
long target = byte_pos + Offset;
if(fs.Position != target)
fs.Position = target;
return fs.Read(buffer, offset, count);
}
public long Length
{
get
{
return length;
}
}
}
public DiscTOCRaw TOCRaw;
/// <summary>
/// this ISector is dumb and only knows how to drag chunks off a source blob
/// The DiscStructure corresponding the the TOCRaw
/// </summary>
class Sector_RawBlob : ISector
{
public IBlob Blob;
public long Offset;
public int Read_2352(byte[] buffer, int offset)
{
return Blob.Read(Offset, buffer, offset, 2352);
}
public int Read_2048(byte[] buffer, int offset)
{
return Blob.Read(Offset, buffer, offset, 2048);
}
}
public DiscStructure Structure;
/// <summary>
/// this ISector always returns zeroes
/// The blobs mounted by this disc for supplying binary content
/// </summary>
class Sector_Zero : ISector
{
public int Read_2352(byte[] buffer, int offset)
{
Array.Clear(buffer, 0, 2352);
return 2352;
}
public int Read_2048(byte[] buffer, int offset)
{
Array.Clear(buffer, 0, 2048);
return 2048;
}
}
// ------ replaced by Blob_ZeroPadAdapter
///// <summary>
///// this ISector adapts another ISector by always returning zeroes
///// TODO I dont like the way this works. I think blobs should get adapted instead to zero-pad to a certain length.
///// </summary>
//class Sector_ZeroPad : ISector
//{
// public ISector BaseSector;
// public int BaseLength;
// public int Read(byte[] buffer, int offset)
// {
// return _Read(buffer, offset, 2352);
// }
// public int Read_2048(byte[] buffer, int offset)
// {
// return _Read(buffer, offset, 2352);
// }
// int _Read(byte[] buffer, int offset, int amount)
// {
// int read = BaseSector.Read(buffer, offset);
// if(read < BaseLength) return read;
// for (int i = BaseLength; i < amount; i++)
// buffer[offset + i] = 0;
// return amount;
// }
//}
abstract class Sector_Mode1_or_Mode2_2352 : ISector
{
public ISector BaseSector;
public abstract int Read_2352(byte[] buffer, int offset);
public abstract int Read_2048(byte[] buffer, int offset);
}
/// <summary>
/// This ISector is a raw MODE1 sector
/// </summary>
class Sector_Mode1_2352 : Sector_Mode1_or_Mode2_2352
{
public override int Read_2352(byte[] buffer, int offset)
{
return BaseSector.Read_2352(buffer, offset);
}
public override int Read_2048(byte[] buffer, int offset)
{
//to get 2048 bytes out of this sector type, start 16 bytes in
int ret = BaseSector.Read_2352(TempSector, 0);
Buffer.BlockCopy(TempSector, 16, buffer, offset, 2048);
System.Diagnostics.Debug.Assert(buffer != TempSector);
return 2048;
}
[ThreadStatic]
static byte[] TempSector = new byte[2352];
}
/// <summary>
/// this ISector is a raw MODE2 sector. could be form 0,1,2... who can say? supposedly:
/// To tell the different Mode 2s apart you have to examine bytes 16-23 of the sector (the first 8 bytes of Mode Data).
/// If bytes 16-19 are not the same as 20-23, then it is Mode 2. If they are equal and bit 5 is on (0x20), then it is Mode 2 Form 2. Otherwise it is Mode 2 Form 1.
/// ...but we're not using this information in any way
/// </summary>
class Sector_Mode2_2352 : Sector_Mode1_or_Mode2_2352
{
public override int Read_2352(byte[] buffer, int offset)
{
return BaseSector.Read_2352(buffer, offset);
}
public override int Read_2048(byte[] buffer, int offset)
{
//to get 2048 bytes out of this sector type, start 24 bytes in
int ret = BaseSector.Read_2352(TempSector, 0);
Buffer.BlockCopy(TempSector, 24, buffer, offset, 2048);
System.Diagnostics.Debug.Assert(buffer != TempSector);
return 2048;
}
[ThreadStatic]
static byte[] TempSector = new byte[2352];
}
private static byte BCD_Byte(byte val)
{
byte ret = (byte)(val % 10);
ret += (byte)(16 * (val / 10));
return ret;
}
//a blob that also has an ECM cache associated with it. maybe one day.
class ECMCacheBlob
{
public ECMCacheBlob(IBlob blob)
{
BaseBlob = blob;
}
public IBlob BaseBlob;
}
/// <summary>
/// this ISector is a MODE1 sector that is generating itself from an underlying MODE1/2048 userdata piece
/// </summary>
class Sector_Mode1_2048 : ISector
{
public Sector_Mode1_2048(int ABA)
{
byte aba_min = (byte)(ABA / 60 / 75);
byte aba_sec = (byte)((ABA / 75) % 60);
byte aba_frac = (byte)(ABA % 75);
bcd_aba_min = BCD_Byte(aba_min);
bcd_aba_sec = BCD_Byte(aba_sec);
bcd_aba_frac = BCD_Byte(aba_frac);
}
byte bcd_aba_min, bcd_aba_sec, bcd_aba_frac;
public ECMCacheBlob Blob;
public long Offset;
byte[] extra_data;
bool has_extra_data;
public int Read_2048(byte[] buffer, int offset)
{
//this is easy. we only have 2048 bytes, and 2048 bytes were requested
return Blob.BaseBlob.Read(Offset, buffer, offset, 2048);
}
public int Read_2352(byte[] buffer, int offset)
{
//user data
int read = Blob.BaseBlob.Read(Offset, buffer, offset + 16, 2048);
//if we read the 2048 physical bytes OK, then return the complete sector
if (read == 2048 && has_extra_data)
{
Buffer.BlockCopy(extra_data, 0, buffer, offset, 16);
Buffer.BlockCopy(extra_data, 16, buffer, offset + 2064, 4 + 8 + 172 + 104);
return 2352;
}
//sync
buffer[offset + 0] = 0x00; buffer[offset + 1] = 0xFF; buffer[offset + 2] = 0xFF; buffer[offset + 3] = 0xFF;
buffer[offset + 4] = 0xFF; buffer[offset + 5] = 0xFF; buffer[offset + 6] = 0xFF; buffer[offset + 7] = 0xFF;
buffer[offset + 8] = 0xFF; buffer[offset + 9] = 0xFF; buffer[offset + 10] = 0xFF; buffer[offset + 11] = 0x00;
//sector address
buffer[offset + 12] = bcd_aba_min;
buffer[offset + 13] = bcd_aba_sec;
buffer[offset + 14] = bcd_aba_frac;
//mode 1
buffer[offset + 15] = 1;
//calculate EDC and poke into the sector
uint edc = ECM.EDC_Calc(buffer, offset, 2064);
ECM.PokeUint(buffer, 2064, edc);
//intermediate
for (int i = 0; i < 8; i++) buffer[offset + 2068 + i] = 0;
//ECC
ECM.ECC_Populate(buffer, offset, buffer, offset, false);
//VALIDATION - check our homemade algorithms against code derived from ECM
////EDC
//GPL_ECM.edc_validateblock(buffer, 2064, buffer, offset + 2064);
////ECC
//GPL_ECM.ecc_validate(buffer, offset, false);
//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];
Buffer.BlockCopy(buffer, 0, extra_data, 0, 16);
Buffer.BlockCopy(buffer, 2064, extra_data, 16, 4 + 8 + 172 + 104);
has_extra_data = true;
return 2352;
}
//otherwise, return a smaller value to indicate an error
else return read;
}
}
//this is a physical 2352 byte sector.
public class SectorEntry
{
public SectorEntry(ISector sec) { Sector = sec; }
public ISector Sector;
//todo - add some PARAMETER fields to this, so that the ISector can use them (so that each ISector doesnt have to be constructed also)
//also then, maybe this could be a struct
//q-subchannel stuff. can be returned directly, or built into the entire subcode sector if you want
/// <summary>
/// ADR and CONTROL
/// </summary>
public byte q_status;
/// <summary>
/// BCD indications of the current track number and index
/// </summary>
public BCD2 q_tno, q_index;
/// <summary>
/// track-relative timestamp
/// </summary>
public BCD2 q_min, q_sec, q_frame;
/// <summary>
/// absolute timestamp
/// </summary>
public BCD2 q_amin, q_asec, q_aframe;
public void Read_SubchannelQ(byte[] buffer, int offset)
{
buffer[offset + 0] = q_status;
buffer[offset + 1] = q_tno.BCDValue;
buffer[offset + 2] = q_index.BCDValue;
buffer[offset + 3] = q_min.BCDValue;
buffer[offset + 4] = q_sec.BCDValue;
buffer[offset + 5] = q_frame.BCDValue;
buffer[offset + 6] = 0;
buffer[offset + 7] = q_amin.BCDValue;
buffer[offset + 8] = q_asec.BCDValue;
buffer[offset + 9] = q_aframe.BCDValue;
ushort crc16 = CRC16_CCITT.Calculate(buffer, 0, 10);
//CRC is stored inverted and big endian
buffer[offset + 10] = (byte)(~(crc16 >> 8));
buffer[offset + 11] = (byte)(~(crc16));
}
}
public List<IBlob> Blobs = new List<IBlob>();
/// <summary>
/// The sectors on the disc
/// </summary>
public List<SectorEntry> Sectors = new List<SectorEntry>();
public DiscTOC TOC = new DiscTOC();
public Disc()
{
}
public void Dispose()
{
@ -494,46 +117,12 @@ FILE ""xarp.barp.marp.farp"" BINARY
CueFileResolver["xarp.barp.marp.farp"] = isoPath;
cue.LoadFromString(isoCueWrapper);
FromCueInternal(cue, cueDir, new CueBinPrefs());
//var session = new DiscTOC.Session();
//session.num = 1;
//TOC.Sessions.Add(session);
//var track = new DiscTOC.Track();
//track.num = 1;
//session.Tracks.Add(track);
//var index = new DiscTOC.Index();
//index.num = 0;
//track.Indexes.Add(index);
//index = new DiscTOC.Index();
//index.num = 1;
//track.Indexes.Add(index);
//var fiIso = new FileInfo(isoPath);
//Blob_RawFile blob = new Blob_RawFile();
//blob.PhysicalPath = fiIso.FullName;
//Blobs.Add(blob);
//int num_aba = (int)(fiIso.Length / 2048);
//track.length_aba = num_aba;
//if (fiIso.Length % 2048 != 0)
// throw new InvalidOperationException("invalid iso file (size not multiple of 2048)");
////TODO - handle this with Final Fantasy 9 cd1.iso
//var ecmCacheBlob = new ECMCacheBlob(blob);
//for (int i = 0; i < num_aba; i++)
//{
// Sector_Mode1_2048 sector = new Sector_Mode1_2048(i+150);
// sector.Blob = ecmCacheBlob;
// sector.Offset = i * 2048;
// Sectors.Add(new SectorEntry(sector));
//}
//TOC.AnalyzeLengthsFromIndexLengths();
}
public CueBin DumpCueBin(string baseName, CueBinPrefs prefs)
{
if (TOC.Sessions.Count > 1)
if (Structure.Sessions.Count > 1)
throw new NotSupportedException("can't dump cue+bin with more than 1 session yet");
CueBin ret = new CueBin();
@ -543,14 +132,14 @@ FILE ""xarp.barp.marp.farp"" BINARY
if (!prefs.OneBlobPerTrack)
{
//this is the preferred mode of dumping things. we will always write full sectors.
string cue = TOC.GenerateCUE_OneBin(prefs);
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;
//skip the mandatory track 1 pregap! cue+bin files do not contain it
for (int i = 150; i < TOC.length_aba; i++)
for (int i = 150; i < Structure.LengthInSectors; i++)
{
bfd.abas.Add(i);
bfd.aba_zeros.Add(false);
@ -558,15 +147,15 @@ FILE ""xarp.barp.marp.farp"" BINARY
}
else
{
//we build our own cue here (unlike above) because we need to build the cue and the output dat aat the same time
//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 < TOC.Sessions[0].Tracks.Count; i++)
for (int i = 0; i < Structure.Sessions[0].Tracks.Count; i++)
{
var track = TOC.Sessions[0].Tracks[i];
var track = Structure.Sessions[0].Tracks[i];
var bfd = new CueBin.BinFileDescriptor
{
name = baseName + string.Format(" (Track {0:D2}).bin", track.num),
name = baseName + string.Format(" (Track {0:D2}).bin", track.Number),
SectorSize = Cue.BINSectorSizeForTrackType(track.TrackType)
};
ret.bins.Add(bfd);
@ -575,7 +164,7 @@ FILE ""xarp.barp.marp.farp"" BINARY
//skip the mandatory track 1 pregap! cue+bin files do not contain it
if (i == 0) aba = 150;
for (; aba < track.length_aba; aba++)
for (; aba < track.LengthInSectors; aba++)
{
int thisaba = track.Indexes[0].aba + aba;
bfd.abas.Add(thisaba);
@ -583,11 +172,11 @@ FILE ""xarp.barp.marp.farp"" BINARY
}
sbCue.AppendFormat("FILE \"{0}\" BINARY\n", bfd.name);
sbCue.AppendFormat(" TRACK {0:D2} {1}\n", track.num, Cue.TrackTypeStringForTrackType(track.TrackType));
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.num == 0 && index.aba == track.Indexes[1].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
}
@ -599,7 +188,7 @@ FILE ""xarp.barp.marp.farp"" BINARY
{
//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.num, new Timestamp(x).Value);
sbCue.AppendFormat(" INDEX {0:D2} {1}\n", index.Number, new Timestamp(x).Value);
}
}
}
@ -610,32 +199,22 @@ FILE ""xarp.barp.marp.farp"" BINARY
return ret;
}
/// <summary>
/// NOT USED RIGHT NOW. AMBIGUOUS, ANYWAY.
/// "bin" is an ill-defined concept.
/// </summary>
[Obsolete]
void DumpBin_2352(string binPath)
{
byte[] temp = new byte[2352];
//a cue's bin probably doesn't contain the first 150 sectors, so skip it
using (FileStream fs = new FileStream(binPath, FileMode.Create, FileAccess.Write, FileShare.None))
for (int i = 150; i < Sectors.Count; i++)
{
ReadLBA_2352(i, temp, 0);
fs.Write(temp, 0, 2352);
}
}
public static Disc FromCuePath(string cuePath, CueBinPrefs prefs)
{
var ret = new Disc();
ret.FromCuePathInternal(cuePath, prefs);
ret.TOC.GeneratePoints();
ret.PopulateQSubchannel();
ret.Structure.Synthesize_TOCPointsFromSessions();
ret.Synthesize_SubcodeFromCurrentTOC();
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?
/// </summary>
@ -643,55 +222,59 @@ FILE ""xarp.barp.marp.farp"" BINARY
{
var ret = new Disc();
ret.FromIsoPathInternal(isoPath);
ret.TOC.GeneratePoints();
ret.PopulateQSubchannel();
ret.Structure.Synthesize_TOCPointsFromSessions();
ret.Synthesize_SubcodeFromCurrentTOC();
return ret;
}
/// <summary>
/// creates subchannel Q data track for this disc
/// Creates the subcode (really, just subchannel Q) for this disc from its current TOC.
/// Depends on the TOCPoints existing in the structure
/// </summary>
void PopulateQSubchannel()
void Synthesize_SubcodeFromCurrentTOC()
{
int aba = 0;
int dpIndex = 0;
while (aba < Sectors.Count)
{
if (dpIndex < TOC.Points.Count - 1)
if (dpIndex < Structure.Points.Count - 1)
{
if (aba >= TOC.Points[dpIndex + 1].ABA)
if (aba >= Structure.Points[dpIndex + 1].ABA)
{
dpIndex++;
}
}
var dp = TOC.Points[dpIndex];
var dp = Structure.Points[dpIndex];
var se = Sectors[aba];
int control = 0;
//choose a control byte depending on whether this is an audio or data track
if(dp.Track.TrackType == ETrackType.Audio)
control = (int)Q_Control.StereoNoPreEmph;
else control = (int)Q_Control.DataUninterrupted;
EControlQ control = EControlQ.None;
//we always use ADR=1 (mode-1 q block)
//this could be more sophisticated but it is almost useless for emulation (only useful for catalog/ISRC numbers)
int adr = 1;
se.q_status = (byte)(adr | (control << 4));
se.q_tno = BCD2.FromDecimal(dp.TrackNum);
se.q_index = BCD2.FromDecimal(dp.IndexNum);
SubchannelQ sq = new SubchannelQ();
sq.q_status = SubchannelQ.ComputeStatus(adr, control);
sq.q_tno = (byte)dp.TrackNum;
sq.q_index = (byte)dp.IndexNum;
int track_relative_aba = aba - dp.Track.Indexes[1].aba;
track_relative_aba = Math.Abs(track_relative_aba);
Timestamp track_relative_timestamp = new Timestamp(track_relative_aba);
se.q_min = BCD2.FromDecimal(track_relative_timestamp.MIN);
se.q_sec = BCD2.FromDecimal(track_relative_timestamp.SEC);
se.q_frame = BCD2.FromDecimal(track_relative_timestamp.FRAC);
sq.min = BCD2.FromDecimal(track_relative_timestamp.MIN);
sq.sec = BCD2.FromDecimal(track_relative_timestamp.SEC);
sq.frame = BCD2.FromDecimal(track_relative_timestamp.FRAC);
sq.zero = 0;
Timestamp absolute_timestamp = new Timestamp(aba);
se.q_amin = BCD2.FromDecimal(absolute_timestamp.MIN);
se.q_asec = BCD2.FromDecimal(absolute_timestamp.SEC);
se.q_aframe = BCD2.FromDecimal(absolute_timestamp.FRAC);
sq.ap_min = BCD2.FromDecimal(absolute_timestamp.MIN);
sq.ap_sec = BCD2.FromDecimal(absolute_timestamp.SEC);
sq.ap_frame = BCD2.FromDecimal(absolute_timestamp.FRAC);
var bss = new BufferedSubcodeSector();
bss.Synthesize_SubchannelQ(ref sq, true);
se.SubcodeSector = bss;
aba++;
}
@ -703,19 +286,6 @@ FILE ""xarp.barp.marp.farp"" BINARY
int tens = Math.DivRem(n,10,out ones);
return (byte)((tens<<4)|ones);
}
private enum Q_Control
{
StereoNoPreEmph = 0,
StereoPreEmph = 1,
MonoNoPreemph = 8,
MonoPreEmph = 9,
DataUninterrupted = 4,
DataIncremental = 5,
CopyProhibitedMask = 0,
CopyPermittedMask = 2,
}
}
/// <summary>
@ -755,52 +325,95 @@ FILE ""xarp.barp.marp.farp"" BINARY
}
}
public class Timestamp
public struct Timestamp
{
/// <summary>
/// creates timestamp of 00:00:00
/// </summary>
public Timestamp()
{
Value = "00:00:00";
}
/// <summary>
/// creates a timestamp from a string in the form mm:ss:ff
/// </summary>
public Timestamp(string value)
{
Value = value;
//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));
ABA = MIN * 60 * 75 + SEC * 75 + FRAC;
Sector = MIN * 60 * 75 + SEC * 75 + FRAC;
_value = null;
}
public readonly string Value;
public readonly int MIN, SEC, FRAC, ABA;
public readonly int MIN, SEC, FRAC, Sector;
public string Value
{
get
{
if (_value != null) return _value;
return _value = string.Format("{0:D2}:{1:D2}:{2:D2}", MIN, SEC, FRAC);
}
}
string _value;
/// <summary>
/// creates timestamp from supplied ABA
/// creates timestamp from supplies MSF
/// </summary>
public Timestamp(int ABA)
public Timestamp(int m, int s, int f)
{
this.ABA = ABA;
MIN = ABA / (60 * 75);
SEC = (ABA / 75) % 60;
FRAC = ABA % 75;
Value = string.Format("{0:D2}:{1:D2}:{2:D2}", MIN, SEC, FRAC);
MIN = m;
SEC = s;
FRAC = f;
Sector = MIN * 60 * 75 + SEC * 75 + FRAC;
_value = null;
}
/// <summary>
/// creates timestamp from supplied SectorNumber
/// </summary>
public Timestamp(int SectorNumber)
{
this.Sector = SectorNumber;
MIN = SectorNumber / (60 * 75);
SEC = (SectorNumber / 75) % 60;
FRAC = SectorNumber % 75;
_value = null;
}
}
/// <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
{
/// <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
}
/// <summary>
/// TODO - this is garbage. It's half input related, and half output related. This needs to be split up.
/// </summary>
public class CueBinPrefs
{
/// <summary>
@ -935,7 +548,7 @@ FILE ""xarp.barp.marp.farp"" BINARY
public void Dump(string directory, CueBinPrefs prefs, ProgressReport progress)
{
byte[] subQ_temp = new byte[12];
byte[] subcodeTemp = new byte[96];
progress.TaskCount = 2;
progress.Message = "Generating Cue";
@ -993,8 +606,8 @@ FILE ""xarp.barp.marp.farp"" BINARY
//write subQ if necessary
if (fsSubQ != null)
{
disc.Sectors[aba].Read_SubchannelQ(subQ_temp, 0);
fsSubQ.Write(subQ_temp, 0, 12);
disc.Sectors[aba].SubcodeSector.ReadSubcodeDeinterleaved(subcodeTemp, 0);
fsSubQ.Write(subcodeTemp, 12, 12);
}
}
}

View File

@ -4,18 +4,32 @@ using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// The TOC contains information directly describing the structure of the disc, as well as some more logically structured information derived from that.
/// Additionally, this class contains methods to synchronize between them.
/// This is a bit weird.. Perhaps the different views of the data should be seaprated.
///
/// One final caveat - I think the TOC should be independent for independent sessions.. the concept of multi-sessioning is so thoroughly ill-supported here, it will require radical renovation ever to support.
///
/// Sessions -> Tracks : These are the highest-level data structures of the disc. We should probably rename this to something like DiscStructure, and save DiscTOC for the actual TOC.
/// TOCPoint : These are the basic logical unit of data stored in the TOC. They're basically bookmarks for tracks.
/// TOCEntry : These are the basic unit of data in the rawest view of the TOC. They're stored in the lead-in Q subchannel, and there are multiple redundant copies, and they store several different types of information.
/// </summary>
public class DiscTOC
{
/// <summary>
/// Sessions contained in the disc. Right now support for anything other than 1 session is totally not working
/// Right now support for anything other than 1 session is totally not working
/// </summary>
public List<Session> Sessions = new List<Session>();
/// <summary>
/// this is an unfinished concept of "TOC Points" which is sometimes more convenient way for organizing the disc contents
/// List of Points described by the TOC
/// </summary>
public List<TOCPoint> Points = new List<TOCPoint>();
/// <summary>
/// Todo - comment about what this actually means
/// TODO - this is redundant with Sectors.Count
@ -66,11 +80,10 @@ namespace BizHawk.Emulation.DiscSystem
}
}
/// <summary>
/// Generates the Points list from the current TOC
/// Generates the Points list from the current logical TOC
/// </summary>
public void GeneratePoints()
public void SynthesizeTOCPointsFromSessions()
{
int num = 0;
Points.Clear();

View File

@ -0,0 +1,38 @@
using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Represents a TOC entry discovered in the Q subchannel data of the lead-in track.
/// 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
{
public SubchannelQ QData;
}
/// <summary>
/// Main unit of organization for reading data from the disc. Represents one physical disc sector.
/// </summary>
public class SectorEntry
{
public SectorEntry(ISector sec) { Sector = sec; }
/// <summary>
/// Access the --whatsitcalled-- normal data for the sector with this
/// </summary>
public ISector Sector;
/// <summary>
/// Access the subcode data for the sector
/// </summary>
public ISubcodeSector SubcodeSector;
//todo - add a PARAMETER fields to this (a long, maybe) so that the ISector can use them (so that each ISector doesnt have to be constructed also)
//also then, maybe this could be a struct
}
}

View File

@ -0,0 +1,16 @@
namespace BizHawk.Emulation.DiscSystem
{
public static class DiscUtils
{
/// <summary>
/// converts the given byte to a BCD value
/// </summary>
public static byte BCD_Byte(this byte val)
{
byte ret = (byte)(val % 10);
ret += (byte)(16 * (val / 10));
return ret;
}
}
}

View File

@ -0,0 +1,199 @@
using System;
using System.IO;
namespace BizHawk.Emulation.DiscSystem
{
public interface ISector
{
/// <summary>
/// reads the entire sector, raw
/// </summary>
int Read_2352(byte[] buffer, int offset);
/// <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
/// </summary>
int Read_2048(byte[] buffer, int offset);
}
/// <summary>
/// this ISector is dumb and only knows how to drag chunks off a source blob
/// </summary>
public class Sector_RawBlob : ISector
{
public IBlob Blob;
public long Offset;
public int Read_2352(byte[] buffer, int offset)
{
return Blob.Read(Offset, buffer, offset, 2352);
}
public int Read_2048(byte[] buffer, int offset)
{
return Blob.Read(Offset, buffer, offset, 2048);
}
}
/// <summary>
/// this ISector always returns zeroes
/// </summary>
class Sector_Zero : ISector
{
public int Read_2352(byte[] buffer, int offset)
{
Array.Clear(buffer, 0, 2352);
return 2352;
}
public int Read_2048(byte[] buffer, int offset)
{
Array.Clear(buffer, 0, 2048);
return 2048;
}
}
abstract class Sector_Mode1_or_Mode2_2352 : ISector
{
public ISector BaseSector;
public abstract int Read_2352(byte[] buffer, int offset);
public abstract int Read_2048(byte[] buffer, int offset);
}
/// <summary>
/// This ISector is a raw MODE1 sector
/// </summary>
class Sector_Mode1_2352 : Sector_Mode1_or_Mode2_2352
{
public override int Read_2352(byte[] buffer, int offset)
{
return BaseSector.Read_2352(buffer, offset);
}
public override int Read_2048(byte[] buffer, int offset)
{
//to get 2048 bytes out of this sector type, start 16 bytes in
int ret = BaseSector.Read_2352(TempSector, 0);
Buffer.BlockCopy(TempSector, 16, buffer, offset, 2048);
System.Diagnostics.Debug.Assert(buffer != TempSector);
return 2048;
}
[ThreadStatic]
static byte[] TempSector = new byte[2352];
}
/// <summary>
/// this ISector is a raw MODE2 sector. could be form 0,1,2... who can say? supposedly:
/// To tell the different Mode 2s apart you have to examine bytes 16-23 of the sector (the first 8 bytes of Mode Data).
/// If bytes 16-19 are not the same as 20-23, then it is Mode 2. If they are equal and bit 5 is on (0x20), then it is Mode 2 Form 2. Otherwise it is Mode 2 Form 1.
/// ...but we're not using this information in any way
/// </summary>
class Sector_Mode2_2352 : Sector_Mode1_or_Mode2_2352
{
public override int Read_2352(byte[] buffer, int offset)
{
return BaseSector.Read_2352(buffer, offset);
}
public override int Read_2048(byte[] buffer, int offset)
{
//to get 2048 bytes out of this sector type, start 24 bytes in
int ret = BaseSector.Read_2352(TempSector, 0);
Buffer.BlockCopy(TempSector, 24, buffer, offset, 2048);
System.Diagnostics.Debug.Assert(buffer != TempSector);
return 2048;
}
[ThreadStatic]
static byte[] TempSector = new byte[2352];
}
//a blob that also has an ECM cache associated with it. maybe one day.
class ECMCacheBlob
{
public ECMCacheBlob(IBlob blob)
{
BaseBlob = blob;
}
public IBlob BaseBlob;
}
/// <summary>
/// this ISector is a MODE1 sector that is generating itself from an underlying MODE1/2048 userdata piece
/// </summary>
class Sector_Mode1_2048 : ISector
{
public Sector_Mode1_2048(int ABA)
{
byte aba_min = (byte)(ABA / 60 / 75);
byte aba_sec = (byte)((ABA / 75) % 60);
byte aba_frac = (byte)(ABA % 75);
bcd_aba_min = aba_min.BCD_Byte();
bcd_aba_sec = aba_sec.BCD_Byte();
bcd_aba_frac = aba_frac.BCD_Byte();
}
byte bcd_aba_min, bcd_aba_sec, bcd_aba_frac;
public ECMCacheBlob Blob;
public long Offset;
byte[] extra_data;
bool has_extra_data;
public int Read_2048(byte[] buffer, int offset)
{
//this is easy. we only have 2048 bytes, and 2048 bytes were requested
return Blob.BaseBlob.Read(Offset, buffer, offset, 2048);
}
public int Read_2352(byte[] buffer, int offset)
{
//user data
int read = Blob.BaseBlob.Read(Offset, buffer, offset + 16, 2048);
//if we read the 2048 physical bytes OK, then return the complete sector
if (read == 2048 && has_extra_data)
{
Buffer.BlockCopy(extra_data, 0, buffer, offset, 16);
Buffer.BlockCopy(extra_data, 16, buffer, offset + 2064, 4 + 8 + 172 + 104);
return 2352;
}
//sync
buffer[offset + 0] = 0x00; buffer[offset + 1] = 0xFF; buffer[offset + 2] = 0xFF; buffer[offset + 3] = 0xFF;
buffer[offset + 4] = 0xFF; buffer[offset + 5] = 0xFF; buffer[offset + 6] = 0xFF; buffer[offset + 7] = 0xFF;
buffer[offset + 8] = 0xFF; buffer[offset + 9] = 0xFF; buffer[offset + 10] = 0xFF; buffer[offset + 11] = 0x00;
//sector address
buffer[offset + 12] = bcd_aba_min;
buffer[offset + 13] = bcd_aba_sec;
buffer[offset + 14] = bcd_aba_frac;
//mode 1
buffer[offset + 15] = 1;
//calculate EDC and poke into the sector
uint edc = ECM.EDC_Calc(buffer, offset, 2064);
ECM.PokeUint(buffer, 2064, edc);
//intermediate
for (int i = 0; i < 8; i++) buffer[offset + 2068 + i] = 0;
//ECC
ECM.ECC_Populate(buffer, offset, buffer, offset, false);
//VALIDATION - check our homemade algorithms against code derived from ECM
////EDC
//GPL_ECM.edc_validateblock(buffer, 2064, buffer, offset + 2064);
////ECC
//GPL_ECM.ecc_validate(buffer, offset, false);
//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];
Buffer.BlockCopy(buffer, 0, extra_data, 0, 16);
Buffer.BlockCopy(buffer, 2064, extra_data, 16, 4 + 8 + 172 + 104);
has_extra_data = true;
return 2352;
}
//otherwise, return a smaller value to indicate an error
else return read;
}
}
}

View File

@ -1,8 +1,215 @@
//a decent little subcode reference
using System;
//a decent little subcode reference
//http://www.jbum.com/cdg_revealed.html
namespace BizHawk.Emulation.DiscSystem
{
public interface ISubcodeSector
{
/// <summary>
/// reads 96 bytes of subcode data (deinterleaved) for this sector into the supplied buffer
/// </summary>
void ReadSubcodeDeinterleaved(byte[] buffer, int offset);
/// <summary>
/// Reads just one of the channels. p=0, q=1, etc.
/// </summary>
void ReadSubcodeChannel(int number, byte[] buffer, int offset);
}
/// <summary>
/// Reads subcode from an internally-managed buffer
/// </summary>
class BufferedSubcodeSector : ISubcodeSector
{
/// <summary>
/// Fills this subcode buffer with subchannel Q data. calculates the required CRC, as well.
/// Returns the crc, calculated or otherwise.
/// </summary>
public ushort Synthesize_SubchannelQ(ref SubchannelQ sq, bool calculateCRC)
{
int offset = 12; //Q subchannel begins after P, 12 bytes in
SubcodeDeinterleaved[offset + 0] = sq.q_status;
SubcodeDeinterleaved[offset + 1] = sq.q_tno;
SubcodeDeinterleaved[offset + 2] = sq.q_index;
SubcodeDeinterleaved[offset + 3] = sq.min.BCDValue;
SubcodeDeinterleaved[offset + 4] = sq.sec.BCDValue;
SubcodeDeinterleaved[offset + 5] = sq.frame.BCDValue;
SubcodeDeinterleaved[offset + 6] = sq.zero;
SubcodeDeinterleaved[offset + 7] = sq.ap_min.BCDValue;
SubcodeDeinterleaved[offset + 8] = sq.ap_sec.BCDValue;
SubcodeDeinterleaved[offset + 9] = sq.ap_frame.BCDValue;
ushort crc16;
if (calculateCRC)
crc16 = CRC16_CCITT.Calculate(SubcodeDeinterleaved, offset, 10);
else crc16 = sq.q_crc;
//CRC is stored inverted and big endian
SubcodeDeinterleaved[offset + 10] = (byte)(~(crc16 >> 8));
SubcodeDeinterleaved[offset + 11] = (byte)(~(crc16));
return crc16;
}
public void ReadSubcodeDeinterleaved(byte[] buffer, int offset)
{
Buffer.BlockCopy(SubcodeDeinterleaved, 0, buffer, offset, 96);
}
public void ReadSubcodeChannel(int number, byte[] buffer, int offset)
{
Buffer.BlockCopy(SubcodeDeinterleaved, number * 12, buffer, offset, 12);
}
public byte[] SubcodeDeinterleaved = new byte[96];
}
public class ZeroSubcodeSector : ISubcodeSector
{
public void ReadSubcodeDeinterleaved(byte[] buffer, int offset)
{
for (int i = 0; i < 96; i++) buffer[i + offset] = 0;
}
public void ReadSubcodeChannel(int number, byte[] buffer, int offset)
{
for (int i = 0; i < 12; i++)
buffer[i + offset] = 0;
}
}
/// <summary>
/// Reads subcode data from a blob, assuming it was already stored in deinterleaved format
/// </summary>
public class BlobSubcodeSectorPreDeinterleaved : ISubcodeSector
{
public void ReadSubcodeDeinterleaved(byte[] buffer, int offset)
{
Blob.Read(Offset, buffer, offset, 96);
}
public void ReadSubcodeChannel(int number, byte[] buffer, int offset)
{
Blob.Read(Offset + number * 12, buffer, offset, 12);
}
public IBlob Blob;
public long Offset;
}
/// <summary>
/// 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,
CopyProhibitedMask = 0,
CopyPermittedMask = 2,
}
/// <summary>
/// Why did I make this a struct? I thought there might be a shitton of these and I was trying to cut down on object creation churn during disc-loading.
/// But I ended up mostly just having a shitton of byte[] for each buffer (I could improve that later to possibly reference a blob on top of a memorystream)
/// So, I should probably change that.
/// </summary>
public struct SubchannelQ
{
/// <summary>
/// ADR and CONTROL
/// </summary>
public byte q_status;
/// <summary>
/// normal track: BCD indications of the current track number
/// leadin track: should be 0
/// </summary>
public byte 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;
/// <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
/// </summary>
public BCD2 min, sec, frame;
/// <summary>
/// This is supposed to be zero.. but CCD format stores it, so maybe it's useful for copy protection or something
/// </summary>
public byte zero;
/// <summary>
/// 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
/// </summary>
public BCD2 ap_min, ap_sec, ap_frame;
/// <summary>
/// The CRC. This is the actual CRC value as would be calculated from our library (it is inverted and written big endian to the disc)
/// Don't assume this CRC is correct-- If this SubchannelQ was read from a dumped disc, the CRC might be wrong.
/// CCD doesnt specify this for TOC entries, so it will be wrong. It may or may not be right for data track sectors from a CCD file.
/// Or we may have computed this SubchannelQ data and generated the correct CRC at that time.
/// </summary>
public ushort q_crc;
/// <summary>
/// Retrieves the initial set of timestamps (min,sec,frac) as a convenient Timestamp
/// </summary>
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
/// </summary>
public Timestamp AP_Timestamp { get { return new Timestamp(ap_min.DecimalValue, ap_sec.DecimalValue, ap_frame.DecimalValue); } }
/// <summary>
/// sets the status byte from the provided adr and control values
/// </summary>
public void SetStatus(byte adr, EControlQ control)
{
q_status = ComputeStatus(adr, control);
}
/// <summary>
/// computes a status byte from the provided adr and control values
/// </summary>
public static byte ComputeStatus(int adr, EControlQ control)
{
return (byte)(adr | (((int)control) << 4));
}
/// <summary>
/// Retrives the ADR field of the q_status member (low 4 bits)
/// </summary>
public int ADR { get { return q_status & 0xF; } }
/// <summary>
/// Retrieves the CONTROL field of the q_status member (high 4 bits)
/// </summary>
public EControlQ CONTROL { get { return (EControlQ)((q_status >> 4) & 0xF); } }
}
//this has been checked against mednafen's and seems to match
//there are a few dozen different ways to do CRC16-CCITT
//this table is backwards or something. at any rate its tailored to the needs of the Q subchannel
@ -43,6 +250,9 @@ namespace BizHawk.Emulation.DiscSystem
public class SubcodeDataDecoder
{
/// <summary>
/// This seems to deinterleave Q from a subcode buffer? Not sure.. it isn't getting used anywhere right now, as you can see.
/// </summary>
public static void Unpack_Q(byte[] output, int out_ofs, byte[] input, int in_ofs)
{
for (int i = 0; i < 12; i++)

View File

@ -0,0 +1,235 @@
using System;
using System.Text;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// 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)
/// </summary>
public class DiscStructure
{
/// <summary>
/// Right now support for anything other than 1 session is totally not working
/// </summary>
public List<Session> Sessions = new List<Session>();
/// <summary>
/// List of Points described by the TOC
/// </summary>
public List<TOCPoint> Points;
/// <summary>
/// How many sectors in the disc, including the 150 lead-in sectors
/// </summary>
public int LengthInSectors;
/// <summary>
/// Length (including lead-in) of the disc as a timestamp
/// </summary>
public Timestamp FriendlyLength { get { return new Timestamp(LengthInSectors); } }
/// <summary>
/// How many bytes of data in the disc (including lead-in). Disc sectors are really 2352 bytes each, so this is LengthInSectors * 2352
/// </summary>
public long BinarySize
{
get { return LengthInSectors * 2352; }
}
/// <summary>
/// Synthesizes the DiscStructure from a DiscTOCRaw
/// </summary>
public class SynthesizeFromDiscTOCRawJob
{
public DiscTOCRaw TOCRaw;
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;
//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;
}
}
/// <summary>
/// seeks the point immediately before (or equal to) this LBA
/// </summary>
public TOCPoint SeekPoint(int lba)
{
int aba = lba + 150;
for(int i=0;i<Points.Count;i++)
{
TOCPoint tp = Points[i];
if (tp.ABA > aba)
return Points[i - 1];
}
return Points[Points.Count - 1];
}
/// <summary>
///
/// </summary>
public class TOCPoint
{
public int Num;
public int ABA, TrackNum, IndexNum;
public Track Track;
public int LBA
{
get { return ABA - 150; }
}
}
/// <summary>
/// Generates the Points list from the current logical TOC
/// </summary>
public void Synthesize_TOCPointsFromSessions()
{
Points = new List<TOCPoint>();
int num = 0;
foreach (var ses in Sessions)
{
foreach (var track in ses.Tracks)
foreach (var index in track.Indexes)
{
var tp = new TOCPoint
{
Num = num++,
ABA = index.aba,
TrackNum = track.Number,
IndexNum = index.Number,
Track = track
};
Points.Add(tp);
}
var tpLeadout = new TOCPoint();
var lastTrack = ses.Tracks[ses.Tracks.Count - 1];
tpLeadout.Num = num++;
tpLeadout.ABA = lastTrack.Indexes[1].aba + lastTrack.LengthInSectors;
tpLeadout.IndexNum = 0;
tpLeadout.TrackNum = 100;
tpLeadout.Track = null; //no leadout track.. now... or ever?
Points.Add(tpLeadout);
}
}
public class Session
{
public int num;
/// <summary>
/// All the tracks in the session.. but... Tracks[0] should be "Track 1". So beware of this.
/// We might should keep this organized as a dictionary as well.
/// </summary>
public List<Track> Tracks = new List<Track>();
//the length of the session (should be the sum of all track lengths)
public int length_aba;
public Timestamp FriendlyLength { get { return new Timestamp(length_aba); } }
}
public class Track
{
/// <summary>
/// The number of the track (1-indexed)
/// </summary>
public int Number;
public ETrackType TrackType;
/// <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)
/// they normally don't; they're useful for describing what type of contents the track is.
/// </summary>
public EControlQ Control;
/// <summary>
/// All the indexes related to the track. These will be 0-Indexed, but they could be non-consecutive.
/// </summary>
public List<Index> Indexes = new List<Index>();
/// <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...
/// </summary>
public int LengthInSectors;
/// <summary>
/// The beginning ABA of the track (index 1). This isn't well-supported, yet
/// </summary>
public int Start_ABA;
/// <summary>
/// The length as a timestamp (for accessing as a MM:SS:FF)
/// </summary>
public Timestamp FriendlyLength { get { return new Timestamp(LengthInSectors); } }
}
public class Index
{
public int Number;
public int aba;
public int LBA
{
get { return aba - 150; }
set { aba = value + 150; }
}
//the length of the section
//HEY! This is commented out because it is a bad idea.
//The length of a `section`? (what's a section?) is almost useless, and if you want it, you are probably making an error.
//public int length_lba;
//public Cue.Timestamp FriendlyLength { get { return new Cue.Timestamp(length_lba); } }
}
public void AnalyzeLengthsFromIndexLengths()
{
//this is a little more complex than it looks, because the length of a thing is not determined by summing it
//but rather by the difference in lbas between start and end
LengthInSectors = 0;
foreach (var session in Sessions)
{
var firstTrack = session.Tracks[0];
var lastTrack = session.Tracks[session.Tracks.Count - 1];
session.length_aba = lastTrack.Indexes[0].aba + lastTrack.LengthInSectors - firstTrack.Indexes[0].aba;
LengthInSectors += session.length_aba;
}
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Text;
using System.Collections.Generic;
namespace BizHawk.Emulation.DiscSystem
{
/// <summary>
/// Represents our best guess at what a disc drive firmware will receive by reading the TOC from the lead-in track, modeled after CCD contents and mednafen/PSX needs.
/// </summary>
public class DiscTOCRaw
{
/// <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.
/// </summary>
public class SynthesizeFromRawTOCEntriesJob
{
public IEnumerable<RawTOCEntry> Entries;
public List<string> Log = new List<string>();
public DiscTOCRaw Result;
public void Run()
{
SynthesizeFromRawTOCEntriesJob job = this;
DiscTOCRaw ret = new DiscTOCRaw();
//just in case this doesnt get set...
ret.FirstRecordedTrackNumber = 0;
ret.LastRecordedTrackNumber = 0;
int maxFoundTrack = 0;
foreach (var te in job.Entries)
{
var q = te.QData;
int point = q.q_index;
//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 <= 99)
{
maxFoundTrack = Math.Max(maxFoundTrack, point);
ret.TOCItems[point].LBATimestamp = q.AP_Timestamp;
ret.TOCItems[point].Control = q.CONTROL;
ret.TOCItems[point].Exists = true;
}
else if (point == 0xA0)
{
ret.FirstRecordedTrackNumber = q.ap_min.DecimalValue;
if (q.ap_frame.DecimalValue != 0) job.Log.Add("PFRAME should be 0 for POINT=0xA0");
if (q.ap_sec.DecimalValue == 0x00) ret.Session1Format = DiscTOCRaw.SessionFormat.Type00_CDROM_CDDA;
else if (q.ap_sec.DecimalValue == 0x10) ret.Session1Format = DiscTOCRaw.SessionFormat.Type10_CDI;
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)
{
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)
{
ret.LeadoutTimestamp = q.AP_Timestamp;
}
}
//this is speculative:
//well, nothing to be done here..
if (ret.FirstRecordedTrackNumber == 0) { }
if (ret.LastRecordedTrackNumber == 0) { ret.LastRecordedTrackNumber = maxFoundTrack; }
job.Result = ret;
}
}
public enum SessionFormat
{
Type00_CDROM_CDDA,
Type10_CDI,
Type20_CDXA
}
/// <summary>
/// The TOC specifies the first recorded track number, independently of whatever may actually be recorded (its unclear what happens if theres a mismatch)
/// </summary>
public int FirstRecordedTrackNumber;
/// <summary>
/// The TOC specifies the last recorded track number, independently of whatever may actually be recorded (its unclear what happens if theres a mismatch)
/// </summary>
public int LastRecordedTrackNumber;
/// <summary>
/// The TOC specifies the format of the session, so here it is.
/// </summary>
public SessionFormat Session1Format = SessionFormat.Type00_CDROM_CDDA;
public struct TOCItem
{
public EControlQ Control;
public Timestamp LBATimestamp;
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.
/// </summary>
public TOCItem[] TOCItems = new TOCItem[100];
/// <summary>
/// POINT=0xA2 specifies this
/// </summary>
public Timestamp LeadoutTimestamp;
}
}

View File

@ -0,0 +1,3 @@
MIT LICENSE
https://code.google.com/p/iso-parser/

View File

@ -1,9 +1,7 @@
//lets leave some notes here until we've rewritten every possible thing -- error recovery, ECM file format, ISO filesystem, etc.
//here is a braindump of urls and stuff for future reference
//check this for some iso stuff but seems like it ripped off corlett's code
//http://lioneditor.googlecode.com/svn/trunk/utils/isopatcherv05/src/
//http://code.ohloh.net/file?fid=185llKM04w3QCqwC2MdFgtUiQ94&cid=yPMRq_HKxUg&s=ecc_computeblock%28pSector%20%2B%200xC%2C%2052%2C%2043%2C%2086%2C%2088%2C%20pSector%20%2B%200x8C8%29&mp=1&ml=1&me=1&md=1&browser=Default#L106
//we used this code
//https://code.google.com/p/iso-parser/
//more gpl edc/ecc
//http://code.ohloh.net/file?fid=BEZeY2fWALJKXTgY3Oe9J988ubQ&cid=Xkpw3SKt7K8&s=edc%20ecc%20&pp=0&fl=C&ff=1&filterChecked=true&mp=1&ml=1&me=1&md=1&browser=Default#L13
@ -33,3 +31,8 @@
//http://www.cs.utsa.edu/~wagner/laws/FFM.html
//http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/reedsolomon/reed_solomon_codes.html
//http://en.wikipedia.org/wiki/Finite_field_arithmetic
//here is a braindump of urls and stuff for future reference
//check this for some iso stuff but seems like it ripped off corlett's code
//http://lioneditor.googlecode.com/svn/trunk/utils/isopatcherv05/src/
//http://code.ohloh.net/file?fid=185llKM04w3QCqwC2MdFgtUiQ94&cid=yPMRq_HKxUg&s=ecc_computeblock%28pSector%20%2B%200xC%2C%2052%2C%2043%2C%2086%2C%2088%2C%20pSector%20%2B%200x8C8%29&mp=1&ml=1&me=1&md=1&browser=Default#L106

View File

@ -0,0 +1 @@
. consider normalizing everything to use -150 for lead-in crappola ?