Index

CD-ROM Programmierung

CD-ROM

Foto: Gaston Thauvin

Einführung

Oft finden sich in diversen VB Foren Fragen wie:

Ich will in diesem Tutorial eine kleine Allzwecklösung anbieten, was heißt Allzwecklösung, es kommt darauf an, inwieweit die eigenen Laufwerke mitarbeiten. Wir werden uns mit der direkten Programmierung der Laufwerke befassen und auf die gängigsten Befehle eingehen.
Ich sollte vielleicht auch noch zusätzlich erwähnen, dass die hier vorgestellten Lösungswege mit einem Samsung SW-252B (ATAPI) getestet wurden und nicht unbedingt mit kommerziellen Lösungen vergleichbar sind.

Plan

Von einer Einführung in Kommunikationsmethoden unter den einzelnen Windows Versionen über
einfache Implementierung von Kommunikationsschnittstellen bis hin zu einigen Beispielen.

Straight down to the nitty-gritty

CD-ROM programmierung läuft über Packete. Ich sende ein Packet, du sendest ein Packet.
Das Ich-Packet nennt man SCSI Command. Dieser Befehl hat für SCSI Einheiten immer eine feste Länge von 6, 10, 12 oder (selten) 16 Bytes. ATAPI will generell 12er Packete, egal ob ein Befehl eine vorgeschriebene Länge von 6 Bytes hat. Das soll uns aber nicht weiter stören.

Wer sich mal intensiver mit CD Ripper Software befasst hat, ist sicher das eine oder andere mal über den Begriff ASPI (Advanced SCSI Programming Interface) gestolpert.
Das ASPI ist ein von Adaptec entwickelter Treiber für die SCSI Geräteprogrammierung. Mit im Bundle eine C DLL für den Userprozess. In der neuen 4.71 Versionen soll es auch zu jeder Windows Version kompatibel sein, leider hat sich oft das Gegenteil breitgemacht, besonders unter XP.

Wir haben aber zusätzlich noch einen zweiten Kandidaten im Rennen.
Das microsoftsche SPTI (SCSI Pass-Through Interface).
Erstmals aufgetaucht in Windows NT 4, hat es so ziemlich die gleichen Features wie das ASPI. Aber auch hier fällt Licht nur auf die eine Seite: Der Prozess braucht Administrator Rechte, um es nutzen zu dürfen. Wir werden später noch eine Klasse zusammenschustern, die uns beide Interfaces benutzen lässt.

Nun war ja bis jetzt immer nur die Rede von SCSI. Was aber ist mit IDE, USB und Firewire?
Adaptec hat sich irgendwann mal erbarmt, und ihrem Schützling IDE Unterstützung geschenkt. Bei USB und Firewire bin ich mir nicht so sicher. Das SPTI hingegen sollte sie alle unterstützen. Ich habe allerdings weder SCSI, USB noch Firewire Geräte, und kann da deshalb keine präzisere Auskunft geben. Allerdings werde ich mich bemühen, SCSI und IDE unter einen Hut zu bringen. Also, liebe SCSI/USB/Firewire User, ich würde mich nach diesem Tutorial über Erfahrungsberichte freuen!

ASPI und andere Katastrophen

Wenn wir das ASPI ansteuern wollen, brauchen wir erstmal einen Startpunkt. Der ist wnaspi32.dll. Eine DLL mit einigen Funktionen, u.a.:

GetASPI32SupportInfo() As Long - ASPI Status und Anzahl an Host Adaptern
SendASPI32Command(udtCmd As Any) As Long - ASPI einen Befehl ausführen lassen

Mehr als diese 2 Funktionen brauchen wir nicht zur ASPI Programmierung.
Und nun, was einigen nicht ganz neu sein wird: VB kann SendASPI32Command nicht aufrufen. Das liegt daran, dass VB mit der CDECL Calling Convention nicht zurecht kommt. Zumindest nicht in der P-Code Variante. Unsere Optionen sind: Wir können uns eine Wrapper DLL schreiben, oder uns der Arbeit anderer bedienen. :) So zum Beispiel der von Paul Caton, der eine CDECL Klasse gebastelt hat.

Wir basteln uns eine ASPI Klasse

Auf frisch ans Werk. Wir basteln uns eine Klasse, um per ASPI Befehle an Laufwerke zu senden. Unbedingt die ASPI Dokumentation lesen!
Am Anfang ist sind die vielen Informationen nicht gerade einfach zu verstehen, und ich will hier auch nicht Tonnen von Seiten füllen, deshalb einfach die ASPI Klasse hier herunterladen.

Moving on... das SPTI

Mit dem DeviceIoControl Control Code IOCTL_SCSI_PASS_THROUGH kann man Packete senden und empfangen. Weil aber SCSI_PASS_THROUGH im Gegensatz zu SCSI_PASS_THROUGH_DIRECT nur einen 512 Bytes kleinen Buffer hat, werden wir SCSI_PASS_THROUGH_DIRECT wählen. Vor- und Nachteile sind hier nachzulesen.
Und hier geht's zum Download der SPTI Klasse.

Das ISCSI Interface

So, clsASPI und clsSPTI implementieren beide die ISCSI Schnittstelle.
Das erleichtert uns u.a. die Auswahl des geeigneten Treibers.
Ich will für clsASPI und clsSPTI nicht ins Detail gehen, da ich wenig Lust habe, hier alle 1000 Zeilen zu besprechen. Stattdessen schauen wir uns das ISCSI Interface genauer an:

Property Interface() As String - Gibt den Namen des verwendeten Interfaces zurück.
Property LastASCQ() As Byte - additional sense code qualifier
Property LastASC() As Byte - additional sense code
Property LastSK() As Byte - Sense Key
Property Initialized() As Boolean - Ist das Interface bereit?
Property Installed() As Boolean - Ist das Interface vorhanden?
Property DriveCount() As Integer - Anzahl an gefundenen Laufwerken
Property DriveChar() As String - Gibt den Laufwerksbuchstaben hinter einem Handle zurück
Property DriveHandle() As String - Ein eindeutiger Schlüssel zu einem gefundenen Laufwerk
Property HostAdapter() As Byte - Bus, an dem das Laufwerk hängt
Property TargetID() As Byte - Target ID des Laufwerks
Property LUN() As Byte - Logical Unit Number des Laufwerks
Function ExecCMD() As Status - Sendet ein Packet an ein Laufwerk

Das Drive ID Model
DriveHandle() gibt eine Drive ID zurück. Die ist nicht etwa systemweit gültig, sondern von clsASPI oder clsSPTI erzeugt worden.
Für clsASPI besteht sie aus 3 Bytes: HA, ID und LUN.
für clsSPTI ist sie der Laufwerksbuchstabe.
Man sollte sie niemals fix irgendwo speichern, sondern dynamisch durch die Klassen ermitteln.

DriveChar
Mit dem SPTI kein Problem, mit dem ASPI allerdings schon, da es mit Bus Adressen anstatt mit dem Windows Dateisystem arbeitet. Unter Windows 9x/Me kann man den Laufwerksbuchstaben mit dem ASPI Befehl GetDiskInfo ermitteln. Da der aber mit INT 13h zusammenarbeitet, der ab NT nicht mehr verwendet wird, arbeitet, muss man da alle Laufwerke abklappern, und per Control Code IOCTL_SCSI_GET_ADDRESS die Bus Adresse vergleichen.
Und wenn das ASPI spinnt, wie bei mir :), dann war's das mit Laufwerksbuchstabe.

SK, ASC und ASCQ
Das sind die Debug Informationen des Laufwerks.
Normalerweise füllt, wenn das Laufwerk CHECK CONDITION anstatt GOOD als Status zurückgibt, das jeweilige Interface den Sense Buffer automatisch mit den Sense Codes. Vergleichbar mit kernel32.GetLastError().

Der erste Low Level Befehl

Wir wissen nun ungefähr, wie die Klassen funktionieren, vielleicht noch nicht 100 prozentig, welcher Begriff was heißt, aber das kommt mit dem Ende des Tutorials. ;)
Bevor wir nun die Klassen benutzen, um unsere ersten Low Level Befehle zu senden, müssen wir uns erstmal bewaffnen: Mit buchdicken Spezifikationen. Anders als viele behaupten, hat nicht jedes Laufwerk seinen eigenen Treiber, nein, da gibt es Standards, die jedes neuere Laufwerk auch befolgt (ab 1997). Leider gibt es immer mal wieder Laufwerke, die verbuggte Firmwares haben. Als kleine Orientierung kann die Liste von CD Master Freeware CDR-DAO helfen. "generic-mmc-(raw)" folgt strikt dem Standard, was aber nicht heißen muss, dass die anderen Laufwerke einen komplett anderen Befehlssatz haben.

T-10
T-10 ist ein Kommitee zur Entwicklung von SCSI Standards.
Wir können die dort veröffentlichten Standards (zumindest die Befehlssätze) getrost auch für ATAPI Laufwerke verwenden, weil die Kompatibilität bei der Entwicklung hoch gehalten wurde.
Das AT Kommitee ist übrigens T-13.
Wie auch immer, auf der T-10 Webseite unter "Drafts" finden sich die Standards SPC-2 und MMC-2. Die sind auch in neueren Versionen erhältlich, aber wir halten es so kompatibel wie möglich.
Öffnen wir SPC-2. Dieses Dokument behandelt SCSI Datenaustausch im Allgemeinen, was uns nicht weiter stören soll, wir springen ins Kapitel 7 (Seite 41 (59 von 293)), in dem uns gleich auf Seite 1 eine Tabelle mit Befehlen entgegenspringt. Der erste Befehl, den wir umsetzen werden, ist "Test Unit Ready" (nächste Seite), mit dem man überprüft, ob ein Gerät bereit ist.
Gesprungen auf Seite 163, ploppt hier wieder eine Tabelle auf.
Die ist logisch aufgebaut, Bytes von oben nach unten, Bits von rechts nach links.

Byte 0: OpCode (00h)
Byte 1-5: reserviert
Byte 5: CONTROL

Byte 0 ist in jedem Befehl der OpCode, an dem das Laufwerk die zu erledigende Aufgabe erkennt.
Die darauf folgenden n Bytes sind dann OpCode spezifisch, bis auf das letzte Byte. Das lassen wir einfach leer. Für VB würde die Tabelle so aussehen:

Dim cdb(5) As Byte

Natürlich, einfach. So sieht der Test Unit Ready Befehl aus.
In Verbindung mit unserem Iscsi Interface:

Dim cdb(5) As Byte

If Iscsi.ExecCMD(DriveID, cdb, 6, DIR_IN, 0, 0) = STATUS_GOOD Then
    ' Laufwerk ist bereit
Else
    ' Laufwerk ist nicht bereit
End If

Besprechung der Parameter:
Param 1: Drive ID, weiter oben behandelt
Param 2: Der Command Descriptor Block, eben der Befehl
Param 3: Länge des CDBs
Param 4: DIR_IN = Daten empfangen, DIR_OUT = Daten senden. Ist hier aber egal.
Param 5: 0. Kein Datenbuffer.
Param 6: 0. Kein Datenbuffer, keine Datenbufferlänge.
Param 7: 0.
Param 8: Optional. Das Timeout steht standardmäßig auf 5 Sekunden, bevor der Befehl abgebrochen wird. 0 stünde hier übrigens für "Wait For Ever".

Noch ein Befehl. Jetzt nehmen wir "Inquiry".
Der wird uns, wenn wir Glück haben :), den Namen des Laufwerks zurückgeben.
Zu finden ist der auf Seite 80, und sieht schon etwas komplizierter aus.
Das schöne an dem ist aber, wir können so ziemlich alle Felder überspringen.
CmdDt, EVPD sowie "Page Or Operation Code", Control sowieso, brauchen wir nicht. Nur den OpCode und die Zuweisungslänge ("Allocation Length"). Sobald der gesendet wurde, wird das Laufwerk die auf Seite 82 zu findende Struktur schicken.
Die kann in VB z.B. so aussehen:

Private Type inquiry
    qualifier   As Byte
    rsvd1       As Byte
    version     As Byte
    respfmt     As Byte
    addlen      As Byte
    rsvd2       As Byte
    stuff(1)    As Byte
    vendor(7)   As Byte
    product(15) As Byte
    revision(3) As Byte
    rsvd3(1)    As Byte
    stuff2(37)  As Byte
End Type

Die Struktur muss nicht exakt so groß sein wie beschrieben, ich hätte auch schon nach "revision" aufhören können. Die Felder "vendor", "product" und "revision" jedenfalls beinhalten den Laufwerksnamen (ASCII). Insgesamt ergibt sich für einen Inquiry Befehl dann:

Dim cdb(5)  As Byte
Dim buffer  As inquiry
 
cdb(0) = &H12
cdb(4) = Len(buffer)
 
If Iscsi.ExecCMD(DriveID, cdb, 6, VarPtr(buffer), Len(buffer)) = STATUS_GOOD Then
    Debug.Print "Hersteller: " & strconv(buffer.product, vbUnicode)
    Debug.Print "Produkt: " & strconv(buffer.product, vbUnicode)
    Debug.Print "Revision: " & strconv(buffer.revision, vbUnicode)
End If

Was aber, wenn gerade bei Test Unit Ready mal nicht STATUS_GOOD am Ende rauskommt?
Dafür gibt es die Sense Codes. Wenn das Resultat CHECK_CONDITION ist, kann man davon ausgehen, dass die Felder SK, ASC und vielleicht auch ASCQ voll sind. Das sind eindeutige Fehler IDs. Wenn beispielsweise kein Medium im Laufwerk ist, gibt Test Unit Ready SK 2h, ASC 3Ah zurück. Eine komplette Liste aller Fehlercodes findet sich im Appendix der Standards.
Wir bauen unser Test Unit Ready aus.

Dim cdb(5) As Byte
 
If Iscsi.ExecCMD(DriveID, cdb, 6, 0, 0) = CHECK_CONDITION Then
    If Iscsi.LastSK = 2 And Iscsi.LastASC = &H3A Then
        MsgBox "Keine CD im Laufwerk!"
    Else
        MsgBox "Laufwerk nicht bereit!"
    End If
Else
    MsgBox "Laufwerk bereit!"
End If

Komplette Test Unit Ready + Inquiry Demo mit ASPI und SPTI unter einem Hut.

Table Of Contents

Jede abgeschlossene Session auf einer CD hat eine TOC. In ihr sind alle zur Session gehörenden Tracks samt deren Startzeiten vermerkt. Öffnen wir mal MMC-2, und suchen da den Read TOC Befehl (Seite 247). Byte 0 ist klar. Byte 1 Bit 1 steht für das Zeitformat. Wir haben zur Auswahl: MSF (Minuten:Sekunden:Frames) und LBA (Logical Block Address).

Das MSF Format
Eine Minute hat 60 Sekunden, eine Sekunde hat 75 Frames. 1 Frame = 1 Sektor.

Das LBA Format
1 LBA = 1 Sektor.

MSF -> LBA
LBA = M * 60 * 75 + (S * 75) + Frames
Solange M nicht größer als 90 ist (> 90 ist für das Lead-In reserviert), werden noch 150 abgezogen.
Andernfalls 450150, da eine Lead-In Adressierung ins Negative geht.

Public Function MSF2LBA( _
    ByVal mins As Long, _
    ByVal secs As Long, _
    ByVal Frames As Long, _
    Optional positive As Boolean _
) As Long
 
    MSF2LBA = CLng(mins) * 60 * 75 + (secs * 75) + Frames
 
    If mins < 90 Or positive Then
        MSF2LBA = MSF2LBA - 150
    Else
        MSF2LBA = MSF2LBA - 450150
    End If
End Function

LBA -> MSF

M = (LBA + start) / (60 * 75)
S = (LBA + start - M * 60 * 75) / 75
F = (LBA + start - M * 60 * 75 - s * 75)

start ist für LBA > -151 150, andernfalls 450150.


Soweit so gut, zurück zur TOC.
Byte 2 steht für das TOC Format. Ich will alles simpel halten, ich nehme also Format 0, formatierte TOC. Byte 6 steht für den Track oder die Session, dieses Feld ist aber abhängig vom TOC Format, und für Format 0 unbedeutend. Der Rest ist klar. Die "Formated TOC" Struktur sieht in VB so aus:

Public Type toc_track
    rsvd1        As Byte
    ADR          As Byte
    Track        As Byte
    rsvd2        As Byte
    addr(3)      As Byte
End Type
 
Public Type formated_toc
    TocLen(1)    As Byte
    FirstTrack   As Byte
    LastTrack    As Byte
    TocTrack(99) As toc_track
End Type

TocLen ist die Anzahl der angekommenen Bytes, LastTrack der letzte TocTrack() Index und gleichzeitig das Lead-Out. Insgesamt 100 Tracks. 99 ist die maximale Anzahl an erlaubten Tracks auf einer CD, in diesem Fall wäre 100 das Lead-Out.
Das ADR Byte ist in 2 Nibbles aufgeteilt: ADR und CONTROL. ADR ist unwichtig für uns, interessanter ist da CONTROL. Hier ist verzeichnet, ob es sich um einen Daten- oder Audiotrack handelt. Hier können wir Daten- von Audiotracks trennen, indem wir einfach Bit 2 überprüfen. Mir ist aber auch mal ein Plextor untergekommen, der einen Audio Track mit "Reserved" gekennzeichnet hat. Deswegen war ich da in der TOC Reader Demo, die hier heruntergeladen werden kann, besonders gründlich.

Beispiele

Hier noch ein paar mehr Beispiele.

CD Informationen
Lauwerksinformationen
Tracks auf Festplatte speichern
Daten CD Brenner
ISO9660 (Joliet) Images erstellen
ISO9660 (Joliet) Images auslesen

Achtung
Zum Daten CD Brenner: Hier werden nur Mode-1 ISO Images gebrannt. Das ISO9660 Projekt erstellt diese.

Open-Source Links

CDR-DAO
CDRecord
Mox CD Burn
Alvise
CD-Freaks homemade Tools
UDF CD-R Writer (Codeproject)