// **************************************************************************** // // CUERipper // Copyright (C) 2008 Gregory S. Chudov (gchudov@gmail.com) // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // **************************************************************************** using System; using System.Runtime.InteropServices; using System.Collections; using System.Collections.Generic; using System.Text; using System.IO; using Bwg.Scsi; using Bwg.Logging; using CUETools.CDImage; using CUETools.Codecs; using CUETools.Ripper; using System.Threading; namespace CUETools.Ripper.SCSI { /// /// /// public class CDDriveReader : ICDRipper { byte[] cdtext = null; private Device m_device; int _sampleOffset = 0; int _driveOffset = 0; int _correctionQuality = 1; int _currentStart = -1, _currentEnd = -1, _currentErrorsCount = 0; const int CB_AUDIO = 4 * 588 + 2 + 294 + 16; const int NSECTORS = 16; //const int MSECTORS = 5*1024*1024 / (4 * 588); const int MSECTORS = 2400; int _currentTrack = -1, _currentIndex = -1, _currentTrackActualStart = -1; Logger m_logger; CDImageLayout _toc; CDImageLayout _toc2; char m_device_letter; InquiryResult m_inqury_result; int m_max_sectors; int _timeout = 10; Crc16Ccitt _crc; public long[,,] UserData; public byte[,] C2Count; public long[] byte2long; BitArray _errors; int _errorsCount; int _crcErrorsCount = 0; AudioBuffer currentData = new AudioBuffer(AudioPCMConfig.RedBook, MSECTORS * 588); short[] _valueScore = new short[256]; bool _debugMessages = false; ReadCDCommand _readCDCommand = ReadCDCommand.Unknown; ReadCDCommand _forceReadCommand = ReadCDCommand.Unknown; Device.MainChannelSelection _mainChannelMode = Device.MainChannelSelection.UserData; Device.C2ErrorMode _c2ErrorMode = Device.C2ErrorMode.Mode296; string _autodetectResult; byte[] _readBuffer = new byte[NSECTORS * CB_AUDIO]; byte[] _subchannelBuffer = new byte[CB_AUDIO]; bool _qChannelInBCD = true; private ReadProgressArgs progressArgs = new ReadProgressArgs(); public event EventHandler ReadProgress; public CDImageLayout TOC { get { return gapsDetected && _toc2 != null ? _toc2 : _toc; } } public BitArray Errors { get { return _errors; } } public int ErrorsCount { get { return _errorsCount; } } public int Timeout { get { return _timeout; } set { _timeout = value; } } public bool DebugMessages { get { return _debugMessages; } set { _debugMessages = value; } } public string AutoDetectReadCommand { get { TestReadCommand(); return _autodetectResult; } } public bool ForceD8 { get { return _forceReadCommand == ReadCDCommand.ReadCdD8h; } set { _forceReadCommand = value ? ReadCDCommand.ReadCdD8h : ReadCDCommand.Unknown; } } public bool ForceBE { get { return _forceReadCommand == ReadCDCommand.ReadCdBEh; } set { _forceReadCommand = value ? ReadCDCommand.ReadCdBEh : ReadCDCommand.Unknown; } } public string CurrentReadCommand { get { return _readCDCommand == ReadCDCommand.Unknown ? "unknown" : string.Format("{0}, {1:X2}h, {2}{3}, {4} blocks at a time", (_readCDCommand == ReadCDCommand.ReadCdBEh ? "BEh" : "D8h"), (_mainChannelMode == Device.MainChannelSelection.UserData ? 0x10 : 0xF8) + (_c2ErrorMode == Device.C2ErrorMode.None ? 0 : _c2ErrorMode == Device.C2ErrorMode.Mode294 ? 2 : 4), (_gapDetection == GapDetectionMethod.ReadCD ? "BEh" : _gapDetection == GapDetectionMethod.ReadSubchannel ? "42h" : ""), _qChannelInBCD ? "" : "nonBCD", m_max_sectors); } } public CDDriveReader() { m_logger = new Logger(); _crc = new Crc16Ccitt(InitialCrcValue.Zeros); byte2long = new long[256]; for (long i = 0; i < 256; i++) { long bl = 0; for (int b = 0; b < 8; b++) bl += ((i >> b) & 1) << (b << 3); byte2long[i] = bl; } } public bool Open(char Drive) { Device.CommandStatus st; // Open the base device m_device_letter = Drive; if (m_device != null) Close(); m_device = new Device(m_logger); if (!m_device.Open(m_device_letter)) throw new Exception("Open failed: " + WinDev.Win32ErrorToString(m_device.LastError)); // Get device info st = m_device.Inquiry(out m_inqury_result); if (st != Device.CommandStatus.Success || !m_inqury_result.Valid) throw new SCSIException("Inquiry", m_device, st); if (m_inqury_result.PeripheralQualifier != 0 || m_inqury_result.PeripheralDeviceType != Device.MMCDeviceType) throw new Exception(Path + " is not an MMC device"); m_max_sectors = Math.Min(NSECTORS, m_device.MaximumTransferLength / CB_AUDIO - 1); //// Open/Initialize the driver //Drive m_drive = new Drive(dev); //DiskOperationError status = m_drive.Initialize(); //if (status != null) // throw new Exception("SCSI error"); // { //Drive.FeatureState readfeature = m_drive.GetFeatureState(Feature.FeatureType.CDRead); //if (readfeature == Drive.FeatureState.Error || readfeature == Drive.FeatureState.NotPresent) // throw new Exception("SCSI error"); // }{ //st = m_device.GetConfiguration(Device.GetConfigType.OneFeature, 0, out flist); //if (st != Device.CommandStatus.Success) // return CreateErrorObject(st, m_device); //Feature f = flist.Features[0]; //ParseProfileList(f.Data); // } //SpeedDescriptorList speed_list; //st = m_device.GetSpeed(out speed_list); //if (st != Device.CommandStatus.Success) // throw new Exception("GetSpeed failed: SCSI error"); //m_device.SetCdSpeed(Device.RotationalControl.CLVandNonPureCav, (ushort)(0x7fff), (ushort)(0x7fff)); //int bytesPerSec = 4 * 588 * 75 * (pass > 8 ? 4 : pass > 4 ? 8 : pass > 0 ? 16 : 32); //Device.CommandStatus st = m_device.SetStreaming(Device.RotationalControl.CLVandNonPureCav, start, end, bytesPerSec, 1, bytesPerSec, 1); //if (st != Device.CommandStatus.Success) // System.Console.WriteLine("SetStreaming: ", (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString())); //st = m_device.SetCdSpeed(Device.RotationalControl.CLVandNonPureCav, (ushort)(bytesPerSec / 1024), (ushort)(bytesPerSec / 1024)); //if (st != Device.CommandStatus.Success) // System.Console.WriteLine("SetCdSpeed: ", (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString())); //st = m_device.SetCdSpeed(Device.RotationalControl.CLVandNonPureCav, 32767/*Device.OptimumSpeed*/, Device.OptimumSpeed); //if (st != Device.CommandStatus.Success) // throw new Exception("SetCdSpeed failed: SCSI error"); IList toc; st = m_device.ReadToc((byte)0, false, out toc); if (st != Device.CommandStatus.Success) throw new SCSIException("ReadTOC", m_device, st); //throw new Exception("ReadTOC: " + (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString())); //byte[] qdata = null; //st = m_device.ReadPMA(out qdata); //if (st != Device.CommandStatus.Success) // throw new SCSIException("ReadPMA", m_device, st); st = m_device.ReadCDText(out cdtext); // new CDTextEncoderDecoder _toc2 = null; _toc = new CDImageLayout(); for (int iTrack = 0; iTrack < toc.Count - 1; iTrack++) _toc.AddTrack(new CDTrack((uint)iTrack + 1, toc[iTrack].StartSector, toc[iTrack + 1].StartSector - toc[iTrack].StartSector - ((toc[iTrack + 1].Control < 4 || iTrack + 1 == toc.Count - 1) ? 0U : 152U * 75U), toc[iTrack].Control < 4, (toc[iTrack].Control & 1) == 1)); if (_toc.AudioLength > 0) { if (_toc[1].IsAudio) _toc[1][0].Start = 0; Position = 0; } UserData = new long[MSECTORS, 2, 4 * 588]; C2Count = new byte[MSECTORS, 294]; return true; } public void Close() { UserData = null; C2Count = null; if (m_device != null) m_device.Close(); m_device = null; _toc = null; _toc2 = null; gapsDetected = false; readCommandFound = false; _currentStart = -1; _currentEnd = -1; } public void Dispose() { Close(); } public int BestBlockSize { get { return m_max_sectors * 588; } } private enum GapDetectionMethod { ReadCD, ReadSubchannel, None } private GapDetectionMethod _gapDetection = GapDetectionMethod.None; private unsafe void LocateLastSector(int sec0, int sec1, int iTrack, int iIndex, ref int maxIndex, out int pos) { if (sec1 <= sec0) { pos = Math.Min(sec0, sec1); return; } int msector = (sec0 + sec1) / 2; int fsector = Math.Max(sec0, msector - 1); int lsector = Math.Min(sec1, msector + 3); if (lsector >= fsector && _gapDetection == GapDetectionMethod.ReadCD) { Device.CommandStatus st = m_device.ReadSubChannel(2, (uint)fsector, (uint)(lsector - fsector + 1), ref _subchannelBuffer, _timeout); if (st != Device.CommandStatus.Success) lsector = fsector - 1; } fixed (byte * data = _subchannelBuffer) for (int sector = fsector; sector <= lsector; sector++) { Device.CommandStatus st = Device.CommandStatus.Success; int sTrack, sIndex, sPos, ctl; switch (_gapDetection) { case GapDetectionMethod.ReadSubchannel: { // seek to given sector if (_readCDCommand == ReadCDCommand.ReadCdBEh) st = m_device.ReadCDAndSubChannel(_mainChannelMode, Device.SubChannelMode.None, _c2ErrorMode, 1, false, (uint)sector, 1, (IntPtr)((void*)data), _timeout); else st = m_device.ReadCDDA(Device.SubChannelMode.None, (uint)sector, 1, (IntPtr)((void*)data), _timeout); if (st != Device.CommandStatus.Success) continue; st = m_device.ReadSubChannel42(1, 0, ref _subchannelBuffer, 0, _timeout); // x x x x 01 adrctl tr ind abs abs abs rel rel rel if (st != Device.CommandStatus.Success) continue; if (_subchannelBuffer[0] != 0 || _subchannelBuffer[2] != 0 || _subchannelBuffer[3] != 12 || _subchannelBuffer[4] != 1) continue; ctl = _subchannelBuffer[5] & 0xf; int adr = (_subchannelBuffer[5] >> 4) & 0xf; sTrack = _subchannelBuffer[6]; sIndex = _subchannelBuffer[7]; sPos = (_subchannelBuffer[8] << 24) | (_subchannelBuffer[9] << 16) | (_subchannelBuffer[10] << 8) | (_subchannelBuffer[11]); //int sRel = (_subchannelBuffer[12] << 24) | (_subchannelBuffer[13] << 16) | (_subchannelBuffer[14] << 8) | (_subchannelBuffer[15]); if (adr != 1) continue; if (sTrack < _toc2.FirstAudio || sTrack >= _toc2.FirstAudio + _toc2.AudioTracks) continue; break; } case GapDetectionMethod.ReadCD: { int offs = 16 * (sector - fsector); ctl = _subchannelBuffer[offs + 0] >> 4; int adr = _subchannelBuffer[offs + 0] & 7; if (!_qChannelInBCD && adr == 1) { _subchannelBuffer[offs + 3] = toBCD(_subchannelBuffer[offs + 3]); _subchannelBuffer[offs + 4] = toBCD(_subchannelBuffer[offs + 4]); _subchannelBuffer[offs + 5] = toBCD(_subchannelBuffer[offs + 5]); _subchannelBuffer[offs + 7] = toBCD(_subchannelBuffer[offs + 7]); _subchannelBuffer[offs + 8] = toBCD(_subchannelBuffer[offs + 8]); _subchannelBuffer[offs + 9] = toBCD(_subchannelBuffer[offs + 9]); } ushort crc = _crc.ComputeChecksum(_subchannelBuffer, offs, 10); crc ^= 0xffff; ushort scrc = (ushort)((_subchannelBuffer[offs + 10] << 8) | _subchannelBuffer[offs + 11]); if (scrc != 0 && scrc != crc) continue; if (adr != 1) continue; sTrack = fromBCD(_subchannelBuffer[offs + 1]); sIndex = fromBCD(_subchannelBuffer[offs + 2]); if (sTrack < _toc2.FirstAudio || sTrack >= _toc2.FirstAudio + _toc2.AudioTracks) continue; int mm = fromBCD(_subchannelBuffer[offs + 7]); int ss = fromBCD(_subchannelBuffer[offs + 8]); int ff = fromBCD(_subchannelBuffer[offs + 9]); sPos = ff + 75 * (ss + 60 * mm) - 150; break; } default: continue; } bool preemph = (ctl & 1) == 1; bool dcp = (ctl & 2) == 2; if (preemph) _toc2[sTrack].PreEmphasis = true; if (dcp) _toc2[sTrack].DCP = true; if (sPos <= sec0 || sPos > sec1) continue; if (sTrack > iTrack || (sTrack == iTrack && iIndex >= 0 && sIndex > iIndex)) { LocateLastSector(sec0, sPos - 1, iTrack, iIndex, ref maxIndex, out pos); return; } if (sTrack < iTrack || (sTrack == iTrack && (iIndex < 0 || sIndex <= iIndex))) { if (sTrack == iTrack && iIndex < 0) maxIndex = sIndex; LocateLastSector(sPos, sec1, iTrack, iIndex, ref maxIndex, out pos); return; } } if (sec1 <= sec0 + 16) { pos = Math.Min(sec0, sec1); return; } // TODO: catch? throw new Exception("gap detection failed"); } private unsafe void TestGaps() { _gapDetection = GapDetectionMethod.None; //st = m_device.Seek((uint)(sector + i * 33) + _toc[_toc.FirstAudio][0].Start); //if (st != Device.CommandStatus.Success) // break; //bool ready; //st = m_device.TestUnitReady(out ready); //if (st != Device.CommandStatus.Success) // break; //if (!ready) //{ // st = Device.CommandStatus.NotSupported; // break; //} // try ReadCD: Device.CommandStatus st; int sector = 3; if (_readCDCommand == ReadCDCommand.ReadCdBEh) { st = m_device.ReadSubChannel(2, (uint)sector + _toc[_toc.FirstAudio][0].Start, (uint)m_max_sectors, ref _subchannelBuffer, _timeout); if (st == Device.CommandStatus.Success) { int[] goodsecs = new int[2]; for (int bcd = 1; bcd >= 0; bcd--) { for (int i = 0; i < m_max_sectors; i++) { int adr = _subchannelBuffer[i * 16 + 0] & 7; if (bcd == 0 && adr == 1) { _subchannelBuffer[i * 16 + 3] = toBCD(_subchannelBuffer[i * 16 + 3]); _subchannelBuffer[i * 16 + 4] = toBCD(_subchannelBuffer[i * 16 + 4]); _subchannelBuffer[i * 16 + 5] = toBCD(_subchannelBuffer[i * 16 + 5]); _subchannelBuffer[i * 16 + 7] = toBCD(_subchannelBuffer[i * 16 + 7]); _subchannelBuffer[i * 16 + 8] = toBCD(_subchannelBuffer[i * 16 + 8]); _subchannelBuffer[i * 16 + 9] = toBCD(_subchannelBuffer[i * 16 + 9]); } ushort crc = _crc.ComputeChecksum(_subchannelBuffer, i * 16, 10); crc ^= 0xffff; ushort scrc = (ushort)((_subchannelBuffer[i * 16 + 10] << 8) | _subchannelBuffer[i * 16 + 11]); if (scrc != 0 && scrc != crc) continue; if (adr != 1) continue; int sTrack = fromBCD(_subchannelBuffer[i * 16 + 1]); int sIndex = fromBCD(_subchannelBuffer[i * 16 + 2]); if (sTrack == 0 || sTrack == 110) continue; int mm = fromBCD(_subchannelBuffer[i * 16 + 7]); int ss = fromBCD(_subchannelBuffer[i * 16 + 8]); int ff = fromBCD(_subchannelBuffer[i * 16 + 9]); int sPos = ff + 75 * (ss + 60 * mm) - 150; if (sPos < sector + i - 8 || sPos > sector + i + 8) continue; goodsecs[bcd]++; } } if (goodsecs[0] > 0 || goodsecs[1] > 0) { _qChannelInBCD = goodsecs[1] >= goodsecs[0]; _gapDetection = GapDetectionMethod.ReadCD; } } } if (_gapDetection == GapDetectionMethod.None) { fixed (byte* data = _subchannelBuffer) { // seek to given sector if (_readCDCommand == ReadCDCommand.ReadCdBEh) st = m_device.ReadCDAndSubChannel(_mainChannelMode, Device.SubChannelMode.None, _c2ErrorMode, 1, false, (uint)sector + _toc[_toc.FirstAudio][0].Start, 1, (IntPtr)((void*)data), _timeout); else st = m_device.ReadCDDA(Device.SubChannelMode.None, (uint)sector + _toc[_toc.FirstAudio][0].Start, 1, (IntPtr)((void*)data), _timeout); } if (st == Device.CommandStatus.Success) { st = m_device.ReadSubChannel42(1, 0, ref _subchannelBuffer, 0, _timeout); if (st == Device.CommandStatus.Success) { if (_subchannelBuffer[0] == 0 && _subchannelBuffer[2] == 0 && _subchannelBuffer[3] == 12 && _subchannelBuffer[4] == 1) { int ctl = _subchannelBuffer[5] & 0xf; int adr = (_subchannelBuffer[5] >> 4) & 0xf; if (adr == 1) { _gapDetection = GapDetectionMethod.ReadSubchannel; } } } } } } public bool GapsDetected { get { return gapsDetected; } } bool gapsDetected = false; public unsafe bool DetectGaps() { if (!TestReadCommand()) throw new Exception("failed to autodetect read command:\n" + _autodetectResult); if (_gapDetection == GapDetectionMethod.None) { gapsDetected = false; return false; } if (gapsDetected) return true; _toc2 = (CDImageLayout)_toc.Clone(); if (_gapDetection == GapDetectionMethod.ReadSubchannel) { Device.CommandStatus st = m_device.ReadSubChannel42(2, 0, ref _subchannelBuffer, 0, _timeout); if (st == Device.CommandStatus.Success) if (_subchannelBuffer[0] == 0 && _subchannelBuffer[2] == 0 && _subchannelBuffer[3] == 20 && _subchannelBuffer[4] == 2 && _subchannelBuffer[8] == 0x80) { string catalog = Encoding.ASCII.GetString(_subchannelBuffer, 9, 13); if (catalog.ToString() != "0000000000000") _toc2.Catalog = catalog.ToString(); } } int sec0 = (int)_toc2[_toc2.FirstAudio][0].Start, disc1 = (int)(_toc2[_toc2.FirstAudio][0].Start + _toc2.AudioLength) - 1; for (int iTrack = _toc2.FirstAudio; iTrack < _toc2.FirstAudio + _toc2.AudioTracks; iTrack++) { if (ReadProgress != null) { progressArgs.Action = "Detecting gaps"; progressArgs.Pass = -1; progressArgs.Position = (iTrack - _toc2.FirstAudio) * 3; progressArgs.PassStart = 0; progressArgs.PassEnd = _toc2.TrackCount * 3 - 1; progressArgs.ErrorsCount = 0; progressArgs.PassTime = DateTime.Now; ReadProgress(this, progressArgs); } int sec1, idx1 = 1; LocateLastSector(sec0, Math.Min(disc1, (int)_toc[iTrack].End + 16), iTrack, -1, ref idx1, out sec1); int isec0 = sec0; for (int idx = 0; idx <= idx1; idx++) { int isec1 = sec1, iidx1 = 1; if (idx < idx1) { if (ReadProgress != null) { progressArgs.Position = (iTrack - _toc2.FirstAudio) * 3 + 1; progressArgs.PassTime = DateTime.Now; ReadProgress(this, progressArgs); } LocateLastSector(isec0, sec1, iTrack, idx, ref iidx1, out isec1); } if (isec1 > isec0) { if (idx == 0 && iTrack > 1) _toc2[iTrack][0].Start = _toc2[iTrack].Start - (uint)(isec1 - isec0 + 1); if (idx > 1) _toc2[iTrack].AddIndex(new CDTrackIndex((uint)idx, (uint)(_toc2[iTrack][0].Start + isec0 - sec0))); } isec0 = isec1 + 1; } if (ReadProgress != null) { progressArgs.Position = (iTrack - _toc2.FirstAudio) * 3 + 2; progressArgs.PassTime = DateTime.Now; ReadProgress(this, progressArgs); } if (_gapDetection == GapDetectionMethod.ReadSubchannel) { Device.CommandStatus st = m_device.ReadSubChannel42(3, iTrack, ref _subchannelBuffer, 0, _timeout); if (st == Device.CommandStatus.Success) if (_subchannelBuffer[0] == 0 && _subchannelBuffer[2] == 0 && _subchannelBuffer[3] == 20 && _subchannelBuffer[4] == 3 && _subchannelBuffer[8] == 0x80) //&& _subchannelBuffer[6] == iTrack) { string isrc = Encoding.ASCII.GetString(_subchannelBuffer, 9, 12); if (!isrc.ToString().Contains("#") && isrc.ToString() != "000000000000") _toc2[iTrack].ISRC = isrc.ToString(); } } if (_gapDetection == GapDetectionMethod.ReadCD) { Device.CommandStatus st = m_device.ReadSubChannel(2, _toc2[iTrack].Start + 16, 100, ref _subchannelBuffer, _timeout); if (st == Device.CommandStatus.Success) { for (int offs = 0; offs < 100 * 16; offs += 16) { int ctl = _subchannelBuffer[offs + 0] >> 4; int adr = _subchannelBuffer[offs + 0] & 7; if (adr != 2 && adr != 3) continue; ushort crc = _crc.ComputeChecksum(_subchannelBuffer, offs, 10); crc ^= 0xffff; ushort scrc = (ushort)((_subchannelBuffer[offs + 10] << 8) | _subchannelBuffer[offs + 11]); if (scrc != 0 && scrc != crc) continue; if (adr == 3 && _toc2[iTrack].ISRC == null) { StringBuilder isrc = new StringBuilder(); isrc.Append(from6bit(_subchannelBuffer[offs + 1] >> 2)); isrc.Append(from6bit(((_subchannelBuffer[offs + 1] & 0x3) << 4) + (0x0f & (_subchannelBuffer[offs + 2] >> 4)))); isrc.Append(from6bit(((_subchannelBuffer[offs + 2] & 0xf) << 2) + (0x03 & (_subchannelBuffer[offs + 3] >> 6)))); isrc.Append(from6bit((_subchannelBuffer[offs + 3] & 0x3f))); isrc.Append(from6bit(_subchannelBuffer[offs + 4] >> 2)); isrc.Append(from6bit(((_subchannelBuffer[offs + 4] & 0x3) << 4) + (0x0f & (_subchannelBuffer[offs + 5] >> 4)))); isrc.AppendFormat("{0:x}", _subchannelBuffer[offs + 5] & 0xf); isrc.AppendFormat("{0:x2}", _subchannelBuffer[offs + 6]); isrc.AppendFormat("{0:x2}", _subchannelBuffer[offs + 7]); isrc.AppendFormat("{0:x}", _subchannelBuffer[offs + 8] >> 4); if (!isrc.ToString().Contains("#") && isrc.ToString() != "000000000000") _toc2[iTrack].ISRC = isrc.ToString(); } if (adr == 2 && _toc2.Catalog == null) { StringBuilder catalog = new StringBuilder(); for (int i = 1; i < 8; i++) catalog.AppendFormat("{0:x2}", _subchannelBuffer[offs + i]); if (catalog.ToString() != "0000000000000") _toc2.Catalog = catalog.ToString(0, 13); } } } } sec0 = sec1 + 1; } gapsDetected = true; return true; } bool readCommandFound = false; public unsafe bool TestReadCommand() { if (readCommandFound) return true; //ReadCDCommand[] readmode = { ReadCDCommand.ReadCdBEh, ReadCDCommand.ReadCdD8h }; ReadCDCommand[] readmode = { ReadCDCommand.ReadCdD8h, ReadCDCommand.ReadCdBEh }; Device.C2ErrorMode[] c2mode = { Device.C2ErrorMode.Mode294, Device.C2ErrorMode.Mode296, Device.C2ErrorMode.None }; Device.MainChannelSelection[] mainmode = { Device.MainChannelSelection.UserData, Device.MainChannelSelection.F8h }; bool found = false; _autodetectResult = ""; _currentStart = 0; _currentTrack = -1; _currentIndex = -1; m_max_sectors = Math.Min(NSECTORS, m_device.MaximumTransferLength / CB_AUDIO - 1); int sector = 3; int pass = 0; for (int c = 0; c <= 2 && !found; c++) for (int r = 0; r <= 1 && !found; r++) for (int m = 0; m <= 1 && !found; m++) { _readCDCommand = readmode[r]; _c2ErrorMode = c2mode[c]; _mainChannelMode = mainmode[m]; if (_forceReadCommand != ReadCDCommand.Unknown && _readCDCommand != _forceReadCommand) continue; if (_readCDCommand == ReadCDCommand.ReadCdD8h) // && (_c2ErrorMode != Device.C2ErrorMode.None || _mainChannelMode != Device.MainChannelSelection.UserData)) continue; Array.Clear(_readBuffer, 0, _readBuffer.Length); // fill with something nasty instead? DateTime tm = DateTime.Now; if (ReadProgress != null) { progressArgs.Action = "Detecting drive features"; progressArgs.Pass = -1; progressArgs.Position = pass++; progressArgs.PassStart = 0; progressArgs.PassEnd = 2 * 3 * 2 - 1; progressArgs.ErrorsCount = 0; progressArgs.PassTime = tm; ReadProgress(this, progressArgs); } Device.CommandStatus st = FetchSectors(sector, m_max_sectors, false); TimeSpan delay = DateTime.Now - tm; _autodetectResult += string.Format("{0}: {1} ({2}ms)\n", CurrentReadCommand, (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString()), delay.TotalMilliseconds); found = st == Device.CommandStatus.Success; //sector += m_max_sectors; } //if (found) // for (int n = 1; n <= m_max_sectors; n++) // { // Device.CommandStatus st = FetchSectors(0, n, false, false); // if (st != Device.CommandStatus.Success) // { // _autodetectResult += string.Format("Maximum sectors: {0}, else {1}; max length {2}\n", n - 1, (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString()), m_device.MaximumTransferLength); // m_max_sectors = n - 1; // break; // } // } TestGaps(); if (found) _autodetectResult += "Chosen " + CurrentReadCommand + "\n"; else _readCDCommand = ReadCDCommand.Unknown; _currentStart = -1; _currentEnd = -1; readCommandFound = found; return found; } //int dbg_pass; //FileStream fs_d, fs_c; private unsafe void ReorganiseSectors(int sector, int Sectors2Read) { int c2Size = _c2ErrorMode == Device.C2ErrorMode.None ? 0 : _c2ErrorMode == Device.C2ErrorMode.Mode294 ? 294 : 296; int oldSize = 4 * 588 + c2Size; fixed (byte* readBuf = _readBuffer, c2Count = C2Count) fixed (long* userData = UserData) { for (int iSector = 0; iSector < Sectors2Read; iSector++) { byte* sectorPtr = readBuf + iSector * oldSize; long* userDataPtr = userData + (sector - _currentStart + iSector) * 8 * 588; byte* c2CountPtr = c2Count + (sector - _currentStart + iSector) * 294; //if (_currentStart > 0) //{ // string nm_d = string.Format("Y:\\Temp\\dbg\\{0:x}-{1:00}.bin", _currentStart, dbg_pass); // string nm_c = string.Format("Y:\\Temp\\dbg\\{0:x}-{1:00}.c2", _currentStart, dbg_pass); // if (fs_d != null && fs_d.Name != nm_d) { fs_d.Close(); fs_d = null; } // if (fs_c != null && fs_c.Name != nm_c) { fs_c.Close(); fs_c = null; } // if (fs_d == null) fs_d = new FileStream(nm_d, FileMode.Create); // if (fs_c == null) fs_c = new FileStream(nm_c, FileMode.Create); // fs_d.Seek((sector - _currentStart + iSector) * 4 * 588, SeekOrigin.Begin); // fs_d.Write(_readBuffer, iSector * oldSize, 4 * 588); // fs_c.Seek((sector - _currentStart + iSector) * 296, SeekOrigin.Begin); // fs_c.Write(_readBuffer, iSector * oldSize + 4 * 588, 296); //} if (_c2ErrorMode != Device.C2ErrorMode.None) { int offs = 0; if (c2Size == 296) { // TODO: sometimes sector C2 byte is placed after C2 info, not before!! int c2 = 0; for (int pos = 2; pos < 294; pos++) c2 |= sectorPtr[4 * 588 + pos]; if (sectorPtr[4 * 588 + 294] == (c2 | sectorPtr[4 * 588 + 0] | sectorPtr[4 * 588 + 1])) offs = 0; else if (sectorPtr[4 * 588] == (c2 | sectorPtr[4 * 588 + 294] | sectorPtr[4 * 588 + 295])) offs = 2; else throw new Exception("invalid C2 pointers"); } for (int pos = 0; pos < 294; pos++) { int c2d = sectorPtr[4 * 588 + pos + offs]; int c2 = ((-c2d) >> 31) & 1; c2CountPtr[pos] += (byte)c2; int sample = pos << 3; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; sample++; userDataPtr[sample + c2 * 4 * 588] += byte2long[sectorPtr[sample]]; } } else { for (int sample = 0; sample < 4 * 588; sample++) userDataPtr[sample] += byte2long[sectorPtr[sample]]; } } } } private unsafe void ClearSectors(int sector, int Sectors2Read) { fixed (long* userData = &UserData[sector - _currentStart, 0, 0]) fixed (byte* c2Count = &C2Count[sector - _currentStart, 0]) { ZeroMemory((byte*)userData, 8 * 2 * 4 * 588 * Sectors2Read); ZeroMemory(c2Count, 294 * Sectors2Read); } } private unsafe Device.CommandStatus FetchSectors(int sector, int Sectors2Read, bool abort) { Device.CommandStatus st; fixed (byte* data = _readBuffer) { if (_debugMessages) { int size = (4 * 588 + (_c2ErrorMode == Device.C2ErrorMode.Mode294 ? 294 : _c2ErrorMode == Device.C2ErrorMode.Mode296 ? 296 : 0)) * (int)Sectors2Read; MemSet(data, size, 0xff); } if (_readCDCommand == ReadCDCommand.ReadCdBEh) st = m_device.ReadCDAndSubChannel(_mainChannelMode, Device.SubChannelMode.None, _c2ErrorMode, 1, false, (uint)sector + _toc[_toc.FirstAudio][0].Start, (uint)Sectors2Read, (IntPtr)((void*)data), _timeout); else st = m_device.ReadCDDA(Device.SubChannelMode.None, (uint)sector + _toc[_toc.FirstAudio][0].Start, (uint)Sectors2Read, (IntPtr)((void*)data), _timeout); } if (st == Device.CommandStatus.Success) { ReorganiseSectors(sector, Sectors2Read); return st; } if (!abort) return st; SCSIException ex = new SCSIException("ReadCD", m_device, st); if (sector != 0 && Sectors2Read > 1 && st == Device.CommandStatus.DeviceFailed && m_device.GetSenseAsc() == 0x64 && m_device.GetSenseAscq() == 0x00) { if (_debugMessages) System.Console.WriteLine("\n{0}: retrying one sector at a time", ex.Message); int iErrors = 0; for (int iSector = 0; iSector < Sectors2Read; iSector++) { if (FetchSectors(sector + iSector, 1, false) != Device.CommandStatus.Success) { iErrors ++; for (int i = 0; i < 294; i++) C2Count[sector + iSector - _currentStart, i] ++; if (_debugMessages) System.Console.WriteLine("\nSector lost"); } } if (iErrors < Sectors2Read) return Device.CommandStatus.Success; } throw ex; } private unsafe void ZeroMemory(short *buf, int count) { if (IntPtr.Size == 4) { Int32* start = (Int32*)buf; Int32* end = (Int32*)(buf + count); while (start < end) *(start++) = 0; } else if (IntPtr.Size == 8) { Int64* start = (Int64*)buf; Int64* end = (Int64*)(buf + count); while (start < end) *(start++) = 0; } else throw new Exception("wierd IntPtr.Size"); } private unsafe void ZeroMemory(byte* buf, int count) { if (IntPtr.Size == 4) { Int32* start = (Int32*)buf; Int32* end = (Int32*)(buf + count); while (start < end) *(start++) = 0; for (int i = 0; i < (count & 3); i++) buf[count - i - 1] = 0; } else if (IntPtr.Size == 8) { Int64* start = (Int64*)buf; Int64* end = (Int64*)(buf + count); while (start < end) *(start++) = 0; for (int i = 0; i < (count & 7); i++) buf[count - i - 1] = 0; } else throw new Exception("wierd IntPtr.Size"); } private unsafe void MemSet(byte* buf, int count, byte val) { Int32 intVal = (((((val << 8) + val) << 8) + val) << 8) + val; if (IntPtr.Size == 4) { Int32* start = (Int32*)buf; Int32* end = (Int32*)(buf + count); while (start < end) *(start++) = intVal; for (int i = 0; i < (count & 3); i++) buf[count - i - 1] = val; } else if (IntPtr.Size == 8) { Int64 int64Val = ((Int64)intVal << 32) + intVal; Int64* start = (Int64*)buf; Int64* end = (Int64*)(buf + count); while (start < end) *(start++) = int64Val; for (int i = 0; i < (count & 7); i++) buf[count - i - 1] = val; } else throw new Exception("wierd IntPtr.Size"); } private void PrintErrors(int pass, int sector, int Sectors2Read, byte[] realData) { //for (int iSector = 0; iSector < Sectors2Read; iSector++) //{ // int pos = sector - _currentStart + iSector; // if (_debugMessages) // { // StringBuilder st = new StringBuilder(); // for (int i = 0; i < 294; i++) // if (C2Data[pos, i] != 0) // { // for (int j = i; j < i + 23; j++) // if (j < 294) // st.AppendFormat("{0:X2}", C2Data[_currentScan, pos, j]); // else // st.Append(" "); // System.Console.WriteLine("\rC2 error @{0}[{1:000}]{2};", CDImageLayout.TimeToString((uint)(sector + iSector)), i, st.ToString()); // return; // } //for (int i = 0; i < 4 * 588; i++) // if (_currentData[pos * 4 * 588 + i] != realData[pos * 4 * 588 + i]) // { // StringBuilder st = new StringBuilder(); // for (int j = i; j < i + 25; j++) // if (j < 4 * 588) // st.AppendFormat("{0:X2}", realData[pos * 4 * 588 + j]); // else // st.Append(" "); // System.Console.WriteLine("\r{0}[--][{1:X3}]{2};", CDImageLayout.TimeToString((uint)(sector + iSector)), i, st.ToString()); // st.Length = 0; // for (int result = 0; result <= pass; result++) // { // for (int j = i; j < i + 25; j++) // if (j < 4 * 588) // st.AppendFormat("{0:X2}", UserData[result, pos, j]); // else // st.Append(" "); // System.Console.WriteLine("\r{0}[{3:X2}][{1:X3}]{2};", CDImageLayout.TimeToString((uint)(sector + iSector)), i, st.ToString(), result); // st.Length = 0; // //int c2Bit = 0x80 >> (i % 8); // //byte value = UserData[result, pos, i]; // //short score = (short)(1 + (((C2Data[result, pos, i >> 3] & c2Bit) == 0) ? (short) 10 : (short)0)); // //st.AppendFormat("{0:X2}[{1:X2}]", value, score); // } // i += 25; // //return; // //while (st.Length < 46) // // st.Append(' '); // //System.Console.WriteLine("\rReal error @{0}[{1:000}]{2};", CDImageLayout.TimeToString((uint)(sector + iSector)), i, st.ToString()); // } // } //} } private unsafe void CorrectSectors(int pass, int sector, int Sectors2Read, bool markErrors) { for (int iSector = 0; iSector < Sectors2Read; iSector++) { int pos = sector - _currentStart + iSector; // avg - pass + 1 // p a l o // 0 1 1 2 // 2 4 3 3 // 4 7 2 4 // 6 10 5 //16 25 10 bool fError = false; const byte c2div = 128; int er_limit = c2div * (1 + _correctionQuality) - 1; // need at least 1 + _correctionQuality good passes for (int iPar = 0; iPar < 4 * 588; iPar++) { long val = UserData[pos, 0, iPar]; long val1 = UserData[pos, 1, iPar]; byte c2 = C2Count[pos, iPar >> 3]; int ave = (pass + 1 - c2) * c2div + c2; int bestValue = 0; for (int i = 0; i < 8; i++) { int sum = ave - 2 * (int)((val & 0xff) * c2div + (val1 & 0xff)); int sig = sum >> 31; // bit value fError |= (sum ^ sig) < er_limit; bestValue += sig & (1 << i); val >>= 8; val1 >>= 8; } currentData.Bytes[pos * 4 * 588 + iPar] = (byte)bestValue; } int newerr = (fError ? 1 : 0); //_currentErrorsCount += newerr; _currentErrorsCount += newerr - errtmp[pos]; errtmp[pos] = newerr; if (markErrors) { _errors[sector + iSector] |= fError; _errorsCount += fError ? 1 : 0; } } } int[] errtmp = new int[MSECTORS]; //private unsafe int CorrectSectorsTest(int start, int end, int c2Score, byte[] realData, int worstScan) //{ // int[] valueScore = new int[256]; // int[] scoreErrors = new int[256]; // int realErrors = 0; // int bestScore = 0; // int _errorsCaught = 0; // int _falsePositives = 0; // for (int iSector = 0; iSector < end - start; iSector++) // { // for (int iPar = 0; iPar < 4 * 588; iPar++) // { // int dataPos = iSector * CB_AUDIO + iPar; // int c2Pos = iSector * CB_AUDIO + 2 + 4 * 588 + iPar / 8; // int c2Bit = 0x80 >> (iPar % 8); // Array.Clear(valueScore, 0, 256); // byte bestValue = _currentScan.Data[dataPos]; // valueScore[bestValue] += 1 + (((_currentScan.Data[c2Pos] & c2Bit) == 0) ? c2Score : 0); // int totalScore = valueScore[bestValue]; // for (int result = 0; result < _scanResults.Count; result++) // { // if (result == worstScan) // continue; // byte value = _scanResults[result].Data[dataPos]; // valueScore[value] += 1 + (((_scanResults[result].Data[c2Pos] & c2Bit) == 0) ? c2Score : 0); // totalScore += 1 + (((_scanResults[result].Data[c2Pos] & c2Bit) == 0) ? c2Score : 0); // if (valueScore[value] > valueScore[bestValue]) // bestValue = value; // } // if (valueScore[bestValue] < (1 + c2Score + totalScore) / 2) // _currentErrorsCount++; // //_currentData[iSector * 4 * 588 + iPar] = bestValue; // if (realData[iSector * 4 * 588 + iPar] != bestValue) // { // if (valueScore[bestValue] > bestScore) // scoreErrors[valueScore[bestValue]]++; // realErrors++; // if (valueScore[bestValue] * 2 <= c2Score + totalScore) // _errorsCaught++; // } else // if (valueScore[bestValue] * 2 <= c2Score + totalScore) // _falsePositives++; // } // } // //string s = ""; // //for (int i = 0; i < 256; i++) // // if (scoreErrors[i] > 0) // // s += string.Format("[{0}]={1};", i, scoreErrors[i]); // //System.Console.WriteLine("RE{0:000} EC{1} FP{2}", realErrors, _errorsCaught, _falsePositives); // return realErrors; //} public unsafe void PrefetchSector(int iSector) { if (iSector >= _currentStart && iSector < _currentEnd) return; if (!TestReadCommand()) throw new Exception("failed to autodetect read command:\n" + _autodetectResult); _currentStart = iSector; _currentEnd = _currentStart + MSECTORS; if (_currentEnd > (int)_toc.AudioLength) { _currentEnd = (int)_toc.AudioLength; _currentStart = Math.Max(0, _currentEnd - MSECTORS); } int neededSize = (_currentEnd - _currentStart) * 588; if (currentData.Size < neededSize) currentData.Prepare(new byte[neededSize * 4], neededSize); currentData.Length = neededSize; //FileStream correctFile = new FileStream("correct.wav", FileMode.Open); //byte[] realData = new byte[MSECTORS * 4 * 588]; //correctFile.Seek(0x2C + _currentStart * 588 * 4, SeekOrigin.Begin); //if (correctFile.Read(realData, _driveOffset * 4, MSECTORS * 4 * 588 - _driveOffset * 4) != MSECTORS * 4 * 588 - _driveOffset * 4) // throw new Exception("read"); //correctFile.Close(); _currentErrorsCount = 0; for (int i = 0; i < MSECTORS; i++) errtmp[i] = 0; //Device.CommandStatus st = m_device.SetCdSpeed(Device.RotationalControl.CLVandNonPureCav, (ushort)(176 * 4), 65535); //if (st != Device.CommandStatus.Success) // System.Console.WriteLine("SetCdSpeed: {0}", (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(m_device.GetSenseAsc(), m_device.GetSenseAscq()) : st.ToString())); // TODO: //int max_scans = 1 << _correctionQuality; int max_scans = 16 << _correctionQuality; for (int pass = 0; pass < max_scans; pass++) { // dbg_pass = pass; DateTime PassTime = DateTime.Now, LastFetch = DateTime.Now; for (int sector = _currentStart; sector < _currentEnd; sector += m_max_sectors) { int Sectors2Read = Math.Min(m_max_sectors, _currentEnd - sector); int speed = pass == 5 ? 300 : pass == 6 ? 150 : pass == 7 ? 75 : 32500; // sectors per second int msToSleep = 1000 * Sectors2Read / speed - (int)((DateTime.Now - LastFetch).TotalMilliseconds); //if (msToSleep > 0) Thread.Sleep(msToSleep); LastFetch = DateTime.Now; if (pass == 0) ClearSectors(sector, Sectors2Read); FetchSectors(sector, Sectors2Read, true); //TimeSpan delay1 = DateTime.Now - LastFetch; //DateTime LastFetched = DateTime.Now; if (pass >= _correctionQuality) { CorrectSectors(pass, sector, Sectors2Read, pass == max_scans - 1); PrintErrors(pass, sector, Sectors2Read, /*realData*/null); } //TimeSpan delay2 = DateTime.Now - LastFetched; //if (sector == _currentStart) //System.Console.WriteLine("\n{0},{1}", delay1.TotalMilliseconds, delay2.TotalMilliseconds); if (ReadProgress != null) { progressArgs.Action = "Ripping"; progressArgs.Position = sector + Sectors2Read; progressArgs.Pass = pass; progressArgs.PassStart = _currentStart; progressArgs.PassEnd = _currentEnd; progressArgs.ErrorsCount = _currentErrorsCount; progressArgs.PassTime = PassTime; ReadProgress(this, progressArgs); } } //System.Console.WriteLine(); //if (CorrectSectorsTest(start, _currentEnd, 10, realData) == 0) // break; if (pass >= _correctionQuality && _currentErrorsCount == 0) break; } } public unsafe int Read(AudioBuffer buff, int maxLength) { if (_toc == null) throw new Exception("Read: invalid TOC"); buff.Prepare(this, maxLength); if (Position >= Length) return 0; if (_sampleOffset >= Length) { for (int i = 0; i < buff.ByteLength; i++) buff.Bytes[i] = 0; _sampleOffset += buff.Length; return buff.Length; // == Remaining } if (_sampleOffset < 0) { buff.Length = Math.Min(buff.Length, -_sampleOffset); for (int i = 0; i < buff.ByteLength; i++) buff.Bytes[i] = 0; _sampleOffset += buff.Length; return buff.Length; } PrefetchSector(/*(int)_toc[_toc.FirstAudio][0].Start +*/ (_sampleOffset / 588)); buff.Length = Math.Min(buff.Length, (int)Length - _sampleOffset); buff.Length = Math.Min(buff.Length, _currentEnd * 588 - _sampleOffset); if ((_sampleOffset - _currentStart * 588) == 0 && (maxLength < 0 || (_currentEnd - _currentStart) * 588 <= buff.Length)) { buff.Swap(currentData); _currentStart = -1; _currentEnd = -1; } else fixed (byte* dest = buff.Bytes, src = ¤tData.Bytes[(_sampleOffset - _currentStart * 588) * 4]) AudioSamples.MemCpy(dest, src, buff.ByteLength); _sampleOffset += buff.Length; return buff.Length; } public long Length { get { if (_toc == null) throw new Exception("invalid TOC"); return 588 * (int)_toc.AudioLength; } } public AudioPCMConfig PCM { get { return AudioPCMConfig.RedBook; } } public string Path { get { string result = m_device_letter + ": "; result += "[" + m_inqury_result.VendorIdentification + " " + m_inqury_result.ProductIdentification + " " + m_inqury_result.FirmwareVersion + "]"; return result; } } public string ARName { get { return m_inqury_result.VendorIdentification.TrimEnd(' ', '\0').PadRight(8, ' ') + " - " + m_inqury_result.ProductIdentification.TrimEnd(' ', '\0'); } } public string EACName { get { return m_inqury_result.VendorIdentification.TrimEnd(' ', '\0') + " " + m_inqury_result.ProductIdentification.TrimEnd(' ', '\0'); } } public long Position { get { return _sampleOffset - _driveOffset; } set { if (_toc.AudioLength <= 0) throw new Exception("no audio"); _currentTrack = -1; _currentIndex = -1; _crcErrorsCount = 0; _errorsCount = 0; _currentStart = -1; _currentEnd = -1; _errors = new BitArray((int)_toc.AudioLength); // !!! _sampleOffset = (int)value + _driveOffset; } } public long Remaining { get { return Length - Position; } } public int DriveOffset { get { return _driveOffset; } set { _driveOffset = value; _sampleOffset = value; } } public int CorrectionQuality { get { return _correctionQuality; } set { if (value < 0 || value > 3) throw new Exception("invalid CorrectionQuality"); _correctionQuality = value; } } public string RipperVersion { get { return "CUERipper v2.0.6 Copyright (C) 2008-10 Gregory S. Chudov"; // ripper.GetName().Name + " " + ripper.GetName().Version; } } private int fromBCD(byte hex) { return (hex >> 4) * 10 + (hex & 15); } private byte toBCD(int val) { return (byte)(((val / 10) << 4) + (val % 10)); } 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]; } } enum ReadCDCommand { ReadCdBEh, ReadCdD8h, Unknown }; public sealed class SCSIException : Exception { public SCSIException(string args, Device device, Device.CommandStatus st) : base(args + ": " + (st == Device.CommandStatus.DeviceFailed ? Device.LookupSenseError(device.GetSenseAsc(), device.GetSenseAscq()) : st.ToString())) { } } }