From d352f34004a3bc8e90c74e88413fbc401c66f7d9 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sat, 4 Jul 2026 20:13:39 +0100 Subject: [PATCH] Add support for offset correction of first track pregap. --- Aaru.Core/Devices/Dumping/CompactDisc/Dump.cs | 7 +- .../Devices/Dumping/CompactDisc/Pregap.cs | 249 ++++++++++++++---- 2 files changed, 205 insertions(+), 51 deletions(-) diff --git a/Aaru.Core/Devices/Dumping/CompactDisc/Dump.cs b/Aaru.Core/Devices/Dumping/CompactDisc/Dump.cs index 39e0b42cb..2dfc65ace 100644 --- a/Aaru.Core/Devices/Dumping/CompactDisc/Dump.cs +++ b/Aaru.Core/Devices/Dumping/CompactDisc/Dump.cs @@ -834,7 +834,12 @@ sealed partial class Dump mediaTags, supportedSubchannel, ref totalDuration, - outputOptical); + outputOptical, + subLog, + offsetBytes, + sectorsForOffset, + desiredSubchannel, + subchannelExtents); } audioExtents = new ExtentsULong(); diff --git a/Aaru.Core/Devices/Dumping/CompactDisc/Pregap.cs b/Aaru.Core/Devices/Dumping/CompactDisc/Pregap.cs index 94460f1c1..13f765bfc 100644 --- a/Aaru.Core/Devices/Dumping/CompactDisc/Pregap.cs +++ b/Aaru.Core/Devices/Dumping/CompactDisc/Pregap.cs @@ -44,6 +44,8 @@ using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Core.Graphics; +using Aaru.Core.Logging; +using Aaru.Decoders.CD; using Aaru.Devices; using Aaru.Logging; using Humanizer; @@ -53,32 +55,56 @@ namespace Aaru.Core.Devices.Dumping; partial class Dump { - // TODO: Fix offset /// Reads the first track pregap from a CompactDisc /// Block size in bytes /// Current speed /// List of media tags /// Subchannel the drive can read /// Total time spent sending commands to a drive - /// + /// Output optical image + /// Subchannel log + /// Drive read offset in bytes + /// Number of sectors needed to cover the offset + /// Subchannel type the user wants written to the image + /// Set of sector LBAs that still need subchannel data void ReadCdFirstTrackPregap(uint blockSize, ref double currentSpeed, Dictionary mediaTags, MmcSubchannel supportedSubchannel, ref double totalDuration, - IWritableOpticalImage outputOptical) + IWritableOpticalImage outputOptical, SubchannelLog subLog, int offsetBytes, + int sectorsForOffset, MmcSubchannel desiredSubchannel, HashSet subchannelExtents) { bool sense; // Sense indicator byte[] cmdBuf; // Data buffer double cmdDuration; // Command execution time ulong sectorSpeedStart = 0; // Used to calculate correct speed - var gotFirstTrackPregap = false; var firstTrackPregapSectorsGood = 0; var firstTrackPregapMs = new MemoryStream(); + uint subSize = supportedSubchannel switch + { + MmcSubchannel.None => 0, + MmcSubchannel.Raw => 96, + _ => 16 + }; + + bool needsOffsetCorrection = _fixOffset && offsetBytes != 0; + + // Expand read range to include extra sectors needed for offset correction. + // For positive offset: we must read a few sectors past LBA -1 (into LBA 0, 1, …). + // For negative offset: we must read a few sectors before LBA -150 (deeper lead-in). + int startLba = needsOffsetCorrection && offsetBytes < 0 ? -150 - sectorsForOffset : -150; + int endLba = needsOffsetCorrection && offsetBytes > 0 ? sectorsForOffset - 1 : -1; + int totalSectorsToRead = endLba - startLba + 1; + + var allSectorsBuffer = new byte[totalSectorsToRead * blockSize]; + var readSuccess = new bool[totalSectorsToRead]; + var sectorIsAudio = new bool[totalSectorsToRead]; + UpdateStatus?.Invoke(Localization.Core.Reading_first_track_pregap); InitProgress?.Invoke(); _speedStopwatch.Restart(); - for(int firstTrackPregapBlock = -150; - firstTrackPregapBlock < 0 && _resume.NextBlock == 0; + for(int firstTrackPregapBlock = startLba; + firstTrackPregapBlock <= endLba && _resume.NextBlock == 0; firstTrackPregapBlock++) { if(_aborted) @@ -88,9 +114,14 @@ partial class Dump break; } - PulseProgress?.Invoke(string.Format(Localization.Core.Trying_to_read_first_track_pregap_sector_0_1, - firstTrackPregapBlock, - ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize())); + int bufferIndex = firstTrackPregapBlock - startLba; + + if(firstTrackPregapBlock >= -150) + { + PulseProgress?.Invoke(string.Format(Localization.Core.Trying_to_read_first_track_pregap_sector_0_1, + firstTrackPregapBlock, + ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize())); + } // ReSharper disable IntVariableOverflowInUncheckedContext sense = _dev.ReadCd(out cmdBuf, @@ -114,54 +145,29 @@ partial class Dump if(!sense && !_dev.Error) { - if(outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors)) + Array.Copy(cmdBuf, 0, allSectorsBuffer, bufferIndex * (int)blockSize, (int)blockSize); + + // Determine if this sector is audio by checking for the CD sync mark + var sectorData = new byte[2352]; + Array.Copy(cmdBuf, 0, sectorData, 0, 2352); + sectorIsAudio[bufferIndex] = !IsData(sectorData); + readSuccess[bufferIndex] = true; + + if(firstTrackPregapBlock >= -150) { - var data = new byte[2352]; - Array.Copy(cmdBuf, 0, data, 0, 2352); - outputOptical.WriteSectorLong(data, (ulong)(firstTrackPregapBlock * -1), true, SectorStatus.Dumped); - - if(supportedSubchannel == MmcSubchannel.Raw) - { - var subchannel = new byte[96]; - Array.Copy(cmdBuf, 2352, subchannel, 0, 96); - - outputOptical.WriteSectorTag(subchannel, - (ulong)(firstTrackPregapBlock * -1), - true, - SectorTagType.CdSectorSubchannel); - } + firstTrackPregapSectorsGood++; + totalDuration += cmdDuration; + (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Green); } - else - firstTrackPregapMs.Write(cmdBuf, 0, (int)blockSize); - - gotFirstTrackPregap = true; - firstTrackPregapSectorsGood++; - totalDuration += cmdDuration; - (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Green); } else { - // Write empty data - if(outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors)) - { - outputOptical.WriteSectorLong(new byte[2352], - (ulong)(firstTrackPregapBlock * -1), - true, - SectorStatus.Errored); - - if(supportedSubchannel == MmcSubchannel.Raw) - { - outputOptical.WriteSectorTag(new byte[96], - (ulong)(firstTrackPregapBlock * -1), - true, - SectorTagType.CdSectorSubchannel); - } - } - else if(gotFirstTrackPregap) firstTrackPregapMs.Write(new byte[blockSize], 0, (int)blockSize); - - (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Red); + if(firstTrackPregapBlock >= -150) + (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Red); } + if(firstTrackPregapBlock < -150) continue; + sectorSpeedStart++; double elapsed = _speedStopwatch.Elapsed.TotalSeconds; @@ -175,6 +181,149 @@ partial class Dump _speedStopwatch.Stop(); + // Apply offset correction if any audio sector is present in the pregap range + if(needsOffsetCorrection) + { + var hasAudioSectors = false; + + for(var i = 0; i < totalSectorsToRead && !hasAudioSectors; i++) + { + int lba = startLba + i; + + if(lba is >= -150 and <= -1 && readSuccess[i] && sectorIsAudio[i]) hasAudioSectors = true; + } + + if(hasAudioSectors) + { + var blocksToRead = (uint)totalSectorsToRead; + + FixOffsetData(offsetBytes, + 2352, + sectorsForOffset, + supportedSubchannel, + ref blocksToRead, + subSize, + ref allSectorsBuffer, + blockSize, + false); + } + } + + // Write sectors LBA -150 to -1 from the (possibly offset-corrected) buffer. + // After FixOffsetData the buffer always contains the corrected 150 pregap sectors + // starting at index 0, regardless of whether the offset was positive or negative. + var firstSectorWritten = false; + + for(int firstTrackPregapBlock = -150; firstTrackPregapBlock <= -1; firstTrackPregapBlock++) + { + int bufferIndex = firstTrackPregapBlock + 150; // 0 = LBA -150, 149 = LBA -1 + + // Map back to the pre-correction read index to check whether the read succeeded. + // For negative offset the extra sectors were prepended, so index shifts by sectorsForOffset. + int originalIndex = needsOffsetCorrection && offsetBytes < 0 ? bufferIndex + sectorsForOffset : bufferIndex; + + bool success = originalIndex < readSuccess.Length && readSuccess[originalIndex]; + + if(outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors)) + { + var data = new byte[2352]; + Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize, data, 0, 2352); + + outputOptical.WriteSectorLong(data, + (ulong)-firstTrackPregapBlock, + true, + success ? SectorStatus.Dumped : SectorStatus.Errored); + + if(supportedSubchannel != MmcSubchannel.None && desiredSubchannel != MmcSubchannel.None) + { + var sub = new byte[subSize]; + Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize + 2352, sub, 0, (int)subSize); + + if(supportedSubchannel == MmcSubchannel.Q16) sub = Subchannel.ConvertQToRaw(sub); + + subLog?.WriteEntry(sub, + supportedSubchannel == MmcSubchannel.Raw, + firstTrackPregapBlock, + 1, + false, + false); + + if(_fixSubchannelPosition) + { + // Write subchannel to the sector address its Q channel indicates, + // which may differ from the read position (positive or negative LBA). + byte[] deSub = Subchannel.Deinterleave(sub); + var q = new byte[12]; + Array.Copy(deSub, 12, q, 0, 12); + + CRC16CcittContext.Data(q, 10, out byte[] crc); + bool crcOk = crc[0] == q[10] && crc[1] == q[11]; + + if(crcOk && (q[0] & 0x3) == 1) + { + var amin = (byte)(q[7] / 16 * 10 + (q[7] & 0x0F)); + var asec = (byte)(q[8] / 16 * 10 + (q[8] & 0x0F)); + var aframe = (byte)(q[9] / 16 * 10 + (q[9] & 0x0F)); + int aPos = amin * 60 * 75 + asec * 75 + aframe - 150; + + if(aPos < 0) + { + outputOptical.WriteSectorTag(sub, (ulong)-aPos, true, SectorTagType.CdSectorSubchannel); + + subchannelExtents.Remove(aPos); + } + else + { + outputOptical.WriteSectorTag(sub, (ulong)aPos, false, SectorTagType.CdSectorSubchannel); + + subchannelExtents.Remove(aPos); + } + } + else + { + // Q CRC bad or not a position packet: fall back to read address + outputOptical.WriteSectorTag(sub, + (ulong)-firstTrackPregapBlock, + true, + SectorTagType.CdSectorSubchannel); + } + } + else + { + outputOptical.WriteSectorTag(sub, + (ulong)-firstTrackPregapBlock, + true, + SectorTagType.CdSectorSubchannel); + } + } + } + else + { + if(success) + { + firstSectorWritten = true; + firstTrackPregapMs.Write(allSectorsBuffer, bufferIndex * (int)blockSize, (int)blockSize); + } + else if(firstSectorWritten) firstTrackPregapMs.Write(new byte[blockSize], 0, (int)blockSize); + + // Log subchannel even when we cannot store it individually in the image + if(supportedSubchannel != MmcSubchannel.None && success) + { + var sub = new byte[subSize]; + Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize + 2352, sub, 0, (int)subSize); + + if(supportedSubchannel == MmcSubchannel.Q16) sub = Subchannel.ConvertQToRaw(sub); + + subLog?.WriteEntry(sub, + supportedSubchannel == MmcSubchannel.Raw, + firstTrackPregapBlock, + 1, + false, + false); + } + } + } + if(firstTrackPregapSectorsGood > 0 && !outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors)) mediaTags.Add(MediaTagType.CD_FirstTrackPregap, firstTrackPregapMs.ToArray());