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)
{
}
else
{
throw (e);
}
}
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.
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();
}
}
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:
...
[MTAThread]
static void Main()
In the same file, look for the method OpenFile()
and change it as follows:
private void OpenFile()
{
OpenDlg.Filter = "Windows Media Files (*.mpe,*.wma, *.asf, *.wmv, *.mp3)
|*.mpe; *.wma;*.asf;*.wmv;*.mp3|All files (*.*)|*.*";
if (OpenDlg.ShowDialog() == DialogResult.OK)
{
CloseFile();
try
{
Yeti.WMFSdk.WmaStream S = new Yeti.WMFSdk.WmaStream(OpenDlg.FileName);
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];
WMProfile profile = new WMProfile(str.Profile);
while (profile.StreamCount > 1)
{
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();
string AttrValue = str[WM.g_wszWMTitle];
if (AttrValue != null)
{
WM_Attr Attr = new WM_Attr(WM.g_wszWMTitle,
WMT_ATTR_DATATYPE.WMT_TYPE_STRING,
string.Copy(AttrValue));
List.Add(Attr);
}
AttrValue = str[WM.g_wszWMAuthor];
if (AttrValue != null)
{
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.