Support dumping CD-i Ready when drive returns data sectors as audio. Fixes #294

This commit is contained in:
2020-06-25 01:13:02 +01:00
parent 651a2df2aa
commit dbbb6812d2
3 changed files with 249 additions and 133 deletions

View File

@@ -37,6 +37,7 @@ using Aaru.CommonTypes.Extents;
using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs; using Aaru.CommonTypes.Structs;
using Aaru.Core.Logging; using Aaru.Core.Logging;
using Aaru.Decoders.CD;
using Aaru.Devices; using Aaru.Devices;
using Schemas; using Schemas;
@@ -48,6 +49,71 @@ namespace Aaru.Core.Devices.Dumping
{ {
partial class Dump partial class Dump
{ {
static bool IsData(byte[] sector)
{
if(sector?.Length != 2352)
return false;
byte[] syncMark =
{
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
};
byte[] testMark = new byte[12];
Array.Copy(sector, 0, testMark, 0, 12);
return syncMark.SequenceEqual(testMark) && (sector[0xF] == 0 || sector[0xF] == 1 || sector[0xF] == 2);
}
static bool IsScrambledData(byte[] sector, int wantedLba, out int? offset)
{
offset = 0;
if(sector?.Length != 2352)
return false;
byte[] syncMark =
{
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
};
byte[] testMark = new byte[12];
for(int i = 0; i <= 2336; i++)
{
Array.Copy(sector, i, testMark, 0, 12);
if(syncMark.SequenceEqual(testMark) &&
(sector[i + 0xF] == 0x60 || sector[i + 0xF] == 0x61 || sector[i + 0xF] == 0x62))
{
// De-scramble M and S
int minute = sector[i + 12] ^ 0x01;
int second = sector[i + 13] ^ 0x80;
int frame = sector[i + 14];
// Convert to binary
minute = ((minute / 16) * 10) + (minute & 0x0F);
second = ((second / 16) * 10) + (second & 0x0F);
frame = ((frame / 16) * 10) + (frame & 0x0F);
// Calculate the first found LBA
int lba = ((minute * 60 * 75) + (second * 75) + frame) - 150;
// Calculate the difference between the found LBA and the requested one
int diff = wantedLba - lba;
offset = i + (2352 * diff);
return true;
}
}
return false;
}
// TODO: Set pregap for Track 1
// TODO: Detect errors in sectors
/// <summary>Reads all CD user data</summary> /// <summary>Reads all CD user data</summary>
/// <param name="audioExtents">Extents with audio sectors</param> /// <param name="audioExtents">Extents with audio sectors</param>
/// <param name="blocks">Total number of positive sectors</param> /// <param name="blocks">Total number of positive sectors</param>
@@ -77,11 +143,11 @@ namespace Aaru.Core.Devices.Dumping
/// <param name="totalDuration">Total commands duration</param> /// <param name="totalDuration">Total commands duration</param>
void ReadCdiReady(uint blockSize, ref double currentSpeed, DumpHardwareType currentTry, ExtentsULong extents, void ReadCdiReady(uint blockSize, ref double currentSpeed, DumpHardwareType currentTry, ExtentsULong extents,
IbgLog ibgLog, ref double imageWriteDuration, ExtentsULong leadOutExtents, IbgLog ibgLog, ref double imageWriteDuration, ExtentsULong leadOutExtents,
ref double maxSpeed, MhddLog mhddLog, ref double minSpeed, bool read6, bool read10, ref double maxSpeed, MhddLog mhddLog, ref double minSpeed, uint subSize,
bool read12, bool read16, bool readcd, uint subSize, MmcSubchannel supportedSubchannel, MmcSubchannel supportedSubchannel, ref double totalDuration, Track[] tracks,
bool supportsLongSectors, ref double totalDuration, Track[] tracks, SubchannelLog subLog, SubchannelLog subLog, MmcSubchannel desiredSubchannel, Dictionary<byte, string> isrcs,
MmcSubchannel desiredSubchannel, Dictionary<byte, string> isrcs, ref string mcn, ref string mcn, HashSet<int> subchannelExtents, ulong blocks, bool cdiReadyReadAsAudio,
HashSet<int> subchannelExtents, ulong blocks) int offsetBytes, int sectorsForOffset)
{ {
ulong sectorSpeedStart = 0; // Used to calculate correct speed ulong sectorSpeedStart = 0; // Used to calculate correct speed
DateTime timeSpeedStart = DateTime.UtcNow; // Time of start for speed calculation DateTime timeSpeedStart = DateTime.UtcNow; // Time of start for speed calculation
@@ -91,13 +157,22 @@ namespace Aaru.Core.Devices.Dumping
double cmdDuration = 0; // Command execution time double cmdDuration = 0; // Command execution time
const uint sectorSize = 2352; // Full sector size const uint sectorSize = 2352; // Full sector size
Track firstTrack = tracks.FirstOrDefault(t => t.TrackSequence == 1); Track firstTrack = tracks.FirstOrDefault(t => t.TrackSequence == 1);
uint blocksToRead = 0; // How many sectors to read at once
if(firstTrack is null) if(firstTrack is null)
return; return;
if(cdiReadyReadAsAudio)
{
_dumpLog.WriteLine("Setting speed to 8x for CD-i Ready reading as audio.");
UpdateStatus?.Invoke("Setting speed to 8x for CD-i Ready reading as audio.");
_dev.SetCdSpeed(out _, RotationalControl.ClvAndImpureCav, 1416, 0, _dev.Timeout, out _);
}
InitProgress?.Invoke(); InitProgress?.Invoke();
for(ulong i = _resume.NextBlock; i < firstTrack.TrackStartSector; i += _maximumReadable) for(ulong i = _resume.NextBlock; i < firstTrack.TrackStartSector; i += blocksToRead)
{ {
if(_aborted) if(_aborted)
{ {
@@ -108,12 +183,24 @@ namespace Aaru.Core.Devices.Dumping
break; break;
} }
if(i >= firstTrack.TrackStartSector)
break;
uint firstSectorToRead = (uint)i; uint firstSectorToRead = (uint)i;
Track track = tracks.OrderBy(t => t.TrackStartSector).LastOrDefault(t => i >= t.TrackStartSector); blocksToRead = _maximumReadable;
if(blocksToRead == 1 && cdiReadyReadAsAudio)
blocksToRead += (uint)sectorsForOffset;
if(cdiReadyReadAsAudio)
{
// TODO: FreeBSD bug
if(offsetBytes < 0)
{
if(i == 0)
firstSectorToRead = uint.MaxValue - (uint)(sectorsForOffset - 1); // -1
else
firstSectorToRead -= (uint)sectorsForOffset;
}
}
#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator
@@ -133,34 +220,11 @@ namespace Aaru.Core.Devices.Dumping
UpdateProgress?.Invoke($"Reading sector {i} of {blocks} ({currentSpeed:F3} MiB/sec.)", (long)i, UpdateProgress?.Invoke($"Reading sector {i} of {blocks} ({currentSpeed:F3} MiB/sec.)", (long)i,
(long)blocks); (long)blocks);
if(readcd) sense = _dev.ReadCd(out cmdBuf, out senseBuf, firstSectorToRead, blockSize, blocksToRead,
{ MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
sense = _dev.ReadCd(out cmdBuf, out senseBuf, firstSectorToRead, blockSize, _maximumReadable, MmcErrorField.None, supportedSubchannel, _dev.Timeout, out cmdDuration);
MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true,
true, MmcErrorField.None, supportedSubchannel, _dev.Timeout, out cmdDuration);
totalDuration += cmdDuration; totalDuration += cmdDuration;
}
else if(read16)
{
sense = _dev.Read16(out cmdBuf, out senseBuf, 0, false, true, false, firstSectorToRead, blockSize,
0, _maximumReadable, false, _dev.Timeout, out cmdDuration);
}
else if(read12)
{
sense = _dev.Read12(out cmdBuf, out senseBuf, 0, false, true, false, false, firstSectorToRead,
blockSize, 0, _maximumReadable, false, _dev.Timeout, out cmdDuration);
}
else if(read10)
{
sense = _dev.Read10(out cmdBuf, out senseBuf, 0, false, true, false, false, firstSectorToRead,
blockSize, 0, (ushort)_maximumReadable, _dev.Timeout, out cmdDuration);
}
else if(read6)
{
sense = _dev.Read6(out cmdBuf, out senseBuf, firstSectorToRead, blockSize, (byte)_maximumReadable,
_dev.Timeout, out cmdDuration);
}
double elapsed; double elapsed;
@@ -172,35 +236,12 @@ namespace Aaru.Core.Devices.Dumping
UpdateProgress?.Invoke($"Reading sector {i + r} of {blocks} ({currentSpeed:F3} MiB/sec.)", UpdateProgress?.Invoke($"Reading sector {i + r} of {blocks} ({currentSpeed:F3} MiB/sec.)",
(long)i + r, (long)blocks); (long)i + r, (long)blocks);
if(readcd) sense = _dev.ReadCd(out cmdBuf, out senseBuf, (uint)(i + r), blockSize,
{ (uint)sectorsForOffset + 1, MmcSectorTypes.AllTypes, false, false, true,
sense = _dev.ReadCd(out cmdBuf, out senseBuf, (uint)(i + r), blockSize, 1, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, supportedSubchannel, _dev.Timeout, out cmdDuration);
true, true, MmcErrorField.None, supportedSubchannel, _dev.Timeout,
out cmdDuration);
totalDuration += cmdDuration; totalDuration += cmdDuration;
}
else if(read16)
{
sense = _dev.Read16(out cmdBuf, out senseBuf, 0, false, true, false, i + r, blockSize, 0, 1,
false, _dev.Timeout, out cmdDuration);
}
else if(read12)
{
sense = _dev.Read12(out cmdBuf, out senseBuf, 0, false, true, false, false, (uint)(i + r),
blockSize, 0, 1, false, _dev.Timeout, out cmdDuration);
}
else if(read10)
{
sense = _dev.Read10(out cmdBuf, out senseBuf, 0, false, true, false, false, (uint)(i + r),
blockSize, 0, 1, _dev.Timeout, out cmdDuration);
}
else if(read6)
{
sense = _dev.Read6(out cmdBuf, out senseBuf, (uint)(i + r), blockSize, 1, _dev.Timeout,
out cmdDuration);
}
if(!sense && if(!sense &&
!_dev.Error) !_dev.Error)
@@ -210,6 +251,10 @@ namespace Aaru.Core.Devices.Dumping
extents.Add(i + r, 1, true); extents.Add(i + r, 1, true);
DateTime writeStart = DateTime.Now; DateTime writeStart = DateTime.Now;
if(cdiReadyReadAsAudio)
FixOffsetData(offsetBytes, sectorSize, sectorsForOffset, supportedSubchannel,
ref blocksToRead, subSize, ref cmdBuf, blockSize, false);
if(supportedSubchannel != MmcSubchannel.None) if(supportedSubchannel != MmcSubchannel.None)
{ {
byte[] data = new byte[sectorSize]; byte[] data = new byte[sectorSize];
@@ -219,12 +264,14 @@ namespace Aaru.Core.Devices.Dumping
Array.Copy(cmdBuf, sectorSize, sub, 0, subSize); Array.Copy(cmdBuf, sectorSize, sub, 0, subSize);
if(cdiReadyReadAsAudio)
data = Sector.Scramble(data);
_outputPlugin.WriteSectorsLong(data, i + r, 1); _outputPlugin.WriteSectorsLong(data, i + r, 1);
bool indexesChanged = bool indexesChanged =
WriteSubchannelToImage(supportedSubchannel, desiredSubchannel, sub, i + r, 1, WriteSubchannelToImage(supportedSubchannel, desiredSubchannel, sub, i + r, 1,
subLog, isrcs, (byte)track.TrackSequence, ref mcn, tracks, subLog, isrcs, 1, ref mcn, tracks, subchannelExtents);
subchannelExtents);
// Set tracks and go back // Set tracks and go back
if(indexesChanged) if(indexesChanged)
@@ -237,25 +284,7 @@ namespace Aaru.Core.Devices.Dumping
} }
else else
{ {
if(supportsLongSectors) _outputPlugin.WriteSectorsLong(cmdBuf, i + r, 1);
{
_outputPlugin.WriteSectorsLong(cmdBuf, i + r, 1);
}
else
{
if(cmdBuf.Length % sectorSize == 0)
{
byte[] data = new byte[2048];
Array.Copy(cmdBuf, 16, data, 2048, 2048);
_outputPlugin.WriteSectors(data, i + r, 1);
}
else
{
_outputPlugin.WriteSectorsLong(cmdBuf, i + r, 1);
}
}
} }
imageWriteDuration += (DateTime.Now - writeStart).TotalSeconds; imageWriteDuration += (DateTime.Now - writeStart).TotalSeconds;
@@ -291,61 +320,68 @@ namespace Aaru.Core.Devices.Dumping
if(!sense && if(!sense &&
!_dev.Error) !_dev.Error)
{ {
if(cdiReadyReadAsAudio)
FixOffsetData(offsetBytes, sectorSize, sectorsForOffset, supportedSubchannel, ref blocksToRead,
subSize, ref cmdBuf, blockSize, false);
mhddLog.Write(i, cmdDuration); mhddLog.Write(i, cmdDuration);
ibgLog.Write(i, currentSpeed * 1024); ibgLog.Write(i, currentSpeed * 1024);
extents.Add(i, _maximumReadable, true); extents.Add(i, blocksToRead, true);
DateTime writeStart = DateTime.Now; DateTime writeStart = DateTime.Now;
if(supportedSubchannel != MmcSubchannel.None) if(supportedSubchannel != MmcSubchannel.None)
{ {
byte[] data = new byte[sectorSize * _maximumReadable]; byte[] data = new byte[sectorSize * blocksToRead];
byte[] sub = new byte[subSize * _maximumReadable]; byte[] sub = new byte[subSize * blocksToRead];
byte[] tmpData = new byte[sectorSize];
for(int b = 0; b < _maximumReadable; b++) for(int b = 0; b < blocksToRead; b++)
{ {
Array.Copy(cmdBuf, (int)(0 + (b * blockSize)), data, sectorSize * b, sectorSize); if(cdiReadyReadAsAudio)
{
Array.Copy(cmdBuf, (int)(0 + (b * blockSize)), tmpData, 0, sectorSize);
tmpData = Sector.Scramble(tmpData);
Array.Copy(tmpData, 0, data, sectorSize * b, sectorSize);
}
else
Array.Copy(cmdBuf, (int)(0 + (b * blockSize)), data, sectorSize * b, sectorSize);
Array.Copy(cmdBuf, (int)(sectorSize + (b * blockSize)), sub, subSize * b, subSize); Array.Copy(cmdBuf, (int)(sectorSize + (b * blockSize)), sub, subSize * b, subSize);
} }
_outputPlugin.WriteSectorsLong(data, i, _maximumReadable); _outputPlugin.WriteSectorsLong(data, i, blocksToRead);
bool indexesChanged = WriteSubchannelToImage(supportedSubchannel, desiredSubchannel, sub, i, bool indexesChanged = WriteSubchannelToImage(supportedSubchannel, desiredSubchannel, sub, i,
_maximumReadable, subLog, isrcs, blocksToRead, subLog, isrcs, 1, ref mcn, tracks,
(byte)track.TrackSequence, ref mcn, tracks,
subchannelExtents); subchannelExtents);
// Set tracks and go back // Set tracks and go back
if(indexesChanged) if(indexesChanged)
{ {
(_outputPlugin as IWritableOpticalImage).SetTracks(tracks.ToList()); (_outputPlugin as IWritableOpticalImage).SetTracks(tracks.ToList());
i -= _maximumReadable; i -= blocksToRead;
continue; continue;
} }
} }
else else
{ {
if(supportsLongSectors) if(cdiReadyReadAsAudio)
{ {
_outputPlugin.WriteSectorsLong(cmdBuf, i, _maximumReadable); byte[] tmpData = new byte[sectorSize];
byte[] data = new byte[sectorSize * blocksToRead];
for(int b = 0; b < blocksToRead; b++)
{
Array.Copy(cmdBuf, (int)(b * sectorSize), tmpData, 0, sectorSize);
tmpData = Sector.Scramble(tmpData);
Array.Copy(tmpData, 0, data, sectorSize * b, sectorSize);
}
_outputPlugin.WriteSectorsLong(data, i, blocksToRead);
} }
else else
{ _outputPlugin.WriteSectorsLong(cmdBuf, i, blocksToRead);
if(cmdBuf.Length % sectorSize == 0)
{
byte[] data = new byte[2048 * _maximumReadable];
for(int b = 0; b < _maximumReadable; b++)
Array.Copy(cmdBuf, (int)(16 + (b * blockSize)), data, 2048 * b, 2048);
_outputPlugin.WriteSectors(data, i, _maximumReadable);
}
else
{
_outputPlugin.WriteSectorsLong(cmdBuf, i, _maximumReadable);
}
}
} }
imageWriteDuration += (DateTime.Now - writeStart).TotalSeconds; imageWriteDuration += (DateTime.Now - writeStart).TotalSeconds;
@@ -357,9 +393,9 @@ namespace Aaru.Core.Devices.Dumping
break; break;
} }
sectorSpeedStart += _maximumReadable; sectorSpeedStart += blocksToRead;
_resume.NextBlock = i + _maximumReadable; _resume.NextBlock = i + blocksToRead;
elapsed = (DateTime.UtcNow - timeSpeedStart).TotalSeconds; elapsed = (DateTime.UtcNow - timeSpeedStart).TotalSeconds;

View File

@@ -112,10 +112,11 @@ namespace Aaru.Core.Devices.Dumping
bool hiddenTrack; // Disc has a hidden track before track 1 bool hiddenTrack; // Disc has a hidden track before track 1
MmcSubchannel supportedSubchannel; // Drive's maximum supported subchannel MmcSubchannel supportedSubchannel; // Drive's maximum supported subchannel
MmcSubchannel desiredSubchannel; // User requested subchannel MmcSubchannel desiredSubchannel; // User requested subchannel
bool bcdSubchannel = false; // Subchannel positioning is in BCD bool bcdSubchannel = false; // Subchannel positioning is in BCD
Dictionary<byte, string> isrcs = new Dictionary<byte, string>(); Dictionary<byte, string> isrcs = new Dictionary<byte, string>();
string mcn = null; string mcn = null;
HashSet<int> subchannelExtents = new HashSet<int>(); HashSet<int> subchannelExtents = new HashSet<int>();
bool cdiReadyReadAsAudio = false;
Dictionary<MediaTagType, byte[]> mediaTags = new Dictionary<MediaTagType, byte[]>(); // Media tags Dictionary<MediaTagType, byte[]> mediaTags = new Dictionary<MediaTagType, byte[]>(); // Media tags
@@ -892,15 +893,6 @@ namespace Aaru.Core.Devices.Dumping
Invoke($"Track {trk.TrackSequence} starts at LBA {trk.TrackStartSector} and ends at LBA {trk.TrackEndSector}"); Invoke($"Track {trk.TrackSequence} starts at LBA {trk.TrackStartSector} and ends at LBA {trk.TrackEndSector}");
#endif #endif
if(dskType == MediaType.CDIREADY &&
!_skipCdireadyHole)
{
_dumpLog.WriteLine("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");
UpdateStatus?.
Invoke("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");
}
// Check offset // Check offset
if(_fixOffset) if(_fixOffset)
{ {
@@ -1041,12 +1033,100 @@ namespace Aaru.Core.Devices.Dumping
// Start reading // Start reading
start = DateTime.UtcNow; start = DateTime.UtcNow;
if(dskType == MediaType.CDIREADY && _skipCdireadyHole) if(dskType == MediaType.CDIREADY)
{ {
ReadCdiReady(blockSize, ref currentSpeed, currentTry, extents, ibgLog, ref imageWriteDuration, Track track0 = tracks.FirstOrDefault(t => t.TrackSequence == 0);
leadOutExtents, ref maxSpeed, mhddLog, ref minSpeed, read6, read10, read12, read16, readcd,
subSize, supportedSubchannel, supportsLongSectors, ref totalDuration, tracks, subLog, track0.TrackType = TrackType.CdMode2Formless;
desiredSubchannel, isrcs, ref mcn, subchannelExtents, blocks);
if(!supportsLongSectors)
{
_dumpLog.WriteLine("Dumping CD-i Ready requires the output image format to support long sectors.");
StoppingErrorMessage?.
Invoke("Dumping CD-i Ready requires the output image format to support long sectors.");
return;
}
if(!readcd)
{
_dumpLog.WriteLine("Dumping CD-i Ready requires the drive to support the READ CD command.");
StoppingErrorMessage?.
Invoke("Dumping CD-i Ready requires the drive to support the READ CD command.");
return;
}
sense = _dev.ReadCd(out cmdBuf, out _, 0, 2352, 1, MmcSectorTypes.AllTypes, false, false, true,
MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None,
_dev.Timeout, out _);
hiddenData = IsData(cmdBuf);
if(!hiddenData)
{
cdiReadyReadAsAudio = IsScrambledData(cmdBuf, 0, out combinedOffset);
if(cdiReadyReadAsAudio)
{
offsetBytes = combinedOffset.Value;
sectorsForOffset = offsetBytes / (int)sectorSize;
if(sectorsForOffset < 0)
sectorsForOffset *= -1;
if(offsetBytes % sectorSize != 0)
sectorsForOffset++;
_dumpLog.WriteLine("Enabling skipping CD-i Ready hole because drive returns data as audio.");
UpdateStatus?.Invoke("Enabling skipping CD-i Ready hole because drive returns data as audio.");
_skipCdireadyHole = true;
if(driveOffset is null)
{
_dumpLog.WriteLine("Drive reading offset not found in database.");
UpdateStatus?.Invoke("Drive reading offset not found in database.");
_dumpLog.
WriteLine($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");
UpdateStatus?.
Invoke($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");
}
else
{
_dumpLog.
WriteLine($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");
UpdateStatus?.
Invoke($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");
discOffset = offsetBytes - driveOffset;
_dumpLog.WriteLine($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");
UpdateStatus?.Invoke($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");
}
}
}
if(!_skipCdireadyHole)
{
_dumpLog.WriteLine("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");
UpdateStatus?.
Invoke("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");
}
if(_skipCdireadyHole)
ReadCdiReady(blockSize, ref currentSpeed, currentTry, extents, ibgLog, ref imageWriteDuration,
leadOutExtents, ref maxSpeed, mhddLog, ref minSpeed, subSize, supportedSubchannel,
ref totalDuration, tracks, subLog, desiredSubchannel, isrcs, ref mcn,
subchannelExtents, blocks, cdiReadyReadAsAudio, offsetBytes, sectorsForOffset);
} }
ReadCdData(audioExtents, blocks, blockSize, ref currentSpeed, currentTry, extents, ibgLog, ReadCdData(audioExtents, blocks, blockSize, ref currentSpeed, currentTry, extents, ibgLog,

View File

@@ -87,7 +87,7 @@ namespace Aaru.Core.Devices.Dumping
Resume _resume; Resume _resume;
Sidecar _sidecarClass; Sidecar _sidecarClass;
uint _skip; uint _skip;
readonly bool _skipCdireadyHole; bool _skipCdireadyHole;
int _speed; int _speed;
int _speedMultiplier; int _speedMultiplier;
bool _supportsPlextorD8; bool _supportsPlextorD8;