technical training sessions in my car. The original rich-media recordings were created with Microsoft® Producer for PowerPoint®, and they combine audio, video, and slides in their content. I used Microsoft Windows Media® Player to save the audio portion from the Windows Media Audio/Video files to audio CD. I found that, just as it is possible to read written material even when the letters have been jumbled (so long as the first and last letter of each word stays put), a surprising amount of value remained despite the loss of the visual component. But, due to their length, not all of the original recordings readily translated to audio CD. In the course of solving that problem, I came up with a general-purpose method of converting and sizing files for audio CD, which I'll explore next.
Back to the top
The Length Problem Takes Many Forms
Windows Media Player can open digital media files in a multitude of formats including WMV (Windows Media Audio/Video), WMA (Windows Media Audio), and WAV (uncompressed Pulse Code Modulated format). Any playlist of one or more digital media files can be saved to a CD within the Windows Media Player UI so long as they will fit onto the CD. If they will not fit, then perhaps the files in the playlist can be separated and put on more than one CD. On rare occasions you may have a single file that is longer than an 80-minute CD, and on these occasions you must turn to one of the Windows Media SDKs (Software Development Kits).
The Windows Media Encoder SDK can be used to split an over-long file into files of any length. But once we decide to use the SDK, are there any other issues we can address with the SDK while we're at it? If we're working with existing playlists that were not designed for CD, then the length problem might take many forms.
The technical training recordings that I worked with had various arrangements. Some consisted of a single 90-minute filea problem we've covered. Others would switch between slides and video at unpredictable places so that the component files would vary greatly in length; some were about a minute in length, and some as much as forty or more minutes. Given that in-car CD players have limited transport control, neither of these is an ideal track length. Also, even if you can separate an overly long playlist onto multiple CDs, the separation point is largely dictated by the track lengths and may not be to your taste.
Ideally, then, it should be possible to convert and consolidate the files in the original playlist into one audio file, slice that file into multiple appropriately-sized chunks (great for track skipping), and arrange those tracks into CD-length collections ready for burning. We'll look at these stages in turnfirst converting to audio and then sizing appropriately for CD.
Back to the top
Converting to Audio
This article is about using the Windows Media 9 Series SDK, and that means writing software. The purpose of the code demonstrated in the article is to convert digital media (whether audio, video, or both) to pure audio of the correct length to be conveniently burned to audio CD-recordables. The code is not itself a complete working project, but it does provide a full set of methods to do the conversion and sizing, so all you need to do is plug the methods into your WinForms or (with minor tweaks) Console application.
The C# functions listed below are assumed to be part of a Form and their containing project must have a reference to the interop assembly Interop.WMEncoderLib.dll, which is part of the Windows Media Encoder SDK. To use this code, you must install both the Windows Media Encoder SDK and Windows Media Encoder.
The GenerateAudioFromVideoInFolder function's playlistFilePath parameter specifies the path and file name of a Windows Media Audio/Video playlist file (.asx), which in turn lists the source media files. The targetDirectoryPath parameter specifies the path of a directory in which the output audio file (.wma) is to be created. This parameter is marked ref because we will append the first media file's Title to it. Finally, the targetFileName parameter names the output audio file (typically the name of an intermediate file which can be subsequently deleted).
private void GenerateAudioFromVideoInFolder(string playlistFilePath,
ref string targetDirectoryPath,
string targetFileName)
{
try
{
Encoder = new WMEncoder();
IWMEncProfileCollection ProColl = Encoder.ProfileCollection;
IWMEncProfile Pro = null;
for (int i = 0; i < ProColl.Count; ++i)
{
if (ProColl.Item(i).Name ==
"Windows Media Audio 8 for Dial-up Modem (CD quality, 64 Kbps)")
{
Pro = ProColl.Item(i);
break;
}
}
if (Pro == null)
return;
string playlistDirectoryPath = string.Empty;
if (playlistFilePath.LastIndexOf(@"\") != -1)
playlistDirectoryPath = playlistFilePath.Substring(0,
playlistFilePath.LastIndexOf(@"\"));
if (playlistFilePath.LastIndexOf(@"/") != -1)
playlistDirectoryPath = playlistFilePath.Substring(0,
playlistFilePath.LastIndexOf(@"/"));
XmlDocument playlist = new XmlDocument();
playlist.Load(playlistFilePath);
XmlNodeList videos = playlist.SelectNodes("//REF");
IWMEncSourceGroupCollection SrcGrpColl =
Encoder.SourceGroupCollection;
for (int i=0; i<videos.Count; ++i)
{
string videoName = videos[i].Attributes["HREF"].InnerText;
IWMEncSourceGroup2 SrcGrp =
SrcGrpColl.Add(videoName) as IWMEncSourceGroup2;
SrcGrp.set_Profile(Pro);
IWMEncSource SrcAud =
SrcGrp.AddSource(WMENC_SOURCE_TYPE.WMENC_AUDIO);
SrcAud.SetInput(playlistDirectoryPath +
@"\" + videoName, "", "");
if (i+1 < videos.Count)
SrcGrp.SetAutoRollover(-1,
videos[i+1].Attributes["HREF"].InnerText);
}
IWMMetadataEditor metadataEditor = null;
if (EditorOpenFile(playlistDirectoryPath + @"\" +
videos[0].Attributes["HREF"].InnerText, out metadataEditor))
{
IWMHeaderInfo3 headerInfo = metadataEditor as IWMHeaderInfo3;
IWMEncDisplayInfo displayInfo = Encoder.DisplayInfo;
displayInfo.Author = GetAttribute(headerInfo, "Author");
displayInfo.Copyright = GetAttribute(headerInfo, "Copyright");
displayInfo.Description = GetAttribute(headerInfo,
"Description");
displayInfo.Rating = GetAttribute(headerInfo, "Rating");
displayInfo.Title = GetAttribute(headerInfo, "Title");
targetDirectoryPath += " - " + GetAttribute(headerInfo, "Title",
true);
Directory.CreateDirectory(targetDirectoryPath);
metadataEditor.Close();
}
IWMEncFile2 File = (IWMEncFile2)Encoder.File;
File.LocalFileName = targetDirectoryPath + @"\" + targetFileName;
Encoder.OnStateChange +=
new _IWMEncoderEvents_OnStateChangeEventHandler(OnStateChange);
Encoder.Start();
convertEvent.WaitOne();
}
catch (Exception ex)
{
traceListener.WriteLine(ex.Message);
}
}
private void OnStateChange( WMENC_ENCODER_STATE enumState )
{
switch( enumState )
{
case WMENC_ENCODER_STATE.WMENC_ENCODER_STOPPED:
this.Cursor = Cursors.Default;
convertEvent.Set();
Application.DoEvents();
break;
case WMENC_ENCODER_STATE.WMENC_ENCODER_STARTING:
this.Cursor = Cursors.WaitCursor;
Application.DoEvents();
break;
}
}
bool EditorOpenFile(string fileName, out IWMMetadataEditor metadataEditor)
{
metadataEditor = null;
try
{
WMFSDKFunctions.WMCreateEditor(out metadataEditor);
metadataEditor.Open(fileName) ;
}
catch(System.Runtime.InteropServices.COMException ex)
{
traceListener.WriteLine(ex.Message);
return false;
}
return true;
}
private string GetAttribute(IWMHeaderInfo3 headerInfo,
string attributeName)
{
return GetAttribute(headerInfo, attributeName, false);
}
private string GetAttribute(IWMHeaderInfo3 headerInfo,
string attributeName, bool asDirectoryName)
{
ushort streamNum = 0;
WMT_ATTR_DATATYPE dataType = WMT_ATTR_DATATYPE.WMT_TYPE_BINARY;
byte[] Array = null;
ushort ArrayLength = 0;
headerInfo.GetAttributeByName(ref streamNum, attributeName,
out dataType, Array, ref ArrayLength);
Array = new Byte[ArrayLength];
headerInfo.GetAttributeByName(ref streamNum, attributeName,
out dataType, Array, ref ArrayLength);
return ByteArrayToString(Array, asDirectoryName);
}
private string ByteArrayToString(byte[] Array, bool asDirectoryName)
{
char[] disallowed = new Char[]{'\\','/',':','*','?'};
StringBuilder returnString = new StringBuilder();
for ( int i = 0; i < Array.Length - 2; i += 2 )
{
char ch = BitConverter.ToChar( Array, i );
if (!asDirectoryName ||
(( new string( ch, 1 ) ).IndexOfAny( disallowed ) == -1 &&
( new string( ch, 1 ) ).IndexOfAny( Path.InvalidPathChars ) ==
-1 ))
returnString.Append( BitConverter.ToChar( Array, i ) );
}
return returnString.ToString();
}
Note that the encoding profile (represented by the IWMEncProfile interface) is set to Windows Media Audio 8 for Dial-up Modem (CD quality, 64 Kbps) because we want to encode for audio CD. The playlist (.asx) file's directory path is found by taking everything up to the last slash character. The playlist will reference digital media files in its own directory, so we locate each file in the playlist by appending its name to the playlist's own directory. The playlist file is an XML file, which is loaded into an instance of the XmlDocument class and its <ref> elements are iterated over to determine the media files referenced in the href attribute values.
Each referenced media file is placed into a source group (represented by the IWMEncSourceGroup2 interface) in the Encoder object's source group collection. (obtained as an IWMEncSourceGroupCollection interface through the Encoder.SourceGroupCollection property). The SetAutoRollover method is called on each source group so that when it is finished encoding, the next source group will be processed. This results in a single output file no matter how many media files are in the input playlist.
The EditorOpenFile function makes use of a C# source code file named WMFSDKFunction.cs, which is not listed here. The file is included in the managed samples as part of the Windows Media Format SDK, and it defines entry points to the unmanaged WMVCore.dll (the Windows Media PlayBack/Authoring DLL). The DLL is used in the function above to read the display information of the input media files, to write the information into the output files, and to decorate the target Directory Path with the Title.
The remainder of the code starts the encoding process, responds to the Stopped and Starting events by altering the cursor of the Form, and finally sets an Event to indicate to the main thread that the encoding has been completed.
Back to the top
Sizing Audio Files for CD
The code in the previous section combined multiple input files into a single WMA file, leaving the display information intact and enriching the target directory's name with the Title of the content (in case this wasn't known in advance). But the WMA file created so far may not fit onto a CD and is certainly not separated into tracks. The following function takes that intermediate file, splits it into tracks, and arranges the tracks into CD-length collections in separate directories. It also generates a playlist for each directory.
The ChunkAudioToCD function's targetDirectoryPath parameter specifies the path into which the track files are to be written. The intermediateFileName parameter specifies the name of the intermediate file produced in the previous step, and the deleteIntermediateFile controls whether this file is deleted when finished with. The cDLength and chunkLength parameters are enumerated constants which control the size in minutes of each track and each collection of tracks in a folder. Reasonable values for these parameters are five minutes, and the length of a CD-R, respectively.
private void ChunkAudioToCD(string targetDirectoryPath,
string intermediateFileName, CDLength cDLength,
ChunkLength chunkLength, bool deleteIntermediateFile)
{
int threeSeconds = 3000;
try
{
WMEncBasicEdit BasicEdit = new WMEncBasicEdit();
BasicEdit.OnStateChange +=
new _IWMEncBasicEditEvents_OnStateChangeEventHandler
(OnBasicEditStateChange);
BasicEdit.MediaFile = targetDirectoryPath + @"\" +
intermediateFileName;
BasicEdit.Index = true;
BasicEdit.MarkIn = 0;
int CDMarkIn = (int)cDLength;
int CDNum = 1;
XmlDocument playlist = null;
DirectoryInfo currentDir = null;
while (BasicEdit.MarkIn < BasicEdit.Duration)
{
if ((int)cDLength - CDMarkIn < (int)chunkLength)
{
if (BasicEdit.MarkIn != 0)
{
playlist.Save(currentDir.FullName + @"\" + "0Media.asx");
}
playlist = new XmlDocument();
InitializePlaylist(playlist);
currentDir = Directory.CreateDirectory(targetDirectoryPath +
@"\Disk " + (CDNum++).ToString());
CDMarkIn = 0;
}
BasicEdit.MarkOut = Math.Min(BasicEdit.MarkIn +
(int)chunkLength - 1, BasicEdit.Duration);
string outputFileName = string.Format("0MM{0:00}.wma",
BasicEdit.MarkIn / (int)chunkLength);
BasicEdit.OutputFile = currentDir.FullName + @"\" +
outputFileName;
BasicEdit.Start();
chunkEvent.WaitOne();
AppendPlaylistEntry(playlist, outputFileName);
BasicEdit.MarkIn = BasicEdit.MarkOut + 1;
// Each CD track transition takes an extra two seconds.
// Make it three to be safe.
CDMarkIn += (int)chunkLength + threeSeconds;
}
playlist.Save(currentDir.FullName + @"\" + "0Media.asx");
if (deleteIntermediateFile)
File.Delete(BasicEdit.MediaFile);
}
catch (Exception ex)
{
traceListener.WriteLine(ex.Message);
}
}
private void InitializePlaylist(XmlDocument playlist)
{
XmlElement root = playlist.CreateElement("ASX");
root.SetAttribute("version", "3.0");
playlist.AppendChild(root);
}
private void AppendPlaylistEntry(XmlDocument playlist,
string outputFileName)
{
XmlElement entry = playlist.CreateElement("ENTRY");
playlist.DocumentElement.AppendChild(entry);
XmlElement refE = playlist.CreateElement("REF");
refE.SetAttribute("HREF", outputFileName);
entry.AppendChild(refE);
}
private void OnBasicEditStateChange( WMENC_BASICEDIT_STATE enumState )
{
switch( enumState )
{
case WMENC_BASICEDIT_STATE.WMENC_BASICEDIT_STOPPED:
this.Cursor = Cursors.Default;
chunkEvent.Set();
Application.DoEvents();
break;
case WMENC_BASICEDIT_STATE.WMENC_BASICEDIT_RUNNING:
this.Cursor = Cursors.WaitCursor;
Application.DoEvents();
break;
}
}
internal enum CDLength
{
Mins74 = 4440000,
Mins80 = 4800000
}
internal enum ChunkLength
{
Mins1 = 60000,
Mins5 = 300000
}
The code above relies on the WMEncBasicEdit class, included in the Windows Media Encoder SDK, which provides services for post-processing digital media files. In this case, we want to set the start and end times (MarkIn and MarkOut) of the intermediate file to consecutive five-minute chunks and then copy that part of the file into a track. The resultant file structure consists of the target directory root under which are directories named after each session code and its name. Under these is one or more directories named Disk 1, Disk2, and so on, inside which are a playlist and its digital media filesenough to fit on a CD.
Again, the encoding process is asynchronous to the main thread, so it is necessary to respond to events that are raised during the encoding process. Thus we can be made aware that the encoding of a track has been completed and that we can move on to the next. The remainder of the code deals with the creation of a playlist file. This is a convenient way to guarantee that the tracks appear in the correct sequence when opened in Windows Media Player, and I'll describe the schema in the next section.
Back to the top
Creating a Playlist
A playlist (.asx) is necessary because naming your files in sequence will not guarantee that they will open in the correct order from the file system. Opening a playlist file causes the digital media files referenced with it to be opened in Windows Media Player in the order they are listed. Playlists are XML files, an example of which is shown here:
<ASX version = "3.0">
<ENTRY>
<REF HREF = "0MM0.wma"/>
</ENTRY>
</ASX>
Although it is possible to create and manipulate playlists using the Windows Media 9 Series SDK, in the managed world it is at least as easy to use the XmlDocument class. In the previous section, the InitializePlaylist function was called to initialize a newly constructed XmlDocument instance with a root element matching the pattern shown in the preceding XML example code. Next, after each track has been written, the AppendPlaylistEntry function is called to record its name in sequence in the playlist.
Back to the top
Personal Digital Audio Players (DAPs)
Encoding to the MP3 format is not within the scope of this article. But there is a growing list of Digital Audio Player devices that will play WMA (Windows Media Audio) files. Many gigabytes of the WMA files generated from the sample in this article can be reasonably quickly copied to such devices over USB and played directly. And even on portable devices, having sessions edited into five-minute tracks makes navigation more convenient.
To learn more about consumer electronic media devices that support Microsoft Windows Media 9 Series, visit the Windows Media Anywhere page at the Microsoft website.
Back to the top
Conclusion
Rich-media presentations consist of audio, video and slides, but the audio alone is often still rich enough to provide enormous value. When the audio is copied to audio CD or DAP, you can benefit from this value on the move, in or out of the car. The code presented here shows how to encode rich-media presentations into audio and then arrange them into collections of manageable-sized tracks ready for burning to CD, or copying to DAP.
Back to the top
For More Information
- You can download the Windows Media 9 Series SDK from the MSDN website.
- To learn more about Windows Media Player 9 Series, see Windows Media Player 9 Series Help. Windows Media Player 9 Series can be downloaded from the Windows Media Download Center page of the Microsoft website.
- To learn more about consumer electronics media devices that support Microsoft Windows Media 9 Series, visit the Windows Media Anywhere page of the Microsoft website.
- For general information about Windows Media technologies, go to the Windows Media page of the Microsoft website.
Back to the top
|