Audio and MIDI library for .NET
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.

416 lines
19 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Runtime.InteropServices;
  6. using System.Runtime.InteropServices.ComTypes;
  7. using NAudio.MediaFoundation;
  8. using NAudio.Utils;
  9. namespace NAudio.Wave
  10. {
  11. /// <summary>
  12. /// Media Foundation Encoder class allows you to use Media Foundation to encode an IWaveProvider
  13. /// to any supported encoding format
  14. /// </summary>
  15. public class MediaFoundationEncoder : IDisposable
  16. {
  17. /// <summary>
  18. /// Queries the available bitrates for a given encoding output type, sample rate and number of channels
  19. /// </summary>
  20. /// <param name="audioSubtype">Audio subtype - a value from the AudioSubtypes class</param>
  21. /// <param name="sampleRate">The sample rate of the PCM to encode</param>
  22. /// <param name="channels">The number of channels of the PCM to encode</param>
  23. /// <returns>An array of available bitrates in average bits per second</returns>
  24. public static int[] GetEncodeBitrates(Guid audioSubtype, int sampleRate, int channels)
  25. {
  26. return GetOutputMediaTypes(audioSubtype)
  27. .Where(mt => mt.SampleRate == sampleRate && mt.ChannelCount == channels)
  28. .Select(mt => mt.AverageBytesPerSecond*8)
  29. .Distinct()
  30. .OrderBy(br => br)
  31. .ToArray();
  32. }
  33. /// <summary>
  34. /// Gets all the available media types for a particular
  35. /// </summary>
  36. /// <param name="audioSubtype">Audio subtype - a value from the AudioSubtypes class</param>
  37. /// <returns>An array of available media types that can be encoded with this subtype</returns>
  38. public static MediaType[] GetOutputMediaTypes(Guid audioSubtype)
  39. {
  40. IMFCollection availableTypes;
  41. try
  42. {
  43. MediaFoundationInterop.MFTranscodeGetAudioOutputAvailableTypes(
  44. audioSubtype, _MFT_ENUM_FLAG.MFT_ENUM_FLAG_ALL, null, out availableTypes);
  45. }
  46. catch (COMException c)
  47. {
  48. if (c.GetHResult() == MediaFoundationErrors.MF_E_NOT_FOUND)
  49. {
  50. // Don't worry if we didn't find any - just means no encoder available for this type
  51. return new MediaType[0];
  52. }
  53. else
  54. {
  55. throw;
  56. }
  57. }
  58. availableTypes.GetElementCount(out int count);
  59. var mediaTypes = new List<MediaType>(count);
  60. for (int n = 0; n < count; n++)
  61. {
  62. availableTypes.GetElement(n, out object mediaTypeObject);
  63. var mediaType = (IMFMediaType)mediaTypeObject;
  64. mediaTypes.Add(new MediaType(mediaType));
  65. }
  66. Marshal.ReleaseComObject(availableTypes);
  67. return mediaTypes.ToArray();
  68. }
  69. /// <summary>
  70. /// Helper function to simplify encoding Window Media Audio
  71. /// Should be supported on Vista and above (not tested)
  72. /// </summary>
  73. /// <param name="inputProvider">Input provider, must be PCM</param>
  74. /// <param name="outputFile">Output file path, should end with .wma</param>
  75. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  76. public static void EncodeToWma(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
  77. {
  78. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_WMAudioV8, inputProvider.WaveFormat, desiredBitRate);
  79. if (mediaType == null) throw new InvalidOperationException("No suitable WMA encoders available");
  80. using (var encoder = new MediaFoundationEncoder(mediaType))
  81. {
  82. encoder.Encode(outputFile, inputProvider);
  83. }
  84. }
  85. /// <summary>
  86. /// Helper function to simplify encoding Window Media Audio
  87. /// Should be supported on Vista and above (not tested)
  88. /// </summary>
  89. /// <param name="inputProvider">Input provider, must be PCM</param>
  90. /// <param name="outputStream">Output stream</param>
  91. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  92. public static void EncodeToWma(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000) {
  93. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_WMAudioV8, inputProvider.WaveFormat, desiredBitRate);
  94. if (mediaType == null) throw new InvalidOperationException("No suitable WMA encoders available");
  95. using (var encoder = new MediaFoundationEncoder(mediaType)) {
  96. encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_ASF);
  97. }
  98. }
  99. /// <summary>
  100. /// Helper function to simplify encoding to MP3
  101. /// By default, will only be available on Windows 8 and above
  102. /// </summary>
  103. /// <param name="inputProvider">Input provider, must be PCM</param>
  104. /// <param name="outputFile">Output file path, should end with .mp3</param>
  105. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  106. public static void EncodeToMp3(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
  107. {
  108. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_MP3, inputProvider.WaveFormat, desiredBitRate);
  109. if (mediaType == null) throw new InvalidOperationException("No suitable MP3 encoders available");
  110. using (var encoder = new MediaFoundationEncoder(mediaType))
  111. {
  112. encoder.Encode(outputFile, inputProvider);
  113. }
  114. }
  115. /// <summary>
  116. /// Helper function to simplify encoding to MP3
  117. /// By default, will only be available on Windows 8 and above
  118. /// </summary>
  119. /// <param name="inputProvider">Input provider, must be PCM</param>
  120. /// <param name="outputStream">Output stream</param>
  121. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  122. public static void EncodeToMp3(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000) {
  123. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_MP3, inputProvider.WaveFormat, desiredBitRate);
  124. if (mediaType == null) throw new InvalidOperationException("No suitable MP3 encoders available");
  125. using (var encoder = new MediaFoundationEncoder(mediaType)) {
  126. encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_MP3);
  127. }
  128. }
  129. /// <summary>
  130. /// Helper function to simplify encoding to AAC
  131. /// By default, will only be available on Windows 7 and above
  132. /// </summary>
  133. /// <param name="inputProvider">Input provider, must be PCM</param>
  134. /// <param name="outputFile">Output file path, should end with .mp4 (or .aac on Windows 8)</param>
  135. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  136. public static void EncodeToAac(IWaveProvider inputProvider, string outputFile, int desiredBitRate = 192000)
  137. {
  138. // Information on configuring an AAC media type can be found here:
  139. // http://msdn.microsoft.com/en-gb/library/windows/desktop/dd742785%28v=vs.85%29.aspx
  140. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_AAC, inputProvider.WaveFormat, desiredBitRate);
  141. if (mediaType == null) throw new InvalidOperationException("No suitable AAC encoders available");
  142. using (var encoder = new MediaFoundationEncoder(mediaType))
  143. {
  144. // should AAC container have ADTS, or is that just for ADTS?
  145. // http://www.hydrogenaudio.org/forums/index.php?showtopic=97442
  146. encoder.Encode(outputFile, inputProvider);
  147. }
  148. }
  149. /// <summary>
  150. /// Helper function to simplify encoding to AAC
  151. /// By default, will only be available on Windows 7 and above
  152. /// </summary>
  153. /// <param name="inputProvider">Input provider, must be PCM</param>
  154. /// <param name="outputStream">Output stream</param>
  155. /// <param name="desiredBitRate">Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type</param>
  156. public static void EncodeToAac(IWaveProvider inputProvider, Stream outputStream, int desiredBitRate = 192000) {
  157. // Information on configuring an AAC media type can be found here:
  158. // http://msdn.microsoft.com/en-gb/library/windows/desktop/dd742785%28v=vs.85%29.aspx
  159. var mediaType = SelectMediaType(AudioSubtypes.MFAudioFormat_AAC, inputProvider.WaveFormat, desiredBitRate);
  160. if (mediaType == null) throw new InvalidOperationException("No suitable AAC encoders available");
  161. using (var encoder = new MediaFoundationEncoder(mediaType)) {
  162. // should AAC container have ADTS, or is that just for ADTS?
  163. // http://www.hydrogenaudio.org/forums/index.php?showtopic=97442
  164. encoder.Encode(outputStream, inputProvider, TranscodeContainerTypes.MFTranscodeContainerType_MPEG4);
  165. }
  166. }
  167. /// <summary>
  168. /// Tries to find the encoding media type with the closest bitrate to that specified
  169. /// </summary>
  170. /// <param name="audioSubtype">Audio subtype, a value from AudioSubtypes</param>
  171. /// <param name="inputFormat">Your encoder input format (used to check sample rate and channel count)</param>
  172. /// <param name="desiredBitRate">Your desired bitrate</param>
  173. /// <returns>The closest media type, or null if none available</returns>
  174. public static MediaType SelectMediaType(Guid audioSubtype, WaveFormat inputFormat, int desiredBitRate)
  175. {
  176. return GetOutputMediaTypes(audioSubtype)
  177. .Where(mt => mt.SampleRate == inputFormat.SampleRate && mt.ChannelCount == inputFormat.Channels)
  178. .Select(mt => new { MediaType = mt, Delta = Math.Abs(desiredBitRate - mt.AverageBytesPerSecond * 8) } )
  179. .OrderBy(mt => mt.Delta)
  180. .Select(mt => mt.MediaType)
  181. .FirstOrDefault();
  182. }
  183. private readonly MediaType outputMediaType;
  184. private bool disposed;
  185. /// <summary>
  186. /// Creates a new encoder that encodes to the specified output media type
  187. /// </summary>
  188. /// <param name="outputMediaType">Desired output media type</param>
  189. public MediaFoundationEncoder(MediaType outputMediaType)
  190. {
  191. if (outputMediaType == null) throw new ArgumentNullException("outputMediaType");
  192. this.outputMediaType = outputMediaType;
  193. }
  194. /// <summary>
  195. /// Encodes a file
  196. /// </summary>
  197. /// <param name="outputFile">Output filename (container type is deduced from the filename)</param>
  198. /// <param name="inputProvider">Input provider (should be PCM, some encoders will also allow IEEE float)</param>
  199. public void Encode(string outputFile, IWaveProvider inputProvider)
  200. {
  201. if (inputProvider.WaveFormat.Encoding != WaveFormatEncoding.Pcm && inputProvider.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
  202. {
  203. throw new ArgumentException("Encode input format must be PCM or IEEE float");
  204. }
  205. var inputMediaType = new MediaType(inputProvider.WaveFormat);
  206. var writer = CreateSinkWriter(outputFile);
  207. try
  208. {
  209. writer.AddStream(outputMediaType.MediaFoundationObject, out int streamIndex);
  210. // n.b. can get 0xC00D36B4 - MF_E_INVALIDMEDIATYPE here
  211. writer.SetInputMediaType(streamIndex, inputMediaType.MediaFoundationObject, null);
  212. PerformEncode(writer, streamIndex, inputProvider);
  213. }
  214. finally
  215. {
  216. if (writer != null)
  217. {
  218. Marshal.ReleaseComObject(writer);
  219. }
  220. if (inputMediaType.MediaFoundationObject != null)
  221. {
  222. Marshal.ReleaseComObject(inputMediaType.MediaFoundationObject);
  223. }
  224. }
  225. }
  226. /// <summary>
  227. /// Encodes a file
  228. /// </summary>
  229. /// <param name="outputStream">Output stream</param>
  230. /// <param name="inputProvider">Input provider (should be PCM, some encoders will also allow IEEE float)</param>
  231. /// <param name="transcodeContainerType">One of <see cref="TranscodeContainerTypes"/></param>
  232. public void Encode(Stream outputStream, IWaveProvider inputProvider, Guid transcodeContainerType)
  233. {
  234. if (inputProvider.WaveFormat.Encoding != WaveFormatEncoding.Pcm && inputProvider.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
  235. {
  236. throw new ArgumentException("Encode input format must be PCM or IEEE float");
  237. }
  238. var inputMediaType = new MediaType(inputProvider.WaveFormat);
  239. var writer = CreateSinkWriter(new ComStream(outputStream), transcodeContainerType);
  240. try
  241. {
  242. writer.AddStream(outputMediaType.MediaFoundationObject, out int streamIndex);
  243. // n.b. can get 0xC00D36B4 - MF_E_INVALIDMEDIATYPE here
  244. writer.SetInputMediaType(streamIndex, inputMediaType.MediaFoundationObject, null);
  245. PerformEncode(writer, streamIndex, inputProvider);
  246. }
  247. finally
  248. {
  249. if (writer != null)
  250. {
  251. Marshal.ReleaseComObject(writer);
  252. }
  253. if (inputMediaType.MediaFoundationObject != null)
  254. {
  255. Marshal.ReleaseComObject(inputMediaType.MediaFoundationObject);
  256. }
  257. }
  258. }
  259. private static IMFSinkWriter CreateSinkWriter(string outputFile)
  260. {
  261. // n.b. could try specifying the container type using attributes, but I think
  262. // it does a decent job of working it out from the file extension
  263. // n.b. AAC encode on Win 8 can have AAC extension, but use MP4 in win 7
  264. // http://msdn.microsoft.com/en-gb/library/windows/desktop/dd389284%28v=vs.85%29.aspx
  265. IMFSinkWriter writer;
  266. var attributes = MediaFoundationApi.CreateAttributes(1);
  267. attributes.SetUINT32(MediaFoundationAttributes.MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, 1);
  268. try
  269. {
  270. MediaFoundationInterop.MFCreateSinkWriterFromURL(outputFile, null, attributes, out writer);
  271. }
  272. catch (COMException e)
  273. {
  274. if (e.GetHResult() == MediaFoundationErrors.MF_E_NOT_FOUND)
  275. {
  276. throw new ArgumentException("Was not able to create a sink writer for this file extension");
  277. }
  278. throw;
  279. }
  280. finally
  281. {
  282. Marshal.ReleaseComObject(attributes);
  283. }
  284. return writer;
  285. }
  286. private static IMFSinkWriter CreateSinkWriter(IStream outputStream, Guid TranscodeContainerType)
  287. {
  288. // n.b. could try specifying the container type using attributes, but I think
  289. // it does a decent job of working it out from the file extension
  290. // n.b. AAC encode on Win 8 can have AAC extension, but use MP4 in win 7
  291. // http://msdn.microsoft.com/en-gb/library/windows/desktop/dd389284%28v=vs.85%29.aspx
  292. IMFSinkWriter writer;
  293. var attributes = MediaFoundationApi.CreateAttributes(1);
  294. attributes.SetGUID(MediaFoundationAttributes.MF_TRANSCODE_CONTAINERTYPE, TranscodeContainerType);
  295. try
  296. {
  297. MediaFoundationInterop.MFCreateMFByteStreamOnStream(outputStream, out var ppByteStream);
  298. MediaFoundationInterop.MFCreateSinkWriterFromURL(null, ppByteStream, attributes, out writer);
  299. }
  300. finally
  301. {
  302. Marshal.ReleaseComObject(attributes);
  303. }
  304. return writer;
  305. }
  306. private void PerformEncode(IMFSinkWriter writer, int streamIndex, IWaveProvider inputProvider)
  307. {
  308. int maxLength = inputProvider.WaveFormat.AverageBytesPerSecond * 4;
  309. var managedBuffer = new byte[maxLength];
  310. writer.BeginWriting();
  311. long position = 0;
  312. long duration;
  313. do
  314. {
  315. duration = ConvertOneBuffer(writer, streamIndex, inputProvider, position, managedBuffer);
  316. position += duration;
  317. } while (duration > 0);
  318. writer.DoFinalize();
  319. }
  320. private static long BytesToNsPosition(int bytes, WaveFormat waveFormat)
  321. {
  322. long nsPosition = (10000000L * bytes) / waveFormat.AverageBytesPerSecond;
  323. return nsPosition;
  324. }
  325. private long ConvertOneBuffer(IMFSinkWriter writer, int streamIndex, IWaveProvider inputProvider, long position, byte[] managedBuffer)
  326. {
  327. long durationConverted = 0;
  328. IMFMediaBuffer buffer = MediaFoundationApi.CreateMemoryBuffer(managedBuffer.Length);
  329. buffer.GetMaxLength(out var maxLength);
  330. IMFSample sample = MediaFoundationApi.CreateSample();
  331. sample.AddBuffer(buffer);
  332. buffer.Lock(out var ptr, out maxLength, out int currentLength);
  333. int read = inputProvider.Read(managedBuffer, 0, maxLength);
  334. if (read > 0)
  335. {
  336. durationConverted = BytesToNsPosition(read, inputProvider.WaveFormat);
  337. Marshal.Copy(managedBuffer, 0, ptr, read);
  338. buffer.SetCurrentLength(read);
  339. buffer.Unlock();
  340. sample.SetSampleTime(position);
  341. sample.SetSampleDuration(durationConverted);
  342. writer.WriteSample(streamIndex, sample);
  343. //writer.Flush(streamIndex);
  344. }
  345. else
  346. {
  347. buffer.Unlock();
  348. }
  349. Marshal.ReleaseComObject(sample);
  350. Marshal.ReleaseComObject(buffer);
  351. return durationConverted;
  352. }
  353. /// <summary>
  354. /// Disposes this instance
  355. /// </summary>
  356. /// <param name="disposing"></param>
  357. protected void Dispose(bool disposing)
  358. {
  359. Marshal.ReleaseComObject(outputMediaType.MediaFoundationObject);
  360. }
  361. /// <summary>
  362. /// Disposes this instance
  363. /// </summary>
  364. public void Dispose()
  365. {
  366. if (!disposed)
  367. {
  368. disposed = true;
  369. Dispose(true);
  370. }
  371. GC.SuppressFinalize(this);
  372. }
  373. /// <summary>
  374. /// Finalizer
  375. /// </summary>
  376. ~MediaFoundationEncoder()
  377. {
  378. Dispose(false);
  379. }
  380. }
  381. }