Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# Windows Media Format SDK Translation

0.00/5 (No votes)
9 May 2004 9  
In this article, I describe a translation of most of the WMF SDK interfaces, data structures, constants, and functions into C#.

Introduction

In my previous article (Windows Media Audio Compressor), I've showed you how to create a Windows Media compressor. I used managed C++ to interact with the Windows Media Format (WMF) SDK, which exposes only unmanaged COM interfaces. While this could be an acceptable solution, it is a little complicated to implement, specially if you need more than simple interaction with the WMF objects, data and functions. In this article, I describe a translation of most of the WMF SDK interfaces, data structures, constants and functions into C#. Note: Digital Rights Management (DRM) support is not included in this translation.

While there are some classes that can be used without deep knowledge of the WMF SDK, the code accompanying this article assumes that you are already familiar with the WMF SDK. Also, a good knowledge of COM Interop and Interop Marshaling may be needed to use some of the translated interfaces and structures.

Some words about the translation

Somebody has asked me about ideas of how to translate the IWMSyncReader interface to use it for concatenating two WMA files. I told him that one idea could be to create an IDL file that publishes the required interfaces and compile it as a Type Library, and then use TLBIMP or just add it as a reference using the VS.NET IDE. This method could be appropriate for using many COM related objects and interfaces, but in the case of the WMF, there are many structures that are not automation compatible, as well as use of C-style arrays instead of SAFEARRAY, etc.

When you try to import such a Type Library, you will have many types and functions that not represent, in some cases, the concepts of the managed definition. Some types and functions can be useless or require too much marshaling effort to use it. In such situations, the .NET Framework documentation recommends that after using TLBIMP, you should use ILDASM to obtain a file in the intermediate language, which you can modify and recompile to obtain the desired results. However, in the case of the WMF SDK, we are talking about more than 50 interfaces.

I think that the best solution to use the WMF SDK is to declare in managed code all the definitions needed, and that was what I did. There is another solution, tough: wait for Microsoft to release their managed version of the SDK.

In this translation, I tried to have as much as possible a managed vision for all definitions and function prototypes in a managed vision, avoiding the use of pointers. However, sometimes this was just not possible. For example, the IDL definition of the method IWMHeaderInfo3.GetAttributeIndices is the following:

...
interface IWMHeaderInfo3 : IWMHeaderInfo2
{
...
 HRESULT GetAttributeIndices( [in] WORD wStreamNum,
                              [in] LPCWSTR pwszName,
                              [in] WORD *pwLangIndex,
                              [out, size_is( *pwCount )] WORD *pwIndices,
                              [in, out] WORD *pwCount );
...
};

I translated it as follows:

...
interface IWMHeaderInfo3 : IWMHeaderInfo2
{
...
 void GetAttributeIndices( [In] ushort wStreamNum,
                           [In, MarshalAs(UnmanagedType.LPWStr)] string pwszName,
                           IntPtr pwLangIndex,
                           [Out, MarshalAs(UnmanagedType.LPArray)]ushort[] pwIndices,
                           [In, Out] ref ushort pwCount );
...

};

Here, it may be preferable to translate the parameter pwLangIndex as ref ushort pwLangIndex. I used IntPtr because this parameter (the pointer value) can be NULL, and if we use the ref keyword, there is no way to pass null references.

Whenever you want to call this method and need to pass a null value, simply pass IntPtr.Zero, if you need a reference to a value then you must use a combination of GCHandle.Alloc and GCHandle.AddressOfPinnedObject. If you use these translated interfaces, you will need to do manual marshaling in many cases, so the wiser option is to write helper classes that wrap this "low level" work. That's why I wrote some helper classes to wrap INSSBuffer, IWMProfile, IWMHeaderInfo and IWMStreamConfig interfaces. In the implementation, you can find examples of how to do manual marshaling when needed.

Another important thing about translation is that in the WMF SDK, errors are handled through HRESULT. In the translated version, a COMException is thrown any time there is an error. Sometimes, we need to know the value of the returned HRESULT. In that case, you must handle the COMException and check for its ErrorCode property. The following code, which is extracted from the Read method of the WmaStream class, demonstrates how to do it:

try 
{ 
  m_Reader.GetNextSample(m_OuputStream, out sample, out SampleTime, 
                         out Duration, out Flags, out m_OutputNumber, 
                         out m_OuputStream); 
} 
catch (COMException e) 
{ 
  if (e.ErrorCode == WM.NS_E_NO_MORE_SAMPLES) 
  { 
    // No more samples, the stream end have been reached, there is not an error

    // Code to handle the end of the stream.

  } 
  else 
  {
    throw (e); //Re-throw, an error that must be catch in an upper level.

  } 
}

Caution: There may be some errors in this translation, I didn't test all the interfaces and functions. If you plan to use this library for any serious work, you must check carefully every translated piece of code against the WMF SDK headers and documentation.

Using the code

I didn't include any demo project in the downloads, instead I'll comment here some simple examples of using the code. As part of the translation, I included a class named WmaStream that allows reading the audio stream of any ASF file and get the uncompressed PCM data out of it. As a complement, there is a class named WmaWriter that allows you to create Windows Media Audio Files from PCM audio data, this class is very similar to the WmaWriter class described in my article Windows Media Audio Compressor, but this time it is implemented in C#. You can compare both versions to get an idea of how easier is the implementation when a managed version of WMF SDK is available.

WmaStream class:

This class is derived from System.IO.Stream so it can be used as any other read-only stream. The following code shows a simple way to copy the audio content of any file that can be read by the WMF objects (*.mp3, *.wma, *.mpe, *.asf, *.wmv, etc.) to a WAV file:

using System; 
using System.IO;
using Yeti.MMedia; 
using Yeti.WMFSdk; 
using WaveLib;
...
using (WmaStream str = new WmaStream("Somefile.wma")) 
{
  byte[] buffer = new byte[str.SampleSize*2];
  AudioWriter writer = new WaveWriter(new FileStream("Somefile.wav",
                                                      FileMode.Create),
                                      str.Format);
  try 
  { 
    int read; 
    while ( (read = str.Read(buffer, 0, buffer.Length)) > 0) 
    {
      writer.Write(buffer, 0, read); 
    } 
  }
  finally 
  { 
    writer.Close(); 
  } 
} //str.Close() is automatically called by Dispose.

WaveWriter is a BinaryWriter that creates a WAV file (or stream) from PCM data received through its Write method. The property WmaStream.Format returns a WaveFormat class describing the PCM format of the data read through its Read method. This format must be the same as the WAV file format, that's why it is passed as parameter in the WaveWriter constructor.

As another example, you can download the source code of the article A low-level Audio Player in C# by Ianier Munoz and do the following changes:

Add a reference to this library to the cswavplay project. In the file MainForm.cs, look for:

...
[STAThread] 
static void Main()

and change it by:

...
//MTA threading is needed is WMF object are planed to be used in 

//multithreaded APPs

[MTAThread] 
static void Main()

In the same file, look for the method OpenFile() and change it as follows:

private void OpenFile() 
{ 
  //Add this line to open the files that can be processed by the 

  //WMF sync reader object

  OpenDlg.Filter = "Windows Media Files (*.mpe,*.wma, *.asf, *.wmv, *.mp3)
  |*.mpe; *.wma;*.asf;*.wmv;*.mp3|All files (*.*)|*.*"; 
  if (OpenDlg.ShowDialog() == DialogResult.OK) 
  { 
    CloseFile(); 
    try
    { 
      //WaveLib.WaveStream S = new WaveLib.WaveStream(OpenDlg.FileName);

      //Change the previous line by this one:

      Yeti.WMFSdk.WmaStream S = new Yeti.WMFSdk.WmaStream(OpenDlg.FileName);
//The rest of the code remains the same

With those few changes, you will have a player capable of playing any ASF file and also MP3 files.

One of the lacks of the WmaStream at this moment is that it doesn't allow reading the compressed data from the ASF file. This could be useful when copying streams without recompression, merging files, etc. Anyway, extending the WmaStream to include this feature is easy.

WmaWriter class:

More details about this class can be found in my article Windows Media Audio Compressor. It has almost the same interface, the difference is that now it is implemented with the real WMF interfaces and that I added the possibility to define metadata that will be included in the final ASF stream.

The following code demonstrates how to extract the audio information from a Windows Media Video (WMV) file and write it to a Windows Media Audio file using the WmaStream and WmaWriter classes:

using System; 
using System.IO;
using Yeti.MMedia; 
using Yeti.WMFSdk; 
using WaveLib;
...
using (WmaStream str = new WmaStream("Somefile.wmv")) 
{ 
  byte[] buffer = new byte[str.SampleSize*2]; 
  //Extract the WMF profile from the original file

  WMProfile profile = new WMProfile(str.Profile);
  while (profile.StreamCount > 1)//Remove any non-audio stream from the profile

  { 
    WMStreamConfig config = new WMStreamConfig(profile.GetStream(0)); 
    if (config.StreamType == MediaTypes.WMMEDIATYPE_Audio) 
    { 
      profile.RemoveStream(profile.GetStream(1)); 
    }
    else 
    { 
      profile.RemoveStream(config.StreamConfig); 
    } 
  }
  System.Collections.ArrayList List = new System.Collections.ArrayList(); 
  //Check if the original file had a title

  string AttrValue = str[WM.g_wszWMTitle];
  if (AttrValue != null) 
  { //The title will be added to the destination file

    WM_Attr Attr = new WM_Attr(WM.g_wszWMTitle,
                               WMT_ATTR_DATATYPE.WMT_TYPE_STRING,
                               string.Copy(AttrValue)); 
    List.Add(Attr);
  }
  //Check by the author metadata

  AttrValue = str[WM.g_wszWMAuthor];
  if (AttrValue != null) 
  { //The actor will be added to the destination file

    WM_Attr Attr = new WM_Attr(WM.g_wszWMAuthor,
                               WMT_ATTR_DATATYPE.WMT_TYPE_STRING,
                               string.Copy(AttrValue));
    List.Add(Attr);
  }
  AudioWriter writer = new WmaWriter(new FileStream("SomeFile.wma",
                                                    FileMode.Create),
                                     str.Format,
                                     profile.Profile,
                                    (WM_Attr[])List.ToArray(typeof(WM_Attr)));
  try 
  {
    int read;
    while ( (read = str.Read(buffer, 0, buffer.Length)) > 0)
    {
      writer.Write(buffer, 0, read);
    }
  }
  finally
  { 
    writer.Close();
   }
}

In the previous code, I used the helper classes WMProfile and WMStreamConfig. Without them, I would have to write much more code. The WM_Attr structure is a helper structure to define metadata information in an easier way. An array of WM_Attr is passed to the WmaWriter constructor to define the metadata information that will be present in the resulting file.

The code str[WM.g_wszWMTitle] wraps the IWMHeaderInfo.GetAttributeByName functionality and returns a string representing the desired metadata attribute whose name is passed as the index value (in this case, the constant WM.g_wszWMTitle). The previous code was shown as a demonstration only, the preferred way to do the same thing is using the functionality of the WMF reader and writer objects, which allow you to read and write, respectively, compressed data samples.

There can be many examples of using the WMF. Here, I just showed some simple cases. You can find more information by looking at the implementation of the helper classes and to the comments included in the code. I didn't comment any of the WMF interfaces, structures, enumerations and functions; you can find that information in the WMF SDK documentation.

Conclusion

Here is an easy solution to use the WMF SDK in managed world (I've already done the dirty work). However, there are two main points you should take into account: forward compatibility and performance.

Regarding compatibility, I don't know what are the Microsoft plans about a managed version of the WMF SDK, but I guess that when they provide one, it will have many differences with the current WMF SDK.

The other problem is performance: this is not a managed version of WMF SDK, but a translation, so any time you use a method of any WMF interface, there is a COM interop operation involved. The performance penalty could be serious for real-time applications or when handling streams with video content. It is up to you to decide whether this solution suits your needs.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here