using System; using System.Runtime.InteropServices; using NAudio.Utils; using NAudio.Wave; namespace NAudio.MediaFoundation { /// /// An abstract base class for simplifying working with Media Foundation Transforms /// You need to override the method that actually creates and configures the transform /// public abstract class MediaFoundationTransform : IWaveProvider, IDisposable { /// /// The Source Provider /// protected readonly IWaveProvider sourceProvider; /// /// The Output WaveFormat /// protected readonly WaveFormat outputWaveFormat; private readonly byte[] sourceBuffer; private byte[] outputBuffer; private int outputBufferOffset; private int outputBufferCount; private IMFTransform transform; private bool disposed; private long inputPosition; // in ref-time, so we can timestamp the input samples private long outputPosition; // also in ref-time private bool initializedForStreaming; /// /// Constructs a new MediaFoundationTransform wrapper /// Will read one second at a time /// /// The source provider for input data to the transform /// The desired output format public MediaFoundationTransform(IWaveProvider sourceProvider, WaveFormat outputFormat) { this.outputWaveFormat = outputFormat; this.sourceProvider = sourceProvider; sourceBuffer = new byte[sourceProvider.WaveFormat.AverageBytesPerSecond]; outputBuffer = new byte[outputWaveFormat.AverageBytesPerSecond + outputWaveFormat.BlockAlign]; // we will grow this buffer if needed, but try to make something big enough } private void InitializeTransformForStreaming() { transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_COMMAND_FLUSH, IntPtr.Zero); transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, IntPtr.Zero); transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_START_OF_STREAM, IntPtr.Zero); initializedForStreaming = true; } /// /// To be implemented by overriding classes. Create the transform object, set up its input and output types, /// and configure any custom properties in here /// /// An object implementing IMFTrasform protected abstract IMFTransform CreateTransform(); /// /// Disposes this MediaFoundation transform /// protected virtual void Dispose(bool disposing) { if (transform != null) { Marshal.ReleaseComObject(transform); } } /// /// Disposes this Media Foundation Transform /// public void Dispose() { if (!disposed) { disposed = true; Dispose(true); GC.SuppressFinalize(this); } } /// /// Destructor /// ~MediaFoundationTransform() { Dispose(false); } /// /// The output WaveFormat of this Media Foundation Transform /// public WaveFormat WaveFormat { get { return outputWaveFormat; } } /// /// Reads data out of the source, passing it through the transform /// /// Output buffer /// Offset within buffer to write to /// Desired byte count /// Number of bytes read public int Read(byte[] buffer, int offset, int count) { if (transform == null) { transform = CreateTransform(); InitializeTransformForStreaming(); } // strategy will be to always read 1 second from the source, and give it to the resampler int bytesWritten = 0; // read in any leftovers from last time if (outputBufferCount > 0) { bytesWritten += ReadFromOutputBuffer(buffer, offset, count - bytesWritten); } while (bytesWritten < count) { var sample = ReadFromSource(); if (sample == null) // reached the end of our input { // be good citizens and send some end messages: EndStreamAndDrain(); // resampler might have given us a little bit more to return bytesWritten += ReadFromOutputBuffer(buffer, offset + bytesWritten, count - bytesWritten); ClearOutputBuffer(); break; } // might need to resurrect the stream if the user has read all the way to the end, // and then repositioned the input backwards if (!initializedForStreaming) { InitializeTransformForStreaming(); } // give the input to the resampler // can get MF_E_NOTACCEPTING if we didn't drain the buffer properly transform.ProcessInput(0, sample, 0); Marshal.ReleaseComObject(sample); int readFromTransform; // n.b. in theory we ought to loop here, although we'd need to be careful as the next time into ReadFromTransform there could // still be some leftover bytes in outputBuffer, which would get overwritten. Only introduce this if we find a transform that // needs it. For most transforms, alternating read/write should be OK //do //{ // keep reading from transform readFromTransform = ReadFromTransform(); bytesWritten += ReadFromOutputBuffer(buffer, offset + bytesWritten, count - bytesWritten); //} while (readFromTransform > 0); } return bytesWritten; } private void EndStreamAndDrain() { transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_END_OF_STREAM, IntPtr.Zero); transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_COMMAND_DRAIN, IntPtr.Zero); int read; do { read = ReadFromTransform(); } while (read > 0); inputPosition = 0; outputPosition = 0; transform.ProcessMessage(MFT_MESSAGE_TYPE.MFT_MESSAGE_NOTIFY_END_STREAMING, IntPtr.Zero); initializedForStreaming = false; } private void ClearOutputBuffer() { outputBufferCount = 0; outputBufferOffset = 0; } /// /// Attempts to read from the transform /// Some useful info here: /// http://msdn.microsoft.com/en-gb/library/windows/desktop/aa965264%28v=vs.85%29.aspx#process_data /// /// private int ReadFromTransform() { var outputDataBuffer = new MFT_OUTPUT_DATA_BUFFER[1]; // we have to create our own for var sample = MediaFoundationApi.CreateSample(); var pBuffer = MediaFoundationApi.CreateMemoryBuffer(outputBuffer.Length); sample.AddBuffer(pBuffer); sample.SetSampleTime(outputPosition); // hopefully this is not needed outputDataBuffer[0].pSample = sample; _MFT_PROCESS_OUTPUT_STATUS status; var hr = transform.ProcessOutput(_MFT_PROCESS_OUTPUT_FLAGS.None, 1, outputDataBuffer, out status); if (hr == MediaFoundationErrors.MF_E_TRANSFORM_NEED_MORE_INPUT) { Marshal.ReleaseComObject(pBuffer); Marshal.ReleaseComObject(sample); // nothing to read return 0; } else if (hr != 0) { Marshal.ThrowExceptionForHR(hr); } IMFMediaBuffer outputMediaBuffer; outputDataBuffer[0].pSample.ConvertToContiguousBuffer(out outputMediaBuffer); IntPtr pOutputBuffer; int outputBufferLength; int maxSize; outputMediaBuffer.Lock(out pOutputBuffer, out maxSize, out outputBufferLength); outputBuffer = BufferHelpers.Ensure(outputBuffer, outputBufferLength); Marshal.Copy(pOutputBuffer, outputBuffer, 0, outputBufferLength); outputBufferOffset = 0; outputBufferCount = outputBufferLength; outputMediaBuffer.Unlock(); outputPosition += BytesToNsPosition(outputBufferCount, WaveFormat); // hopefully not needed Marshal.ReleaseComObject(pBuffer); sample.RemoveAllBuffers(); // needed to fix memory leak in some cases Marshal.ReleaseComObject(sample); Marshal.ReleaseComObject(outputMediaBuffer); return outputBufferLength; } private static long BytesToNsPosition(int bytes, WaveFormat waveFormat) { long nsPosition = (10000000L * bytes) / waveFormat.AverageBytesPerSecond; return nsPosition; } private IMFSample ReadFromSource() { // we always read a full second int bytesRead = sourceProvider.Read(sourceBuffer, 0, sourceBuffer.Length); if (bytesRead == 0) return null; var mediaBuffer = MediaFoundationApi.CreateMemoryBuffer(bytesRead); IntPtr pBuffer; int maxLength, currentLength; mediaBuffer.Lock(out pBuffer, out maxLength, out currentLength); Marshal.Copy(sourceBuffer, 0, pBuffer, bytesRead); mediaBuffer.Unlock(); mediaBuffer.SetCurrentLength(bytesRead); var sample = MediaFoundationApi.CreateSample(); sample.AddBuffer(mediaBuffer); // we'll set the time, I don't think it is needed for Resampler, but other MFTs might need it sample.SetSampleTime(inputPosition); long duration = BytesToNsPosition(bytesRead, sourceProvider.WaveFormat); sample.SetSampleDuration(duration); inputPosition += duration; Marshal.ReleaseComObject(mediaBuffer); return sample; } private int ReadFromOutputBuffer(byte[] buffer, int offset, int needed) { int bytesFromOutputBuffer = Math.Min(needed, outputBufferCount); Array.Copy(outputBuffer, outputBufferOffset, buffer, offset, bytesFromOutputBuffer); outputBufferOffset += bytesFromOutputBuffer; outputBufferCount -= bytesFromOutputBuffer; if (outputBufferCount == 0) { outputBufferOffset = 0; } return bytesFromOutputBuffer; } /// /// Indicate that the source has been repositioned and completely drain out the transforms buffers /// public void Reposition() { if (initializedForStreaming) { EndStreamAndDrain(); ClearOutputBuffer(); InitializeTransformForStreaming(); } } } }