using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using NAudio.CoreAudioApi.Interfaces;
using NAudio.MediaFoundation;
using NAudio.Utils;
// ReSharper disable once CheckNamespace
namespace NAudio.Wave
{
///
/// Class for reading any file that Media Foundation can play
/// Will only work in Windows Vista and above
/// Automatically converts to PCM
/// If it is a video file with multiple audio streams, it will pick out the first audio stream
///
public class MediaFoundationReader : WaveStream
{
private WaveFormat waveFormat;
private long length;
private MediaFoundationReaderSettings settings;
private readonly string file;
private IMFSourceReader pReader;
private long position;
///
/// Allows customisation of this reader class
///
public class MediaFoundationReaderSettings
{
///
/// Sets up the default settings for MediaFoundationReader
///
public MediaFoundationReaderSettings()
{
RepositionInRead = true;
}
///
/// Allows us to request IEEE float output (n.b. no guarantee this will be accepted)
///
public bool RequestFloatOutput { get; set; }
///
/// If true, the reader object created in the constructor is used in Read
/// Should only be set to true if you are working entirely on an STA thread, or
/// entirely with MTA threads.
///
public bool SingleReaderObject { get; set; }
///
/// If true, the reposition does not happen immediately, but waits until the
/// next call to read to be processed.
///
public bool RepositionInRead { get; set; }
}
///
/// Default constructor
///
protected MediaFoundationReader()
{
}
///
/// Creates a new MediaFoundationReader based on the supplied file
///
/// Filename (can also be a URL e.g. http:// mms:// file://)
public MediaFoundationReader(string file)
: this(file, null)
{
}
///
/// Creates a new MediaFoundationReader based on the supplied file
///
/// Filename
/// Advanced settings
public MediaFoundationReader(string file, MediaFoundationReaderSettings settings)
{
this.file = file;
Init(settings);
}
///
/// Initializes
///
protected void Init(MediaFoundationReaderSettings initialSettings)
{
MediaFoundationApi.Startup();
settings = initialSettings ?? new MediaFoundationReaderSettings();
var reader = CreateReader(settings);
waveFormat = GetCurrentWaveFormat(reader);
reader.SetStreamSelection(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true);
length = GetLength(reader);
if (settings.SingleReaderObject)
{
pReader = reader;
}
else
{
Marshal.ReleaseComObject(reader);
}
}
private WaveFormat GetCurrentWaveFormat(IMFSourceReader reader)
{
reader.GetCurrentMediaType(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, out IMFMediaType uncompressedMediaType);
// Two ways to query it, first is to ask for properties (second is to convert into WaveFormatEx using MFCreateWaveFormatExFromMFMediaType)
var outputMediaType = new MediaType(uncompressedMediaType);
Guid actualMajorType = outputMediaType.MajorType;
Debug.Assert(actualMajorType == MediaTypes.MFMediaType_Audio);
Guid audioSubType = outputMediaType.SubType;
int channels = outputMediaType.ChannelCount;
int bits = outputMediaType.BitsPerSample;
int sampleRate = outputMediaType.SampleRate;
if (audioSubType == AudioSubtypes.MFAudioFormat_PCM)
return new WaveFormat(sampleRate, bits, channels);
if (audioSubType == AudioSubtypes.MFAudioFormat_Float)
return WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
var subTypeDescription = FieldDescriptionHelper.Describe(typeof (AudioSubtypes), audioSubType);
throw new InvalidDataException($"Unsupported audio sub Type {subTypeDescription}");
}
private static MediaType GetCurrentMediaType(IMFSourceReader reader)
{
reader.GetCurrentMediaType(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, out IMFMediaType mediaType);
return new MediaType(mediaType);
}
///
/// Creates the reader (overridable by )
///
protected virtual IMFSourceReader CreateReader(MediaFoundationReaderSettings settings)
{
IMFSourceReader reader;
MediaFoundationInterop.MFCreateSourceReaderFromURL(file, null, out reader);
reader.SetStreamSelection(MediaFoundationInterop.MF_SOURCE_READER_ALL_STREAMS, false);
reader.SetStreamSelection(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true);
// Create a partial media type indicating that we want uncompressed PCM audio
var partialMediaType = new MediaType();
partialMediaType.MajorType = MediaTypes.MFMediaType_Audio;
partialMediaType.SubType = settings.RequestFloatOutput ? AudioSubtypes.MFAudioFormat_Float : AudioSubtypes.MFAudioFormat_PCM;
var currentMediaType = GetCurrentMediaType(reader);
// mono, low sample rate files can go wrong on Windows 10 unless we specify here
partialMediaType.ChannelCount = currentMediaType.ChannelCount;
partialMediaType.SampleRate = currentMediaType.SampleRate;
try
{
// set the media type
// can return MF_E_INVALIDMEDIATYPE if not supported
reader.SetCurrentMediaType(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, partialMediaType.MediaFoundationObject);
}
catch (COMException ex) when (ex.GetHResult() == MediaFoundationErrors.MF_E_INVALIDMEDIATYPE)
{
// HE-AAC (and v2) seems to halve the samplerate
if (currentMediaType.SubType == AudioSubtypes.MFAudioFormat_AAC && currentMediaType.ChannelCount == 1)
{
partialMediaType.SampleRate = currentMediaType.SampleRate *= 2;
partialMediaType.ChannelCount = currentMediaType.ChannelCount *= 2;
reader.SetCurrentMediaType(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, partialMediaType.MediaFoundationObject);
}
else { throw; }
}
Marshal.ReleaseComObject(currentMediaType.MediaFoundationObject);
return reader;
}
private long GetLength(IMFSourceReader reader)
{
var variantPtr = Marshal.AllocHGlobal(Marshal.SizeOf());
try
{
// http://msdn.microsoft.com/en-gb/library/windows/desktop/dd389281%28v=vs.85%29.aspx#getting_file_duration
int hResult = reader.GetPresentationAttribute(MediaFoundationInterop.MF_SOURCE_READER_MEDIASOURCE,
MediaFoundationAttributes.MF_PD_DURATION, variantPtr);
if (hResult == MediaFoundationErrors.MF_E_ATTRIBUTENOTFOUND)
{
// this doesn't support telling us its duration (might be streaming)
return 0;
}
if (hResult != 0)
{
Marshal.ThrowExceptionForHR(hResult);
}
var variant = Marshal.PtrToStructure(variantPtr);
var lengthInBytes = (((long)variant.Value) * waveFormat.AverageBytesPerSecond) / 10000000L;
return lengthInBytes;
}
finally
{
PropVariant.Clear(variantPtr);
Marshal.FreeHGlobal(variantPtr);
}
}
private byte[] decoderOutputBuffer;
private int decoderOutputOffset;
private int decoderOutputCount;
private void EnsureBuffer(int bytesRequired)
{
if (decoderOutputBuffer == null || decoderOutputBuffer.Length < bytesRequired)
{
decoderOutputBuffer = new byte[bytesRequired];
}
}
///
/// Reads from this wave stream
///
/// Buffer to read into
/// Offset in buffer
/// Bytes required
/// Number of bytes read; 0 indicates end of stream
public override int Read(byte[] buffer, int offset, int count)
{
if (pReader == null)
{
pReader = CreateReader(settings);
}
if (repositionTo != -1)
{
Reposition(repositionTo);
}
int bytesWritten = 0;
// read in any leftovers from last time
if (decoderOutputCount > 0)
{
bytesWritten += ReadFromDecoderBuffer(buffer, offset, count - bytesWritten);
}
while (bytesWritten < count)
{
pReader.ReadSample(MediaFoundationInterop.MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0,
out int actualStreamIndex, out MF_SOURCE_READER_FLAG dwFlags, out ulong timestamp, out IMFSample pSample);
if ((dwFlags & MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_ENDOFSTREAM) != 0)
{
// reached the end of the stream
break;
}
else if ((dwFlags & MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) != 0)
{
waveFormat = GetCurrentWaveFormat(pReader);
OnWaveFormatChanged();
// carry on, but user must handle the change of format
}
else if (dwFlags != 0)
{
throw new InvalidOperationException($"MediaFoundationReadError {dwFlags}");
}
pSample.ConvertToContiguousBuffer(out IMFMediaBuffer pBuffer);
pBuffer.Lock(out IntPtr pAudioData, out int pcbMaxLength, out int cbBuffer);
EnsureBuffer(cbBuffer);
Marshal.Copy(pAudioData, decoderOutputBuffer, 0, cbBuffer);
decoderOutputOffset = 0;
decoderOutputCount = cbBuffer;
bytesWritten += ReadFromDecoderBuffer(buffer, offset + bytesWritten, count - bytesWritten);
pBuffer.Unlock();
Marshal.ReleaseComObject(pBuffer);
Marshal.ReleaseComObject(pSample);
}
position += bytesWritten;
return bytesWritten;
}
private int ReadFromDecoderBuffer(byte[] buffer, int offset, int needed)
{
int bytesFromDecoderOutput = Math.Min(needed, decoderOutputCount);
Array.Copy(decoderOutputBuffer, decoderOutputOffset, buffer, offset, bytesFromDecoderOutput);
decoderOutputOffset += bytesFromDecoderOutput;
decoderOutputCount -= bytesFromDecoderOutput;
if (decoderOutputCount == 0)
{
decoderOutputOffset = 0;
}
return bytesFromDecoderOutput;
}
///
/// WaveFormat of this stream (n.b. this is after converting to PCM)
///
public override WaveFormat WaveFormat
{
get { return waveFormat; }
}
///
/// The bytesRequired of this stream in bytes (n.b may not be accurate)
///
public override long Length
{
get
{
return length;
}
}
///
/// Current position within this stream
///
public override long Position
{
get { return position; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("value", "Position cannot be less than 0");
if (settings.RepositionInRead)
{
repositionTo = value;
position = value; // for gui apps, make it look like we have alread processed the reposition
}
else
{
Reposition(value);
}
}
}
private long repositionTo = -1;
private void Reposition(long desiredPosition)
{
long nsPosition = (10000000L * repositionTo) / waveFormat.AverageBytesPerSecond;
var pv = PropVariant.FromLong(nsPosition);
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(pv));
try
{
Marshal.StructureToPtr(pv, ptr, false);
// should pass in a variant of type VT_I8 which is a long containing time in 100nanosecond units
pReader.SetCurrentPosition(Guid.Empty, ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
decoderOutputCount = 0;
decoderOutputOffset = 0;
position = desiredPosition;
repositionTo = -1;// clear the flag
}
///
/// Cleans up after finishing with this reader
///
/// true if called from Dispose
protected override void Dispose(bool disposing)
{
if (pReader != null)
{
Marshal.ReleaseComObject(pReader);
pReader = null;
}
base.Dispose(disposing);
}
///
/// WaveFormat has changed
///
public event EventHandler WaveFormatChanged;
private void OnWaveFormatChanged()
{
var handler = WaveFormatChanged;
if (handler != null) handler(this, EventArgs.Empty);
}
}
}