You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

714 lines
23 KiB

#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
{
/// <summary>LAME encoding presets</summary>
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*/
/// <summary>8-kbit ABR</summary>
ABR_8 = 8,
/// <summary>16-kbit ABR</summary>
ABR_16 = 16,
/// <summary>32-kbit ABR</summary>
ABR_32 = 32,
/// <summary>48-kbit ABR</summary>
ABR_48 = 48,
/// <summary>64-kbit ABR</summary>
ABR_64 = 64,
/// <summary>96-kbit ABR</summary>
ABR_96 = 96,
/// <summary>128-kbit ABR</summary>
ABR_128 = 128,
/// <summary>160-kbit ABR</summary>
ABR_160 = 160,
/// <summary>256-kbit ABR</summary>
ABR_256 = 256,
/// <summary>320-kbit ABR</summary>
ABR_320 = 320,
/*Vx to match Lame and VBR_xx to match FhG*/
/// <summary>VBR Quality 9</summary>
V9 = 410,
/// <summary>FhG: VBR Q10</summary>
VBR_10 = 410,
/// <summary>VBR Quality 8</summary>
V8 = 420,
/// <summary>FhG: VBR Q20</summary>
VBR_20 = 420,
/// <summary>VBR Quality 7</summary>
V7 = 430,
/// <summary>FhG: VBR Q30</summary>
VBR_30 = 430,
/// <summary>VBR Quality 6</summary>
V6 = 440,
/// <summary>FhG: VBR Q40</summary>
VBR_40 = 440,
/// <summary>VBR Quality 5</summary>
V5 = 450,
/// <summary>FhG: VBR Q50</summary>
VBR_50 = 450,
/// <summary>VBR Quality 4</summary>
V4 = 460,
/// <summary>FhG: VBR Q60</summary>
VBR_60 = 460,
/// <summary>VBR Quality 3</summary>
V3 = 470,
/// <summary>FhG: VBR Q70</summary>
VBR_70 = 470,
/// <summary>VBR Quality 2</summary>
V2 = 480,
/// <summary>FhG: VBR Q80</summary>
VBR_80 = 480,
/// <summary>VBR Quality 1</summary>
V1 = 490,
/// <summary>FhG: VBR Q90</summary>
VBR_90 = 490,
/// <summary>VBR Quality 0</summary>
V0 = 500,
/// <summary>FhG: VBR Q100</summary>
VBR_100 = 500,
/*still there for compatibility*/
/// <summary>R3Mix quality - </summary>
R3MIX = 1000,
/// <summary>Standard Quality</summary>
STANDARD = 1001,
/// <summary>Extreme Quality</summary>
EXTREME = 1002,
/// <summary>Insane Quality</summary>
INSANE = 1003,
/// <summary>Fast Standard Quality</summary>
STANDARD_FAST = 1004,
/// <summary>Fast Extreme Quality</summary>
EXTREME_FAST = 1005,
/// <summary>Medium Quality</summary>
MEDIUM = 1006,
/// <summary>Fast Medium Quality</summary>
MEDIUM_FAST = 1007
}
/// <summary>Delegate for receiving output messages</summary>
/// <param name="text">Text to output</param>
/// <remarks>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.</remarks>
public delegate void OutputHandler(string text);
/// <summary>Delegate for progress feedback from encoder</summary>
/// <param name="writer"><see cref="LameMP3FileWriter"/> instance that the progress update is for</param>
/// <param name="inputBytes">Total number of bytes passed to encoder</param>
/// <param name="outputBytes">Total number of bytes written to output</param>
/// <param name="finished">True if encoding process is completed</param>
public delegate void ProgressHandler(object writer, long inputBytes, long outputBytes, bool finished);
/// <summary>MP3 encoding class, uses libmp3lame DLL to encode.</summary>
public class LameMP3FileWriter : Stream
{
/// <summary>Static initializer, ensures that the correct library is loaded</summary>
/*
static LameMP3FileWriter()
{
Loader.Init();
}
*/
// Ensure that the Loader is initialized correctly
//static bool init_loader = Loader.Initialized;
/// <summary>Union class for fast buffer conversion</summary>
/// <remarks>
/// <para>
/// 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.
/// </para><para>
/// 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.
/// </para><para>
/// CodeAnalysis does not like this class, with good reason. It should never be
/// exposed beyond the scope of the MP3FileWriter.
/// </para>
/// </remarks>
// 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
{
/// <summary>Length of the byte array</summary>
[FieldOffset(0)]
public readonly int nBytes;
/// <summary>Array of unsigned 8-bit integer values, length will be misreported</summary>
[FieldOffset(16)]
public readonly byte[] bytes;
/// <summary>Array of signed 16-bit integer values, length will be misreported</summary>
[FieldOffset(16)]
public readonly short[] shorts;
/// <summary>Array of signed 32-bit integer values, length will be misreported</summary>
[FieldOffset(16)]
public readonly int[] ints;
/// <summary>Array of signed 64-bit integer values, length will be correct</summary>
[FieldOffset(16)]
public readonly long[] longs;
/// <summary>Array of signed 32-bit floating point values, length will be misreported</summary>
[FieldOffset(16)]
public readonly float[] floats;
/// <summary>Array of signed 64-bit floating point values, length will be correct</summary>
/// <remarks>This is the actual array allocated by the constructor</remarks>
[FieldOffset(16)]
public readonly double[] doubles;
// True sizes of the various array types, calculated from number of bytes
/// <summary>Actual length of the 'shorts' member array</summary>
public int nShorts { get { return nBytes / 2; } }
/// <summary>Actual length of the 'ints' member array</summary>
public int nInts { get { return nBytes / 4; } }
/// <summary>Actual length of the 'longs' member array</summary>
public int nLongs { get { return nBytes / 8; } }
/// <summary>Actual length of the 'floats' member array</summary>
public int nFloats { get { return nBytes / 4; } }
/// <summary>Actual length of the 'doubles' member array</summary>
public int nDoubles { get { return doubles.Length; } }
/// <summary>Initialize array to hold the requested number of bytes</summary>
/// <param name="reqBytes">Minimum byte count of array</param>
/// <remarks>
/// 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.
/// </remarks>
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
/// <summary>Create MP3FileWriter to write to a file on disk</summary>
/// <param name="outFileName">Name of file to create</param>
/// <param name="format">Input WaveFormat</param>
/// <param name="quality">LAME quality preset</param>
/// <param name="id3">Optional ID3 data block</param>
public LameMP3FileWriter(string outFileName, WaveFormat format, NAudio.Lame.LAMEPreset quality, ID3TagData id3 = null)
: this(File.Create(outFileName), format, quality, id3)
{
this.disposeOutput = true;
}
/// <summary>Create MP3FileWriter to write to supplied stream</summary>
/// <param name="outStream">Stream to write encoded data to</param>
/// <param name="format">Input WaveFormat</param>
/// <param name="quality">LAME quality preset</param>
/// <param name="id3">Optional ID3 data block</param>
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();
}
/// <summary>Create MP3FileWriter to write to a file on disk</summary>
/// <param name="outFileName">Name of file to create</param>
/// <param name="format">Input WaveFormat</param>
/// <param name="bitRate">Output bit rate in kbps</param>
/// <param name="id3">Optional ID3 data block</param>
public LameMP3FileWriter(string outFileName, WaveFormat format, int bitRate, ID3TagData id3 = null)
: this(File.Create(outFileName), format, bitRate, id3)
{
this.disposeOutput = true;
}
/// <summary>Create MP3FileWriter to write to supplied stream</summary>
/// <param name="outStream">Stream to write encoded data to</param>
/// <param name="format">Input WaveFormat</param>
/// <param name="bitRate">Output bit rate in kbps</param>
/// <param name="id3">Optional ID3 data block</param>
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
/// <summary>Dispose of object</summary>
/// <param name="final">True if called from destructor, false otherwise</param>
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
/// <summary>Get internal LAME library instance</summary>
/// <returns>LAME library instance</returns>
public LibMp3Lame GetLameInstance()
{
return _lame;
}
#region Internal encoder operations
// Input buffer
private ArrayUnion inBuffer = null;
/// <summary>Current write position in input buffer</summary>
private int inPosition;
/// <summary>Output buffer, size determined by call to Lame.beInitStream</summary>
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
/// <summary>Write-only stream. Always false.</summary>
public override bool CanRead { get { return false; } }
/// <summary>Non-seekable stream. Always false.</summary>
public override bool CanSeek { get { return false; } }
/// <summary>True when encoder can accept more data</summary>
public override bool CanWrite { get { return outStream != null && _lame != null; } }
/// <summary>Dummy Position. Always 0.</summary>
public override long Position
{
get { return 0; }
set { throw new NotImplementedException(); }
}
/// <summary>Dummy Length. Always 0.</summary>
public override long Length
{
get { return 0; }
}
/// <summary>Add data to output buffer, sending to encoder when buffer full</summary>
/// <param name="buffer">Source buffer</param>
/// <param name="offset">Offset of data in buffer</param>
/// <param name="count">Length of data</param>
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();
}
}
/// <summary>Finalise compression, add final output to output stream and close encoder</summary>
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;
}
/// <summary>Reading not supported. Throws NotImplementedException.</summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
/// <summary>Setting length not supported. Throws NotImplementedException.</summary>
/// <param name="value">Length value</param>
public override void SetLength(long value)
{
throw new NotImplementedException();
}
/// <summary>Seeking not supported. Throws NotImplementedException.</summary>
/// <param name="offset">Seek offset</param>
/// <param name="origin">Seek origin</param>
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<int, string> _genres;
/// <summary>Dictionary of Genres supported by LAME's ID3 tag support</summary>
public static Dictionary<int, string> Genres
{
get
{
if (_genres == null)
_genres = LibMp3Lame.ID3GenreList();
return _genres;
}
}
#endregion
#region LAME library print hooks
/// <summary>Set output function for Error output</summary>
/// <param name="fn">Function to call for Error output</param>
public void SetErrorFunction(OutputHandler fn)
{
GetLameInstance().SetErrorFunc(t => fn(t));
}
/// <summary>Set output function for Debug output</summary>
/// <param name="fn">Function to call for Debug output</param>
public void SetDebugFunction(OutputHandler fn)
{
GetLameInstance().SetMsgFunc(t => fn(t));
}
/// <summary>Set output function for Message output</summary>
/// <param name="fn">Function to call for Message output</param>
public void SetMessageFunction(OutputHandler fn)
{
GetLameInstance().SetMsgFunc(t => fn(t));
}
/// <summary>Get configuration of LAME context, results passed to Message function</summary>
public void PrintLAMEConfig()
{
GetLameInstance().PrintConfig();
}
/// <summary>Get internal settings of LAME context, results passed to Message function</summary>
public void PrintLAMEInternals()
{
GetLameInstance().PrintInternals();
}
#endregion
#region Progress event
int _minProgressTime = 100;
/// <summary>Minimimum time between progress events in ms, or 0 for no limit</summary>
/// <remarks>Defaults to 100ms</remarks>
public int MinProgressTime
{
get { return _minProgressTime; }
set
{
_minProgressTime = Math.Max(0, value);
}
}
/// <summary>Called when data is written to the output file from Encode or Flush</summary>
public event ProgressHandler OnProgress;
DateTime _lastProgress = DateTime.Now;
/// <summary>Call any registered OnProgress handlers</summary>
/// <param name="finished">True if called at end of output</param>
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
}
}