Click here to Skip to main content
Click here to Skip to main content

SCSI Library in C# - Burn CDs and DVDs, Access Hard Disks, etc.

, 25 May 2010 Ms-PL
Rate this:
Please Sign up or sign in to vote.
Ever wonder how programs like Nero work? They make their own SCSI libraries... like this!


Have you ever wondered how CD/DVD burning programs like Nero and Roxio work? They can't just treat CDs as hard disks and write to them; Windows, at least, doesn't support using file writing functions to write to optical drives. Because of this fact, the only way they can do so is to send commands directly to the drive, bypassing most drivers between the application and the device. Over time, companies have used different methods to achieve this, in most cases by writing their own kernel-mode drivers and communicating with the device directly. It turns out, however, that in the NT family of Windows (including Windows NT, 2000, XP, Vista, 7, etc.), there is a nice generic function that exists precisely for bypassing the OS: DeviceIoControl(), using the IOCTL_SCSI_PASS_THROUGH_DIRECT control code.

Notice the word SCSI? It turns out that CD burning is related to the Small Computer System Interface specifications published by the T10 Committee. In fact, it also turns out that these specifications are useful for communicating with non-multimedia devices (as CD/DVD drives are called) as well, including block devices (e.g., hard disks) and stream devices (e.g., tapes). Although it is true that most computers nowadays use the Advanced Technology Attachment (ATA) standard for peripheral devices, it happens that we can also use SCSI commands to communicate with them. (How exactly this occurs, I still don't know.)

It took me a long time to figure all this out and to put it to use (read: more than just a year or two), because 99%+ of web searches for topics on "CD burning" either yield results for applications like Nero or Operating System features like the Windows Image Mastering API (IMAPI), and for that 1% that talks about SCSI, I probably ignored them, thinking they were irrelevant. Not being one to give up, though, I kept on going back to the problem, until by some magic, I learned about the SCSI standard. The next step, then, became getting hold of a copy of the standard, which at the time was an easy feat: I just went on the T10 website and downloaded all the drafts I could find. Now if you try to do this, however, you won't be so lucky: they restricted access to the documents a year or two ago, and it's pretty darn hard to find other copies on the internet. In fact, I have only found one copy of the old version 3 in my searches, and so I recommend you grab a copy before it's too late:

Now, you could go ahead and start reading the 471-page document, but I'm pretty sure you wouldn't want to, so I did that for you. (Actually, that's a lie; I just read the parts that I needed to get this working, not the whole 471 pages.) The result? This library. SCSI Command Descriptor Blocks (commands, for short), by their nature, are tightly packed into a handful of bytes (mostly 6 to 12, although on very rare occasions, I have seen 32-byte ones too) and then sent to the drive, so you can imagine how cumbersome it would be for the programmer to have to insert a 5-bit integer into a 6-byte CDB with all those bit shifts and bit masks, especially if you have to worry about marshaling from managed to unmanaged code. That's why I made this library; instead of writing cryptic code like this:

//BAD WAY to send the CLOSE TRACK/SESSION command
    //Allocate memory on the stack (heap is fine too but this is handier for me)
    byte* cdb = stackalloc[10];
    cdb[0] = 0x5B; //Imagine yourself reading someone else's code and seeing this
    cdb[1] = 1;
    cdb[2] = 3; //What do these flags mean?
    cdb[4] = (byte)((trackNo & 0x0000FF00) > 8); //Careful -- big endian
    cdb[5] = (byte)((trackNo & 0x000000FF) > 0);
    ExecuteCommand(cdb, 10, buffer, bufferOffset, ...);

you can write code like this:

cd.CloseTrackOrSession(new CloseSessionTrackCommand()
    Function = TrackSessionCloseFunction.CloseSessionOrStopBGFormat,
    TrackNumber = 2,
    Immediate = true

It's more readable, more maintainable, and more easily debuggable.

What this Library is Not

By now, it probably looks like - or at least I hope it looks like - this library is the magic key to CD burning. Well, that's both true and false. As unbelievable as this might seem, you can't just throw bytes onto a disc and insert it in a computer, expecting to magically get files in return. That's what all those file systems (ISO 9660, Joliet, Universal Disc Format, Rock Ridge, and HFS come to mind) are for. Of course, you have to be able to know both how and what to write to a disc, and so this library's purpose is to take care of the first of those two steps for you. If you know what to write to a disc, you don't have to worry about the details of SCSI communication; this library will handle that.

How it Works

There really isn't anything too tricky about how the library works; the difficulty is in actually implementing the packed data structures while presenting a nice interface to the programmer. Marshaling becomes a bit tricky (especially when converting integers to and from big endian format), and so the classes in this library are designed to provide a simple interface without letting the user worry about the implementation details.

Using the code is easy. Here's a sample method that burns an ISO image file:

//You need to call this method with the path of the ISO image file you want to burn
static void TestBurn(string filePath)
    //I could write a whole article on the Win32FileStream class, but its whole purpose here
    //is to get a handle to the CD drive, which you can't get with just the .NET framework
    using (var file = new Win32FileStream(@"\\.\CdRom0" /*First CD drive*/, 
    using (var cd = new MultimediaDevice(new Win32Spti(file.SafeFileHandle, true), false))
        //Lock the volume (this is different from the FileShare value)

            //Dismount the file system since we want to overwrite it
            cd.SynchronizeCache(new SynchronizeCache10Command());
            //Flush any leftover data

            //Set the drive to the highest possible speed
            cd.SetCDSpeed(new SetCDSpeedCommand(ushort.MaxValue, 
               ushort.MaxValue, RotationControl.ConstantLinearVelocity));

            if (cd.CurrentProfile == MultimediaProfile.CDRW)
            //CD-RWs should be erased
                Console.WriteLine("Blanking (erasing) CD-RW...");
                cd.Blank(new BlankCommand(
                         BlankingType.BlankMinimal, true, 0));

            //Get the current write parameters and change the 
            //appropriate settings
            var writeParams = cd.GetWriteParameters(
                              new ModeSense10Command(PageControl.CurrentValues));
            writeParams.MultiSession = MultiSession.Multisession;
            writeParams.DataBlockType = DataBlockType.Mode1;
            writeParams.WriteType = WriteType.TrackAtOnce;
            writeParams.SessionFormat = SessionFormat.CdromOrCddaOrOtherDataDisc;
            writeParams.TrackMode = TrackMode.Other;
            cd.SetWriteParameters(new ModeSelect10Command(false, true), writeParams);

            bool closeNeeded; //We'll need this later
            IMultimediaDevice iMMD = cd; //Get the easy-to-use interface
            //Find last track
            ushort trackNumber = 
              (ushort)(iMMD.FirstTrackNumber + iMMD.TrackCount - 1);
            //Open/create the track
            using (var track = iMMD.CreateTrack(trackNumber, out closeNeeded))
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            //Open the image
                //Buffer size of 1 sector is good enough here
                var buffer = new byte[cd.CDSectorSize];
                while (fs.Position < fs.Length)
                //While not all of the image is burned
                    //Fill the buffer from the image file
                    fs.Read(buffer, 0, buffer.Length);
                    //Write the buffer's data to the drive
                    track.Write(buffer, 0, buffer.Length);
                    Console.WriteLine("Burn progress: {0:P2}", 
                                     (double)fs.Position / fs.Length);

            cd.SynchronizeCache(new SynchronizeCache10Command());
            //Flush buffer data to disc (required)

            if (closeNeeded) //Not all discs need to be closed
                             //(e.g.: CD-RWs do, but DVD+RWs don't)
                //Start closing (method returns immediately)
                cd.CloseTrackOrSession(new CloseSessionOrTrackCommand(true,
        finally { cd.Interface.UnlockVolume(); } //Unlock the volume

This method pauses until the drive is ready, printing progress information in fixed time intervals:

static void WaitForDeviceReady(MultimediaDevice cd)
    Thread.Sleep(50); //Just a pause that I've sometimes found helpful
    //"Sense" data is just info about the status of the drive, nothing fancy
    SenseData sense;
    while ((sense = cd.RequestSense()).SenseKey == SenseKey.NotReady &&
            sense.AdditionalSenseCodeAndQualifier == 
        //Print the progress here
        Console.WriteLine("Progress: {0:P2} done", 
        Thread.Sleep(500); //Sleep since we don't want to use up CPU

The code should be self-explanatory with the comments, but here are some notes:

  • Whenever a function like CloseTrackOrSession() is called, it requires a ScsiCommand object as its input. The commands should be newly created every time, but if they are cached for performance, then their contents should not be assumed to be preserved.
  • This sample code burns the entire session in one track. As soon as the data is flushed (either because you requested it or because the unit's write buffer becomes empty), the drive automatically writes the lead-out. This means that you must keep on feeding the drive data until you are done, with no gaps in between, since any gap will cause the lead-out to be written and the session to be closed.

Complete Example: UDF 1.02 CD/DVD Burning Program

If you want to test the basic features of the library, take a look at the ISOBurn program. It can burn both ISO images and individual files and folders to CDs/DVDs. Through weeks of debugging and reading ECMA, ISO, and OSTA standards, I've managed to make my program burn with the UDF 1.02 file system. The code for that section isn't pretty or robust, but I'll improve it in future releases; it's just to show the capabilities of this library. Here are a couple of screenshots:




This project is also on the CodePlex website with the same name, SCSI. I will try to keep both up to date, but check that site every once in a while too, in case I forget.


Regarding multisession burns: I have tried to make multisession burning work, but please note that it is not ideally implemented; old files are logically erased instead of being kept. Nevertheless, it should work fine without any errors, assuming the last session on the disc is not finalized.

Make sure to check out the MMC-3 documentation mentioned in the introduction as a reference, though you certainly don't need to concern yourself with the minute details. If you have any questions or comments, please ask! I want to make this a better article, and I can't do that without your help. Smile | :)


This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


About the Author

Occam's Razor

United States United States
No Biography provided

Comments and Discussions

QuestionSystem.Runtime.InteropServices.COMException PinmemberDhirajanytime30-Dec-14 21:34 
QuestionGet the file name which is being written to CD ROM Pinmemberrussel.ak14-Mar-13 20:07 
AnswerRe: Get the file name which is being written to CD ROM PinmemberOccam's Razor14-Mar-13 20:15 
GeneralRe: Get the file name which is being written to CD ROM Pinmemberrussel.ak14-Mar-13 21:49 
QuestionTape Designated position read and write? Pinmemberlsyfg9-Jan-13 22:35 
AnswerRe: Tape Designated position read and write? PinmemberOccam's Razor9-Jan-13 23:55 
GeneralRe: Tape Designated position read and write? PinmemberMember 1076207822-May-14 12:18 
QuestionHelper. Marshaler types of initial value setting item cause abnormal. Pinmemberlsyfg13-Dec-12 3:52 
AnswerRe: Helper. Marshaler types of initial value setting item cause abnormal. PinmemberOccam's Razor9-Jan-13 23:54 
QuestionHelper. Marshaler types of initial value setting item cause abnormal. Pinmemberlsyfg4-Dec-12 1:49 
QuestionIssues with .net framework 4.0 Pinmemberfrankhzhang9-Nov-12 8:38 
AnswerRe: Issues with .net framework 4.0 PinmemberOccam's Razor9-Nov-12 10:55 
QuestionBecause of I/O device error, can't run the request Pinmemberlsyfg4-Nov-12 6:13 
AnswerRe: Because of I/O device error, can't run the request PinmemberOccam's Razor4-Nov-12 10:27 
QuestionHelp About Tape Media changer Pinmemberseyed ali ehbali23-May-12 2:51 
AnswerRe: Help About Tape Media changer PinmemberOccam's Razor23-May-12 17:22 
GeneralRe: Help About Tape Media changer [modified] Pinmemberseyed ali ehbali26-May-12 23:05 
GeneralRe: Help About Tape Media changer PinmemberOccam's Razor27-May-12 14:06 
QuestionError PinmemberBenny11239-Feb-12 15:48 
QuestionRe: Error PinmemberOccam's Razor9-Feb-12 16:20 
AnswerRe: Error PinmemberBenny11239-Feb-12 22:46 
GeneralRe: Error PinmemberOccam's Razor9-Feb-12 23:00 
GeneralRe: Error PinmemberBenny11239-Feb-12 23:57 
GeneralRe: Error PinmemberOccam's Razor10-Feb-12 0:00 
GeneralRe: Error PinmemberBenny112310-Feb-12 0:01 
GeneralRe: Error PinmemberOccam's Razor10-Feb-12 0:10 
GeneralRe: Error PinmemberBenny112310-Feb-12 2:08 
AnswerRe: Error PinmemberOccam's Razor10-Feb-12 6:18 
GeneralMy vote of 4 Pinmemberjfriedman21-Dec-11 13:03 
QuestionObtain CD information (burning app etc.)? PinmemberGandzy26-Mar-11 3:55 
AnswerRe: Obtain CD information (burning app etc.)? PinmemberOccam's Razor26-Mar-11 9:48 
GeneralRe: Obtain CD information (burning app etc.)? PinmemberGandzy26-Mar-11 10:39 
GeneralMy vote of 5 Pinmemberdivyang448124-Feb-11 2:23 
GeneralSCSI tape devices PinmemberMrSpackle22-Oct-10 9:49 
GeneralRe: SCSI tape devices PinmemberMehrdad N.22-Oct-10 15:48 
GeneralThe request is not supported.(Error) Pinmembermahnazfekri17-Sep-10 23:23 
GeneralRe: The request is not supported.(Error) PinmemberMehrdad N.18-Sep-10 8:52 
General"It's more readable, more maintainable, and more easily debuggable." Pinmemberfat_boy4-Jun-10 0:48 
GeneralRe: "It's more readable, more maintainable, and more easily debuggable." [modified] PinmemberMehrdad N.4-Jun-10 6:15 
GeneralRe: "It's more readable, more maintainable, and more easily debuggable." Pinmemberfat_boy15-Jun-10 4:32 
GeneralRe: "It's more readable, more maintainable, and more easily debuggable." Pinmemberfat_boy15-Jun-10 23:54 
AnswerRe: "It's more readable, more maintainable, and more easily debuggable." PinmemberMehrdad N.16-Jun-10 6:44 
Generalthanks Pinmemberemarti25-May-10 14:12 
AnswerRe: Seeking documentation or sample code... PinmemberMehrdad N.11-Mar-10 8:03 
Yes, of course! Smile | :)
In order to use your own type of device, you should create a new class that inherits from ScsiDevice, just like the MultimediaDevice and BlockDevice classes in the library. So let's say you call that class Scsi.MyDeviceKind.MyDevice. Create a constructor, preferably exactly the following, that calls the ScsiDevice constructor and optionally initializes any private fields (which you may or may not have):
public MyDevice(IScsiPassThrough @interface, bool leaveOpen) : base(@interface, leaveOpen) { /*Perform any initialization*/ }
Now create a new C# code file to contain your command(s). Say you want to create a new command for your device called MY COMMAND; you should then create a class called MyCommand that inherits from FixedLengthScsiCommand (variable-lengths are just a bit more tricky and I haven't come across them, but let me know if you need help with them). Your command class might look like the following. Because commands are stored in big-endian format, you want to create a private field of the type you want (e.g. ushort here) and expose it through a property that flips the byte order upon storage and retrieval, possibly by using the Bits.BigEndian() method I used here. Note that the ScsiCommand constructor does not check the validity of the value of ScsiCommandCode, permitting you to cast your own value to the enumeration, like shown below.
[StructLayout(LayoutKind.Sequential, Pack = 1)] //Don't forget this!
public class MyCommand : ScsiCommand
	public MyCommand() : base((ScsiCommandCode)0xFE /*OpCode here*/) { }
	private byte byte1;
	private byte byte2;
	private ushort _AllocationLength; //(For example)
	public ushort AllocationLength { get { return Bits.BigEndian(this._AllocationLength); } set { this._AllocationLength = Bits.BigEndian(value); } }
	//keep going for all fields
	private CommandControl _Control; //You must have this structure and the property below as the LAST field of your class
	public override CommandControl Control { get { return this._Control; } set { this._Control = value; } }
A couple of important notes to keep in mind:
- Marshaling is automated; you don't need to implement marshaling code in your class.
- Because marshaling is automated, that means that your class cannot contain any managed types; in other words, it can contain unmanaged structures or primitive types. (Unmanaged structures are those whose members are either primitive types or other unmanaged structures; they can't contain any managed types like string nested at any depth.) This should not be a problem in the implementation, as managed types should not be necessary inside a command. Please let me know if this presents a problem.
- The sizes of the fields, when added up (including the _Control field) and also added to the size of ScsiCommand (which is one byte), must equal the total size of your command.

After doing these, create your own methods inside your MyDevice device class for each command that you chose, preferably following this pattern:
public void CallMyCommand(MyCommand command, BufferWithSize buffer) { /*Implementation here*/ }
Depending on the complexity of the command, your method might even be exactly this simple:
public void CallMyCommand(MyCommand command, BufferWithSize buffer /*If there is any data transfer*/)
{ this.ExecuteCommand(command, DataTransferDirection.ReceiveData /*Or send data, depending on what you're doing*/, buffer); }
If you need more processing or parameter validation, you could put that code in CallMyCommand; take a look at ScsiDevice.Read12() for an example.
Note: The reason we accept the MyCommand object instead of having parameters about its fields is that, if you change the command class, you will no longer have to change this method -- it would stay the same.
If you have any questions please don't hesitate to ask! Smile | :)
AnswerRe: Seeking documentation or sample code... [modified] PinmemberMehrdad N.16-Mar-10 9:03 
GeneralMissing readDVDstructure and READBDStructure PinmemberJan Weltmeyer25-Jan-10 3:03 
AnswerRe: Missing readDVDstructure and READBDStructure PinmemberMehrdad N.25-Jan-10 13:25 
GeneralRe: Missing readDVDstructure and READBDStructure [modified] PinmemberJan Weltmeyer25-Jan-10 21:10 
GeneralRe: Missing readDVDstructure and READBDStructure PinmemberJan Weltmeyer26-Jan-10 3:54 
GeneralRe: Missing readDVDstructure and READBDStructure PinmemberMehrdad N.26-Jan-10 19:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150327.1 | Last Updated 25 May 2010
Article Copyright 2009 by Occam's Razor
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid