Initial commit — M3U playlist tool with MP3/AAC encoding
This commit is contained in:
+468
@@ -0,0 +1,468 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace m3uTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that a problem existed when parsing an mp3 file
|
||||
/// </summary>
|
||||
public class Mp3FormatException : Exception
|
||||
{
|
||||
string mFilename;
|
||||
|
||||
string mFormatExceptionString;
|
||||
|
||||
public string Filename
|
||||
{
|
||||
get { return mFilename; }
|
||||
}
|
||||
|
||||
public string FormatExceptionString
|
||||
{
|
||||
get { return mFormatExceptionString; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="filename"></param>
|
||||
/// <param name="formatExceptionString"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user