Click here to Skip to main content
15,860,844 members
Articles / Programming Languages / C#

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

Rate me:
Please Sign up or sign in to vote.
4.77/5 (48 votes)
19 Jun 2017Ms-PL6 min read 144.5K   8.1K   146   62
Ever wonder how programs like Nero work? They make their own SCSI libraries... like this!

Introduction

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: http://www.13thmonkey.org/documentation/SCSI/mmc3r10g.pdf.

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:

C#
//BAD WAY to send the CLOSE TRACK/SESSION command
unsafe
{
    //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:

C#
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:

C#
//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*/, 
                                          FileAccess.ReadWrite))
    using (var cd = new MultimediaDevice(new Win32Spti(file.SafeFileHandle, true), false))
    {
        cd.Interface.LockVolume();
        //Lock the volume (this is different from the FileShare value)

        try
        {
            //Dismount the file system since we want to overwrite it
            cd.Interface.DismountVolume();
            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));
                WaitForDeviceReady(cd);
            }

            //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,
                   TrackSessionCloseFunction.CloseSessionOrStopBGFormat, 
                   trackNumber));
                Console.WriteLine("Closing...");
                WaitForDeviceReady(cd);
            }
        }
        finally { cd.Interface.UnlockVolume(); } //Unlock the volume
    }
}

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

C#
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 == 
                  AdditionalSenseInformation.LogicalUnitNotReady_OperationInProgress)
    {
        //Print the progress here
        Console.WriteLine("Progress: {0:P2} done", 
                sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction);
        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:

Screenshot

Screenshot

Finally...

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. :)

License

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionMicrosoft.CSharp.Targets not found Pin
Onur Guzel12-May-18 5:44
Onur Guzel12-May-18 5:44 
AnswerRe: Microsoft.CSharp.Targets not found Pin
Occam's Razor15-Jun-18 3:07
Occam's Razor15-Jun-18 3:07 
BugIdentifying Tracks as Audio or Data with READ TOC/PMA/ATIP Command Pin
jonmjames6-Jul-16 7:23
jonmjames6-Jul-16 7:23 
SuggestionUsing with .NET 4.0+ Pin
jonmjames17-Jun-16 7:44
jonmjames17-Jun-16 7:44 
QuestionHi, Is it Possible to implement this in Asp.Net MVC ? Pin
Pratap A Patil23-Apr-15 19:17
Pratap A Patil23-Apr-15 19:17 
QuestionSystem.Runtime.InteropServices.COMException Pin
Dhirajanytime30-Dec-14 20:34
Dhirajanytime30-Dec-14 20:34 
QuestionGet the file name which is being written to CD ROM Pin
russel.ak14-Mar-13 19:07
russel.ak14-Mar-13 19:07 
AnswerRe: Get the file name which is being written to CD ROM Pin
Occam's Razor14-Mar-13 19:15
Occam's Razor14-Mar-13 19:15 
GeneralRe: Get the file name which is being written to CD ROM Pin
russel.ak14-Mar-13 20:49
russel.ak14-Mar-13 20:49 
QuestionTape Designated position read and write? Pin
lsyfg9-Jan-13 21:35
lsyfg9-Jan-13 21:35 
AnswerRe: Tape Designated position read and write? Pin
Occam's Razor9-Jan-13 22:55
Occam's Razor9-Jan-13 22:55 
GeneralRe: Tape Designated position read and write? Pin
Member 1076207822-May-14 11:18
Member 1076207822-May-14 11:18 
QuestionHelper. Marshaler types of initial value setting item cause abnormal. Pin
lsyfg13-Dec-12 2:52
lsyfg13-Dec-12 2:52 
AnswerRe: Helper. Marshaler types of initial value setting item cause abnormal. Pin
Occam's Razor9-Jan-13 22:54
Occam's Razor9-Jan-13 22:54 
QuestionHelper. Marshaler types of initial value setting item cause abnormal. Pin
lsyfg4-Dec-12 0:49
lsyfg4-Dec-12 0:49 
QuestionIssues with .net framework 4.0 Pin
frankhzhang9-Nov-12 7:38
frankhzhang9-Nov-12 7:38 
AnswerRe: Issues with .net framework 4.0 Pin
Occam's Razor9-Nov-12 9:55
Occam's Razor9-Nov-12 9:55 
QuestionBecause of I/O device error, can't run the request Pin
lsyfg4-Nov-12 5:13
lsyfg4-Nov-12 5:13 
AnswerRe: Because of I/O device error, can't run the request Pin
Occam's Razor4-Nov-12 9:27
Occam's Razor4-Nov-12 9:27 
QuestionHelp About Tape Media changer Pin
String Sharp23-May-12 1:51
String Sharp23-May-12 1:51 
AnswerRe: Help About Tape Media changer Pin
Occam's Razor23-May-12 16:22
Occam's Razor23-May-12 16:22 
GeneralRe: Help About Tape Media changer Pin
String Sharp26-May-12 22:05
String Sharp26-May-12 22:05 
GeneralRe: Help About Tape Media changer Pin
Occam's Razor27-May-12 13:06
Occam's Razor27-May-12 13:06 
QuestionError Pin
Benny11239-Feb-12 14:48
Benny11239-Feb-12 14:48 
QuestionRe: Error Pin
Occam's Razor9-Feb-12 15:20
Occam's Razor9-Feb-12 15:20 

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

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