I needed a command line tool for automatic extraction (ripping) of CDDA discs (CD Digital Audio). After long hours of searching the only tool that came up was
AKRip. However, AKRip is a library (DLL) and only a demo application providing a command line interface exists. As the link on the AKRip homepage is dead, I downloaded it from
Sonic Spot. It's not working though, and always fails with an exception when trying to list the drives available in my system.
So I finally fell back to the ported linux application
cdda2wav, running under windows using
cygwin. Unfortunately cdda2wav accesses CD-ROM drives using (emulated and virtual) SCSI devices in the form of
Bus,ID,Lun, e.g. 0,3,0. Here is what
cdda2wav.exe -scanbus tells me what I have:
scsibus0:
0,0,0 0) *
0,1,0 1) *
0,2,0 2) 'HL-DT-ST' 'CD-RW GCE-8320B ' '1.04' Removable CD-ROM
0,3,0 3) 'LG (KOR)' 'DVD-ROM DRD8120B' '1e05' Removable CD-ROM
0,4,0 4) *
0,5,0 5) *
0,6,0 6) *
0,7,0 7) HOST ADAPTOR
scsibus1:
1,0,0 100) 'KB9081Z ' 'YHQ276H ' '1.0 ' Removable CD-ROM
1,1,0 101) *
1,2,0 102) *
1,3,0 103) *
1,4,0 104) *
1,5,0 105) *
1,6,0 106) *
1,7,0 107) HOST ADAPTOR
Note that running cdda2wav with and without Administrator priviledges might return different results! Now I need to find a way to map these virtual SCSI devices to the drive letters Windows uses. It should look like this:
X: -> 1,0,0
Y: -> 0,3,0
Z: -> 0,2,0
I figured out a solution in three varying ways that should work on Windows 2000 systems and newer (XP, 2003). I will start to explain these with the easyest, most simple yet most specific solution, and fall back step by step to the most complicated but compatible one.
WMIC
WMIC, the Windows Management Instrumentation Console, is a native tool since Windows XP Professional and 2003 Server. Although I only need it to query information, it needs Administrator priviledges. Okay, what information does WMIC provide for my CD-ROM drives? Using the command
WMIC.EXE Path Win32_CDROMDrive GET * /VALUE /format:list tells me this (output shortened to one drive only):
Availability=3
Capabilities={3,7}
CapabilityDescriptions=
Caption=HL-DT-ST CD-RW GCE-8320B
CompressionMethod=
ConfigManagerErrorCode=0
ConfigManagerUserConfig=FALSE
CreationClassName=Win32_CDROMDrive
DefaultBlockSize=
Description=CD-ROM Drive
DeviceID=IDE\CDROMHL-DT-ST_CD-RW_GCE-8320B________________1.04____\4&2EA472A
E&0&1.0.0
Drive=Z:
DriveIntegrity=
ErrorCleared=
ErrorDescription=
ErrorMethodology=
FileSystemFlags=
FileSystemFlagsEx=
Id=Z:
InstallDate=
LastErrorCode=
Manufacturer=(Standard CD-ROM drives)
MaxBlockSize=
MaximumComponentLength=
MaxMediaSize=
MediaLoaded=FALSE
MediaType=CD-ROM
MfrAssignedRevisionLevel=
MinBlockSize=
Name=HL-DT-ST CD-RW GCE-8320B
NeedsCleaning=
NumberOfMediaSupported=
PNPDeviceID=IDE\CDROMHL-DT-ST_CD-RW_GCE-8320B________________1.04____\4&2EA4
72AE&0&1.0.0
PowerManagementCapabilities=
PowerManagementSupported=
RevisionLevel=
SCSIBus=0
SCSILogicalUnit=0
SCSIPort=0
SCSITargetId=2
Size=
Status=OK
StatusInfo=
SystemCreationClassName=Win32_ComputerSystem
SystemName=CYPRESSOR
TransferRate=
VolumeName=
VolumeSerialNumber=
I marked the important information. There seems to be some information in SCSI writing, namely
SCSIBus,
SCSITargetId and
SCSILogicalUnit. Using these I get 0,2,0 for drive Z: which is exactly what I wanted (see beginning). But how bad - looking at Drive X:, WMIC says 0,0,0 which is obviously wrong.
AFAIK only happy guessing could lead to the correct Bus number, but I don't like guessing in automated scripts. That's why I marked the drive name, too, because it seems to be similar to the name cdda2wav told me. Let's compare drive names. First, extract the information from cdda2wav's output to return the device for a specified name:
:: Returns the cygwin SCSI CD-ROM device Bus,ID,Lun (e.g. 1,3,0) for the
:: provided drive name (e.g. "HL-DT-ST CD-RW GCE-8320B").
::
:: @PARAM String drive name
:: @RETURN String cygwin SCSI device
:: @REQUIRES tools\cdda2wav.exe
:: @REQUIRES lib\string.cmd
:GET_SCSI_CDROM_DEVICE_FOR_NAME
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
FOR /F "usebackq tokens=1,3,5,7 delims=' " %%A IN (`^
%~dps0tools\cdda2wav.exe -scanbus 2^>^&1 ^| FIND "Removable CD-ROM"`) DO (
:: A=device, B=vendor, C=model, D=version
CALL %~dps0lib\string.cmd :TRIM "%%B"
SET VENDOR=!RESULT!
CALL %~dps0lib\string.cmd :TRIM "%%C"
SET MODEL=!RESULT!
SET NAME=!VENDOR! !MODEL!
IF "!NAME!" == "%~1" (
ENDLOCAL & SET RESULT=%%A
GOTO :EOF
)
)
ECHO ERROR: No CD-ROM device by the name '%~1' could be found. >&2
ENDLOCAL & SET RESULT=
GOTO :EOF
Now, I can query the drive name for a specific drive letter using the command
WMIC.EXE Path Win32_CDROMDrive WHERE Drive='X:' GET Name /format:csv >NUL. SCSI drive name's will be postfixed with "SCSI CdRom Device", so that string has to be manually removed from the name. Here is all what it takes to get the device for a specific drive letter:
:: Returns the cygwin SCSI device Bus,ID,Lun (e.g. 1,3,0) for the provided
:: Windows drive letter (e.g. D:) using WMIC.EXE.
:: NOTE: WMIC is natively available since Windows XP/2003 and requires
:: Administrator priviledges.
::
:: @INTERNAL use :GET_SCSI_CDROM_DEVICE_FOR_DRIVE instead
:: @PARAM String Windows drive letter including colon
:: @RETURN String cygwin SCSI device
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_WMIC
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
FOR /F "usebackq skip=2 tokens=2 delims=," %%N IN (`^
WMIC.EXE Path Win32_CDROMDrive ^
WHERE Drive^='%~1' GET Name /format:csv 2^>NUL`) DO (
SET DRIVENAME=%%N
SET DRIVENAME=!DRIVENAME: SCSI CdRom Device=!
)
IF DEFINED DRIVENAME (
CALL :GET_SCSI_CDROM_DEVICE_FOR_NAME "!DRIVENAME!"
) ELSE (
ECHO ERROR: No CD-ROM drive could be found for drive letter '%~1'. >&2
SET RESULT=
)
ENDLOCAL & SET RESULT=%RESULT%
GOTO :EOF
REG
Unfortunately WMIC need Administrator priviledges as stated above. So I thought about another method for retrieving data about CD-ROM drives. All the information that WMIC returns is available in the registry.
First, the used drive letters can be found in HKLM\SYSTEM\MountedDevices, named like
\DosDevices\X:. The value is a 16bit little-endian hex string, storing something like
\??\SCSI#CdRom&Ven_KB9081Z&Prod_YHQ276H&Rev_1.0#5&3a3a60d9&0&000#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}. Replace the # through backslashes, remove the first 4 characters and the last element
\{53f5630d-b6bf-11d0-94f2-00a0c91efb8b} and you get the registry path below
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\. And here I find the
FriendlyName of our drive, in that case
KB9081Z YHQ276H SCSI CdRom Device. From here on it's the very same as with WMIC.
Three simple steps to be taken:
- Query the drive letter from registry, convert the hex value to an ASCII string, and fix the registry path.
- Open the registry path and check if device's Class is CDROM.
- Query the FriendlyName.
:: Returns the cygwin SCSI CD-ROM device Bus,ID,Lun (e.g. 1,3,0) for the
:: provided Windows drive letter (e.g. D:) using REG.EXE.
:: NOTE: REG.EXE is natively available since Windows XP/2003.
::
:: @INTERNAL use :GET_SCSI_CDROM_DEVICE_FOR_DRIVE instead
:: @PARAM String Windows drive letter including colon
:: @RETURN String cygwin SCSI device
:: @REQUIRES lib\string.cmd
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REG
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
FOR /F "usebackq tokens=3 delims= " %%B IN (`^
REG.EXE QUERY "HKLM\SYSTEM\MountedDevices" ^| ^
FINDSTR /R "\\DosDevices\\%~1.*REG_BINARY.*5C00"`) DO (
SET LINE=%%B
:: remove high bytes (reduce 16bit hex to 8bit hex numbers)
SET LINE=!LINE:000=0,!
SET LINE=!LINE:00=,!
:: extract registry path
CALL %~dps0lib\string.cmd :HEX2ASC "!LINE:~0,-1!" ","
SET REGPATH=!RESULT:~4,-39!
SET REGPATH=HKLM\SYSTEM\CurrentControlSet\Enum\!REGPATH:#=\!
)
SET CDROM_DEVICE=
IF DEFINED REGPATH (
FOR /F "tokens=1,2* delims= " %%C IN ('^
REG.EXE QUERY "!REGPATH!" ^| ^
FINDSTR /R "Class[^^A-Za-z0-9_-].*REG_SZ"') DO (
IF "%%C" == "Class" IF "%%E" == "CDROM" (
FOR /F "tokens=2* delims= " %%M IN ('^
REG.EXE QUERY "!REGPATH!" ^| FINDSTR /R "FriendlyName.*REG_SZ"') DO (
SET DRIVENAME=%%N
CALL :GET_SCSI_CDROM_DEVICE_FOR_NAME "!DRIVENAME: SCSI CdRom Device=!"
SET CDROM_DEVICE=!RESULT!
)
)
)
)
IF NOT DEFINED CDROM_DEVICE (
ECHO ERROR: No CD-ROM drive could be found for drive letter '%~1'. >&2
)
ENDLOCAL & SET RESULT=%CDROM_DEVICE%
GOTO :EOF
Instead of converting little endian to big endian on the hex string, I simply remove all high bytes - which are always 00. Additionally I insert a comma as a delimiter as needed by the
:HEX2ASC method. At first, I walked through the string, copying every first and second character, inserting a comma and simply omitting every third and fourth character. As this method was veeeery slow, I switched to the quick and dirty solution. Nevertheless, here is the good one:
:: remove high bytes and replace by comma
SET LINE_FIXED=
CALL %~dps0lib\string.cmd :STRLEN "!LINE!"
SET /A MAX=!RESULT! - 1
FOR /L %%I IN (0,2,!MAX!) DO (
SET /A MOD=%%I%%4
IF !MOD! == 0 (
CALL %~dps0lib\string.cmd :SUBSTR "!LINE!" %%I 2
SET LINE_FIXED=!LINE_FIXED!!RESULT!,
)
)
REGEDIT
The last solution that should also work on Windows 2000 systems is much more complex. It works basically the same way as the REG.EXE one, however I have to export the registry keys to a file and work on that one. The problem is that long keys are spanned over multiple lines, indicated by a backslash at the end of the line. So I have to scan the file until finding my drive, and then starting concatenating the string until the last backslash. However I am using a little performance tweak here: Instead of checking each read line for my drive letter, I first retrieve the line where it is using FINDSTR, then start scanning until that line and begin concatenation. Testing each line was way to slow...
:: Returns the cygwin SCSI CD-ROM device Bus,ID,Lun (e.g. 1,3,0) for the
:: provided Windows drive letter (e.g. D:) using REGEDIT.EXE.
::
:: @INTERNAL use :GET_SCSI_CDROM_DEVICE_FOR_DRIVE instead
:: @PARAM String Windows drive letter including colon
:: @RETURN String cygwin SCSI device
:: @REQUIRES lib\io.cmd
:: @REQUIRES lib\string.cmd
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REGEDIT
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
:: export registry key HKLM\SYSTEM\MountedDevices to a file
CALL %~dps0lib\io.cmd :CREATE_TEMPFILE
SET REG_FILENAME=!RESULT!
REGEDIT.EXE /E "!REG_FILENAME!" "HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices"
:: parse registry file excluding empty lines
SET HEX=
FOR /F "usebackq tokens=1 delims=:" %%I IN (`^
TYPE "!REG_FILENAME!" ^| FINDSTR /R /V "^^$" ^| ^
FINDSTR /R /N "^^.\\\\DosDevices\\\\%~1.=hex:5c,00"`) DO (
:: %%I is the line number where the searched key starts
SET CURRENTLINE=
SET LINESTR=
SET LINENR=0
:: as a key might span over multiple lines, concat these lines
FOR /F "usebackq tokens=1,2* delims=: " %%M IN (`TYPE data.reg ^| FINDSTR /R /V "^^$"`) DO (
SET /A LINENR=!LINENR! + 1
:: found the key, check for spanning (line ending with backslash "\")
IF !LINENR! == %%I (
SET CURRENTLINE=%%O
IF "!CURRENTLINE:~-1!" == "\" (
SET LINESTR="!CURRENTLINE:~0,-1!"
) ELSE (
SET HEX=!CURRENTLINE!
GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REGEDIT_GET_NAME
)
) ELSE IF DEFINED LINESTR (
SET CURRENTLINE=%%M
IF "!CURRENTLINE:~-1!" == "\" (
SET LINESTR="!LINESTR:~1,-1!!CURRENTLINE:~0,-1!"
) ELSE (
SET HEX=!LINESTR:~1,-1!!CURRENTLINE!
SET LINESTR=
GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REGEDIT_GET_NAME
)
)
)
)
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REGEDIT_GET_NAME
SET CDROM_DEVICE=
IF DEFINED HEX (
:: quick fix: convert 16bit hex to 8bit by removing high byte
SET HEX=!HEX:,00=!
CALL %~dps0lib\string.cmd :HEX2ASC "!HEX!" ","
SET REGPATH=!RESULT:~4,-39!
SET REGPATH=HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\!REGPATH:#=\!
REGEDIT.EXE /E "!REG_FILENAME!" "!REGPATH!"
FOR /F %%X IN ('TYPE "!REG_FILENAME!" ^| FINDSTR /R "^^.Class.=.CDROM"') DO (
FOR /F "tokens=2 delims==" %%N IN ('TYPE "!REG_FILENAME!" ^| FINDSTR /R "^^.FriendlyName.=."') DO (
SET DRIVENAME=%%N
CALL :GET_SCSI_CDROM_DEVICE_FOR_NAME !DRIVENAME: SCSI CdRom Device=!
SET CDROM_DEVICE=!RESULT!
)
)
)
IF NOT DEFINED CDROM_DEVICE (
ECHO ERROR: No CD-ROM drive could be found for drive letter '%~1'. >&2
)
DEL /F "!REG_FILENAME!"
ENDLOCAL & SET RESULT=%CDROM_DEVICE%
GOTO :EOF
Finally, here is my main method that checks what solution is available and selectes that one:
:: Returns the cygwin SCSI CD-ROM device Bus,ID,Lun (e.g. 1,3,0) for the
:: provided Windows drive letter (e.g. D:).
::
:: @PARAM String Windows drive letter including colon
:: @RETURN String cygwin SCSI device
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE
:: Provides 3 internal methods to detect the device with auto-fallback.
:: 1) WMIC.EXE (since Windows XP/2003, requires Administrator priviledges)
:: 2) Registry through REG.EXE (since Windows XP/2003)
:: 3) Registry through REGEDIT.EXE (since Windows NT)
WMIC.EXE ALIAS /? >NUL 2>&1 || GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_REG_LABEL
FOR /F "skip=2 tokens=2 delims=:" %%T IN ('WMIC.EXE ALIAS /? 2^>^&1') DO (
IF "%%T" == "Win32 Error" GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_REG_LABEL
GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_WMIC_LABEL
)
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_WMIC_LABEL
CALL :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_WMIC "%~1"
GOTO :EOF
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_REG_LABEL
REG.EXE EXPORT /? >NUL 2>&1 || GOTO :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_REGEDIT_LABEL
CALL :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REG "%~1"
GOTO :EOF
:GET_SCSI_CDROM_DEVICE_FOR_DRIVE_REGEDIT_LABEL
CALL :GET_SCSI_CDROM_DEVICE_FOR_DRIVE_BY_REGEDIT "%~1"
GOTO :EOF
Copy that stuff into one file, copy my
libraries to lib\io.cmd, lib\string.cmd and lib\number.cmd and add tools\cdda2wav.exe including the tools\cygwin1.dll. Now by typing
CALL :GET_SCSI_CDROM_DEVICE_FOR_DRIVE "X:" you will receive the SCSI device in the variable
%RESULT%.
cypressor - 28. Mär, 17:55