using System; using System.IO; using System.Text; namespace m3uTool { /// /// Indicates that a problem existed when parsing an mp3 file /// public class Mp3FormatException : Exception { string mFilename; string mFormatExceptionString; public string Filename { get { return mFilename; } } public string FormatExceptionString { get { return mFormatExceptionString; } } /// /// Constructor. /// /// /// public Mp3FormatException(string filename, string formatExceptionString) { mFilename = filename; mFormatExceptionString = formatExceptionString; } } /* ---------------------------------------------------------- original C++ code by: Gustav "Grim Reaper" Munkby http://floach.pimpin.net/grd/ grimreaperdesigns@gmx.net modified and converted to C# by: Robert A. Wlodarczyk http://rob.wincereview.com:8080 rwlodarc@hotmail.com ---------------------------------------------------------- */ public class Mp3Header { //// Public variables for storing the information about the MP3 //public int intBitRate; //public string strFileName; //public long lngFileSize; //public int intFrequency; //public string strMode; //public int intLength; //public string strLengthFormatted; // Private variables used in the process of reading in the MP3 files private ulong bithdr; private bool boolVBitRate; private int intVFrames; public Mp3FileDetails ReadMP3Information(string fileName) { mFileDetails = new Mp3FileDetails(); FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read); // Set the filename not including the path information mFileDetails.strFileName = @fs.Name; char[] chrSeparators = new char[]{'\\','/'}; string[] strSeparator = mFileDetails.strFileName.Split(chrSeparators); int intUpper = strSeparator.GetUpperBound(0); mFileDetails.strFileName = strSeparator[intUpper]; // Replace ' with '' for the SQL INSERT statement mFileDetails.strFileName = mFileDetails.strFileName.Replace("'", "''"); // Set the file size mFileDetails.lngFileSize = fs.Length; byte[] bytHeader = new byte[4]; byte[] bytVBitRate = new byte[12]; int intPos = 0; // Keep reading 4 bytes from the header until we know for sure that in // fact it's an MP3 do { fs.Position = intPos; fs.Read(bytHeader,0,4); intPos++; LoadMP3Header(bytHeader); } while(!IsValidHeader() && (fs.Position!=fs.Length)); // If the current file stream position is equal to the length, // that means that we've read the entire file and it's not a valid MP3 file if(fs.Position != fs.Length) { intPos += 3; if(getVersionIndex() == 3) // MPEG Version 1 { if(getModeIndex() == 3) // Single Channel { intPos += 17; } else { intPos += 32; } } else // MPEG Version 2.0 or 2.5 { if(getModeIndex() == 3) // Single Channel { intPos += 9; } else { intPos += 17; } } // Check to see if the MP3 has a variable bitrate fs.Position = intPos; fs.Read(bytVBitRate,0,12); boolVBitRate = LoadVBRHeader(bytVBitRate); // Once the file's read in, then assign the properties of the file to the public variables mFileDetails.intBitRate = getBitrate(); mFileDetails.intFrequency = getFrequency(); mFileDetails.strMode = getMode(); mFileDetails.intLength = getLengthInSeconds(); mFileDetails.strLengthFormatted = getFormattedLength(); fs.Close(); return mFileDetails; } throw new Mp3FormatException(fileName, "Invalid mp3 file"); } private void LoadMP3Header(byte[] c) { // this thing is quite interesting, it works like the following // c[0] = 00000011 // c[1] = 00001100 // c[2] = 00110000 // c[3] = 11000000 // the operator << means that we'll move the bits in that direction // 00000011 << 24 = 00000011000000000000000000000000 // 00001100 << 16 = 000011000000000000000000 // 00110000 << 24 = 0011000000000000 // 11000000 = 11000000 // +_________________________________ // 00000011000011000011000011000000 bithdr = (ulong)(((c[0] & 255) << 24) | ((c[1] & 255) << 16) | ((c[2] & 255) << 8) | ((c[3] & 255))); } private bool LoadVBRHeader(byte[] inputheader) { // If it's a variable bitrate MP3, the first 4 bytes will read 'Xing' // since they're the ones who added variable bitrate-edness to MP3s if(inputheader[0] == 88 && inputheader[1] == 105 && inputheader[2] == 110 && inputheader[3] == 103) { int flags = (((inputheader[4] & 255) << 24) | ((inputheader[5] & 255) << 16) | ((inputheader[6] & 255) << 8) | ((inputheader[7] & 255))); if((flags & 0x0001) == 1) { intVFrames = (((inputheader[8] & 255) << 24) | ((inputheader[9] & 255) << 16) | ((inputheader[10] & 255) << 8) | ((inputheader[11] & 255))); return true; } else { intVFrames = -1; return true; } } return false; } private bool IsValidHeader() { return (((getFrameSync() & 2047)==2047) && ((getVersionIndex() & 3)!= 1) && ((getLayerIndex() & 3)!= 0) && ((getBitrateIndex() & 15)!= 0) && ((getBitrateIndex() & 15)!= 15) && ((getFrequencyIndex() & 3)!= 3) && ((getEmphasisIndex() & 3)!= 2) ); } private int getFrameSync() { return (int)((bithdr>>21) & 2047); } private int getVersionIndex() { return (int)((bithdr>>19) & 3); } private int getLayerIndex() { return (int)((bithdr>>17) & 3); } //private int getProtectionBit() //{ // return (int)((bithdr>>16) & 1); //} private int getBitrateIndex() { return (int)((bithdr>>12) & 15); } private int getFrequencyIndex() { return (int)((bithdr>>10) & 3); } //private int getPaddingBit() //{ // return (int)((bithdr>>9) & 1); //} //private int getPrivateBit() //{ // return (int)((bithdr>>8) & 1); //} private int getModeIndex() { return (int)((bithdr>>6) & 3); } //private int getModeExtIndex() //{ // return (int)((bithdr>>4) & 3); //} //private int getCoprightBit() //{ // return (int)((bithdr>>3) & 1); //} //private int getOrginalBit() //{ // return (int)((bithdr>>2) & 1); //} private int getEmphasisIndex() { return (int)(bithdr & 3); } //private double getVersion() //{ // double[] table = {2.5, 0.0, 2.0, 1.0}; // return table[getVersionIndex()]; //} //private int getLayer() //{ // return (int)(4 - getLayerIndex()); //} private int getBitrate() { // If the file has a variable bitrate, then we return an integer average bitrate, // otherwise, we use a lookup table to return the bitrate if(boolVBitRate) { double medFrameSize = (double)mFileDetails.lngFileSize/ (double)getNumberOfFrames(); return (int)((medFrameSize * getFrequency()) / (1000.0 * ((getLayerIndex()==3) ? 12.0 : 144.0))); } else { int[,,] table = { { // MPEG 2 & 2.5 {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0}, // Layer III {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0}, // Layer II {0, 32, 48, 56, 64, 80, 96,112,128,144,160,176,192,224,256,0} // Layer I }, { // MPEG 1 {0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,0}, // Layer III {0, 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384,0}, // Layer II {0, 32, 64, 96,128,160,192,224,256,288,320,352,384,416,448,0} // Layer I } }; return table[getVersionIndex() & 1, getLayerIndex()-1, getBitrateIndex()]; } } private int getFrequency() { int[,] table = { {32000, 16000, 8000}, // MPEG 2.5 { 0, 0, 0}, // reserved {22050, 24000, 16000}, // MPEG 2 {44100, 48000, 32000} // MPEG 1 }; return table[getVersionIndex(), getFrequencyIndex()]; } private string getMode() { switch(getModeIndex()) { default: return "Stereo"; case 1: return "Joint Stereo"; case 2: return "Dual Channel"; case 3: return "Single Channel"; } } private int getLengthInSeconds() { // "intKilBitFileSize" made by dividing by 1000 in order to match the "Kilobits/second" int intKiloBitFileSize = ((8 * (int)mFileDetails.lngFileSize) / 1000); return (intKiloBitFileSize/getBitrate()); } private string getFormattedLength() { // Complete number of seconds int s = getLengthInSeconds(); // Seconds to display int ss = s%60; // Complete number of minutes int m = (s-ss)/60; // Minutes to display int mm = m%60; // Complete number of hours int h = (m-mm)/60; // Make "hh:mm:ss" return h.ToString("D2") + ":" + mm.ToString("D2") + ":" + ss.ToString("D2"); } private int getNumberOfFrames() { // Again, the number of MPEG frames is dependant on whether it's a variable bitrate MP3 or not if (!boolVBitRate) { double medFrameSize = (((getLayerIndex()==3) ? 12 : 144) *((1000.0 * (float)getBitrate())/(float)getFrequency())); return (int)(mFileDetails.lngFileSize/medFrameSize); } else return intVFrames; } public static void ReadMP3Tag(ref Mp3FileDetails paramMP3) { // Read the 128 byte ID3 tag into a byte array FileStream oFileStream; oFileStream = new FileStream(paramMP3.fileComplete, FileMode.Open); byte[] bBuffer = new byte[128]; oFileStream.Seek(-128, SeekOrigin.End); oFileStream.Read(bBuffer, 0, 128); oFileStream.Close(); // Convert the Byte Array to a String Encoding instEncoding = new ASCIIEncoding(); // NB: Encoding is an Abstract class string id3Tag = instEncoding.GetString(bBuffer); // If there is an attched ID3 v1.x TAG then read it if (id3Tag.Substring(0, 3) == "TAG") { paramMP3.id3Title = id3Tag.Substring(3, 30).Trim(); paramMP3.id3Artist = id3Tag.Substring(33, 30).Trim(); paramMP3.id3Album = id3Tag.Substring(63, 30).Trim(); paramMP3.id3Year = id3Tag.Substring(93, 4).Trim(); paramMP3.id3Comment = id3Tag.Substring(97, 28).Trim(); // Get the track number if TAG conforms to ID3 v1.1 if (id3Tag[125] == 0) paramMP3.id3TrackNumber = bBuffer[126]; else paramMP3.id3TrackNumber = 0; paramMP3.id3Genre = bBuffer[127]; paramMP3.hasID3Tag = true; // ********* IF USED IN ANGER: ENSURE to test for non-numeric year } else { // ID3 Tag not found so create an empty TAG in case the user saces later paramMP3.id3Title = ""; paramMP3.id3Artist = ""; paramMP3.id3Album = ""; paramMP3.id3Year = ""; paramMP3.id3Comment = ""; paramMP3.id3TrackNumber = 0; paramMP3.id3Genre = 0; paramMP3.hasID3Tag = false; } } public static void UpdateMP3ID3Tag(ref Mp3FileDetails paramMP3) { // Trim any whitespace paramMP3.id3Title = paramMP3.id3Title.Trim(); paramMP3.id3Artist = paramMP3.id3Artist.Trim(); paramMP3.id3Album = paramMP3.id3Album.Trim(); paramMP3.id3Year = paramMP3.id3Year.Trim(); paramMP3.id3Comment = paramMP3.id3Comment.Trim(); // Ensure all properties are correct size if (paramMP3.id3Title.Length > 30) paramMP3.id3Title = paramMP3.id3Title.Substring(0, 30); if (paramMP3.id3Artist.Length > 30) paramMP3.id3Artist = paramMP3.id3Artist.Substring(0, 30); if (paramMP3.id3Album.Length > 30) paramMP3.id3Album = paramMP3.id3Album.Substring(0, 30); if (paramMP3.id3Year.Length > 4) paramMP3.id3Year = paramMP3.id3Year.Substring(0, 4); if (paramMP3.id3Comment.Length > 28) paramMP3.id3Comment = paramMP3.id3Comment.Substring(0, 28); // Build a new ID3 Tag (128 Bytes) byte[] tagByteArray = new byte[128]; for (int i = 0; i < tagByteArray.Length; i++) tagByteArray[i] = 0; // Initialise array to nulls // Convert the Byte Array to a String Encoding instEncoding = new ASCIIEncoding(); // NB: Encoding is an Abstract class // ************ To DO: Make a shared instance of ASCIIEncoding so we don't keep creating/destroying it // Copy "TAG" to Array byte[] workingByteArray = instEncoding.GetBytes("TAG"); Array.Copy(workingByteArray, 0, tagByteArray, 0, workingByteArray.Length); // Copy Title to Array workingByteArray = instEncoding.GetBytes(paramMP3.id3Title); Array.Copy(workingByteArray, 0, tagByteArray, 3, workingByteArray.Length); // Copy Artist to Array workingByteArray = instEncoding.GetBytes(paramMP3.id3Artist); Array.Copy(workingByteArray, 0, tagByteArray, 33, workingByteArray.Length); // Copy Album to Array workingByteArray = instEncoding.GetBytes(paramMP3.id3Album); Array.Copy(workingByteArray, 0, tagByteArray, 63, workingByteArray.Length); // Copy Year to Array workingByteArray = instEncoding.GetBytes(paramMP3.id3Year); Array.Copy(workingByteArray, 0, tagByteArray, 93, workingByteArray.Length); // Copy Comment to Array workingByteArray = instEncoding.GetBytes(paramMP3.id3Comment); Array.Copy(workingByteArray, 0, tagByteArray, 97, workingByteArray.Length); // Copy Track and Genre to Array tagByteArray[126] = paramMP3.id3TrackNumber; tagByteArray[127] = paramMP3.id3Genre; // SAVE TO DISK: Replace the final 128 Bytes with our new ID3 tag FileStream oFileStream = new FileStream(paramMP3.fileComplete, FileMode.Open); if (paramMP3.hasID3Tag) oFileStream.Seek(-128, SeekOrigin.End); else oFileStream.Seek(0, SeekOrigin.End); oFileStream.Write(tagByteArray, 0, 128); oFileStream.Close(); paramMP3.hasID3Tag = true; } } }