419 lines
20 KiB
C#
419 lines
20 KiB
C#
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.IO;
|
|||
|
using System.Linq;
|
|||
|
using System.Runtime.InteropServices;
|
|||
|
using System.Runtime.InteropServices.ComTypes;
|
|||
|
using NAudio.MediaFoundation;
|
|||
|
using NAudio.Utils;
|
|||
|
|
|||
|
namespace NAudio.Wave
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Media Foundation Encoder class allows you to use Media Foundation to encode an IWaveProvider
|
|||
|
/// to any supported encoding format
|
|||
|
/// </summary>
|
|||
|
public class MediaFoundationEncoder : IDisposable
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Queries the available bitrates for a given encoding output type, sample rate and number of channels
|
|||
|
/// </summary>
|
|||
|
/// <param name="audioSubtype">Audio subtype - a value from the AudioSubtypes class</param>
|
|||
|
/// <param name="sampleRate">The sample rate of the PCM to encode</param>
|
|||
|
/// <param name="channels">The number of channels of the PCM to encode</param>
|
|||
|
/// <returns>An array of available bitrates in average bits per second</returns>
|
|||
|
public static int[] GetEncodeBitrates(Guid audioSubtype, int sampleRate, int channels)
|
|||
|
{
|
|||
|
return GetOutputMediaTypes(audioSubtype)
|
|||
|
.Where(mt => mt.SampleRate == sampleRate && mt.ChannelCount == channels)
|
|||
|
.Select(mt => mt.AverageBytesPerSecond*8)
|
|||
|
.Distinct()
|
|||
|
.OrderBy(br => br)
|
|||
|
.ToArray();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets all the available media types for a particular
|
|||
|
/// </summary>
|
|||
|
/// <param name="audioSubtype">Audio subtype - a value from the AudioSubtypes class</param>
|
|||
|
/// <returns>An array of available media types that can be encoded with this subtype</returns>
|
|||
|
public static MediaType[] GetOutputMediaTypes(Guid audioSubtype)
|
|||
|
{
|
|||
|
MediaFoundationApi.Startup();
|
|||
|
IMFCollection availableTypes;
|
|||
|
try
|
|||
|
{
|
|||
|
MediaFoundationInterop.MFTranscodeGetAudioOutputAvailableTypes(
|
|||
|
audioSubtype, _MFT_ENUM_FLAG.MFT_ENUM_FLAG_ALL, null, out availableTypes);
|
|||
|
}
|
|||
|
catch (COMException c)
|
|||
|
{
|
|||
|
if (c.GetHResult() == MediaFoundationErrors.MF_E_NOT_FOUND)
|
|||
|
{
|
|||
|
// Don't worry if we didn't find any - just means no encoder available for this type
|
|||
|
return new MediaType[0];
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
throw;
|
|||
|
}
|
|||
|
}
|
|||
|
availableTypes.GetElementCount(out int count);
|
|||
|
var mediaTypes = new List<MediaType>(count);
|
|||
|
for (int n = 0; n < count; n++)
|
|||
|
{
|
|||
|
availableTypes.GetElement(n, out object mediaTypeObject);
|
|||
|
var mediaType = (IMFMediaType)mediaTypeObject;
|
|||
|
mediaTypes.Add(new MediaType(mediaType));
|
|||
|
}
|
|||
|
Marshal.ReleaseComObject(availableTypes);
|
|||
|
return mediaTypes.ToArray();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding Window Media Audio
|
|||
|
/// Should be supported on Vista and above (not tested)
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputFile">Output file path, should end with .wma</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToWma(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
|
|||
|
{
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_WMAudioV8, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable WMA encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType))
|
|||
|
{
|
|||
|
encoder.Encode(outputFile, inputProvider);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding Window Media Audio
|
|||
|
/// Should be supported on Vista and above (not tested)
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputStream">Output stream</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToWma(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000) {
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_WMAudioV8, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable WMA encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType)) {
|
|||
|
encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_ASF);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding to MP3
|
|||
|
/// By default, will only be available on Windows 8 and above
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputFile">Output file path, should end with .mp3</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToMp3(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
|
|||
|
{
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_MP3, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable MP3 encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType))
|
|||
|
{
|
|||
|
encoder.Encode(outputFile, inputProvider);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding to MP3
|
|||
|
/// By default, will only be available on Windows 8 and above
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputStream">Output stream</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToMp3(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000)
|
|||
|
{
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_MP3, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable MP3 encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType)) {
|
|||
|
encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_MP3);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding to AAC
|
|||
|
/// By default, will only be available on Windows 7 and above
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputFile">Output file path, should end with .mp4 (or .aac on Windows 8)</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToAac(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
|
|||
|
{
|
|||
|
// Information on configuring an AAC media type can be found here:
|
|||
|
// http://msdn.microsoft.com/en-gb/library/windows/desktop/dd742785%28v=vs.85%29.aspx
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_AAC, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable AAC encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType))
|
|||
|
{
|
|||
|
// should AAC container have ADTS, or is that just for ADTS?
|
|||
|
// http://www.hydrogenaudio.org/forums/index.php?showtopic=97442
|
|||
|
encoder.Encode(outputFile, inputProvider);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper function to simplify encoding to AAC
|
|||
|
/// By default, will only be available on Windows 7 and above
|
|||
|
/// </summary>
|
|||
|
/// <param name="inputProvider">Input provider, must be PCM</param>
|
|||
|
/// <param name="outputStream">Output stream</param>
|
|||
|
/// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
|
|||
|
public static void EncodeToAac(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000) {
|
|||
|
// Information on configuring an AAC media type can be found here:
|
|||
|
// http://msdn.microsoft.com/en-gb/library/windows/desktop/dd742785%28v=vs.85%29.aspx
|
|||
|
var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_AAC, inputProvider.WaveFormat, desiredBitRate);
|
|||
|
if (mediaType == null) throw new InvalidOperationException("No suitable AAC encoders available");
|
|||
|
using (var encoder = new MediaFoundationEncoder(mediaType)) {
|
|||
|
// should AAC container have ADTS, or is that just for ADTS?
|
|||
|
// http://www.hydrogenaudio.org/forums/index.php?showtopic=97442
|
|||
|
encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_MPEG4);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Tries to find the encoding media type with the closest bitrate to that specified
|
|||
|
/// </summary>
|
|||
|
/// <param name="audioSubtype">Audio subtype, a value from AudioSubtypes</param>
|
|||
|
/// <param name="inputFormat">Your encoder input format (used to check sample rate and channel count)</param>
|
|||
|
/// <param name="desiredBitRate">Your desired bitrate</param>
|
|||
|
/// <returns>The closest media type, or null if none available</returns>
|
|||
|
public static MediaType SelectMediaType(Guid audioSubtype, WaveFormat inputFormat, int desiredBitRate)
|
|||
|
{
|
|||
|
MediaFoundationApi.Startup();
|
|||
|
return GetOutputMediaTypes(audioSubtype)
|
|||
|
.Where(mt => mt.SampleRate == inputFormat.SampleRate && mt.ChannelCount == inputFormat.Channels)
|
|||
|
.Select(mt => new { MediaType = mt, Delta = Math.Abs(desiredBitRate - mt.AverageBytesPerSecond * 8) } )
|
|||
|
.OrderBy(mt => mt.Delta)
|
|||
|
.Select(mt => mt.MediaType)
|
|||
|
.FirstOrDefault();
|
|||
|
}
|
|||
|
|
|||
|
public int DefaultReadBufferSize { get; set; }
|
|||
|
private readonly MediaType outputMediaType;
|
|||
|
private bool disposed;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Creates a new encoder that encodes to the specified output media type
|
|||
|
/// </summary>
|
|||
|
/// <param name="outputMediaType">Desired output media type</param>
|
|||
|
public MediaFoundationEncoder(MediaType outputMediaType)
|
|||
|
{
|
|||
|
if (outputMediaType == null) throw new ArgumentNullException("outputMediaType");
|
|||
|
this.outputMediaType = outputMediaType;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Encodes a file
|
|||
|
/// </summary>
|
|||
|
/// <param name="outputFile">Output filename (container type is deduced from the filename)</param>
|
|||
|
/// <param name="inputProvider">Input provider (should be PCM, some encoders will also allow IEEE float)</param>
|
|||
|
public void Encode(string outputFile, IWaveProvider inputProvider)
|
|||
|
{
|
|||
|
if (inputProvider.WaveFormat.Encoding != WaveFormatEncoding.Pcm && inputProvider.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
|
|||
|
{
|
|||
|
throw new ArgumentException("Encode input format must be PCM or IEEE float");
|
|||
|
}
|
|||
|
|
|||
|
var inputMediaType = new MediaType(inputProvider.WaveFormat);
|
|||
|
|
|||
|
var writer = CreateSinkWriter(outputFile);
|
|||
|
try
|
|||
|
{
|
|||
|
writer.AddStream(outputMediaType.MediaFoundationObject, out int streamIndex);
|
|||
|
|
|||
|
// n.b. can get 0xC00D36B4 - MF_E_INVALIDMEDIATYPE here
|
|||
|
writer.SetInputMediaType(streamIndex, inputMediaType.MediaFoundationObject, null);
|
|||
|
|
|||
|
PerformEncode(writer, streamIndex, inputProvider);
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
if (writer != null)
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(writer);
|
|||
|
}
|
|||
|
if (inputMediaType.MediaFoundationObject != null)
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(inputMediaType.MediaFoundationObject);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Encodes a file
|
|||
|
/// </summary>
|
|||
|
/// <param name="outputStream">Output stream</param>
|
|||
|
/// <param name="inputProvider">Input provider (should be PCM, some encoders will also allow IEEE float)</param>
|
|||
|
/// <param name="transcodeContainerType">One of <see cref="TranscodeContainerTypes"/></param>
|
|||
|
public void Encode(Stream outputStream, IWaveProvider inputProvider, Guid transcodeContainerType)
|
|||
|
{
|
|||
|
if (inputProvider.WaveFormat.Encoding != WaveFormatEncoding.Pcm && inputProvider.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
|
|||
|
{
|
|||
|
throw new ArgumentException("Encode input format must be PCM or IEEE float");
|
|||
|
}
|
|||
|
|
|||
|
var inputMediaType = new MediaType(inputProvider.WaveFormat);
|
|||
|
|
|||
|
var writer = CreateSinkWriter(new ComStream(outputStream), transcodeContainerType);
|
|||
|
try
|
|||
|
{
|
|||
|
writer.AddStream(outputMediaType.MediaFoundationObject, out int streamIndex);
|
|||
|
|
|||
|
// n.b. can get 0xC00D36B4 - MF_E_INVALIDMEDIATYPE here
|
|||
|
writer.SetInputMediaType(streamIndex, inputMediaType.MediaFoundationObject, null);
|
|||
|
|
|||
|
PerformEncode(writer, streamIndex, inputProvider);
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
if (writer != null)
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(writer);
|
|||
|
}
|
|||
|
if (inputMediaType.MediaFoundationObject != null)
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(inputMediaType.MediaFoundationObject);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static IMFSinkWriter CreateSinkWriter(string outputFile)
|
|||
|
{
|
|||
|
// n.b. could try specifying the container type using attributes, but I think
|
|||
|
// it does a decent job of working it out from the file extension
|
|||
|
// n.b. AAC encode on Win 8 can have AAC extension, but use MP4 in win 7
|
|||
|
// http://msdn.microsoft.com/en-gb/library/windows/desktop/dd389284%28v=vs.85%29.aspx
|
|||
|
IMFSinkWriter writer;
|
|||
|
var attributes = MediaFoundationApi.CreateAttributes(1);
|
|||
|
attributes.SetUINT32(MediaFoundationAttributes.MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, 1);
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
MediaFoundationInterop.MFCreateSinkWriterFromURL(outputFile, null, attributes, out writer);
|
|||
|
}
|
|||
|
catch (COMException e)
|
|||
|
{
|
|||
|
if (e.GetHResult() == MediaFoundationErrors.MF_E_NOT_FOUND)
|
|||
|
{
|
|||
|
throw new ArgumentException("Was not able to create a sink writer for this file extension");
|
|||
|
}
|
|||
|
throw;
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(attributes);
|
|||
|
}
|
|||
|
return writer;
|
|||
|
}
|
|||
|
|
|||
|
private static IMFSinkWriter CreateSinkWriter(IStream outputStream, Guid TranscodeContainerType)
|
|||
|
{
|
|||
|
// n.b. could try specifying the container type using attributes, but I think
|
|||
|
// it does a decent job of working it out from the file extension
|
|||
|
// n.b. AAC encode on Win 8 can have AAC extension, but use MP4 in win 7
|
|||
|
// http://msdn.microsoft.com/en-gb/library/windows/desktop/dd389284%28v=vs.85%29.aspx
|
|||
|
IMFSinkWriter writer;
|
|||
|
var attributes = MediaFoundationApi.CreateAttributes(1);
|
|||
|
attributes.SetGUID(MediaFoundationAttributes.MF_TRANSCODE_CONTAINERTYPE, TranscodeContainerType);
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
MediaFoundationInterop.MFCreateMFByteStreamOnStream(outputStream, out var ppByteStream);
|
|||
|
MediaFoundationInterop.MFCreateSinkWriterFromURL(null, ppByteStream, attributes, out writer);
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(attributes);
|
|||
|
}
|
|||
|
return writer;
|
|||
|
}
|
|||
|
|
|||
|
private void PerformEncode(IMFSinkWriter writer, int streamIndex, IWaveProvider inputProvider)
|
|||
|
{
|
|||
|
if (DefaultReadBufferSize== 0) DefaultReadBufferSize = inputProvider.WaveFormat.AverageBytesPerSecond * 4;
|
|||
|
var managedBuffer = new byte[DefaultReadBufferSize];
|
|||
|
|
|||
|
writer.BeginWriting();
|
|||
|
|
|||
|
long position = 0;
|
|||
|
long duration;
|
|||
|
do
|
|||
|
{
|
|||
|
duration = ConvertOneBuffer(writer, streamIndex, inputProvider, position, managedBuffer);
|
|||
|
position += duration;
|
|||
|
} while (duration > 0);
|
|||
|
|
|||
|
writer.DoFinalize();
|
|||
|
}
|
|||
|
|
|||
|
private static long BytesToNsPosition(int bytes, WaveFormat waveFormat)
|
|||
|
{
|
|||
|
long nsPosition = (10000000L * bytes) / waveFormat.AverageBytesPerSecond;
|
|||
|
return nsPosition;
|
|||
|
}
|
|||
|
|
|||
|
private long ConvertOneBuffer(IMFSinkWriter writer, int streamIndex, IWaveProvider inputProvider, long position, byte[] managedBuffer)
|
|||
|
{
|
|||
|
long durationConverted = 0;
|
|||
|
IMFMediaBuffer buffer = MediaFoundationApi.CreateMemoryBuffer(managedBuffer.Length);
|
|||
|
buffer.GetMaxLength(out var maxLength);
|
|||
|
|
|||
|
IMFSample sample = MediaFoundationApi.CreateSample();
|
|||
|
sample.AddBuffer(buffer);
|
|||
|
|
|||
|
int read = inputProvider.Read(managedBuffer, 0, maxLength);
|
|||
|
if (read > 0)
|
|||
|
{
|
|||
|
buffer.Lock(out var ptr, out maxLength, out int currentLength);
|
|||
|
durationConverted = BytesToNsPosition(read, inputProvider.WaveFormat);
|
|||
|
Marshal.Copy(managedBuffer, 0, ptr, read);
|
|||
|
buffer.SetCurrentLength(read);
|
|||
|
buffer.Unlock();
|
|||
|
sample.SetSampleTime(position);
|
|||
|
sample.SetSampleDuration(durationConverted);
|
|||
|
writer.WriteSample(streamIndex, sample);
|
|||
|
//writer.Flush(streamIndex);
|
|||
|
}
|
|||
|
|
|||
|
Marshal.ReleaseComObject(sample);
|
|||
|
Marshal.ReleaseComObject(buffer);
|
|||
|
return durationConverted;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Disposes this instance
|
|||
|
/// </summary>
|
|||
|
/// <param name="disposing"></param>
|
|||
|
protected void Dispose(bool disposing)
|
|||
|
{
|
|||
|
Marshal.ReleaseComObject(outputMediaType.MediaFoundationObject);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Disposes this instance
|
|||
|
/// </summary>
|
|||
|
public void Dispose()
|
|||
|
{
|
|||
|
if (!disposed)
|
|||
|
{
|
|||
|
disposed = true;
|
|||
|
Dispose(true);
|
|||
|
}
|
|||
|
GC.SuppressFinalize(this);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finalizer
|
|||
|
/// </summary>
|
|||
|
~MediaFoundationEncoder()
|
|||
|
{
|
|||
|
Dispose(false);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|