Creating a Custom Reader Class for Ogg Vorbis/FLAC Comments
In Dino Esposito’s “Cutting Edge” column in MSDN Magazine, he talks about the various readers (BinaryReader
, XmlReader
, etc.) available in .NET.
At the end of the column, he says a couple of things about how to design and implement a custom reader.
Here’s how I went about implementing a custom reader for reading Vorbis comments from FLAC files.
The first thing that we need to do is decide on what methods and properties the custom reader should support. We’re not tied to a specific interface, because Reader
is a .NET pattern, rather than a specified type.
As it’s a reader, we’ll probably want to construct it with a stream. Most of the other readers defined in .NET allow this, so it seems like a good idea.
public class VorbisCommentReader { public VorbisCommentReader(Stream stream) { } }
What about the rest of the methods and properties? Vorbis comments begin with a vendor ID, which is followed by a sequence of tag=value
comments.
It seems sensible, then, to add a VendorString
property to our class:
public string VendorString { get { return this.vendorString; } }
Interface Options
As for the tag=value
comments, we could just suck them up into a collection, and iterate over them with foreach
, like this:
VorbisCommentReader r = new VorbisCommentReader(stream); foreach (UserComment uc in r.UserComments) { string fieldName = uc.FieldName; string fieldValue = uc.FieldValue; }
That’s not bad. It requires that we load all the comments at once, unless we want to do something a little more complicated in our IEnumerator
implementation.
We could do this:
VorbisCommentReader r = new VorbisCommentReader(stream); for (int i = 0; i < r.ReadCommentCount(); ++i) { string fieldName = r.ReadFieldName(); string fieldValue = r.ReadFieldValue(); }
That’s not bad, but the user might accidentally reverse the two calls, which makes it more fragile than it could be.
For something like this, I’ve started opting for a DataReader
-style interface:
VorbisCommentReader r = new VorbisCommentReader(stream); while (r.Read()) { string fieldName = r.GetFieldName(); string fieldValue = r.GetFieldValue(); }
Implementing the constructor
The constructor will assume that the stream is correctly positioned at the start of the block. Because Stream
only defines some very basic reading methods (Read
, which reads an array of bytes; ReadByte
which reads a single byte), we’ll need to use a BinaryReader
internally:
public VorbisCommentReader(Stream stream) { this.binaryReader = new BinaryReader(stream); int vendorLength = binaryReader.ReadInt32(); // The next thing is a UTF8 string. byte[] vendorBytes = binaryReader.ReadBytes(vendorLength); this.vendorString = Encoding.UTF8.GetString(vendorBytes); this.userCommentListLength = binaryReader.ReadInt32(); this.userCommentListIndex = 0; }
We read the various fixed values from the comment block, and reset our list index.
Implementing Read
We can then implement the Read
method like this:
public bool Read() { if (userCommentListIndex < userCommentListLength) { int userCommentLength = binaryReader.ReadInt32(); byte[] userCommentBytes = binaryReader.ReadBytes(userCommentLength); string userCommentString = Encoding.UTF8.GetString(userCommentBytes); int pos = userCommentString.IndexOf('='); this.fieldName = userCommentString.Substring(0, pos); this.fieldValue = userCommentString.Substring(pos + 1); ++this.userCommentListIndex; return true; } return false; }
If we’re being nitpicky, this isn’t completely correct. Only the field value (after the =) is allowed to be UTF8, but since 7-bit field names are a subset of UTF8, we can probably get away with it.
More implementation
With this in place, the other methods are a doddle:
public string GetFieldName() { return this.fieldName; } public string GetFieldValue() { return this.fieldValue; }
Conclusion
Writing custom reader objects is easy, and it makes your code feel a bit more idiomatic; it feels like the other .NET readers.
I’ve also implemented a custom reader to go with this one that allows reading of the metadata blocks in a FLAC file. Source code for both will appear at some point.