Reader Level:
ARTICLE

Gapless Audio CD Recording with VB or C Sharp

Posted by Mickey Marshall Articles | Current Affairs October 03, 2011
I wrote this because I needed it. Being a card carrying member of Old Timers Associated, I have a lot of beautiful music on vinyl. Being a card carrying member of Poor Folks Associated, I can’t afford to re-buy all of them on CD.
  • 0
  • 0
  • 7289
Download Files:
 

Before I launch into the how-to portion of this document I would like to take time to say thanks to a few influences.

Thank God for inventing aspirin, otherwise I would have certainly died from a headache trying to figure this out.
Thanks to my wife for not killing me for being a neglectful husband.

Thanks to Eric Haden for his very helpful web page on Track at Once CD Burning.

I wrote this because I needed it. Being a card carrying member of Old Timers Associated, I have a lot of beautiful music on vinyl. Being a card carrying member of Poor Folks Associated, I can't afford to re-buy all of them on CD. Most CD recording packages do a wonderful job for 99% of them. For the 1% that are live concerts (many tracks and no gaps), they don't seem so good.

IMAPI2 is Microsoft's updated Image Mastering API. This is the API to use for burning data and audio to optical discs. For languages like VB and C Sharp, you can access most of the features directly but for event handling you will need an "IMAPI2 Interop". There are several available but the only ones I have found are in C Sharp so VB users are stuck using a wrapper (minor problems). Hence the slightly over complicated examples brought here.

I started with a document from MS that gave 8 simple steps for burning Disc at Once CDs. A ton of aspirin later I hope this helps in your quest.

  1. Detect proper drive (IDiscMaster2 => IDiscRecorder2)
  2. Create ImageIRawCDImageCreator

     
  3. Configure burnerIDiscFormat2RawCD & burn your image

A ton of aspirin later I hope that what I have done helps in your quest.

Thanks MS!

There are many separate classes inside IMAPI but the two main components needed for gapless audio recording are MsftRawCDImageCreator (which inherits IRawCDImageCreator) and MsftDiscFormat2RawCD (which inherits IDiscFormat2RawCD and DiscFormat2RawCD_Event). Instances of these two classes can not be created with the standard new function. Instead you must call the CoCreateInstance function. Use __uuidof(MsftDisc ...) for the class identifier and __uuidof(IDisc ...) for the interface identifier.

Dll Code:

// imapi2 class that is resposible for creating the image that
        MsftRawCDImageCreator DiscAtOnceImage = default( MsftRawCDImageCreator);
// imapi2 class that writes the image to the disc
        MsftDiscFormat2RawCD DiscFormat2RawCD = default( MsftDiscFormat2RawCD);
        // these two classes can not be created with the 'new' call
        // they need to be created with calls from CoCreateInstance
        // CoCreateInstance Disc... requires guiids for the IDisc... and MsftDisc... (which incorporates the interface and the associated events)
        Guid IRawCDImageCreatorGUID = new Guid("25983550-9D65-49CE-B335-40630D901227");
        Guid MsftRawCDImageCreatorGUID = new System.Guid("25983561-9D65-49CE-B335-40630D901227");

        Guid IDiscFormat2RawCDGUID = new Guid("27354155-8F64-5B0F-8F00-5D77AFBE261E");
        Guid MsftDiscFormat2RawCDGUID = new Guid("27354128-7F64-5B0F-8F00-5D77AFBE261E");
                IntPtr ip = new IntPtr(0);
                CoInitialize(ip);
                DiscAtOnceImage = ( MsftRawCDImageCreator)CoCreateInstance(MsftRawCDImageCreatorGUID, null, CLSCTX.CLSCTX_ALL, IRawCDImageCreatorGUID);
                DiscFormat2RawCD = ( MsftDiscFormat2RawCD)CoCreateInstance(MsftDiscFormat2RawCDGUID, null, CLSCTX.CLSCTX_ALL, IDiscFormat2RawCDGUID);


There are other classes used in the example and I will try to explain them as we encounter them.
OK, first things first, later things later.

The list of available recorders must be retrieved and shown to the user. In this example, the recorders are found in the constructor for AudioRecorder. Use MsftDiscMaster2 to cycle through all available recorders. MsftDiscMaster2 lists all available recorder and their "UniqueId" strings.

After that you must create an instance of IDiscRecorder2 and then you must initialize the desired recorder using it's ID.

GUI Code:

MsftDiscMaster2 discMaster = new  MsftDiscMaster2();
private  IDiscRecorder2[] discRecorders;// an array of all available recorders
...................................................
                IDs[i] = (uniqueRecorderID);
                MsftDiscRecorder2 discRecorder = new  MsftDiscRecorder2();
                discRecorders[i] = discRecorder;
                discRecorder.InitializeDiscRecorder(IDs[i]);


Before anything else happens you must delare an instance of the OpticalBurner.AudioRecorder.

Public WithEvents adr As New OpticalBurner.AudioRecorder ' instanciate the OpticalBurner.Audio Recorder

It is also important to make all projects COM-Visible to ensure that all events from IMAPI travel all the way up the ladder to the user.

GapCD.gif

In the GUI's Form.Load event, I display each available recorder in a combo box:

GUI Code:

       Do ' find as many recorders as you can
            s = ""
            If adr.GetDriveName(i, s) Then
                devicesComboBox.Items.Add(s)
            Else
                If i > 0 Then ' by default choose the first one
                    devicesComboBox.SelectedIndex = 0
                End If
                Exit Do
            End If
            i += 1
        Loop While
True

 
You must be careful to pick the proper recorder when the selected index of the combo box is changed.

GUI Code:

adr.CurrentIdIndex = devicesComboBox.SelectedIndex
adr.InitAudiorecorder()

Now that we have found all available recorders and picked the one we want we need to add some music. IMAPI is very particular about the music it will accept. Don't worry, it has selective bit taste, not music taste. There is a proper format to the music. It must be in stereo .wav format. It must be uncompressed PCM with a sampling rate of 44.1 KHz (44100 samples per second). Fortunately, this is the most common wav format available. For more info about wav format check out "The Sonic Spot's" web page on the subject.

If the file is in the proper format you must strip off the header and put the music into an IStream object. The stream must contain all the music and must also be an even whole number of sectors. A CD's sector size is 2,352 bytes.
This is handled in the PrepareStream routine in the AudioRecorder class. The is called when the user calls AddTracks.

DLL Code:

        //
        // The stream to be burned is just to music portion of the wav file
        //
        private IStream PrepareStream(int i)
        {
            IStream wavStream = null;
// define a new stream object

            waveData = new byte[Files[i].SizeOnDisc - 1 + 1]; // set aside an array of bytes big enough to hold all the data
            IntPtr fileData = Marshal.AllocHGlobal((IntPtr)(Files[i].SizeOnDisc));
// create a pointer for use when we load the stream

            FileStream fileStream = File.OpenRead(Files[i].Filepath);// Create a FileStream to read the file
            int sizeOfHeader = (int)(Files[i].MusicStart); // the length of the header has already been determined therefore we know where the music starts
            int MusicSize = (int)(Files[i].SizeOnDisc - Files[i].MusicStart);
// the length of the music portion can be easily calculated

            fileStream.Read(waveData, 0, System.Convert.ToInt32(Files[i].SizeOnDisc - 1));// read the whole file
            Marshal.Copy(waveData, sizeOfHeader, fileData, MusicSize);
// copy just the music data into the pointer location

            CreateStreamOnHGlobal(fileData, true, ref wavStream);// create the stream
            Files[i].Stream = wavStream;// use this stream to burn to the disc
            return wavStream;
        }

You may begin the burn once all tracks have been loaded into streams.

As stated before, the burn process needs to run in a different thread than the main program so that burn progress can be reported. Create a thread that will run the burn sub. Start the thread.

DLL Code:

        //
        // Start the burn thread
        //
        public void Burn()
        {
                oThread = new Thread(new ThreadStart(this.BurnDAO));
 
                oThread.Start();
        }


The burn thread has several checks to perform to ensure that all conditions are ready. For instance, there needs to be a blank cd in the drive, it must be a drive that supports audio recording, the disc must be the proper type for audio and DiscAtOnce classes must be registered on the machine. Once all these checks are completed you can procede to step three of MS's helpful guide to using DAO (configureing the burner).

Is the disc to be gapless? If so then turn off Gapless recording in MsftRawCDImageCreator. When disabled the audio tracks will have the standard 2-second (150 sectors) silent gap between tracks. When enabled, the last 2 seconds of audio data from the previous audio track are encoded in the pre-gap area of the next audio track, enabling seamless transitions between tracks.

DLL Code:

DiscAtOnceImage.DisableGaplessAudio = false;// enable gapless recording

There is also the 2 second gap before the first track to be aware of. For this there is a sub routine in MsftRawCDImageCreator called AddSpecialPregap. It is optional but this example uses the first two seconds of the first track. This stream is created in FindSpecialPreGap. It is very similar to PrepareStream so I won't go into the gory details again except to say the we limit the stream size to 352800 bytes. (2 seconds * 2 channels * 2 bytes per sample * 44100 samples per second = 352800).

We now need to inform the burner which routine to call for burn progress updates:

DLL Code:

DiscFormat2RawCD.Update += new  DiscFormat2RawCD_EventHandler(DiscAtOnce_Update);// tell imapi where your progress handler is

Next we need to add the tracks to the image creator

DLL Code:

for (i = 0; i <= tracklist.Length - 1; i++) // one at a time add the tracks to the stream to be burned
      {
           j = DiscAtOnceImage.AddTrack( IMAPI2 .Interop . IMAPI_CD_SECTOR_TYPE.IMAPI_CD_SECTOR_AUDIO, Files[i].Stream);
           l += Files[i].PlayLength;
      }


After adding all the tracks you need to use the image creator to create the final stream (an amalgum of all the little streams. The size of the final stream must me an even multiple of 2048 so while adding tracks we keep track of the size we are adding and then we bump that number up to a even multiple of 2048. We create a stream of that size and let the image creator load it.

DLL Code:

       j = l / 2048;
       j = (j + 1) * 2048;// the size of the Final Prepared Stream
       m = new IntPtr(j - 1);
       IntPtr fileData = Marshal.AllocHGlobal(m);//j - 1);
       CreateStreamOnHGlobal(fileData, true, ref FinalStream); //Create the Stream
       FinalStream.SetSize(j); // Set the stream size
       FinalStream = DiscAtOnceImage.CreateResultImage();
// let imapi prepare the stream for burn


We are now ready to call DiscFormat2RawCD.WriteMedia.
 
Putting it all together we have:

DLL Code:

        //
        //The burn should take place in its own thread so that events can be handled
        //
        private void BurnDAO()
        {
            try
            {
                int i = 0;
                long l = 0;
                long j = 0;
                IntPtr m;
                if (!DAO_Available)// quit if the enviroment does not support Disc at Once
                    return;
                canceling = false;
                Boolean Doit = false;
 
                IMAPI_MEDIA_PHYSICAL_TYPE mediaType;
                IMAPI_FORMAT2_DATA_MEDIA_STATE curMediaStatus;
                MsftDiscFormat2Data datawriter = new  MsftDiscFormat2Data();
                IStream FinalStream = null;
                Burning = true;
                datawriter.Recorder = (MsftDiscRecorder2)AudioDiscRecorder;// choose the recorder for media status data
                try
                {
                    mediaType =(IMAPI_MEDIA_PHYSICAL_TYPE ) datawriter.CurrentPhysicalMediaType;// get the current media
                    curMediaStatus = (IMAPI_FORMAT2_DATA_MEDIA_STATE)datawriter.CurrentMediaStatus;// get the current media status
                    // if the media is blank or appendable
                    if ((curMediaStatus &  IMAPI_FORMAT2_DATA_MEDIA_STATE.IMAPI_FORMAT2_DATA_MEDIA_STATE_BLANK) ==  IMAPI_FORMAT2_DATA_MEDIA_STATE.IMAPI_FORMAT2_DATA_MEDIA_STATE_BLANK)
                    {
                        try
                        {
                            System.Array a = DiscFormat2RawCD.SupportedMediaTypes; // find out if this media is supported
                            foreach ( IMAPI_MEDIA_PHYSICAL_TYPE mt in a)
                            {
                                if (mt == mediaType)
                                {
                                    Doit = true;
                                }
                            }
                            if (Doit)//MediaSupported?
                            {
                                DiscFormat2RawCD.Recorder = AudioDiscRecorder;
// choose the recorder

                                if (gapless)// gapless recording
                                {
                                    DiscAtOnceImage.DisableGaplessAudio = false;// enable gapless recording
                                    IStream PregapStream = FindSpecialPreGap(); // find the spegial data for the first two seconds
                                    DiscAtOnceImage.AddSpecialPregap(PregapStream);// add that stream
                                }
                                else
                                {
 
                                    DiscAtOnceImage.DisableGaplessAudio = true; //disable gapless recording
                                }
 
                                DiscFormat2RawCD.Update += new  DiscFormat2RawCD_EventHandler(DiscAtOnce_Update);// tell imapi where your progress handler is
                                for (i = 0; i <= tracklist.Length - 1; i++) // one at a time add the tracks to the stream to be burned
                                {
                                    j = DiscAtOnceImage.AddTrack( IMAPI2 .Interop . IMAPI_CD_SECTOR_TYPE.IMAPI_CD_SECTOR_AUDIO, Files[i].Stream);
                                    l += Files[i].PlayLength;
                                }
                                j = l / 2048;
                                j = (j + 1) * 2048;// the size of the Final Prepared Stream
                                m = new IntPtr(j - 1);
                                IntPtr fileData = Marshal.AllocHGlobal(m);//j - 1);
                                CreateStreamOnHGlobal(fileData, true, ref FinalStream); //Create the Stream
                                FinalStream.SetSize(j); // Set the stream size
                                FinalStream = DiscAtOnceImage.CreateResultImage();
// let imapi prepare the stream for burn

                                DiscFormat2RawCD.ClientName = sClientName;
                                DiscFormat2RawCD.PrepareMedia(); // locks the drive
                                DiscFormat2RawCD.RequestedSectorType =  IMAPI2.Interop .IMAPI_FORMAT2_RAW_CD_DATA_SECTOR_TYPE.IMAPI_FORMAT2_RAW_CD_SUBCODE_IS_RAW;// this seems to be the only sector type viable for this kind of DAO
                                DiscFormat2RawCD.BufferUnderrunFreeDisabled = bufferunderrun;
                                if (!canceling)
                                {
                                    DiscFormat2RawCD.WriteMedia(FinalStream);// burn
                                }
                                else
                                {
                                    if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.BurnCanceled;
                                }
                            }
                            else // mediatype not supported
                            {
                                if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.DiscNotSupported;
                            }
                        }
                        catch
                        {
                            if (!canceling)
                            {
                                if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.UnknownBurnError;
                            }
                            else
                            {
                                if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.BurnCanceled;
                            }
                        }
                    }
                    else // disc not blank
                    {
                        if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.DiscNotBlank;
                    }
                }
                catch // no disc in the drive
                {
                    if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.DriveEmpty;
                }
                try
                {
                    DiscFormat2RawCD.Update -= new  DiscFormat2RawCD_EventHandler(DiscAtOnce_Update); // remove the progrss handler
                    DiscFormat2RawCD.ReleaseMedia(); // unlock the drive
                    datawriter = null;
                }
                catch
                {
                    if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.UnknownBurnError;
 
                }
            }
            catch
            {
                if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.UnknownBurnError;
            }
            if (eject)
                AudioDiscRecorder.EjectMedia();
            InitObjects();// re init the obects so we can burn another disc wiithout stopping and starting again
            if (BurnError ==  BurnErrors.BurnNotDone) BurnError =  BurnErrors.BurnDoneSuccess;
            canceling = false;
            Burning = false;
            oThread.Abort();
            oThread = null;
        }

 

COMMENT USING