.cdi support, seems to work

also expand the search for the jaguar cd header a bit, seems it can sometimes be at the second sector?
This commit is contained in:
CasualPokePlayer 2023-03-14 05:14:31 -07:00
parent 825c144d6a
commit d9ac4166cf
6 changed files with 614 additions and 6 deletions

View File

@ -898,7 +898,7 @@ namespace BizHawk.Client.Common
/// <remarks>TODO add and handle <see cref="FilesystemFilter.LuaScripts"/> (you can drag-and-drop scripts and there are already non-rom things in this list, so why not?)</remarks>
public static readonly FilesystemFilterSet RomFilter = new(
new FilesystemFilter("Music Files", Array.Empty<string>(), devBuildExtraExts: new[] { "psf", "minipsf", "sid", "nsf", "gbs" }),
new FilesystemFilter("Disc Images", new[] { "cue", "ccd", "mds", "m3u" }),
new FilesystemFilter("Disc Images", new[] { "cue", "ccd", "cdi", "mds", "m3u" }),
new FilesystemFilter("NES", RomFileExtensions.NES.Concat(new[] { "nsf" }).ToList(), addArchiveExts: true),
new FilesystemFilter("Super NES", RomFileExtensions.SNES, addArchiveExts: true),
new FilesystemFilter("PlayStation", new[] { "cue", "ccd", "mds", "m3u" }),

View File

@ -3880,7 +3880,7 @@ namespace BizHawk.Client.EmuHawk
var ext = Path.GetExtension(xmlGame.AssetFullPaths[xg])?.ToLowerInvariant();
var (filename, data) = xmlGame.Assets[xg];
if (ext == ".cue" || ext == ".ccd" || ext == ".toc" || ext == ".mds")
if (ext is ".cue" or ".ccd" or ".cdi" or ".toc" or ".mds")
{
xSw.WriteLine(Path.GetFileNameWithoutExtension(filename));
xSw.WriteLine("SHA1:N/A");

View File

@ -122,6 +122,6 @@ namespace BizHawk.Emulation.DiscSystem
{}
public static bool IsValidExtension(string extension)
=> extension.ToLowerInvariant() is ".ccd" or ".cue" or ".iso" or ".mds";
=> extension.ToLowerInvariant() is ".ccd" or ".cdi" or ".cue" or ".iso" or ".mds";
}
}

View File

@ -0,0 +1,599 @@
using System;
using System.IO;
using System.Globalization;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using BizHawk.Common;
using BizHawk.Common.IOExtensions;
using BizHawk.Emulation.DiscSystem.CUE;
//DiscJuggler CDI images
//https://problemkaputt.de/psxspx-cdrom-disk-images-cdi-discjuggler.htm
//https://github.com/cdemu/cdemu/blob/1f90f74/libmirage/images/image-cdi/parser.c
namespace BizHawk.Emulation.DiscSystem
{
public class CDI_Format
{
/// <summary>
/// Represents a CDI file, faithfully. Minimal interpretation of the data happens.
/// </summary>
public class CDIFile
{
/// <summary>
/// Number of sessions
/// </summary>
public byte NumSessions;
/// <summary>
/// The session blocks
/// </summary>
public readonly IList<CDISession> Sessions = new List<CDISession>();
/// <summary>
/// The track blocks
/// </summary>
public readonly IList<CDITrack> Tracks = new List<CDITrack>();
/// <summary>
/// The disc info block
/// </summary>
public readonly CDIDiscInfo DiscInfo = new();
/// <summary>
/// Footer size in bytes
/// </summary>
public uint Entrypoint;
}
/// <summary>
/// Represents a session block from a CDI file
/// </summary>
public class CDISession
{
/// <summary>
/// Number of tracks in session (1..99) (or 0 = no more sessions)
/// </summary>
public byte NumTracks;
}
/// <summary>
/// Represents a track/disc info block header from a CDI track
/// </summary>
public class CDITrackHeader
{
/// <summary>
/// Number of tracks on disc (1..99)
/// </summary>
public byte NumTracks;
/// <summary>
/// Full Path/Filename (may be empty)
/// </summary>
public string Path;
/// <summary>
/// 0x0098 = CD-ROM, 0x0038 = DVD-ROM
/// </summary>
public ushort MediumType;
}
/// <summary>
/// Represents a CD text block from a CDI track
/// </summary>
public class CDICDText
{
/// <summary>
/// A CD text block has 0-18 strings, each of variable length
/// </summary>
public readonly IList<string> CdTexts = new List<string>();
}
/// <summary>
/// Represents a track block from a CDI file
/// </summary>
public class CDITrack : CDITrackHeader
{
/// <summary>
/// The sector count of each index specified for the track
/// </summary>
public readonly IList<uint> IndexSectorCounts = new List<uint>();
/// <summary>
/// CD text blocks
/// </summary>
public readonly IList<CDICDText> CdTextBlocks = new List<CDICDText>();
/// <summary>
/// The specified track mode (0 = Audio, 1 = Mode1, 2 = Mode2/Mixed)
/// </summary>
public byte TrackMode;
/// <summary>
/// Session number (0-indexed)
/// </summary>
public uint SessionNumber;
/// <summary>
/// Track number (0-indexed, releative to session)
/// </summary>
public uint TrackNumber;
/// <summary>
/// Track start address
/// </summary>
public uint TrackStartAddress;
/// <summary>
/// Track length, in sectors
/// </summary>
public uint TrackLength;
/// <summary>
/// The specified read mode (0 = Mode1, 1 = Mode2, 2 = Audio, 3 = Raw+Q, 4 = Raw+PQRSTUVW)
/// </summary>
public uint ReadMode;
/// <summary>
/// Upper 4 bits of ADR/Control
/// </summary>
public uint Control;
/// <summary>
/// 12-letter/digit string (may be empty)
/// </summary>
public string IsrcCode;
/// <summary>
/// Any non-zero is valid?
/// </summary>
public uint IsrcValidFlag;
/// <summary>
/// Only present on last track of a session (0 = Audio/CD-DA, 1 = Mode1/CD-ROM, 2 = Mode2/CD-XA)
/// </summary>
public uint SessionType;
}
/// <summary>
/// Represents a disc info block from a CDI file
/// </summary>
public class CDIDiscInfo : CDITrackHeader
{
/// <summary>
/// Total number of sectors
/// </summary>
public uint DiscSize;
/// <summary>
/// probably junk for non-ISO data discs
/// </summary>
public string VolumeId;
/// <summary>
/// 13-digit string (may be empty)
/// </summary>
public string Ean13Code;
/// <summary>
/// Any non-zero is valid?
/// </summary>
public uint Ean13CodeValid;
/// <summary>
/// CD text (for lead-in?)
/// </summary>
public string CdText;
}
public class CDIParseException : Exception
{
public CDIParseException(string message) : base(message) { }
}
/// <exception cref="CDIParseException">malformed cdi format</exception>
public static CDIFile ParseFrom(Stream stream)
{
var cdif = new CDIFile();
using var br = new BinaryReader(stream);
try
{
stream.Seek(-4, SeekOrigin.End);
cdif.Entrypoint = br.ReadUInt32();
stream.Seek(-cdif.Entrypoint, SeekOrigin.End);
cdif.NumSessions = br.ReadByte();
if (cdif.NumSessions == 0)
{
throw new CDIParseException("Malformed CDI format: 0 sessions!");
}
void ParseTrackHeader(CDITrackHeader header)
{
stream.Seek(15, SeekOrigin.Current); // unknown bytes
header.NumTracks = br.ReadByte();
var pathLen = br.ReadByte();
header.Path = br.ReadStringFixedUtf8(pathLen);
stream.Seek(29, SeekOrigin.Current); // unknown bytes
header.MediumType = br.ReadUInt16();
switch (header.MediumType)
{
case 0x0038:
throw new CDIParseException("Malformed CDI format: DVD was specified, but this is not supported!");
case 0x0098:
return;
default:
throw new CDIParseException("Malformed CDI format: Invalid medium type!");
}
}
for (var i = 0; i <= cdif.NumSessions; i++)
{
var session = new CDISession();
stream.Seek(1, SeekOrigin.Current); // unknown byte
session.NumTracks = br.ReadByte();
stream.Seek(13, SeekOrigin.Current); // unknown bytes
cdif.Sessions.Add(session);
// the last session block should have 0 tracks (as it indicates no more sessions)
if (session.NumTracks == 0 && i != cdif.NumSessions)
{
throw new CDIParseException("Malformed CDI format: No tracks in session!");
}
if (session.NumTracks + cdif.Tracks.Count > 99)
{
throw new CDIParseException("Malformed CDI format: More than 99 tracks on disc!");
}
for (var j = 0; j < session.NumTracks; j++)
{
var track = new CDITrack();
ParseTrackHeader(track);
var indexes = br.ReadUInt16();
if (indexes < 2) // We should have at least 2 indexes (one pre-gap, and one "real" one)
{
throw new CDIParseException("Malformed CDI format: Less than 2 indexes in track!");
}
for (var k = 0; k < indexes; k++)
{
track.IndexSectorCounts.Add(br.ReadUInt32());
}
var numCdTextBlocks = br.ReadUInt32();
for (var k = 0; k < numCdTextBlocks; k++)
{
var cdTextBlock = new CDICDText();
for (var l = 0; l < 18; l++)
{
var cdTextLen = br.ReadByte();
if (cdTextLen > 0)
{
cdTextBlock.CdTexts.Add(br.ReadStringFixedUtf8(cdTextLen));
}
}
track.CdTextBlocks.Add(cdTextBlock);
}
stream.Seek(2, SeekOrigin.Current); // unknown bytes
track.TrackMode = br.ReadByte();
if (track.TrackMode > 2)
{
throw new CDIParseException("Malformed CDI format: Invalid track mode!");
}
stream.Seek(7, SeekOrigin.Current); // unknown bytes
track.SessionNumber = br.ReadUInt32();
if (track.SessionNumber != i)
{
throw new CDIParseException("Malformed CDI format: Session number mismatch!");
}
track.TrackNumber = br.ReadUInt32();
if (track.TrackNumber != j) // I think this is relative to the session?
{
throw new CDIParseException("Malformed CDI format: Track number mismatch!");
}
track.TrackStartAddress = br.ReadUInt32();
track.TrackLength = br.ReadUInt32();
stream.Seek(16, SeekOrigin.Current); // unknown bytes
track.ReadMode = br.ReadUInt32();
if (track.ReadMode > 4)
{
throw new CDIParseException("Malformed CDI format: Invalid read mode!");
}
track.Control = br.ReadUInt32();
if ((track.Control & ~0xF) != 0)
{
throw new CDIParseException("Malformed CDI format: Invalid control!");
}
stream.Seek(1, SeekOrigin.Current); // unknown byte
var redundantTrackLen = br.ReadUInt32();
if (track.TrackLength != redundantTrackLen)
{
throw new CDIParseException("Malformed CDI format: Track length mismatch!");
}
stream.Seek(4, SeekOrigin.Current); // unknown bytes
track.IsrcCode = br.ReadStringFixedUtf8(12);
track.IsrcValidFlag = br.ReadUInt32();
if (track.IsrcValidFlag == 0)
{
track.IsrcCode = string.Empty;
}
stream.Seek(87, SeekOrigin.Current); // unknown bytes
track.SessionType = br.ReadByte();
switch (track.SessionType)
{
case > 2:
throw new CDIParseException("Malformed CDI format: Invalid session type!");
case > 0 when j != session.NumTracks - 1:
throw new CDIParseException("Malformed CDI format: Session type was specified, but this is only supposed to be present on the last track!");
}
stream.Seek(5, SeekOrigin.Current); // unknown bytes
var notLastTrackInSession = br.ReadByte();
switch (notLastTrackInSession)
{
case 0 when j != session.NumTracks - 1:
throw new CDIParseException("Malformed CDI format: Track was specified to be the last track of the session, but more tracks are available!");
case > 1:
throw new CDIParseException("Malformed CDI format: Invalid not last track of session flag!");
}
stream.Seek(5, SeekOrigin.Current); // unknown bytes
// well, the last 4 bytes here are said to be the "address for last track of a session? (otherwise 00,00,FF,FF)"
// except I'm not sure what the address is meant to be, by bytes? by sectors? relative to file? relative session start?
// for now I just ignore it
cdif.Tracks.Add(track);
}
}
ParseTrackHeader(cdif.DiscInfo);
cdif.DiscInfo.DiscSize = br.ReadUInt32();
if (cdif.DiscInfo.DiscSize != cdif.Tracks.Sum(t => t.TrackLength))
{
//throw new CDIParseException("Malformed CDI format: Disc size mismatch!");
//this seems to be wrong?
}
var volumeIdLen = br.ReadByte();
cdif.DiscInfo.VolumeId = br.ReadStringFixedUtf8(volumeIdLen);
stream.Seek(9, SeekOrigin.Current); // unknown bytes
cdif.DiscInfo.Ean13Code = br.ReadStringFixedUtf8(13);
cdif.DiscInfo.Ean13CodeValid = br.ReadUInt32();
if (cdif.DiscInfo.Ean13CodeValid == 0)
{
cdif.DiscInfo.Ean13Code = string.Empty;
}
var cdTextLengh = br.ReadUInt32();
if (cdTextLengh > int.MaxValue)
{
// suppose technically this might not be considered too large purely going off the format
// but it's a bit silly to have a >2GB string so this is probably not valid
throw new CDIParseException("Malformed CDI format: CD text too large!");
}
cdif.DiscInfo.CdText = br.ReadStringFixedUtf8((int)cdTextLengh);
stream.Seek(12, SeekOrigin.Current); // unknown bytes
if (cdif.Tracks.Any(track => track.NumTracks != cdif.Tracks.Count) || cdif.DiscInfo.NumTracks != cdif.Tracks.Count)
{
throw new CDIParseException("Malformed CDI format: Total track number mismatch!");
}
if (stream.Position != stream.Length - 4)
{
throw new CDIParseException("Malformed CDI format: Did not reach end of footer after parsing!");
}
return cdif;
}
catch (EndOfStreamException)
{
throw new CDIParseException("Malformed CDI format: Unexpected stream end!");
}
}
public class LoadResults
{
public CDIFile ParsedCDIFile;
public bool Valid;
public CDIParseException FailureException;
public string CdiPath;
}
public static LoadResults LoadCDIPath(string path)
{
var ret = new LoadResults
{
CdiPath = path
};
try
{
if (!File.Exists(path)) throw new CDIParseException("Malformed CDI format: nonexistent CDI file!");
CDIFile cdif;
using (var infCDI = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
cdif = ParseFrom(infCDI);
ret.ParsedCDIFile = cdif;
ret.Valid = true;
}
catch (CDIParseException ex)
{
ret.FailureException = ex;
}
return ret;
}
private class SS_CDI_RawQ : SS_Base
{
public override void Synth(SectorSynthJob job)
{
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2352);
if ((job.Parts & ESectorSynthPart.SubchannelP) != 0)
{
SynthUtils.SubP(job.DestBuffer2448, job.DestOffset + 2352, Pause);
}
// Q is present in the blob and non-interleaved
if ((job.Parts & ESectorSynthPart.SubchannelQ) != 0)
{
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset + 2352 + 12, 12);
}
//clear R-W if needed
if ((job.Parts & ESectorSynthPart.Subchannel_RSTUVW) != 0)
{
Array.Clear(job.DestBuffer2448, job.DestOffset + 2352 + 12 + 12, 12 * 6);
}
//subcode has been generated deinterleaved; we may still need to interleave it
if ((job.Parts & ESectorSynthPart.SubcodeAny) != 0 && (job.Parts & ESectorSynthPart.SubcodeDeinterleave) == 0)
{
SynthUtils.InterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
}
}
}
private class SS_CDI_RawPQRSTUVW : SS_Base
{
public override void Synth(SectorSynthJob job)
{
// all subcode is present and interleaved, just read it all
Blob.Read(BlobOffset, job.DestBuffer2448, job.DestOffset, 2448);
// deinterleave it if needed
if ((job.Parts & ESectorSynthPart.SubcodeDeinterleave) != 0)
{
SynthUtils.DeinterleaveSubcodeInplace(job.DestBuffer2448, job.DestOffset + 2352);
}
}
}
/// <exception cref="CDIParseException">file <paramref name="cdiPath"/> not found</exception>
public static Disc LoadCDIToDisc(string cdiPath, DiscMountPolicy IN_DiscMountPolicy)
{
var loadResults = LoadCDIPath(cdiPath);
if (!loadResults.Valid)
throw loadResults.FailureException;
var disc = new Disc();
var cdif = loadResults.ParsedCDIFile;
IBlob cdiBlob = new Blob_RawFile { PhysicalPath = cdiPath };
disc.DisposableResources.Add(cdiBlob);
var trackOffset = 0;
var blobOffset = 0;
for (var i = 0; i < cdif.NumSessions; i++)
{
var session = new DiscSession { Number = i + 1 };
for (var j = 0; j < cdif.Sessions[i].NumTracks; j++)
{
var track = cdif.Tracks[trackOffset + j];
RawTOCEntry EmitRawTOCEntry()
{
var q = default(SubchannelQ);
//absent some kind of policy for how to set it, this is a safe assumption
const byte kADR = 1;
q.SetStatus(kADR, (EControlQ)track.Control);
q.q_tno = BCD2.FromDecimal(0);
q.q_index = BCD2.FromDecimal(trackOffset + j + 1);
q.Timestamp = 0;
q.zero = 0;
q.AP_Timestamp = disc._Sectors.Count;
q.q_crc = 0;
return new() { QData = q };
}
var sectorSize = track.ReadMode switch
{
0 => 2048,
1 => 2336,
2 => 2352,
3 => 2368,
4 => 2448,
_ => throw new InvalidOperationException()
};
var curIndex = 0;
var relMSF = -track.IndexSectorCounts[0];
var indexSectorOffset = 0U;
for (var k = 0; k < track.TrackLength; k++)
{
if (track.IndexSectorCounts[curIndex] == k - indexSectorOffset)
{
indexSectorOffset += track.IndexSectorCounts[curIndex];
curIndex++;
if (track.IndexSectorCounts.Count == curIndex)
{
throw new CDIParseException("Malformed CDI Format: Reached end of index list unexpectedly");
}
if (curIndex == 1)
{
session.RawTOCEntries.Add(EmitRawTOCEntry());
}
}
//note that CDIs contain the pregap data themselves...
SS_Base synth = track.ReadMode switch
{
0 => new SS_Mode1_2048(),
1 => throw new NotSupportedException("Mode2/2336"), // TODO
2 => new SS_2352(),
3 => new SS_CDI_RawQ(),
4 => new SS_CDI_RawPQRSTUVW(),
_ => throw new InvalidOperationException()
};
synth.Blob = cdiBlob;
synth.BlobOffset = blobOffset;
synth.Policy = IN_DiscMountPolicy;
//TODO: subchannel here is all wrong for gaps, probably
const byte kADR = 1;
synth.sq.SetStatus(kADR, (EControlQ)track.Control);
synth.sq.q_tno = BCD2.FromDecimal(trackOffset + j + 1);
synth.sq.q_index = BCD2.FromDecimal(curIndex);
synth.sq.Timestamp = (int)relMSF;
synth.sq.zero = 0;
synth.sq.AP_Timestamp = disc._Sectors.Count;
synth.sq.q_crc = 0;
synth.Pause = curIndex == 0;
disc._Sectors.Add(synth);
blobOffset += sectorSize;
if (curIndex != 0)
{
relMSF++;
}
}
}
var TOCMiscInfo = new Synthesize_A0A1A2_Job(
firstRecordedTrackNumber: trackOffset + 1,
lastRecordedTrackNumber: trackOffset + cdif.Sessions[i].NumTracks + 1,
sessionFormat: (SessionFormat)(cdif.Tracks[trackOffset + cdif.Sessions[i].NumTracks - 1].SessionType * 0x10),
leadoutTimestamp: disc._Sectors.Count);
TOCMiscInfo.Run(session.RawTOCEntries);
disc.Sessions.Add(session);
trackOffset += cdif.Sessions[i].NumTracks;
}
return disc;
}
}
}

View File

@ -403,9 +403,15 @@ namespace BizHawk.Emulation.DiscSystem
if (_disc.Sessions.Count > 2 && !_disc.Sessions[2].TOC.TOCItems[_disc.Sessions[2].TOC.FirstRecordedTrackNumber].IsData)
{
var data = new byte[2352];
_dsr.ReadLBA_2352(_disc.Sessions[2].Tracks[1].LBA, data, 0);
var s = Encoding.ASCII.GetString(data);
return s.Contains("ATARI APPROVED DATA HEADER ATRI") || s.Contains("TARA IPARPVODED TA AEHDAREA RT");
for (var i = 0; i < 2; i++)
{
_dsr.ReadLBA_2352(_disc.Sessions[2].Tracks[1].LBA + i, data, 0);
var s = Encoding.ASCII.GetString(data);
if (s.Contains("ATARI APPROVED DATA HEADER ATRI") || s.Contains("TARA IPARPVODED TA AEHDAREA RT"))
{
return true;
}
}
}
return false;

View File

@ -193,6 +193,9 @@ namespace BizHawk.Emulation.DiscSystem
case ".ccd":
OUT_Disc = CCD_Format.LoadCCDToDisc(IN_FromPath, IN_DiscMountPolicy);
break;
case ".cdi":
OUT_Disc = CDI_Format.LoadCDIToDisc(IN_FromPath, IN_DiscMountPolicy);
break;
case ".cue":
LoadCue(dir, File.ReadAllText(IN_FromPath));
break;