#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
}
}