More robustness to ripper

This commit is contained in:
chudov
2008-12-07 00:33:20 +00:00
parent 764f9f2368
commit 3fd37e6afe
3 changed files with 198 additions and 77 deletions

View File

@@ -2131,11 +2131,31 @@ namespace Bwg.Scsi
return CommandStatus.Success;
}
public enum SubChannelMode
{
None,
QOnly, /// + 16 bytes
RWMode /// + 96 bytes
};
public enum C2ErrorMode
{
None,
Mode294, /// +294 bytes
Mode296, /// +296 bytes
};
public enum MainChannelSelection
{
UserData,
F8h
};
/// <summary>
///
/// </summary>
/// <param name="mode">subchannel mode (+16 bytes if equals 2)</param>
/// <param name="c2">report C2 errors (+296 bytes if true)</param>
/// <param name="submode">subchannel mode</param>
/// <param name="c2mode">C2 errors report mode</param>
/// <param name="exp">expected sector type</param>
/// <param name="dap"></param>
/// <param name="start"></param>
@@ -2143,28 +2163,33 @@ namespace Bwg.Scsi
/// <param name="data">the memory area </param>
/// <param name="size">the size of the memory area given by the data parameter</param>
/// <returns></returns>
public CommandStatus ReadCDAndSubChannel(byte mode, bool c2, byte exp, bool dap, uint start, uint length, IntPtr data, int size)
public CommandStatus ReadCDAndSubChannel(MainChannelSelection mainmode, SubChannelMode submode, C2ErrorMode c2mode, byte exp, bool dap, uint start, uint length, IntPtr data, int timeout)
{
if (m_logger != null)
{
string args = exp.ToString() + ", " + dap.ToString() + ", " + start.ToString() + ", " + length.ToString() + ", data, " + size.ToString();
string args = exp.ToString() + ", " + dap.ToString() + ", " + start.ToString() + ", " + length.ToString() + ", data";
m_logger.LogMessage(new UserMessage(UserMessage.Category.Debug, 8, "Bwg.Scsi.Device.ReadCD(" + args + ")"));
}
if (mode != 1 && mode != 2 && mode != 4)
throw new Exception("invalid read mode for ReadSubchannel() call");
int size = (4 * 588 +
(submode == SubChannelMode.QOnly ? 16 : submode == SubChannelMode.RWMode ? 96 : 0) +
(c2mode == C2ErrorMode.Mode294 ? 294 : c2mode == C2ErrorMode.Mode296 ? 296 : 0)) * (int) length;
byte mode = (byte) (submode == SubChannelMode.QOnly ? 2 : submode == SubChannelMode.RWMode ? 4 : 0);
if (exp != 1 && exp != 2 && exp != 3 && exp != 4 && exp != 5)
return CommandStatus.NotSupported;
using (Command cmd = new Command(ScsiCommandCode.ReadCd, 12, data, size, Command.CmdDirection.In, 5 * 60))
using (Command cmd = new Command(ScsiCommandCode.ReadCd, 12, data, size, Command.CmdDirection.In, timeout))
{
byte b = (byte)((exp & 0x07) << 2);
if (dap)
b |= 0x02;
byte byte9 = 0x10;
if (c2)
byte byte9 = (byte) (mainmode == MainChannelSelection.UserData ? 0x10 : 0xF8);
if (c2mode == C2ErrorMode.Mode294)
byte9 |= 0x02;
else if (c2mode == C2ErrorMode.Mode296)
byte9 |= 0x04;
cmd.SetCDB8(1, b);
cmd.SetCDB32(2, start);
cmd.SetCDB24(6, length);
@@ -2250,7 +2275,7 @@ namespace Bwg.Scsi
/// <param name="data"></param>
/// <param name="mode">the subchannel mode</param>
/// <returns></returns>
public CommandStatus ReadSubChannel(byte mode, uint sector, uint length, ref byte[] data)
public CommandStatus ReadSubChannel(byte mode, uint sector, uint length, ref byte[] data, int timeout)
{
byte bytes_per_sector;
@@ -2271,7 +2296,7 @@ namespace Bwg.Scsi
throw new Exception("data buffer is not large enough to hold the data requested");
using (Command cmd = new Command(ScsiCommandCode.ReadCd, 12, (int)(length * bytes_per_sector), Command.CmdDirection.In, 10 * 60))
using (Command cmd = new Command(ScsiCommandCode.ReadCd, 12, (int)(length * bytes_per_sector), Command.CmdDirection.In, timeout))
{
cmd.SetCDB32(2, sector); // The sector number to start with
cmd.SetCDB24(6, length); // The length in sectors

View File

@@ -79,6 +79,7 @@ namespace CUERipper
Console.WriteLine("-P, --paranoid maximum level of error correction;");
Console.WriteLine("-D, --drive <letter> use a specific CD drive, e.g. {0};", drives);
Console.WriteLine("-O, --offset <samples> use specific drive read offset;");
Console.WriteLine("-T, --test detect read command;");
}
static void Main(string[] args)
@@ -92,11 +93,14 @@ namespace CUERipper
int correctionQuality = 1;
string driveLetter = null;
int driveOffset = 0;
bool test = false;
for (int arg = 0; arg < args.Length; arg++)
{
bool ok = true;
if (args[arg] == "-P" || args[arg] == "--paranoid")
correctionQuality = 4;
if (args[arg] == "-T" || args[arg] == "--test")
test = true;
//else if (args[arg] == "-B" || args[arg] == "--burst")
// correctionQuality = 1;
else if ((args[arg] == "-D" || args[arg] == "--drive") && ++arg < args.Length)
@@ -145,8 +149,14 @@ namespace CUERipper
if (!AccurateRipVerify.FindDriveReadOffset(audioSource.ARName, out driveOffset))
Console.WriteLine("Unknown read offset for drive {0}!!!", audioSource.Path);
//throw new Exception("Failed to find drive read offset for drive" + audioSource.ARName);
if (test)
{
Console.Write(audioSource.TestReadCommand());
return;
}
audioSource.DriveOffset = driveOffset;
audioSource.CorrectionQuality = correctionQuality;
audioSource.DebugMessages = true;
AccurateRipVerify arVerify = new AccurateRipVerify(audioSource.TOC);
int[,] buff = new int[audioSource.BestBlockSize, audioSource.ChannelCount];

View File

@@ -30,6 +30,7 @@ using Bwg.Scsi;
using Bwg.Logging;
using CUETools.CDImage;
using CUETools.Codecs;
using System.Threading;
namespace CUETools.Ripper.SCSI
{
@@ -44,24 +45,26 @@ namespace CUETools.Ripper.SCSI
int _driveOffset = 0;
int _correctionQuality = 1;
int _currentStart = -1, _currentEnd = -1, _currentErrorsCount = 0;
const bool DoC2 = true;
const int CB_AUDIO = 588 * 4 + 16 + (DoC2 ? 294 : 0);
//const int REDUNDANCY = 8;
const int NSECTORS = 64; //255 - REDUNDANCY;
const int CB_AUDIO = 588 * 4 + 2 + 294 + 16;
const int NSECTORS = 32;
const int MSECTORS = 10000000 / CB_AUDIO;
int _currentTrack = -1, _currentIndex = -1, _currentTrackActualStart = -1;
Logger m_logger;
CDImageLayout _toc;
DeviceInfo m_info;
Crc16Ccitt _crc;
//RsEncode _rsEncoder;
//RsDecode _rsDecoder;
List<ScanResults> _scanResults;
ScanResults _currentScan;
BitArray _errors;
int _errorsCount;
byte[] _currentData = new byte[MSECTORS * 4 * 588];
int[] valueScore = new int[256];
bool _debugMessages = false;
Device.MainChannelSelection _mainChannelMode = Device.MainChannelSelection.UserData;
Device.SubChannelMode _subChannelMode = Device.SubChannelMode.None;
Device.C2ErrorMode _c2ErrorMode = Device.C2ErrorMode.Mode296;
byte[] _readBuffer = new byte[NSECTORS * CB_AUDIO];
byte[] _subchannelBuffer = new byte[NSECTORS * 16];
public event EventHandler<ReadProgressArgs> ReadProgress;
@@ -89,12 +92,22 @@ namespace CUETools.Ripper.SCSI
}
}
public bool DebugMessages
{
get
{
return _debugMessages;
}
set
{
_debugMessages = value;
}
}
public CDDriveReader()
{
m_logger = new Logger();
_crc = new Crc16Ccitt(InitialCrcValue.Zeros);
//_rsEncoder = new RsEncode(REDUNDANCY);
//_rsDecoder = new RsDecode(REDUNDANCY);
}
public bool Open(char Drive)
@@ -186,8 +199,9 @@ namespace CUETools.Ripper.SCSI
}
}
private void ProcessSubchannel(int sector, int Sectors2Read, bool updateMap)
private int ProcessSubchannel(int sector, int Sectors2Read, bool updateMap)
{
int posCount = 0;
for (int iSector = 0; iSector < Sectors2Read; iSector++)
{
int q_pos = (sector - _currentStart + iSector + 1) * CB_AUDIO - 16;
@@ -205,27 +219,32 @@ namespace CUETools.Ripper.SCSI
int ff = fromBCD(_currentScan.Data[q_pos + 9]);
int sec = ff + 75 * (ss + 60 * mm) - 150; // sector + iSector;
//if (sec != sector + iSector)
// System.Console.WriteLine("\rLost sync: {0} vs {1} ({2:X} vs {3:X})", CDImageLayout.TimeToString((uint)(sector + iSector)), CDImageLayout.TimeToString((uint)sec), sector + iSector, sec);
//ushort crc = _crc.ComputeChecksum(_currentScan.Data, q_pos, 10);
//crc ^= 0xffff;
//if (_currentScan.Data[q_pos + 10] != 0 && _currentScan.Data[q_pos + 11] != 0 &&
// ((crc & 0xff) != _currentScan.Data[q_pos + 11] ||
// (crc >> 8) != _currentScan.Data[q_pos + 10])
// )
//{
// System.Console.WriteLine("CRC error at {0}", CDImageLayout.TimeToString((uint)(sector + iSector)));
//}
// System.Console.WriteLine("\rLost sync: {0} vs {1} ({2:X} vs {3:X})", CDImageLayout.TimeToString((uint)(sector + iSector)), CDImageLayout.TimeToString((uint)sec), sector + iSector, sec);
ushort crc = _crc.ComputeChecksum(_currentScan.Data, q_pos, 10);
crc ^= 0xffff;
if (_currentScan.Data[q_pos + 10] != 0 && _currentScan.Data[q_pos + 11] != 0 &&
((crc & 0xff) != _currentScan.Data[q_pos + 11] ||
(crc >> 8) != _currentScan.Data[q_pos + 10])
)
{
if (_debugMessages)
System.Console.WriteLine("\nCRC error at {0}", CDImageLayout.TimeToString((uint)(sector + iSector)));
continue;
}
if (iTrack == 110)
{
if (sector + iSector + 75 < _toc.AudioLength)
throw new Exception("lead out area encountred");
// make sure that data is zero?
return;
return posCount;
}
if (iTrack == 0)
throw new Exception("lead in area encountred");
posCount++;
if (!updateMap)
break;
if (iTrack > _toc.AudioTracks)
throw new Exception("strange track number encountred");
if (iTrack != _currentTrack)
{
_currentTrack = iTrack;
@@ -265,13 +284,12 @@ namespace CUETools.Ripper.SCSI
if (_toc[_currentTrack].ISRC == null)
{
StringBuilder isrc = new StringBuilder();
char[] ISRC6 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '#', '#', '#', '#', '#', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
isrc.Append(ISRC6[_currentScan.Data[q_pos + 1] >> 2]);
isrc.Append(ISRC6[((_currentScan.Data[q_pos + 1] & 0x3) << 4) + (_currentScan.Data[q_pos + 2] >> 4)]);
isrc.Append(ISRC6[((_currentScan.Data[q_pos + 2] & 0xf) << 2) + (_currentScan.Data[q_pos + 3] >> 6)]);
isrc.Append(ISRC6[(_currentScan.Data[q_pos + 3] & 0x3f)]);
isrc.Append(ISRC6[_currentScan.Data[q_pos + 4] >> 2]);
isrc.Append(ISRC6[((_currentScan.Data[q_pos + 4] & 0x3) << 4) + (_currentScan.Data[q_pos + 5] >> 4)]);
isrc.Append(from6bit(_currentScan.Data[q_pos + 1] >> 2));
isrc.Append(from6bit(((_currentScan.Data[q_pos + 1] & 0x3) << 4) + (0x0f & (_currentScan.Data[q_pos + 2] >> 4))));
isrc.Append(from6bit(((_currentScan.Data[q_pos + 2] & 0xf) << 2) + (0x03 & (_currentScan.Data[q_pos + 3] >> 6))));
isrc.Append(from6bit((_currentScan.Data[q_pos + 3] & 0x3f)));
isrc.Append(from6bit(_currentScan.Data[q_pos + 4] >> 2));
isrc.Append(from6bit(((_currentScan.Data[q_pos + 4] & 0x3) << 4) + (0x0f & (_currentScan.Data[q_pos + 5] >> 4))));
isrc.AppendFormat("{0:x}", _currentScan.Data[q_pos + 5] & 0xf);
isrc.AppendFormat("{0:x2}", _currentScan.Data[q_pos + 6]);
isrc.AppendFormat("{0:x2}", _currentScan.Data[q_pos + 7]);
@@ -281,11 +299,57 @@ namespace CUETools.Ripper.SCSI
break;
}
}
return posCount;
}
public unsafe string TestReadCommand()
{
byte[] test = new byte[CB_AUDIO];
string s = "";
fixed (byte* data = test)
{
for (int m = 0; m <= 1; m++)
for (int q = 0; q <= 2; q++)
for (int c = 0; c <= 2; c++)
{
Device.MainChannelSelection[] mainmode = { Device.MainChannelSelection.UserData, Device.MainChannelSelection.F8h };
Device.SubChannelMode[] submode = { Device.SubChannelMode.None, Device.SubChannelMode.QOnly, Device.SubChannelMode.RWMode };
Device.C2ErrorMode[] c2mode = { Device.C2ErrorMode.None, Device.C2ErrorMode.Mode294, Device.C2ErrorMode.Mode296 };
Device.CommandStatus st = m_device.ReadCDAndSubChannel(mainmode[m], submode[q], c2mode[c], 1, false, (uint)0, (uint)1, (IntPtr)((void*)data), 3);
s += string.Format("M{0}Q{1}C{2}: {3}\n", m, q, c, (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString()));
}
}
return s;
}
private void ReorganiseSectors(int sector, int Sectors2Read)
{
if (_subChannelMode == Device.SubChannelMode.QOnly && _c2ErrorMode == Device.C2ErrorMode.Mode296)
{
Array.Copy(_readBuffer, 0, _currentScan.Data, (sector - _currentStart) * CB_AUDIO, Sectors2Read * CB_AUDIO);
return;
}
// fill Q subchannel
if (_subChannelMode == Device.SubChannelMode.None)
{
Device.CommandStatus st = m_device.ReadSubChannel(2, (uint)sector, (uint)Sectors2Read, ref _subchannelBuffer, 10);
if (st != Device.CommandStatus.Success)
throw new Exception("ReadSubChannel: " + (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString()));
for (int iSector = 0; iSector < Sectors2Read; iSector++)
Array.Copy(_subchannelBuffer, iSector * 16, _currentScan.Data, (sector - _currentStart + iSector + 1) * CB_AUDIO - 16, 16);
}
int c2Size = _c2ErrorMode == Device.C2ErrorMode.None ? 0 : _c2ErrorMode == Device.C2ErrorMode.Mode294 ? 294 : 296;
int oldSize = 4 * 588 + c2Size + (_subChannelMode == Device.SubChannelMode.None ? 0 : 16);
// fill Data & C2
// TODO: handle other c2 modes here...
for (int iSector = 0; iSector < Sectors2Read; iSector++)
Array.Copy(_readBuffer, iSector * oldSize, _currentScan.Data, (sector - _currentStart + iSector) * CB_AUDIO, 4 * 588 + 296);
}
private unsafe void FetchSectors(int sector, int Sectors2Read)
{
fixed (byte* data = _currentScan.Data)
fixed (byte* data = _readBuffer)
{
//Device.CommandStatus st;
//using (Command cmd = new Command(ScsiCommandCode.Read12, 12, (IntPtr)((void*)data), Sectors2Read * 588 * 4, Command.CmdDirection.In, 5 * 60))
@@ -296,42 +360,46 @@ namespace CUETools.Ripper.SCSI
// //cmd.SetCDB8(10, 0x80); // streaming
// st= m_device.SendCommand(cmd);
//}
Device.CommandStatus st = m_device.ReadCDAndSubChannel(2, DoC2, 1, false, (uint)sector, (uint)Sectors2Read, (IntPtr)((void*)(data + (sector - _currentStart) * CB_AUDIO)), Sectors2Read * CB_AUDIO);
if (st != Device.CommandStatus.Success)
Device.CommandStatus st = m_device.ReadCDAndSubChannel(_mainChannelMode, _subChannelMode, _c2ErrorMode, 1, false, (uint)sector, (uint)Sectors2Read, (IntPtr)((void*)data), 10);
if (st == Device.CommandStatus.Success)
{
if (st == Device.CommandStatus.DeviceFailed && m_device.GetSenseAsc() == 0x64 && m_device.GetSenseAscq() == 0x00)
{
int iErrors = 0;
for (int iSector = 0; iSector < Sectors2Read; iSector++)
{
st = m_device.ReadCDAndSubChannel(2, DoC2, 1, false, (uint)(sector + iSector), 1U, (IntPtr)((void*)(data + (sector + iSector - _currentStart) * CB_AUDIO)), CB_AUDIO);
if (st != Device.CommandStatus.Success)
{
iErrors ++;
for (int iPos = 0; iPos < CB_AUDIO; iPos++)
data[(sector + iSector - _currentStart) * CB_AUDIO + iPos] = (DoC2 && iPos >= 4 * 588 && iPos < 4 * 588 + 294) ? (byte)255 : (byte)0;
}
}
if (iErrors < Sectors2Read)
return;
}
string status = (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString());
st = m_device.ReadCDAndSubChannel(0, false, 1, false, (uint)sector, (uint)Sectors2Read, (IntPtr)((void*)(data + (sector - _currentStart) * 4 * 588)), Sectors2Read * 4 * 588);
if (st != Device.CommandStatus.Success)
status += "; ReadWithoutSubchannel: " + (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString());
else
status += "; ReadWithoutSubchannel: might work";
throw new Exception("ReadCD: " + status);
ReorganiseSectors(sector, Sectors2Read);
return;
}
//int localC2 = 0;
//if (DoC2)
// for (int iSector = 0; iSector < Sectors2Read; iSector++)
// {
// _currentScan.C2Errors[sector + iSector] = data[(iSector + 1) * CB_AUDIO - 18] != 0;
// localC2 += (data[(iSector + 1) * CB_AUDIO - 18] != 0) ? 1 : 0;
// }
//_currentScan.C2Count += localC2;
if (sector == 0 && _subChannelMode != Device.SubChannelMode.None)
{
_subChannelMode = Device.SubChannelMode.None;
if (_debugMessages)
System.Console.WriteLine("\nFailed to read CD data with subchannel. Switching to ReadCD without subchannel + ReadSubchannel.");
st = m_device.ReadCDAndSubChannel(_mainChannelMode, _subChannelMode, _c2ErrorMode, 1, false, (uint)sector, (uint)Sectors2Read, (IntPtr)((void*)data), 10);
if (st == Device.CommandStatus.Success)
{
ReorganiseSectors(sector, Sectors2Read);
return;
}
}
if (sector != 0 && st == Device.CommandStatus.DeviceFailed && m_device.GetSenseAsc() == 0x64 && m_device.GetSenseAscq() == 0x00)
{
int iErrors = 0;
for (int iSector = 0; iSector < Sectors2Read; iSector++)
{
st = m_device.ReadCDAndSubChannel(_mainChannelMode, _subChannelMode, _c2ErrorMode, 1, false, (uint)(sector + iSector), 1U, (IntPtr)((void*)data), 5);
if (st != Device.CommandStatus.Success)
{
iErrors ++;
for (int iPos = 0; iPos < CB_AUDIO; iPos++)
_currentScan.Data[(sector + iSector - _currentStart) * CB_AUDIO + iPos] = (iPos == 4 * 588 || (iPos >= 4 * 588 + 2 && iPos < 4 * 588 + 2 + 294)) ? (byte)255 : (byte)0;
if (_debugMessages)
System.Console.WriteLine("\nSector lost");
} else
ReorganiseSectors(sector+iSector, 1);
}
if (iErrors < Sectors2Read)
return;
}
string status = (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString());
string test = TestReadCommand();
throw new Exception("ReadCD: " + status + "; Autodetect: " + test);
}
}
@@ -343,7 +411,7 @@ namespace CUETools.Ripper.SCSI
for (int iPar = 0; iPar < 4 * 588; iPar++)
{
int dataPos = (sector - _currentStart + iSector) * CB_AUDIO + iPar;
int c2Pos = (sector - _currentStart + iSector) * CB_AUDIO + 4 * 588 + iPar / 8;
int c2Pos = (sector - _currentStart + iSector) * CB_AUDIO + 2 + 4 * 588 + iPar / 8;
int c2Bit = 0x80 >> (iPar % 8);
Array.Clear(valueScore, 0, 256);
@@ -391,7 +459,7 @@ namespace CUETools.Ripper.SCSI
// for (int iPar = 0; iPar < 4 * 588; iPar++)
// {
// int dataPos = iSector * CB_AUDIO + iPar;
// int c2Pos = iSector * CB_AUDIO + 4 * 588 + iPar / 8;
// int c2Pos = iSector * CB_AUDIO + 2 + 4 * 588 + iPar / 8;
// int c2Bit = 0x80 >> (iPar % 8);
// Array.Clear(valueScore, 0, 256);
@@ -453,16 +521,27 @@ namespace CUETools.Ripper.SCSI
for (int pass = 0; pass <= nPasses + nExtraPasses; pass++)
{
DateTime PassTime = DateTime.Now;
DateTime PassTime = DateTime.Now, LastFetch = DateTime.Now;
_currentScan = new ScanResults(MSECTORS, CB_AUDIO);
_currentErrorsCount = 0;
int nSectors = Math.Min(NSECTORS, m_device.MaximumTransferLength / CB_AUDIO - 1);
for (int sector = _currentStart; sector < _currentEnd; sector += nSectors)
{
int Sectors2Read = Math.Min(nSectors, _currentEnd - sector);
int speed = pass == 4 || pass == 5 ? 4 : pass == 8 || pass == 9 ? 2 : pass == 17 || pass == 18 ? 1 : 0;
if (speed != 0)
Thread.Sleep(Math.Max(1, 1000 * nSectors / (75 * speed) - (int)((DateTime.Now - LastFetch).TotalMilliseconds)));
LastFetch = DateTime.Now;
FetchSectors(sector, Sectors2Read);
if (ProcessSubchannel(sector, Sectors2Read, pass == 0) == 0 && _subChannelMode != Device.SubChannelMode.None && sector == 0)
{
if (_debugMessages)
System.Console.WriteLine("\nGot no subchannel using ReadCD. Switching to ReadSubchannel.");
_subChannelMode = Device.SubChannelMode.None;
FetchSectors(sector, Sectors2Read);
}
CorrectSectors(sector, Sectors2Read, pass > nPasses, pass == nPasses + nExtraPasses);
ProcessSubchannel(sector, Sectors2Read, pass == 0);
if (ReadProgress != null)
ReadProgress(this, new ReadProgressArgs(sector + Sectors2Read, pass, _currentStart, _currentEnd, _currentErrorsCount, PassTime));
}
@@ -699,6 +778,13 @@ namespace CUETools.Ripper.SCSI
return (hex >> 4) * 10 + (hex & 15);
}
private char from6bit(int bcd)
{
char[] ISRC6 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '#', '#', '#', '#', '#', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
bcd &= 0x3f;
return bcd >= ISRC6.Length ? '#' : ISRC6[bcd];
}
public static char[] DrivesAvailable()
{
List<char> result = new List<char>();