From 1a0bdc521cb8904639a4ae939a189142fbf7c06d Mon Sep 17 00:00:00 2001 From: CasualPokePlayer <50538166+CasualPokePlayer@users.noreply.github.com> Date: Thu, 2 May 2024 15:28:53 -0700 Subject: [PATCH] HAWK'd CHD support in DiscoHawk --- .../MainDiscoForm.Designer.cs | 67 +-- src/BizHawk.Client.DiscoHawk/MainDiscoForm.cs | 6 +- .../DiscFormats/CHD_format.cs | 435 +++++++++++++++++- .../DiscoHawkLogic/AudioExtractor.cs | 3 +- .../DiscoHawkLogic/DiscoHawkLogic.cs | 41 +- src/BizHawk.Emulation.DiscSystem/LibChdr.cs | 36 +- 6 files changed, 525 insertions(+), 63 deletions(-) diff --git a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs index 02a5c8e994..1bf3a13136 100644 --- a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs +++ b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.Designer.cs @@ -42,8 +42,8 @@ this.label3 = new System.Windows.Forms.Label(); this.radioButton2 = new System.Windows.Forms.RadioButton(); this.groupBox2 = new System.Windows.Forms.GroupBox(); - this.checkEnableOutput = new System.Windows.Forms.CheckBox(); - this.radioButton4 = new System.Windows.Forms.RadioButton(); + this.ccdOutputButton = new System.Windows.Forms.RadioButton(); + this.chdOutputButton = new System.Windows.Forms.RadioButton(); this.label6 = new System.Windows.Forms.Label(); this.label7 = new System.Windows.Forms.Label(); this.lvCompareTargets = new System.Windows.Forms.ListView(); @@ -69,7 +69,7 @@ this.lblMagicDragArea.AllowDrop = true; this.lblMagicDragArea.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; this.lblMagicDragArea.Controls.Add(this.label1); - this.lblMagicDragArea.Location = new System.Drawing.Point(286, 31); + this.lblMagicDragArea.Location = new System.Drawing.Point(290, 31); this.lblMagicDragArea.Name = "lblMagicDragArea"; this.lblMagicDragArea.Size = new System.Drawing.Size(223, 109); this.lblMagicDragArea.TabIndex = 1; @@ -82,14 +82,14 @@ this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(166, 47); this.label1.TabIndex = 0; - this.label1.Text = "Drag here to HAWK your disc - dump it out as a clean CCD"; + this.label1.Text = "Drag here to HAWK your disc - dump it out as a clean CCD/CHD"; // // lblMp3ExtractMagicArea // this.lblMp3ExtractMagicArea.AllowDrop = true; this.lblMp3ExtractMagicArea.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; this.lblMp3ExtractMagicArea.Controls.Add(this.label2); - this.lblMp3ExtractMagicArea.Location = new System.Drawing.Point(286, 146); + this.lblMp3ExtractMagicArea.Location = new System.Drawing.Point(290, 146); this.lblMp3ExtractMagicArea.Name = "lblMp3ExtractMagicArea"; this.lblMp3ExtractMagicArea.Size = new System.Drawing.Size(223, 100); this.lblMp3ExtractMagicArea.TabIndex = 2; @@ -132,10 +132,10 @@ this.groupBox1.Controls.Add(this.label3); this.groupBox1.Controls.Add(this.radioButton2); this.groupBox1.Controls.Add(this.radioButton1); - this.groupBox1.Enabled = false; + this.groupBox1.Enabled = true; this.groupBox1.Location = new System.Drawing.Point(9, 12); this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(271, 234); + this.groupBox1.Size = new System.Drawing.Size(276, 234); this.groupBox1.TabIndex = 5; this.groupBox1.TabStop = false; this.groupBox1.Text = "Disc Reading Engine"; @@ -152,13 +152,14 @@ // this.label3.Location = new System.Drawing.Point(20, 39); this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(235, 33); + this.label3.Size = new System.Drawing.Size(253, 33); this.label3.TabIndex = 7; this.label3.Text = "- Uses FFMPEG for audio decoding\r\n- Loads ISO, CUE, CCD, CDI, CHD, MDS, and NRG"; // // radioButton2 // this.radioButton2.AutoSize = true; + this.radioButton2.Enabled = false; this.radioButton2.Location = new System.Drawing.Point(6, 75); this.radioButton2.Name = "radioButton2"; this.radioButton2.Size = new System.Drawing.Size(73, 17); @@ -168,9 +169,9 @@ // // groupBox2 // - this.groupBox2.Controls.Add(this.checkEnableOutput); - this.groupBox2.Controls.Add(this.radioButton4); - this.groupBox2.Enabled = false; + this.groupBox2.Controls.Add(this.ccdOutputButton); + this.groupBox2.Controls.Add(this.chdOutputButton); + this.groupBox2.Enabled = true; this.groupBox2.Location = new System.Drawing.Point(9, 252); this.groupBox2.Name = "groupBox2"; this.groupBox2.Size = new System.Drawing.Size(271, 69); @@ -178,29 +179,29 @@ this.groupBox2.TabStop = false; this.groupBox2.Text = "Output Format"; // - // checkEnableOutput + // ccdOutputButton // - this.checkEnableOutput.AutoSize = true; - this.checkEnableOutput.Checked = true; - this.checkEnableOutput.CheckState = System.Windows.Forms.CheckState.Checked; - this.checkEnableOutput.Location = new System.Drawing.Point(177, 19); - this.checkEnableOutput.Name = "checkEnableOutput"; - this.checkEnableOutput.Size = new System.Drawing.Size(59, 17); - this.checkEnableOutput.TabIndex = 7; - this.checkEnableOutput.Text = "Enable"; - this.checkEnableOutput.UseVisualStyleBackColor = true; + this.ccdOutputButton.AutoSize = true; + this.ccdOutputButton.Checked = true; + this.ccdOutputButton.Location = new System.Drawing.Point(12, 19); + this.ccdOutputButton.Name = "ccdOutputButton"; + this.ccdOutputButton.Size = new System.Drawing.Size(47, 17); + this.ccdOutputButton.TabIndex = 5; + this.ccdOutputButton.TabStop = true; + this.ccdOutputButton.Text = "CCD"; + this.ccdOutputButton.UseVisualStyleBackColor = true; // - // radioButton4 + // chdOutputButton // - this.radioButton4.AutoSize = true; - this.radioButton4.Checked = true; - this.radioButton4.Location = new System.Drawing.Point(12, 19); - this.radioButton4.Name = "radioButton4"; - this.radioButton4.Size = new System.Drawing.Size(47, 17); - this.radioButton4.TabIndex = 5; - this.radioButton4.TabStop = true; - this.radioButton4.Text = "CCD"; - this.radioButton4.UseVisualStyleBackColor = true; + this.chdOutputButton.AutoSize = true; + this.chdOutputButton.Checked = false; + this.chdOutputButton.Location = new System.Drawing.Point(65, 19); + this.chdOutputButton.Name = "chdOutputButton"; + this.chdOutputButton.Size = new System.Drawing.Size(47, 17); + this.chdOutputButton.TabIndex = 6; + this.chdOutputButton.TabStop = true; + this.chdOutputButton.Text = "CHD"; + this.chdOutputButton.UseVisualStyleBackColor = true; // // label6 // @@ -283,9 +284,9 @@ private System.Windows.Forms.GroupBox groupBox2; private System.Windows.Forms.Label label6; private System.Windows.Forms.Label label7; - private System.Windows.Forms.RadioButton radioButton4; + private System.Windows.Forms.RadioButton ccdOutputButton; + private System.Windows.Forms.RadioButton chdOutputButton; private System.Windows.Forms.ListView lvCompareTargets; private System.Windows.Forms.ColumnHeader columnHeader1; - private System.Windows.Forms.CheckBox checkEnableOutput; } } \ No newline at end of file diff --git a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.cs b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.cs index c64bbf18b1..c26bf3b157 100644 --- a/src/BizHawk.Client.DiscoHawk/MainDiscoForm.cs +++ b/src/BizHawk.Client.DiscoHawk/MainDiscoForm.cs @@ -39,13 +39,17 @@ namespace BizHawk.Client.DiscoHawk { lblMagicDragArea.AllowDrop = false; Cursor = Cursors.WaitCursor; + var outputFormat = DiscoHawkLogic.HawkedFormats.CCD; + if (ccdOutputButton.Checked) outputFormat = DiscoHawkLogic.HawkedFormats.CCD; + if (chdOutputButton.Checked) outputFormat = DiscoHawkLogic.HawkedFormats.CHD; try { foreach (var file in ValidateDrop(e.Data)) { var success = DiscoHawkLogic.HawkAndWriteFile( inputPath: file, - errorCallback: err => MessageBox.Show(err, "Error loading disc")); + errorCallback: err => MessageBox.Show(err, "Error loading disc"), + hawkedFormat: outputFormat); if (!success) break; } } diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs index 4bb2daab4a..a8085deda6 100644 --- a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs +++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs @@ -3,13 +3,16 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; + using BizHawk.Common; using BizHawk.Emulation.DiscSystem.CUE; #pragma warning disable BHI1005 -//MAME CHD images, using the standard libchdr for reading +// MAME CHD images, using the standard libchdr for reading +// helpful reference: https://problemkaputt.de/psxspx-cdrom-disk-images-chd-mame.htm namespace BizHawk.Emulation.DiscSystem { @@ -237,7 +240,7 @@ namespace BizHawk.Emulation.DiscSystem if (ret.PregapInChd && ret.Pregap == 0) { - throw new CHDParseException("Malformed CHD format: CHD track type indicate it contained pregap data, but no pregap data is present"); + throw new CHDParseException("Malformed CHD format: CHD track type indicates it contained pregap data, but no pregap data is present"); } ret.IsCDI = strs[1] == "CDI/2352"; @@ -384,7 +387,7 @@ namespace BizHawk.Emulation.DiscSystem i, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _); if (err == LibChdr.chd_error.CHDERR_NONE) { - var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen); + var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0'); chdf.CdMetadatas.Add(ParseMetadata2(metadata)); continue; } @@ -393,7 +396,7 @@ namespace BizHawk.Emulation.DiscSystem i, metadataOutput, (uint)metadataOutput.Length, out resultLen, out _, out _); if (err == LibChdr.chd_error.CHDERR_NONE) { - var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen); + var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0'); chdf.CdMetadatas.Add(ParseMetadata(metadata)); continue; } @@ -443,9 +446,12 @@ namespace BizHawk.Emulation.DiscSystem chdExpectedNumSectors += cdMetadata.Frames + cdMetadata.Padding; } - var chdActualNumSectors = chdf.Header.hunkcount * (chdf.Header.hunkbytes / LibChdr.CD_FRAME_SIZE); - //if (chdExpectedNumSectors != chdActualNumSectors) // i see some chds with 4 extra sectors of padding in the end? - if (chdExpectedNumSectors > chdActualNumSectors) + // pad expected sectors up to the next hunk + var sectorsPerHunk = chdf.Header.hunkbytes / LibChdr.CD_FRAME_SIZE; + chdExpectedNumSectors = (chdExpectedNumSectors + sectorsPerHunk - 1) / sectorsPerHunk * sectorsPerHunk; + + var chdActualNumSectors = chdf.Header.hunkcount * sectorsPerHunk; + if (chdExpectedNumSectors != chdActualNumSectors) { throw new CHDParseException("Malformed CHD format: Mismatch in expected and actual number of sectors present"); } @@ -789,5 +795,418 @@ namespace BizHawk.Emulation.DiscSystem throw; } } + + // crc16 table taken from https://github.com/mamedev/mame/blob/26b5eb211924acbe4b78f67da8d0ae3cbe77aa6d/src/lib/util/hashing.cpp#L400C1-L434C4 + private static readonly ushort[] _crc16Table = + { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, + }; + + private static ushort CalcCrc16(ReadOnlySpan bytes) + { + ushort crc16 = 0xFFFF; + foreach (var b in bytes) + { + crc16 = (ushort)((crc16 << 8) ^ _crc16Table[(crc16 >> 8) ^ b]); + } + + return crc16; + } + + private class ChdHunkMapEntry + { + public uint CompressedLength; + public long HunkOffset; + public ushort Crc16; + } + + private static readonly byte[] _chdTag = Encoding.ASCII.GetBytes("MComprHD"); + // 8 frames is apparently the standard, but we can probably afford to go a little extra ;) + private const uint CD_FRAMES_PER_HUNK = 75; // 1 second + + public static void Dump(Disc disc, string path) + { + // limited dumping support, v5 only with zstd compression + // however, this is a lot better than a lot of other dumps + // as the reference chdman can't easily dump subcode data + // this is important as chds don't have index info listed! + + // check if we have a multisession disc (CHD doesn't support those) + if (disc.Sessions.Count > 2) + { + throw new NotSupportedException("CHD does not support multisession discs"); + } + + using var fs = File.OpenWrite(path); + using var bw = new BinaryWriter(fs); + + // write header + // note CHD header has values in big endian, while BinaryWriter will write in little endian + bw.Write(_chdTag); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_V5_HEADER_SIZE)); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_HEADER_VERSION)); + // v5 chd allows for 4 different compression types + // we only have 1 implemented here + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_CODEC_ZSTD)); + bw.Write(0); + bw.Write(0); + bw.Write(0); + bw.Write(0L); // total size of all uncompressed data (written later) + bw.Write(0L); // offset to hunk map (written later) + bw.Write(0L); // offset to first metadata (written later) + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK)); // bytes per hunk + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE)); // bytes per sector (always CD_FRAME_SIZE) + var blankSha1 = new byte[LibChdr.CHD_SHA1_BYTES]; + bw.Write(blankSha1); // SHA1 of raw data (written later) + bw.Write(blankSha1); // SHA1 of raw data + metadata (written later) + bw.Write(blankSha1); // SHA1 of raw data + metadata for parent (N/A, always 0 for us) + + // collect metadata + var cdMetadatas = new List(); + var session = disc.Session1; + for (var i = 1; i <= session.InformationTrackCount; i++) + { + var track = session.Tracks[i]; + // frames includes the pregap, so we need to make sure to include the first pregap + // other pregaps can be included as part of the previous track + // not really so bad in practice, since we do full raw tracks + var firstIndexLba = track.Number == 1 ? 0 : track.LBA; + + var cdMetadata = new CHDCdMetadata + { + Track = (uint)track.Number, + IsCDI = track.Mode == 2 && session.TOC.SessionFormat == SessionFormat.Type10_CDI, + TrackType = track.Mode switch + { + 0 => LibChdr.chd_track_type.CD_TRACK_AUDIO, + 1 => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW, + 2 => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW, + _ => throw new InvalidOperationException(), + }, + SubType = LibChdr.chd_sub_type.CD_SUB_RAW, + SectorSize = 2352, + SubSize = 96, + Frames = (uint)(track.NextTrack.LBA - firstIndexLba), + Pregap = (uint)(track.LBA - firstIndexLba), + PostGap = 0, + }; + + cdMetadata.PregapInChd = cdMetadata.Pregap > 0; + cdMetadata.Padding = (0 - cdMetadata.Frames) & 3; + cdMetadata.PregapTrackType = cdMetadata.TrackType; + cdMetadatas.Add(cdMetadata); + } + + // we'll need to collect hunk locations for the hunk map + var hunkMapEntries = new List(); + // a "proper" CHD should have a SHA1 of the uncompressed contents + using var sha1Inc = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); + using var zstd = new Zstd(); + var dsr = new DiscSectorReader(disc) { Policy = { DeinterleavedSubcode = true, DeterministicClearBuffer = true } }; + var sectorBuf = new byte[LibChdr.CD_FRAME_SIZE]; + var cdLba = 0; + uint chdLba = 0, chdPos; + var curHunk = new byte[LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK]; +#if false // TODO: cdzs + const uint COMPRESSION_LEN_BYTES = LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK < 65536 ? 2 : 3; + const uint ECC_BYTES = (CD_FRAMES_PER_HUNK + 7) / 8; + var hunkHeader = new byte[COMPRESSION_LEN_BYTES + ECC_BYTES]; +#endif + + void EndHunk(uint hashLen) + { + var hunkOffset = bw.BaseStream.Position; + + // TODO: adjust compression level? + // note: it's fairly important a high compression level is chosen + // libchdr will assume a compressed hunk with not be larger than an uncompressed hunk + // but with low compression levels, this is not necessarily true + using (var cstream = zstd.CreateZstdCompressionStream(bw.BaseStream, Zstd.MaxCompressionLevel)) + { + cstream.Write(curHunk, 0, curHunk.Length); + } + + hunkMapEntries.Add(new() + { + CompressedLength = (uint)(bw.BaseStream.Position - hunkOffset), + HunkOffset = hunkOffset, + Crc16 = CalcCrc16(curHunk), + }); + + sha1Inc.AppendData(curHunk, 0, (int)hashLen); + Array.Clear(curHunk, 0, curHunk.Length); + } + + foreach (var cdMetadata in cdMetadatas) + { + for (var i = 0; i < cdMetadata.Frames; i++) + { + dsr.ReadLBA_2448(cdLba, sectorBuf, 0); + + // audio samples are byteswapped, so make sure to account for that + var trackType = i < cdMetadata.Pregap ? cdMetadata.PregapTrackType : cdMetadata.TrackType; + if (trackType == LibChdr.chd_track_type.CD_TRACK_AUDIO) + { + EndiannessUtils.MutatingByteSwap16(sectorBuf.AsSpan()[..2352]); + } + + chdPos = chdLba % CD_FRAMES_PER_HUNK; +#if false // TODO: cdzs + Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(2352U * chdPos), 2352); + Buffer.BlockCopy(sectorBuf, 2352, curHunk, (int)(2352U * CD_FRAMES_PER_HUNK + 96U * chdPos), 96); +#else + Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(LibChdr.CD_FRAME_SIZE * chdPos), (int)LibChdr.CD_FRAME_SIZE); +#endif + if (chdPos == CD_FRAMES_PER_HUNK - 1) + { + EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE); + } + + cdLba++; + chdLba++; + } + + for (var i = 0; i < cdMetadata.Padding; i++) + { + chdPos = chdLba % CD_FRAMES_PER_HUNK; + if (chdPos == CD_FRAMES_PER_HUNK - 1) + { + EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE); + } + + chdLba++; + } + } + + // make sure to write out any remaining pending hunk + chdPos = chdLba % CD_FRAMES_PER_HUNK; + if (chdPos != 0) + { + EndHunk(chdPos * LibChdr.CD_FRAME_SIZE); + } + + static string TrackTypeStr(LibChdr.chd_track_type trackType, bool isCdi) + { + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + return trackType switch + { + LibChdr.chd_track_type.CD_TRACK_AUDIO => "AUDIO", + LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => "MODE1_RAW", + LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => "CDI/2352", + LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => "MODE2_RAW", + _ => throw new InvalidOperationException(), + }; + } + + var metadataOffset = bw.BaseStream.Position; + var metadataHashes = new byte[cdMetadatas.Count][]; + // write metadata + for (var i = 0; i < cdMetadatas.Count; i++) + { + var cdMetadata = cdMetadatas[i]; + var trackType = TrackTypeStr(cdMetadata.TrackType, cdMetadata.IsCDI); + var pgTrackType = TrackTypeStr(cdMetadata.PregapTrackType, cdMetadata.IsCDI); + if (cdMetadata.PregapInChd) + { + pgTrackType = $"V{pgTrackType}"; + } + + var metadataStr = $"TRACK:{cdMetadata.Track} TYPE:{trackType} SUBTYPE:RW_RAW FRAMES:{cdMetadata.Frames} PREGAP:{cdMetadata.Pregap} PGTYPE:{pgTrackType} PGSUB:RW_RAW POSTGAP:0\0"; + var metadataBytes = Encoding.ASCII.GetBytes(metadataStr); + + bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CDROM_TRACK_METADATA2_TAG)); + bw.Write(LibChdr.CHD_MDFLAGS_CHECKSUM); + var chunkDataSize = new byte[3]; // 24 bit integer + chunkDataSize[0] = (byte)((metadataBytes.Length >> 16) & 0xFF); + chunkDataSize[1] = (byte)((metadataBytes.Length >> 8) & 0xFF); + chunkDataSize[2] = (byte)(metadataBytes.Length & 0xFF); + bw.Write(chunkDataSize); + + bw.Write(i == cdMetadatas.Count - 1 + ? 0L + : BinaryPrimitives.ReverseEndianness(bw.BaseStream.Position + 8 + metadataBytes.Length)); // offset to next chunk + + // last chunk + bw.Write(metadataBytes); + + metadataHashes[i] = SHA1Checksum.Compute(metadataBytes); + } + + var uncompressedHunkMap = new byte[hunkMapEntries.Count * 12]; + // compute uncompressed hunk map + for (var i = 0; i < hunkMapEntries.Count; i++) + { + var hunkMapEntry = hunkMapEntries[i]; + var mapEntryOffset = i * 12; + uncompressedHunkMap[mapEntryOffset + 0] = 0; // Codec 0 + uncompressedHunkMap[mapEntryOffset + 1] = (byte)((hunkMapEntry.CompressedLength >> 16) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 2] = (byte)((hunkMapEntry.CompressedLength >> 8) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 3] = (byte)(hunkMapEntry.CompressedLength & 0xFF); + uncompressedHunkMap[mapEntryOffset + 4] = (byte)((hunkMapEntry.HunkOffset >> 40) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 5] = (byte)((hunkMapEntry.HunkOffset >> 32) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 6] = (byte)((hunkMapEntry.HunkOffset >> 24) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 7] = (byte)((hunkMapEntry.HunkOffset >> 16) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 8] = (byte)((hunkMapEntry.HunkOffset >> 8) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 9] = (byte)(hunkMapEntry.HunkOffset & 0xFF); + uncompressedHunkMap[mapEntryOffset + 10] = (byte)((hunkMapEntry.Crc16 >> 8) & 0xFF); + uncompressedHunkMap[mapEntryOffset + 11] = (byte)(hunkMapEntry.Crc16 & 0xFF); + } + + File.WriteAllBytes("rawmap_expected.bin", uncompressedHunkMap); + var hunkMapCrc16 = CalcCrc16(uncompressedHunkMap); + var hunkMapOffset = bw.BaseStream.Position; + + var firstOffset = new byte[6]; + // write hunk map header + bw.Write(0); // compressed map length (written later) + Buffer.BlockCopy(uncompressedHunkMap, 4, firstOffset, 0, 6); + bw.Write(firstOffset); // first hunk offset + bw.Write(BinaryPrimitives.ReverseEndianness(hunkMapCrc16)); // uncompressed map crc16 + bw.Write((byte)24); // num bits used to stored compression length + bw.Write((byte)0); // num bits used to stored self refs (not used) + bw.Write((byte)0); // num bits used to stored parent unit refs (not used) + bw.Write((byte)0); // reserved (should just be 0) + + // huffman map + // we always have anything decoded return 0 + // so we can be lazy here an define a somewhat bogus map which allows us to skip compression + + bw.Write((byte)0x11); // makes command 0 take 1 bit (thus limits compression length to 31 bits if we want to keep a byte level stream) + + // 60 bits are now left to write for the huffman map, we'll want them all to be 0 + // also, after the huffman map proceeds the compression type bits, which for us will just be a ton of 0 bits + // each hunk needing a bit set to 0 to indicate compression type 0 + + // basic bit writing code + byte curByte = 0; + var curBit = 60 + hunkMapEntries.Count; + while (curBit >= 8) + { + bw.Write((byte)0); + curBit -= 8; + } + + void WriteByteBits(byte b) + { + for (var i = 0; i < 8; i++) + { + var bit = ((b >> (7 - i)) & 1) != 0; + if (bit) + { + curByte |= (byte)(1 << (7 - curBit)); + } + + curBit++; + if (curBit == 8) + { + bw.Write(curByte); + curBit = 0; + curByte = 0; + } + } + } + + for (var i = 0; i < hunkMapEntries.Count; i++) + { + var mapEntryOffset = i * 12; + // length + WriteByteBits(uncompressedHunkMap[mapEntryOffset + 1]); + WriteByteBits(uncompressedHunkMap[mapEntryOffset + 2]); + WriteByteBits(uncompressedHunkMap[mapEntryOffset + 3]); + // crc16 + WriteByteBits(uncompressedHunkMap[mapEntryOffset + 10]); + WriteByteBits(uncompressedHunkMap[mapEntryOffset + 11]); + } + + // write final byte if present + if (curBit != 0) + { + bw.Write(curByte); + } + + // finish everything up + + var hunkMapEnd = bw.BaseStream.Position; + bw.BaseStream.Seek(hunkMapOffset, SeekOrigin.Begin); + // hunk map length sans header + bw.Write(BinaryPrimitives.ReverseEndianness((uint)(hunkMapEnd - hunkMapOffset - 16))); + + bw.BaseStream.Seek(0x20, SeekOrigin.Begin); + bw.Write(BinaryPrimitives.ReverseEndianness(chdLba * (long)LibChdr.CD_FRAME_SIZE)); + bw.Write(BinaryPrimitives.ReverseEndianness(hunkMapOffset)); + bw.Write(BinaryPrimitives.ReverseEndianness(metadataOffset)); + + var rawSha1 = sha1Inc.GetHashAndReset(); + // calc overall sha1 now (uses raw sha1 and metadata hashes) + sha1Inc.AppendData(rawSha1); + // apparently these are expected to be sorted with memcmp semantics + Array.Sort(metadataHashes, static (x, y) => + { + for (var i = 0; i < x.Length; i++) + { + if (x[i] < y[i]) + { + return -1; + } + + if (x[i] > y[i]) + { + return 1; + } + } + + return 0; + }); + + // tag is hashed alongside the hash + // we use the same tag every time, so we can just reuse this array + var metadataTag = new byte[4]; + metadataTag[0] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 24) & 0xFF); + metadataTag[1] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 16) & 0xFF); + metadataTag[2] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 8) & 0xFF); + metadataTag[3] = (byte)(LibChdr.CDROM_TRACK_METADATA2_TAG & 0xFF); + foreach (var metadataHash in metadataHashes) + { + sha1Inc.AppendData(metadataTag); + sha1Inc.AppendData(metadataHash); + } + + var overallSha1 = sha1Inc.GetHashAndReset(); + + bw.BaseStream.Seek(0x40, SeekOrigin.Begin); + bw.Write(rawSha1); + bw.Write(overallSha1); + } } -} \ No newline at end of file +} diff --git a/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/AudioExtractor.cs b/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/AudioExtractor.cs index f2c1d03c3c..0a134d4027 100644 --- a/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/AudioExtractor.cs +++ b/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/AudioExtractor.cs @@ -1,11 +1,10 @@ using System; using System.Threading.Tasks; using System.IO; -using BizHawk.Emulation.DiscSystem; using BizHawk.Common; -namespace BizHawk.Client.DiscoHawk +namespace BizHawk.Emulation.DiscSystem { public static class AudioExtractor { diff --git a/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/DiscoHawkLogic.cs b/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/DiscoHawkLogic.cs index 8e235d7d06..c656c5fddb 100644 --- a/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/DiscoHawkLogic.cs +++ b/src/BizHawk.Emulation.DiscSystem/DiscoHawkLogic/DiscoHawkLogic.cs @@ -4,7 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using BizHawk.Client.DiscoHawk; using BizHawk.Common.PathExtensions; namespace BizHawk.Emulation.DiscSystem @@ -246,7 +245,16 @@ namespace BizHawk.Emulation.DiscSystem return ret; } - public static bool HawkAndWriteFile(string inputPath, Action errorCallback, DiscInterface discInterface = DiscInterface.BizHawk) + /// + /// Formats supported with HawkAndWriteFile + /// + public enum HawkedFormats + { + CCD, + CHD, + } + + public static bool HawkAndWriteFile(string inputPath, Action errorCallback, DiscInterface discInterface = DiscInterface.BizHawk, HawkedFormats hawkedFormat = HawkedFormats.CCD) { DiscMountJob job = new(inputPath, discInterface); job.Run(); @@ -257,8 +265,25 @@ namespace BizHawk.Emulation.DiscSystem return false; } var (dir, baseName, _) = inputPath.SplitPathToDirFileAndExt(); - var outfile = Path.Combine(dir!, $"{baseName}_hawked.ccd"); - CCD_Format.Dump(disc, outfile); + var ext = hawkedFormat switch + { + HawkedFormats.CCD => ".ccd", + HawkedFormats.CHD => ".chd", + _ => throw new InvalidOperationException(), + }; + var outfile = Path.Combine(dir!, $"{baseName}_hawked{ext}"); + switch (hawkedFormat) + { + case HawkedFormats.CCD: + CCD_Format.Dump(disc, outfile); + break; + case HawkedFormats.CHD: + CHD_Format.Dump(disc, outfile); + break; + default: + throw new InvalidOperationException(); + } + return true; } @@ -268,6 +293,7 @@ namespace BizHawk.Emulation.DiscSystem string dirArg = null; string infile = null; var loadDiscInterface = DiscInterface.BizHawk; + var outputFormat = HawkedFormats.CCD; var compareDiscInterfaces = new List(); bool hawk = false; bool music = false; @@ -296,6 +322,10 @@ namespace BizHawk.Emulation.DiscSystem { overwrite = true; } + else if (au is "OUTPUT") + { + outputFormat = (HawkedFormats)Enum.Parse(typeof(HawkedFormats), args[idx++], true); + } else infile = a; } @@ -305,7 +335,8 @@ namespace BizHawk.Emulation.DiscSystem HawkAndWriteFile( inputPath: infile, errorCallback: err => Console.WriteLine($"failed to convert {infile}:\n{err}"), - discInterface: loadDiscInterface); + discInterface: loadDiscInterface, + hawkedFormat: outputFormat); } if (music) diff --git a/src/BizHawk.Emulation.DiscSystem/LibChdr.cs b/src/BizHawk.Emulation.DiscSystem/LibChdr.cs index 23b3aa5dba..942d51689b 100644 --- a/src/BizHawk.Emulation.DiscSystem/LibChdr.cs +++ b/src/BizHawk.Emulation.DiscSystem/LibChdr.cs @@ -15,6 +15,28 @@ namespace BizHawk.Emulation.DiscSystem /// public static class LibChdr { + public const uint CHD_HEADER_VERSION = 5; + public const uint CHD_V5_HEADER_SIZE = 124; + + public const int CHD_MD5_BYTES = 16; + public const int CHD_SHA1_BYTES = 20; + + public const byte CHD_MDFLAGS_CHECKSUM = 0x01; + + public const uint CHD_CODEC_ZSTD = 0x7A737464; // zstd + public const uint CHD_CODEC_CD_ZSTD = 0x63647A73; // cdzs + + public const uint CDROM_OLD_METADATA_TAG = 0x43484344; // CHCD + public const uint CDROM_TRACK_METADATA_TAG = 0x43485452; // CHTR + public const uint CDROM_TRACK_METADATA2_TAG = 0x43485432; // CHT2 + + // these formats are more for sscanf, they aren't suitable for C# + public const string CDROM_TRACK_METADATA_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d"; + public const string CDROM_TRACK_METADATA2_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d PREGAP:%d PGTYPE:%s PGSUB:%s POSTGAP:%d"; + + public const int CHD_OPEN_READ = 1; + public const int CHD_OPEN_READWRITE = 2; + public enum chd_error : int { CHDERR_NONE, @@ -47,9 +69,6 @@ namespace BizHawk.Emulation.DiscSystem CHDERR_UNSUPPORTED_FORMAT } - public const int CHD_OPEN_READ = 1; - public const int CHD_OPEN_READWRITE = 2; - [StructLayout(LayoutKind.Sequential)] public struct chd_core_file { @@ -191,9 +210,6 @@ namespace BizHawk.Emulation.DiscSystem [DllImport("chdr")] public static extern void chd_close(IntPtr chd); - public const int CHD_MD5_BYTES = 16; - public const int CHD_SHA1_BYTES = 20; - // extracted chd header (not the same as the one on disk, but rather an interpreted one by libchdr) [StructLayout(LayoutKind.Sequential)] public struct chd_header @@ -229,14 +245,6 @@ namespace BizHawk.Emulation.DiscSystem [DllImport("chdr")] public static extern chd_error chd_read(IntPtr chd, uint hunknum, byte[] buffer); - public const uint CDROM_TRACK_METADATA2_TAG = 0x43485432; // CHT2 - public const uint CDROM_TRACK_METADATA_TAG = 0x43485452; // CHTR - public const uint CDROM_OLD_METADATA_TAG = 0x43484344; // CHCD - - // these formats are more for sscanf, they aren't suitable for C# - public const string CDROM_TRACK_METADATA2_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d PREGAP:%d PGTYPE:%s PGSUB:%s POSTGAP:%d"; - public const string CDROM_TRACK_METADATA_FORMAT = "TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d"; - public enum chd_track_type : uint { CD_TRACK_MODE1 = 0, // mode 1 2048 bytes/sector