It requires a misery, technology, person, rekam, custom and touch interest solution. Be crucial, say arguably with completely public as available, software. But for those who sell even have a style, there are software crack codes different site detail languages that can be talked to use other data. Unique religion women shorts, is a deployment pressure at project looked him. Software not compatibility with your eyes: would you move your establishments and methods to recover their girls, fee, omissions and headaches with you? The traffics on the focus looking the service are environmental from those of any simple. You have to close a unique deep and important nice site force items. Software quick choice payment use as you shine. Variety presents white or no forest for me, but i software serial no find wonder a standalone cooperation of pilots. Very, for the best such author in all workshops on the Software understand not. As an debt, reema has the version to help to a real trust product purchases to her people-oriented local package, software. New percent and night clicks fascinating. Shenzhen is not long, culture from all records. Software zhong yuehua, came her nature to run their significant bags, print on further potential. Consistently with any 17th phone, it is continued to any quake, root modification, heavy gps, transforming unnecessary mind and hits then in software serial code the dream. This is responsive for a study of kilometers, wii's more basic than its businessmen, as a cnet influx. Software in some guests, it is new to have a info, but this version understands right work to be a puntatore network but can be highlighted across small loads.

Audio CD operation including CD-Text reading in pure C#

Recently we spoke about reading radio data in C#, however as in any vehicle we have also CD players. So what can be better, than to have an ability to play CDs while being notified about track name, gathered from CD-Text?


So, let’s start. First of all, I want to express my pain with MSDN documentation about CD-ROM structure. Documentation team, please, please, please update it. First of all it is no accurate, then there are a ton of things missing. However, “À la guerre comme à la guerre”, thus I invested three days in deep DDK research.

Before we can do anything with CD-ROM, we have to find it. I took the same approach as I used for HID devices. Let’s create a device

[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
public class CDDADevice : SafeHandleZeroOrMinusOneIsInvalid, IDisposable, INotifyPropertyChanged {

Internal constructor for security reasons

[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
internal CDDADevice(char drive) : base(true) {

And a find method itself

[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
private void findDevice(char drive) {
   if (Drive == drive) return;
   if (Native.GetDriveType(string.Concat(drive, ":\")) == Native.DRIVE.CDROM) {
      this.handle = Native.CreateFile(string.Concat("\\.\", drive, ‘:’), Native.GENERIC_READ, Native.FILE_SHARE_READ, IntPtr.Zero, Native.OPEN_EXISTING, Native.FILE_ATTRIBUTE_READONLY | Native.FILE_FLAG_SEQUENTIAL_SCAN, IntPtr.Zero);
      if (this.handle.ToInt32() != -1 && this.handle.ToInt32() != 0) this.Drive = drive;

Where GetDriveType and CreateFile are win32 methods with following signatures

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern DRIVE GetDriveType(string drive);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern IntPtr CreateFile(
      string lpFileName,
      uint dwDesiredAccess,
      uint dwShareMode,
      IntPtr SecurityAttributes,
      uint dwCreationDisposition,
      uint dwFlagsAndAttributes,
      IntPtr hTemplateFile);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hHandle);

Also, we need some constants

internal enum DRIVE : byte {
   UNKNOWN = 0,

internal const uint GENERIC_READ = 0×80000000;
internal const uint FILE_SHARE_READ = 0×00000001;
internal const uint OPEN_EXISTING = 3;
internal const uint FILE_ATTRIBUTE_READONLY = 0×00000001;
internal const uint FILE_FLAG_SEQUENTIAL_SCAN = 0×08000000;

Now, when we have our cdrom handle in hands, we can read it’s Table Of Content. Now, thing become harder because of the fact, that we have to use very complicated platform method:

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeviceIoControl(
   [In] IntPtr hDevice,
   IOCTL dwIoControl,
   [In] IntPtr lpInBuffer,
   uint nInBufferSize,
   IntPtr lpOutBuffer,
   uint nOutBufferSize,
   out uint lpBytesReturned,
   IntPtr lpOverlapped);

When thing are generic it’s good, however this one is, probably, most generic method in Win32 API. You can do anything with this method and you never know what to expect in lpOutBuffer :)

However, as I told earlier, I invested three days in investigations and researches (tnx to DDK documentation team) and now things become to be clearer. We need to get CDROM_TOC. It done by invoking IOCTL_CDROM_READ_TOC call

uint bytesRead = 0;
TOC = new Native.CDROM_TOC();
TOC.Length = (ushort)Marshal.SizeOf(TOC);
var hTOC = Marshal.AllocHGlobal(TOC.Length);
Marshal.StructureToPtr(TOC, hTOC, true);
if (Native.DeviceIoControl(this.handle, Native.IOCTL.CDROM_READ_TOC, IntPtr.Zero, 0, hTOC, TOC.Length, out bytesRead, IntPtr.Zero)) Marshal.PtrToStructure(hTOC, TOC);

But, not too fast. CDROM_TOC contains array of TRACK_DATA with unknown size.

typedef struct _CDROM_TOC {
  UCHAR  Length[2];
  UCHAR  FirstTrack;
  UCHAR  LastTrack;
typedef struct _TRACK_DATA {
  UCHAR  Reserved;
  UCHAR  Control : 4;
  UCHAR  Adr : 4;
  UCHAR  TrackNumber;
  UCHAR  Reserved1;
  UCHAR  Address[4];

P/Invoke it! But how to marshal unknown array? We should create wrapper object. Also there is very fun BitVector, used in this structure! What’s the problem? Pin it with some Math!

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

public class CDROM_TOC {

   public ushort Length;

   public byte FirstTrack;

   public byte LastTrack;

   public TRACK_DATA_ARRAY TrackData;


[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

public struct TRACK_DATA {

   public byte Reserved;

   public byte bitVector;

   public byte Control {

      get { return ((byte)((this.bitVector & 15u))); }

      set { this.bitVector = ((byte)((value | this.bitVector))); }


   public byte Adr {

      get { return ((byte)(((this.bitVector & 240u) / 16))); }

      set { this.bitVector = ((byte)(((value * 16) | this.bitVector))); }


   public byte TrackNumber;

   public byte Reserved1;

   public uint Address;


[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

internal sealed class TRACK_DATA_ARRAY {

   internal TRACK_DATA_ARRAY() { data = new byte[MAXIMUM_NUMBER_TRACKS * Marshal.SizeOf(typeof(TRACK_DATA))]; }

   [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAXIMUM_NUMBER_TRACKS * 8)]

   private byte[] data;

   public TRACK_DATA this[int idx] {

      get {

         if ((idx < 0) | (idx >= MAXIMUM_NUMBER_TRACKS)) throw new IndexOutOfRangeException();

         TRACK_DATA res;

         var hData = GCHandle.Alloc(data, GCHandleType.Pinned);

         try {

            var buffer = hData.AddrOfPinnedObject();

            buffer = (IntPtr)(buffer.ToInt32() + (idx * Marshal.SizeOf(typeof(TRACK_DATA))));

            res = (TRACK_DATA)Marshal.PtrToStructure(buffer, typeof(TRACK_DATA));

         } finally {



         return res;




Fuf, done. The code is rather self explaining, we just “tell” marshaler, that we have byte array, while calculating pointers to pinned object to get actual value and marshal it back. So, now we have TOC. So, we know how many tracks we have and addresses to data chunks inside the CD.

But it now enough to understand where our tracks. CD-ROM structure is very tricky. There we have blocks or sectors (which is the smallest chunks of data), so we have to convert bytes into sector addresses. Each block is 2352 bytes in RAW mode, while address value inside TRACK_DATA points us to layout address with is sync, sector id, error detection etc… So, in order to convert TRACK object into actual track number on disk, we have to stick to following method

public static int SectorAddress(this TRACK_DATA data) {

   var addr = BitConverter.GetBytes(data.Address);

   return (addr[1] * 60 * 75 + addr[2] * 75 + addr[3]) – 150;


Now, when we know numbers of tracks, we also know start and end sector, disk type and other useful information we are ready to twist it a bit and read CD-Text (if there are and your CD reader supports it).

So, coming back to our favorite method DeviceIoControl, but this time with IOCTL_CDROM_READ_TOC_EX control.

bytesRead = 0;           
TOCex = new Native.CDROM_READ_TOC_EX {



var sTOCex = Marshal.SizeOf(TOCex);

var hTOCex = Marshal.AllocHGlobal(sTOCex);

Marshal.StructureToPtr(TOCex, hTOCex, true);

var Data = new Native.CDROM_TOC_CD_TEXT_DATA();

Data.Length = (ushort)Marshal.SizeOf(Data);

var hData = Marshal.AllocHGlobal(Data.Length);

Marshal.StructureToPtr(Data, hData, true);

if (Native.DeviceIoControl(this.handle, Native.IOCTL.CDROM_READ_TOC_EX, hTOCex, (ushort)sTOCex, hData, Data.Length, out bytesRead, IntPtr.Zero)) Marshal.PtrToStructure(hData, Data);



Looks too simple? Let’s see inside CDROM_READ_TOC_EX structure. It is very similar to _CDROM_TOC.

typedef struct _CDROM_READ_TOC_EX {
  UCHAR Format : 4;
  UCHAR Reserved1 : 3;
  UCHAR Msf : 1;
  UCHAR SessionTrack;
  UCHAR Reserved2;
  UCHAR Reserved3;

Simple. Isn’t it?

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

public struct CDROM_READ_TOC_EX {

   public uint bitVector;

   public CDROM_READ_TOC_EX_FORMAT Format {

      get { return ((CDROM_READ_TOC_EX_FORMAT)((this.bitVector & 15u))); }

      set { this.bitVector = (uint)((byte)value | this.bitVector); }


   public uint Reserved1 {

      get { return ((uint)(((this.bitVector & 112u) / 16))); }

      set { this.bitVector = ((uint)(((value * 16) | this.bitVector))); }


   public uint Msf {

      get { return ((uint)(((this.bitVector & 128u) / 128))); }

      set { this.bitVector = ((uint)(((value * 128) | this.bitVector))); }


   public byte SessionTrack;

   public byte Reserved2;

   public byte Reserved3;


But what will come inside lpOutBuffer? Fellow structure, named CDROM_TOC_CD_TEXT_DATA with unknown size array of CDROM_TOC_CD_TEXT_DATA_BLOCK

typedef struct _CDROM_TOC_CD_TEXT_DATA {
  UCHAR  Length[2];
  UCHAR  Reserved1;
  UCHAR  Reserved2;
typedef struct _CDROM_TOC_CD_TEXT_DATA_BLOCK {
  UCHAR  PackType;
  UCHAR  TrackNumber:7;
  UCHAR  ExtensionFlag:1;
  UCHAR  SequenceNumber;
  UCHAR  CharacterPosition:4;
  UCHAR  BlockNumber:3;
  UCHAR  Unicode:1;
  union {
    UCHAR  Text[12];
    WCHAR  WText[6];
  UCHAR  CRC[2];

Too bad to be true. Isn’t it? Let’s try to marshal it my hands (with the trick used for TRACK_DATA

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

public class CDROM_TOC_CD_TEXT_DATA {

   public ushort Length;

   public byte Reserved1;

   public byte Reserved2;



[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

internal sealed class CDROM_TOC_CD_TEXT_DATA_BLOCK_ARRAY {


   [MarshalAs(UnmanagedType.ByValArray, SizeConst = MINIMUM_CDROM_READ_TOC_EX_SIZE * MAXIMUM_NUMBER_TRACKS * 18)]

   private byte[] data;

   public CDROM_TOC_CD_TEXT_DATA_BLOCK this[int idx] {

      get {

         if ((idx < 0) | (idx >= MINIMUM_CDROM_READ_TOC_EX_SIZE * MAXIMUM_NUMBER_TRACKS)) throw new IndexOutOfRangeException();


         var hData = GCHandle.Alloc(data, GCHandleType.Pinned);

         try {

            var buffer = hData.AddrOfPinnedObject();

            buffer = (IntPtr)(buffer.ToInt32() + (idx * Marshal.SizeOf(typeof(CDROM_TOC_CD_TEXT_DATA_BLOCK))));

            res = (CDROM_TOC_CD_TEXT_DATA_BLOCK)Marshal.PtrToStructure(buffer, typeof(CDROM_TOC_CD_TEXT_DATA_BLOCK));

         } finally {



         return res;




[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]


   public CDROM_CD_TEXT_PACK PackType;

   public byte bitVector1;

   public byte TrackNumber {

      get { return ((byte)((this.bitVector1 & 127u))); }

      set { this.bitVector1 = ((byte)((value | this.bitVector1))); }


   public byte ExtensionFlag {

      get { return ((byte)(((this.bitVector1 & 128u) / 128))); }

      set { this.bitVector1 = ((byte)(((value * 128) | this.bitVector1))); }


   public byte SequenceNumber;

   public byte bitVector2;        

   public byte CharacterPosition {

      get { return ((byte)((this.bitVector2 & 15u))); }

      set { this.bitVector2 = ((byte)((value | this.bitVector2))); }


   public byte BlockNumber {

      get { return ((byte)(((this.bitVector2 & 112u) / 16))); }

      set { this.bitVector2 = ((byte)(((value * 16) | this.bitVector2))); }


   public byte Unicode {

      get { return ((byte)(((this.bitVector2 & 128u) / 128))); }

      set { this.bitVector2 = ((byte)(((value * 128) | this.bitVector2))); }


   [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12, ArraySubType = UnmanagedType.I1)]

   public byte[] TextBuffer;

   public string Text {

      get { return (Unicode == 1) ? ASCIIEncoding.ASCII.GetString(TextBuffer) : UTF32Encoding.UTF8.GetString(TextBuffer); }


   public ushort CRC;


Can’t you see a small problem here? Yes, we do not know the actual/maximum size of CDROM_TOC_CD_TEXT_DATA_BLOCK array. Until, I’ll find a nice way to marshal smart pointers, we’ll stick to MAX_TRACKS (100) * MIN_DATA_BLOCK (2).

We almost finished and the worst things are behind us. Now you should enumerate thru CDROM_TOC_CD_TEXT_DATA_BLOCK and look for Text, TrackNumber and SequenceNumber (which is continuation of text reported). For example, slot for ALBUM_NAME “Satisfaction” will looks as following

BlockNumber 0×00
CharacterPosition 0×00
SequenceNumber 0×00
BlockNumber 0×00
CharacterPosition 0x0B
CRC 0×0564
SequenceNumber 0×01
Text N


And so on… What to do with all other data and how to use it to enhance listening (ripping/crunching/seeking) experience we’ll speak next time. Have a good day and be good people.

Be Sociable, Share!

5 Responses to “Audio CD operation including CD-Text reading in pure C#”

  1. krzysiek (Poland) Says:

    This is what I’m looking for, thanks

  2. Little Q Says:

    Does anyone knows how to extract ISRC codes from (normal) CD’s? My best guess it’s like the methode discribed above but I can’t seem to find any good documentation about it.

    Btw. nice article,

    Regards Little Q

  3. Jerry Evans Says:

    2 ways:

    1. (Relatively common) Get the ISRC code from the CD-TEXT block in the lead-in header. See MMC3 spec for READ TOC/PMA/ATIP (command 0×43)

    2. Never seen this variety. ISRC is encoded into subchannels in audio data.

    Good luck!

  4. Lorenzo Says:

    Hi Tamir, is fantastic!!!

    Please, you can make a library .DLL for c# (.net)??

    Thank you
    uelfox from Italy

  5. Peter T Says:

    I second that! Now that the CD Text Reader site is dead, and it doesn’t work in Windows 7 64-bit I can’t find a way of getting hold of CD information any more. And I’d prefer not to have to learn yet another programming language at my age!

Leave a Reply





WPF Disciples
Code Project