#region MIT license // // MIT license // // Copyright (c) 2013 Corey Murtagh // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #endregion using System; using System.IO; using System.Runtime.InteropServices; using NAudio.Wave; using LameDLLWrap; using System.Collections.Generic; namespace NAudio.Lame { /// LAME encoding presets public enum LAMEPreset : int { /*values from 8 to 320 should be reserved for abr bitrates*/ /*for abr I'd suggest to directly use the targeted bitrate as a value*/ /// 8-kbit ABR ABR_8 = 8, /// 16-kbit ABR ABR_16 = 16, /// 32-kbit ABR ABR_32 = 32, /// 48-kbit ABR ABR_48 = 48, /// 64-kbit ABR ABR_64 = 64, /// 96-kbit ABR ABR_96 = 96, /// 128-kbit ABR ABR_128 = 128, /// 160-kbit ABR ABR_160 = 160, /// 256-kbit ABR ABR_256 = 256, /// 320-kbit ABR ABR_320 = 320, /*Vx to match Lame and VBR_xx to match FhG*/ /// VBR Quality 9 V9 = 410, /// FhG: VBR Q10 VBR_10 = 410, /// VBR Quality 8 V8 = 420, /// FhG: VBR Q20 VBR_20 = 420, /// VBR Quality 7 V7 = 430, /// FhG: VBR Q30 VBR_30 = 430, /// VBR Quality 6 V6 = 440, /// FhG: VBR Q40 VBR_40 = 440, /// VBR Quality 5 V5 = 450, /// FhG: VBR Q50 VBR_50 = 450, /// VBR Quality 4 V4 = 460, /// FhG: VBR Q60 VBR_60 = 460, /// VBR Quality 3 V3 = 470, /// FhG: VBR Q70 VBR_70 = 470, /// VBR Quality 2 V2 = 480, /// FhG: VBR Q80 VBR_80 = 480, /// VBR Quality 1 V1 = 490, /// FhG: VBR Q90 VBR_90 = 490, /// VBR Quality 0 V0 = 500, /// FhG: VBR Q100 VBR_100 = 500, /*still there for compatibility*/ /// R3Mix quality - R3MIX = 1000, /// Standard Quality STANDARD = 1001, /// Extreme Quality EXTREME = 1002, /// Insane Quality INSANE = 1003, /// Fast Standard Quality STANDARD_FAST = 1004, /// Fast Extreme Quality EXTREME_FAST = 1005, /// Medium Quality MEDIUM = 1006, /// Fast Medium Quality MEDIUM_FAST = 1007 } /// Delegate for receiving output messages /// Text to output /// Output from the LAME library is very limited. At this stage only a few direct calls will result in output. No output is normally generated during encoding. public delegate void OutputHandler(string text); /// Delegate for progress feedback from encoder /// instance that the progress update is for /// Total number of bytes passed to encoder /// Total number of bytes written to output /// True if encoding process is completed public delegate void ProgressHandler(object writer, long inputBytes, long outputBytes, bool finished); /// MP3 encoding class, uses libmp3lame DLL to encode. public class LameMP3FileWriter : Stream { /// Static initializer, ensures that the correct library is loaded /* static LameMP3FileWriter() { Loader.Init(); } */ // Ensure that the Loader is initialized correctly //static bool init_loader = Loader.Initialized; /// Union class for fast buffer conversion /// /// /// Because of the way arrays work in .NET, all of the arrays will have the same /// length value. To prevent unaware code from trying to read/write from out of /// bounds, allocation is done at the grain of the Least Common Multiple of the /// sizes of the contained types. In this case the LCM is 8 bytes - the size of /// a double or a long - which simplifies allocation. /// /// This means that when you ask for an array of 500 bytes you will actually get /// an array of 63 doubles - 504 bytes total. Any code that uses the length of /// the array will see only 63 bytes, shorts, etc. /// /// CodeAnalysis does not like this class, with good reason. It should never be /// exposed beyond the scope of the MP3FileWriter. /// /// // uncomment to suppress CodeAnalysis warnings for the ArrayUnion class: [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Portability", "CA1900:ValueTypeFieldsShouldBePortable", Justification = "This design breaks portability, but is never exposed outside the class. Tested on x86 and x64 architectures.")] [StructLayout(LayoutKind.Explicit)] private class ArrayUnion { /// Length of the byte array [FieldOffset(0)] public readonly int nBytes; /// Array of unsigned 8-bit integer values, length will be misreported [FieldOffset(16)] public readonly byte[] bytes; /// Array of signed 16-bit integer values, length will be misreported [FieldOffset(16)] public readonly short[] shorts; /// Array of signed 32-bit integer values, length will be misreported [FieldOffset(16)] public readonly int[] ints; /// Array of signed 64-bit integer values, length will be correct [FieldOffset(16)] public readonly long[] longs; /// Array of signed 32-bit floating point values, length will be misreported [FieldOffset(16)] public readonly float[] floats; /// Array of signed 64-bit floating point values, length will be correct /// This is the actual array allocated by the constructor [FieldOffset(16)] public readonly double[] doubles; // True sizes of the various array types, calculated from number of bytes /// Actual length of the 'shorts' member array public int nShorts { get { return nBytes / 2; } } /// Actual length of the 'ints' member array public int nInts { get { return nBytes / 4; } } /// Actual length of the 'longs' member array public int nLongs { get { return nBytes / 8; } } /// Actual length of the 'floats' member array public int nFloats { get { return nBytes / 4; } } /// Actual length of the 'doubles' member array public int nDoubles { get { return doubles.Length; } } /// Initialize array to hold the requested number of bytes /// Minimum byte count of array /// /// Since all arrays will have the same apparent count, allocation /// is done on the array with the largest data type. This helps /// to prevent out-of-bounds reads and writes by methods that do /// not know about the union. /// public ArrayUnion(int reqBytes) { // Calculate smallest number of doubles required to store the // requested byte count int reqDoubles = (reqBytes + 7) / 8; this.doubles = new double[reqDoubles]; this.nBytes = reqDoubles * 8; } private ArrayUnion() { throw new Exception("Default constructor cannot be called for ArrayUnion"); } }; #region Properties // LAME library context private LibMp3Lame _lame; // Format of input wave data private readonly WaveFormat inputFormat; // Output stream to write encoded data to private Stream outStream; // Flag to control whether we should dispose of output stream private bool disposeOutput = false; #endregion #region Structors /// Create MP3FileWriter to write to a file on disk /// Name of file to create /// Input WaveFormat /// LAME quality preset /// Optional ID3 data block public LameMP3FileWriter(string outFileName, WaveFormat format, NAudio.Lame.LAMEPreset quality, ID3TagData id3 = null) : this(File.Create(outFileName), format, quality, id3) { this.disposeOutput = true; } /// Create MP3FileWriter to write to supplied stream /// Stream to write encoded data to /// Input WaveFormat /// LAME quality preset /// Optional ID3 data block public LameMP3FileWriter(Stream outStream, WaveFormat format, NAudio.Lame.LAMEPreset quality, ID3TagData id3 = null) : base() { //Loader.Init(); //if (!Loader.Initialized) // Loader.Initialized = false; // sanity check if (outStream == null) throw new ArgumentNullException("outStream"); if (format == null) throw new ArgumentNullException("format"); // check for unsupported wave formats if (format.Channels != 1 && format.Channels != 2) throw new ArgumentException(string.Format("Unsupported number of channels {0}", format.Channels), "format"); if (format.Encoding != WaveFormatEncoding.Pcm && format.Encoding != WaveFormatEncoding.IeeeFloat) throw new ArgumentException(string.Format("Unsupported encoding format {0}", format.Encoding.ToString()), "format"); if (format.Encoding == WaveFormatEncoding.Pcm && format.BitsPerSample != 16) throw new ArgumentException(string.Format("Unsupported PCM sample size {0}", format.BitsPerSample), "format"); if (format.Encoding == WaveFormatEncoding.IeeeFloat && format.BitsPerSample != 32) throw new ArgumentException(string.Format("Unsupported Float sample size {0}", format.BitsPerSample), "format"); if (format.SampleRate < 8000 || format.SampleRate > 48000) throw new ArgumentException(string.Format("Unsupported Sample Rate {0}", format.SampleRate), "format"); // select encoder function that matches data format if (format.Encoding == WaveFormatEncoding.Pcm) { if (format.Channels == 1) _encode = encode_pcm_16_mono; else _encode = encode_pcm_16_stereo; } else { if (format.Channels == 1) _encode = encode_float_mono; else _encode = encode_float_stereo; } // Set base properties this.inputFormat = format; this.outStream = outStream; this.disposeOutput = false; // Allocate buffers based on sample rate this.inBuffer = new ArrayUnion(format.AverageBytesPerSecond); this.outBuffer = new byte[format.SampleRate * 5 / 4 + 7200]; // Initialize lame library this._lame = new LibMp3Lame(); this._lame.InputSampleRate = format.SampleRate; this._lame.NumChannels = format.Channels; this._lame.SetPreset((int)quality); if (id3 != null) ApplyID3Tag(id3); this._lame.InitParams(); } /// Create MP3FileWriter to write to a file on disk /// Name of file to create /// Input WaveFormat /// Output bit rate in kbps /// Optional ID3 data block public LameMP3FileWriter(string outFileName, WaveFormat format, int bitRate, ID3TagData id3 = null) : this(File.Create(outFileName), format, bitRate, id3) { this.disposeOutput = true; } /// Create MP3FileWriter to write to supplied stream /// Stream to write encoded data to /// Input WaveFormat /// Output bit rate in kbps /// Optional ID3 data block public LameMP3FileWriter(Stream outStream, WaveFormat format, int bitRate, ID3TagData id3 = null) : base() { //Loader.Init(); //if (!Loader.Initialized) // Loader.Initialized = false; // sanity check if (outStream == null) throw new ArgumentNullException("outStream"); if (format == null) throw new ArgumentNullException("format"); // check for unsupported wave formats if (format.Channels != 1 && format.Channels != 2) throw new ArgumentException(string.Format("Unsupported number of channels {0}", format.Channels), "format"); if (format.Encoding != WaveFormatEncoding.Pcm && format.Encoding != WaveFormatEncoding.IeeeFloat) throw new ArgumentException(string.Format("Unsupported encoding format {0}", format.Encoding.ToString()), "format"); if (format.Encoding == WaveFormatEncoding.Pcm && format.BitsPerSample != 16) throw new ArgumentException(string.Format("Unsupported PCM sample size {0}", format.BitsPerSample), "format"); if (format.Encoding == WaveFormatEncoding.IeeeFloat && format.BitsPerSample != 32) throw new ArgumentException(string.Format("Unsupported Float sample size {0}", format.BitsPerSample), "format"); if (format.SampleRate < 8000 || format.SampleRate > 48000) throw new ArgumentException(string.Format("Unsupported Sample Rate {0}", format.SampleRate), "format"); // select encoder function that matches data format if (format.Encoding == WaveFormatEncoding.Pcm) { if (format.Channels == 1) _encode = encode_pcm_16_mono; else _encode = encode_pcm_16_stereo; } else { if (format.Channels == 1) _encode = encode_float_mono; else _encode = encode_float_stereo; } // Set base properties this.inputFormat = format; this.outStream = outStream; this.disposeOutput = false; // Allocate buffers based on sample rate this.inBuffer = new ArrayUnion(format.AverageBytesPerSecond); this.outBuffer = new byte[format.SampleRate * 5 / 4 + 7200]; // Initialize lame library this._lame = new LibMp3Lame(); this._lame.InputSampleRate = format.SampleRate; this._lame.NumChannels = format.Channels; this._lame.BitRate = bitRate; if (id3 != null) ApplyID3Tag(id3); this._lame.InitParams(); } // Close LAME instance and output stream on dispose /// Dispose of object /// True if called from destructor, false otherwise protected override void Dispose(bool final) { if (_lame != null && outStream != null) Flush(); if (_lame != null) { _lame.Dispose(); _lame = null; } if (outStream != null && disposeOutput) { outStream.Dispose(); outStream = null; } base.Dispose(final); } #endregion /// Get internal LAME library instance /// LAME library instance public LibMp3Lame GetLameInstance() { return _lame; } #region Internal encoder operations // Input buffer private ArrayUnion inBuffer = null; /// Current write position in input buffer private int inPosition; /// Output buffer, size determined by call to Lame.beInitStream protected byte[] outBuffer; long _inputByteCount = 0; long _outputByteCount = 0; // encoder write functions, one for each supported input wave format private int encode_pcm_16_mono() { return _lame.Write(inBuffer.shorts, inPosition / 2, outBuffer, outBuffer.Length, true); } private int encode_pcm_16_stereo() { return _lame.Write(inBuffer.shorts, inPosition / 2, outBuffer, outBuffer.Length, false); } private int encode_float_mono() { return _lame.Write(inBuffer.floats, inPosition / 4, outBuffer, outBuffer.Length, true); } private int encode_float_stereo() { return _lame.Write(inBuffer.floats, inPosition / 4, outBuffer, outBuffer.Length, false); } // Selected encoding write function delegate int delEncode(); delEncode _encode = null; // Pass data to encoder private void Encode() { // check if encoder closed if (outStream == null || _lame == null) throw new InvalidOperationException("Output Stream closed."); // If no data to encode, do nothing if (inPosition < inputFormat.Channels * 2) return; // send to encoder int rc = _encode(); if (rc > 0) { outStream.Write(outBuffer, 0, rc); _outputByteCount += rc; } _inputByteCount += inPosition; inPosition = 0; // report progress RaiseProgress(false); } #endregion #region Stream implementation /// Write-only stream. Always false. public override bool CanRead { get { return false; } } /// Non-seekable stream. Always false. public override bool CanSeek { get { return false; } } /// True when encoder can accept more data public override bool CanWrite { get { return outStream != null && _lame != null; } } /// Dummy Position. Always 0. public override long Position { get { return 0; } set { throw new NotImplementedException(); } } /// Dummy Length. Always 0. public override long Length { get { return 0; } } /// Add data to output buffer, sending to encoder when buffer full /// Source buffer /// Offset of data in buffer /// Length of data public override void Write(byte[] buffer, int offset, int count) { while (count > 0) { int blockSize = Math.Min(inBuffer.nBytes - inPosition, count); Buffer.BlockCopy(buffer, offset, inBuffer.bytes, inPosition, blockSize); inPosition += blockSize; count -= blockSize; offset += blockSize; if (inPosition >= inBuffer.nBytes) Encode(); } } /// Finalise compression, add final output to output stream and close encoder public override void Flush() { // write remaining data if (inPosition > 0) Encode(); // finalize compression int rc = _lame.Flush(outBuffer, outBuffer.Length); if (rc > 0) { outStream.Write(outBuffer, 0, rc); _outputByteCount += rc; } // report progress RaiseProgress(true); // Cannot continue after flush, so clear output stream if (disposeOutput) outStream.Dispose(); outStream = null; } /// Reading not supported. Throws NotImplementedException. /// /// /// /// public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } /// Setting length not supported. Throws NotImplementedException. /// Length value public override void SetLength(long value) { throw new NotImplementedException(); } /// Seeking not supported. Throws NotImplementedException. /// Seek offset /// Seek origin public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } #endregion #region ID3 support private void ApplyID3Tag(ID3TagData tag) { if (tag == null) return; if (!string.IsNullOrEmpty(tag.Title)) _lame.ID3SetTitle(tag.Title); if (!string.IsNullOrEmpty(tag.Artist)) _lame.ID3SetArtist(tag.Artist); if (!string.IsNullOrEmpty(tag.Album)) _lame.ID3SetAlbum(tag.Album); if (!string.IsNullOrEmpty(tag.Year)) _lame.ID3SetYear(tag.Year); if (!string.IsNullOrEmpty(tag.Comment)) _lame.ID3SetComment(tag.Comment); if (!string.IsNullOrEmpty(tag.Genre)) _lame.ID3SetGenre(tag.Genre); if (!string.IsNullOrEmpty(tag.Track)) _lame.ID3SetTrack(tag.Track); if (!string.IsNullOrEmpty(tag.Subtitle)) _lame.ID3SetFieldValue(string.Format("TIT3={0}", tag.Subtitle)); if (!string.IsNullOrEmpty(tag.AlbumArtist)) _lame.ID3SetFieldValue(string.Format("TPE2={0}", tag.AlbumArtist)); if (tag.AlbumArt != null && tag.AlbumArt.Length > 0 && tag.AlbumArt.Length < 131072) _lame.ID3SetAlbumArt(tag.AlbumArt); } private static Dictionary _genres; /// Dictionary of Genres supported by LAME's ID3 tag support public static Dictionary Genres { get { if (_genres == null) _genres = LibMp3Lame.ID3GenreList(); return _genres; } } #endregion #region LAME library print hooks /// Set output function for Error output /// Function to call for Error output public void SetErrorFunction(OutputHandler fn) { GetLameInstance().SetErrorFunc(t => fn(t)); } /// Set output function for Debug output /// Function to call for Debug output public void SetDebugFunction(OutputHandler fn) { GetLameInstance().SetMsgFunc(t => fn(t)); } /// Set output function for Message output /// Function to call for Message output public void SetMessageFunction(OutputHandler fn) { GetLameInstance().SetMsgFunc(t => fn(t)); } /// Get configuration of LAME context, results passed to Message function public void PrintLAMEConfig() { GetLameInstance().PrintConfig(); } /// Get internal settings of LAME context, results passed to Message function public void PrintLAMEInternals() { GetLameInstance().PrintInternals(); } #endregion #region Progress event int _minProgressTime = 100; /// Minimimum time between progress events in ms, or 0 for no limit /// Defaults to 100ms public int MinProgressTime { get { return _minProgressTime; } set { _minProgressTime = Math.Max(0, value); } } /// Called when data is written to the output file from Encode or Flush public event ProgressHandler OnProgress; DateTime _lastProgress = DateTime.Now; /// Call any registered OnProgress handlers /// True if called at end of output protected void RaiseProgress(bool finished) { var timeDelta = DateTime.Now - _lastProgress; if (finished || timeDelta.TotalMilliseconds >= _minProgressTime) { _lastProgress = DateTime.Now; var prog = OnProgress; if (prog != null) prog(this, _inputByteCount, _outputByteCount, finished); } } #endregion } }