#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using Nuclex.Support;
namespace Nuclex.Audio.Metadata {
/// Decodes an XMCD database file into the matching structure
internal class XmcdDecoder {
/// Whitespace characters
private static readonly char[] Whitespaces = { ' ', '\t' };
/// Numeric characters
private static readonly char[] Numbers = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
#region enum State
/// States the XMCD decoder can be in
private enum State {
/// Expecting the XMCD file id
ExpectingFileId,
///
/// Decoder is in the comments section and expects optional properties
///
ExpectingCommentProperties,
///
/// Decoder has encountered the "Track frame offsets" comment and is
/// expecting track offsets or their termination by another property
///
ExpectingCommentTrackOffsets,
///
/// Decoder has left the comments section and expects keywords
///
ExpectingKeywords
}
#endregion
/// Initializes a new XMCD database file decoder
public XmcdDecoder() {
this.state = State.ExpectingFileId;
this.trackOffsets = new List();
this.trackTitles = new List();
}
/// Processes a line from the XMCD database file
/// Line from the database file to process
public void ProcessLine(string line) {
switch(this.state) {
case State.ExpectingFileId: {
parseFileId(line);
break;
}
case State.ExpectingCommentProperties: {
parseCommentProperty(line);
break;
}
case State.ExpectingCommentTrackOffsets: {
parseCommentTrackOffset(line);
break;
}
case State.ExpectingKeywords: {
parseKeyword(line);
break;
}
}
}
/// Returns the parsed data as a XMCD database entry structure
/// The parsed XMCD database entry structure
public Cddb.DatabaseEntry ToDatabaseEntry() {
// The XMCD file specification says that all these values have to be provided
// in a valid XMCD file.
bool isValid =
this.haveDiscId &&
this.haveTitle &&
this.haveYear &&
this.haveGenre &&
this.havePlayOrder &&
this.haveExtendedData;
if(!isValid) {
throw new FormatException("XMCD file is missing required fields");
}
// Take over the track and track offset lists
this.entry.Tracks = this.trackTitles.ToArray();
this.entry.TrackFrameOffsets = this.trackOffsets.ToArray();
// When we extracted the tracks, we had to leave the artist field empty for
// any tracks without an explicit artist specification. Now that we have
// the disc title, we can fill in this field for those tracks.
for(int index = 0; index < this.entry.Tracks.Length; ++index) {
if(this.entry.Tracks[index].Artist == null) {
this.entry.Tracks[index].Artist = this.entry.Artist;
}
}
// Okay, the entry is filled, time to hand it over to our user
return this.entry;
}
/// Parses the file id of an XMCD database file
/// Line containing the XMCD database file id
private void parseFileId(string line) {
bool hasFileId = line.StartsWith("# xmcd");
if(!hasFileId) {
throw new FormatException("Not an XMCD database file");
}
this.state = State.ExpectingCommentProperties;
}
/// Parses a comment line possibly containing a property
/// Line containing a comment and possible a property
private void parseCommentProperty(string line) {
// If this line doesn't start with a comment sign, we must have reached the end
// of the comment section and should switch to normal keyword processing.
if(!line.StartsWith("#")) {
this.state = State.ExpectingKeywords;
parseKeyword(line);
return;
}
// Locate any characters in this line
int propertyStartIndex = -1;
bool hasPropertyStart =
(line.Length > 1) &&
((propertyStartIndex = StringHelper.IndexNotOfAny(line, Whitespaces, 1)) != -1);
if(!hasPropertyStart) {
return; // blank line or other comment
}
// If there were characters on the line, we assume them to contain a property
// that is terminated by a ':'. Look for a ':' character.
int propertyEndIndex = -1;
bool hasPropertyEnd =
(line.Length > propertyStartIndex + 1) &&
((propertyEndIndex = line.IndexOf(':', propertyStartIndex + 1)) != -1);
if(!hasPropertyEnd) {
return; // other comment
}
// We found a one or more characters in the comment line, terminated by
// a ':' character, so this seems to be a property. Extract the individual
// elements and process the property assignment
string name = line.Substring(
propertyStartIndex, propertyEndIndex - propertyStartIndex
);
int valueStart = StringHelper.IndexNotOfAny(
line, Whitespaces, propertyEndIndex + 1
);
if(valueStart == -1) {
interpretCommentProperty(name, line.Substring(propertyEndIndex + 1));
} else {
interpretCommentProperty(name, line.Substring(valueStart));
}
}
///
/// Analyzes a property that appeared in the comments section and adds its
/// information to the database entry
///
/// Name of the encountered property
/// Value that has been assigned to the property
private void interpretCommentProperty(string name, string value) {
if(name == "Track frame offsets") {
this.state = State.ExpectingCommentTrackOffsets;
} else if(name == "Disc length") {
int endIndex = StringHelper.IndexNotOfAny(value, Numbers);
if(endIndex == -1) {
this.entry.DiscLengthSeconds = Convert.ToInt32(value);
} else {
this.entry.DiscLengthSeconds = Convert.ToInt32(value.Substring(0, endIndex));
}
} else if(name == "Revision") {
this.entry.Revision = Convert.ToInt32(value);
} else if(name == "Submitted via") {
this.entry.Submitter = value;
}
}
/// Parses a comment line possibly containing track offset
/// Line containing a comment and possible a track offset
private void parseCommentTrackOffset(string line) {
// If this line doesn't begin with a comment sign, we must have reached
// the end of the comment section and should continue parsing for keywords.
if(!line.StartsWith("#")) {
this.state = State.ExpectingKeywords;
parseKeyword(line);
return;
}
// Locate the start of any characters within this comment. If the first
// character encountered is not a number, we must have reached the end of
// the track offset list and should continue normal comment processing.
int offsetStartIndex = -1;
bool hasTrackOffset =
(line.Length > 1) &&
((offsetStartIndex = StringHelper.IndexNotOfAny(line, Whitespaces, 1)) != -1) &&
char.IsNumber(line[offsetStartIndex]);
if(!hasTrackOffset) {
this.state = State.ExpectingCommentProperties;
parseCommentProperty(line);
return;
}
// Find out where the offset specification ends
int offsetEndIndex = StringHelper.IndexNotOfAny(
line, Numbers, offsetStartIndex + 1
);
// All positions have been determined, now extract the track offset and add
// it to our list
if(offsetEndIndex == -1) {
this.trackOffsets.Add(Convert.ToInt32(line.Substring(offsetStartIndex)));
} else {
this.trackOffsets.Add(
Convert.ToInt32(
line.Substring(offsetStartIndex, offsetEndIndex - offsetStartIndex)
)
);
}
}
/// Parses a comment line possibly containing a property
/// Line containing a comment and possible a property
private void parseKeyword(string line) {
// Look for the start of the keyword
int keywordStartIndex = -1;
bool hasKeyword =
(line.Length > 0) &&
((keywordStartIndex = StringHelper.IndexNotOfAny(line, Whitespaces)) != -1);
if(!hasKeyword) {
return; // blank line
}
// Look for the equals sign
int equalsIndex = -1;
bool hasAssignment =
(line.Length > keywordStartIndex + 1) &&
((equalsIndex = line.IndexOf('=', keywordStartIndex + 1)) != -1);
if(!hasAssignment) {
return; // strange line!
}
// Find out where the value starts
int valueIndex = -1;
bool hasValue =
(line.Length > equalsIndex + 1) &&
((valueIndex = StringHelper.IndexNotOfAny(line, Whitespaces, equalsIndex + 1)) != -1);
// All found, now extract the informations we found an process them
string name = line.Substring(keywordStartIndex, equalsIndex - keywordStartIndex);
if(hasValue) {
interpretKeyword(name, line.Substring(valueIndex));
} else {
interpretKeyword(name, string.Empty);
}
}
/// Interprets a keyword assignment appearing in an XMCD file
/// Name of the keyword
/// Value assigned to the keyword
private void interpretKeyword(string name, string value) {
if(name == "DISCID") {
this.haveDiscId = true;
this.entry.DiscIds = parseDiscIds(value);
} else if(name == "DTITLE") {
this.haveTitle = true;
Cddb.DTitle dTitle = Cddb.SplitDiscTitle(value);
this.entry.Artist = dTitle.Artist;
this.entry.Album = dTitle.Title;
} else if(name == "DYEAR") {
this.haveYear = true;
this.entry.Year = Convert.ToInt32(value);
} else if(name == "DGENRE") {
this.haveGenre = true;
this.entry.Genre = value;
} else if(name == "EXTD") {
this.haveExtendedData = true;
} else if(name == "PLAYORDER") {
this.havePlayOrder = true;
} else if(name.StartsWith("TTITLE")) {
int trackIndex = Convert.ToInt32(name.Substring(6));
// Make sure there are enough entries in the list (minus one because it allows
// us to use the Add() method then, instead of adding an empty entry and replacing
// it further down
while(this.trackTitles.Count < trackIndex) {
this.trackTitles.Add(new Cddb.DTitle());
}
// Get the track title. If a " / " sequence is contained in the title, the track
// contains an artist specification. Otherwise, leave the artist field set to
// null because we can't be sure that the disc title field has been filled already.
Cddb.DTitle track;
if(value.Contains(" / ")) {
track = Cddb.SplitDiscTitle(value);
} else {
track = new Cddb.DTitle(null, value);
}
// Finally, add the track to the track list. If it is an out-of-order track, we
// will replace the existing, empty entry. Otherwise, the list will be just one
// element short of the track and we can normally add it.
if(this.trackTitles.Count == trackIndex) {
this.trackTitles.Add(track);
} else {
this.trackTitles[trackIndex] = track;
}
} else if(name.StartsWith("EXTT")) {
// Ignore for now
}
}
/// Parses the CDDB disc ids from a disc id list
/// List of disc ids to parse
/// An array containing any parsed disc ids
private int[] parseDiscIds(string discIds) {
List discIdList = new List();
int lastCommaIndex = -1;
int commaIndex = discIds.IndexOf(',');
while(commaIndex != -1) {
string discId = discIds.Substring(
lastCommaIndex + 1, commaIndex - lastCommaIndex
);
discIdList.Add(Convert.ToInt32(discId.Trim(Whitespaces), 16));
lastCommaIndex = commaIndex;
commaIndex = discIds.IndexOf(',', commaIndex + 1);
}
string finalDiscId = discIds.Substring(lastCommaIndex + 1);
discIdList.Add(Convert.ToInt32(finalDiscId.Trim(Whitespaces), 16));
return discIdList.ToArray();
}
/// Current state the decoder is in
private State state;
/// Database entry structure that is being filled by the decoder
private Cddb.DatabaseEntry entry;
/// Frame offsets that were specified for the tracks in the XMCD file
private List trackOffsets;
/// Track titles for all tracks listed in the XMCD file
private List trackTitles;
/// Whether the decoder has encountered a disc id field
private bool haveDiscId;
/// Whether the decoder has encountered a disc title field
private bool haveTitle;
/// Whether the decoder has encountered a production year field
private bool haveYear;
/// Whether the decoder has encountered a genre field
private bool haveGenre;
/// Whether the decoder has encountered a play order field
private bool havePlayOrder;
/// Whether the decoder has encountered an extended data field
private bool haveExtendedData;
}
} // namespace Nuclex.Audio.Metadata