vbAccelerator - Contents of code file: WaveStreamWriter.cs

This file is part of the download WaveStream CSharp, which is described in the article WaveStreamReader and WaveStreamWriter.

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace vbAccelerator.Audio.WaveStream
{
   /// <summary>
   /// A <c>Stream</c> implementation which wraps the Windows Multi-media
   /// IO to write a Wave file.
   /// </summary>
   public class WaveStreamWriter : Stream
   {
      private string waveFile;
      private IntPtr hMmio = IntPtr.Zero;
      private bool disposed = false;
      private WinMMInterop.WAVEFORMATEX format;
      private int dataOffset = 0;
      private int audioLength = 0;
      private WinMMInterop.MMCKINFO mmckInfoChild;
      private WinMMInterop.MMCKINFO mmckInfoParent;

      /// <summary>
      /// Default constructor: 16bit, 44.1kHz, Stereo.  No file is created.
      /// </summary>
      public WaveStreamWriter() : base()
      {
         format.cbSize = 0;
         format.nChannels = 2;
         format.nSamplesPerSec = 44100;
         format.wBitsPerSample = 16;
         format.nBlockAlign = 4;
         format.wFormatTag = 1;
      }

      /// <summary>
      /// Constructor: 16bit, 44.1kHz, Stereo,  file is created.
      /// </summary>
      /// <param name="file">File name of the wave file to write</param>
      public WaveStreamWriter(string file) : this()
      {
         Filename = file;
      }

      /// <summary>
      /// Constructor: 16bit, Stereo,  file is created.
      /// </summary>
      /// <param name="file">File name of the wave file to write</param>
      /// <param name="samplingFrequency">Sampling frequency</param>
      public WaveStreamWriter(string file, int samplingFrequency) : this()
      {
         SamplingFrequency = samplingFrequency;
         Filename = file;
      }

      /// <summary>
      /// Constructor: 16bit, file is created.
      /// </summary>   
      /// <param name="file">File name of the wave file to write</param>
      /// <param name="samplingFrequency">Sampling frequency</param>
      /// <param name="channels">Number of audio channels</param>
      public WaveStreamWriter(string file, int samplingFrequency, short
       channels) : this()
      {
         format.nSamplesPerSec = samplingFrequency;
         format.nChannels = channels;
         Filename = file;
      }

      /// <summary>
      /// Constructor: file is created.
      /// </summary>
      /// <param name="file">File name of the wave file to write</param>
      /// <param name="samplingFrequency">Sampling frequency</param>
      /// <param name="channels">Number of audio channels</param>
      /// <param name="bitsPerSample">Number of bits per sample</param>
      public WaveStreamWriter(string file, int samplingFrequency, short
       channels, short bitsPerSample) : this()
      {
         format.nSamplesPerSec = samplingFrequency;
         format.nChannels = channels;
         format.wBitsPerSample = bitsPerSample;
         Filename = file;
      }

      /// <summary>
      /// Destructor: ensures that the wave file handle is closed.
      /// </summary>
      ~WaveStreamWriter()
      {
         Dispose(false);
      }

      /// <summary>
      /// Clears up resources associated with this class.
      /// </summary>
      public void Dispose()
      {
         Dispose(true);
         GC.SuppressFinalize(this);
      }

      /// <summary>
      /// Gets the Multi-media IO handle to the wave file.
      /// </summary>
      protected virtual IntPtr Handle
      {
         get
         {
            return hMmio;
         }
      }

      /// <summary>
      /// Clears up resources associated with this class.
      /// </summary>
      /// <param name="disposing"><code>true</code> if disposing from the
       <c>Dispose</c>
      /// method, otherwise <c>false</c>.</param>
      protected virtual void Dispose(bool disposing)
      {
         if (!disposed)
         {
            if (disposing)
            {
               // nothing to do
            }
            CloseWaveFile();
            disposed = true;
         }
      }

      public override void Flush()
      {
         //
      }

      /// <summary>
      /// Gets/sets the wave file name.
      /// </summary>
      public string Filename
      {
         get
         {
            return waveFile;
         }
         set
         {
            if (hMmio != IntPtr.Zero)
            {
               CloseWaveFile();
            }
            waveFile = value;
            CreateWaveFile();
         }
      }   
   
      /// <summary>
      /// Gets/sets the number of audio channels in the file.
      /// </summary>
      /// <exception cref="InvalidOperationException">If attempt made to change
       the value when
      /// a file is open.  Must set this prior to setting the
       filename.</exception>
      public short Channels
      {
         get
         {
            return format.nChannels;
         }
         set
         {
            if (hMmio != IntPtr.Zero)
            {
               throw new InvalidOperationException("Cannot change number of
                audio channels on an open file.");
            }
            format.nChannels = value;
         }
      }

      /// <summary>
      /// Gets/sets the sample frequency of the file.
      /// </summary>
      /// <exception cref="InvalidOperationException">If attempt made to change
       the value when
      /// a file is open.  Must set this prior to setting the
       filename.</exception>
      public int SamplingFrequency
      {
         get
         {
            return format.nSamplesPerSec;
         }
         set
         {
            if (hMmio != IntPtr.Zero)
            {
               throw new InvalidOperationException("Cannot change sampling
                frequency on an open file.");
            }
            format.nSamplesPerSec = value;
         }
      }

      /// <summary>
      /// Gets/sets the number of bits per sample in the wave file.
      /// </summary>
      /// <exception cref="InvalidOperationException">If attempt made to change
       the value when
      /// a file is open.  Must set this prior to setting the
       filename.</exception>
      public short BitsPerSample
      {
         get
         {
            return format.wBitsPerSample;
         }
         set
         {
            if (hMmio != IntPtr.Zero)
            {
               throw new InvalidOperationException("Cannot change bits/sample
                on an open file.");
            }
            format.wBitsPerSample = value;
         }
      }
      

      /// <summary>
      /// Gets whether the stream can be read or not (true whenever a wave file
      /// is open).
      /// </summary>
      public override bool CanRead
      {
         get
         {
            return (hMmio != IntPtr.Zero);
         }
      }

      /// <summary>
      /// Gets whether the stream is seekable or not (true whenever a wave file
      /// is open).
      /// </summary>
      public override bool CanSeek
      {
         get
         {
            return (hMmio != IntPtr.Zero);
         }
      }

      /// <summary>
      /// Gets whether the stream can be written or not (true whenever a wave
       file
      /// is open).
      /// </summary>
      public override bool CanWrite
      {
         get
         {
            return (hMmio != IntPtr.Zero);
         }
      }

      /// <summary>
      /// Gets the length of this wave file, in bytes.
      /// </summary>
      public override long Length
      {
         get
         {
            return audioLength;
         }
      }

      /// <summary>
      /// Throws an exception; setting the length of the stream is meaningless
      /// </summary>
      /// <exception cref="InvalidOperationException">Thrown
       exception</exception>
      public override void SetLength(long length)
      {
         throw new InvalidOperationException(
            "This class can only read files.  Use the WaveStreamWriter class to
             write files.");
      }

      /// <summary>
      /// Gets/sets the position within the wave file.
      /// </summary>
      public override long Position
      {
         get
         {
            return 0;
         }
         set
         {
            Seek(value, SeekOrigin.Begin);
         }
      }

      /// <summary>
      /// Reads <c>count</c> bytes into the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read into</param>
      /// <param name="count">Number of bytes to read</param>
      /// <returns>Number of bytes read.</returns>
      public virtual int Read ( byte[] buffer, int count )
      {
         return Read(buffer, 0, count);
      }

      /// <summary>
      /// Reads <c>count</c> bytes into the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read into</param>
      /// <param name="count">Number of bytes to read</param>
      /// <param name="offset">Offset from the current file position to start
       reading from</param>
      /// <returns>Number of bytes read.</returns>
      public override int Read ( byte[] buffer , int offset , int count )
      {
         if (hMmio == IntPtr.Zero)
         {
            throw new InvalidOperationException("No wave data is open");
         }

         if (offset != 0)
         {
            Seek((long) offset, SeekOrigin.Current);
         }

         GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);        
             
         IntPtr ptrBuffer = handle.AddrOfPinnedObject();

         int dataRemaining = (dataOffset + audioLength - 
            WinMMInterop.mmioSeek(hMmio, 0, WinMMInterop.SEEK_CUR));
         int read = 0;
         if (count < dataRemaining)
         {
            read = WinMMInterop.mmioRead(hMmio, ptrBuffer, count);
         }
         else if (dataRemaining > 0)
         {
            read = WinMMInterop.mmioRead(hMmio, ptrBuffer, dataRemaining);
         }

         if (handle.IsAllocated)
         {
            handle.Free();
         }
         return read;
      }

      /// <summary>
      /// Reads <c>count</c> shorts into the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read into</param>
      /// <param name="count">Number of shorts to read</param>
      /// <returns>Number of bytes read.</returns>
      public virtual int Read (short[] buffer, int count)
      {
         return Read(buffer, 0, count);
      }

      /// <summary>
      /// Reads <c>count</c> shorts into the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read into</param>
      /// <param name="count">Number of shorts to read</param>
      /// <param name="offset">Offset in shorts (2 bytes) from the current file
       position to start 
      /// reading from</param>
      /// <returns>Number of bytes read.</returns>
      public virtual int Read ( short[] buffer, int offset, int count )
      {
         if (hMmio == IntPtr.Zero)
         {
            throw new InvalidOperationException("No wave data is open");
         }

         if (offset != 0)
         {
            Seek((long) (offset * 2), SeekOrigin.Current);
         }

         GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);        
             
         IntPtr ptrBuffer = handle.AddrOfPinnedObject();

         int dataRemaining = (dataOffset + audioLength - 
            WinMMInterop.mmioSeek(hMmio, 0, WinMMInterop.SEEK_CUR)) / 2;
         int read = 0;
         if (count < dataRemaining)
         {
            read = WinMMInterop.mmioRead(hMmio, ptrBuffer, count * 2);
         }
         else if (dataRemaining > 0)
         {
            read = WinMMInterop.mmioRead(hMmio, ptrBuffer, dataRemaining * 2);
         }

         if (handle.IsAllocated)
         {
            handle.Free();
         }
         return read;
      }

      /// <summary>
      /// Writes <c>count</c> bytes from the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read from</param>
      /// <param name="count">Number of bytes to write</param>
      public virtual void Write( byte[] buffer, int count)
      {
         Write(buffer, 0, count);
      }
         
      /// <summary>
      /// Writes <c>count</c> bytes from the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read from</param>
      /// <param name="count">Number of bytes to write</param>
      /// <param name="offset">Offset from the current file position to start
       writing</param>
      public override void Write( byte[] buffer, int offset, int count)
      {
         if (hMmio == IntPtr.Zero)
         {
            throw new InvalidOperationException("No wave file is open");
         }

         if (offset != 0)
         {
            Seek((long) offset, SeekOrigin.Current);
         }
         GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);        
             
         IntPtr ptrBuffer = handle.AddrOfPinnedObject();
   
         int write = WinMMInterop.mmioWrite(hMmio, ptrBuffer, count);
         if (write != count)
         {
            throw new IOException(String.Format(
               "Data truncation: only wrote {0} of {1} requested bytes", write,
                count));
         }

         if (handle.IsAllocated)
         {
            handle.Free();
         }

      }

      /// <summary>
      /// Writes <c>count</c> shorts from the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read from</param>
      /// <param name="count">Number of shorts to write</param>
      public virtual void Write( short[] buffer, int count)
      {
         Write(buffer, 0, count);
      }
         
      /// <summary>
      /// Writes <c>count</c> shorts from the buffer.
      /// </summary>
      /// <param name="buffer">Buffer to read from</param>
      /// <param name="count">Number of shorts to write</param>
      /// <param name="offset">Offset from the current file position to start
       writing</param>
      /// <returns>Number of shorts write.</returns>
      public virtual void Write( short[] buffer, int offset, int count)
      {
         if (hMmio == IntPtr.Zero)
         {
            throw new InvalidOperationException("No wave file is open");
         }

         if (offset != 0)
         {
            Seek((long) (offset * 2), SeekOrigin.Current);
         }
         GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);        
             
         IntPtr ptrBuffer = handle.AddrOfPinnedObject();
   
         int write = WinMMInterop.mmioWrite(hMmio, ptrBuffer, count * 2);
         if (write != (count * 2))
         {
            throw new IOException(String.Format(
               "Data truncation: only wrote {0} of {1} requested bytes", write,
                count));
         }

         if (handle.IsAllocated)
         {
            handle.Free();
         }

      }

      /// <summary>
      /// Seeks to the specified position in the stream, in bytes
      /// </summary>
      /// <param name="position">Position to seek to</param>
      /// <param name="origin">Specifies the starting postion of the
       seek</param>
      public override long Seek(long position, SeekOrigin origin)
      {
         if (hMmio == IntPtr.Zero)
         {
            throw new InvalidOperationException("No wave file is open");
         }
         
         int offset = (int) position;
         int mmOrigin = WinMMInterop.SEEK_CUR;
         if (origin == SeekOrigin.Begin)
         {
            offset += dataOffset;
            mmOrigin = WinMMInterop.SEEK_SET;
         }
         else if (origin == SeekOrigin.End)
         {
            mmOrigin = WinMMInterop.SEEK_END;
         }
         int result = WinMMInterop.mmioSeek(hMmio, offset, mmOrigin);
         if (result == -1)
         {
            throw new WaveStreamException(
               String.Format("Failed to seek to position {0} in file",
                position));
         }
         return (long) result;
      }
      
      private void CreateWaveFile()
      {
         CloseWaveFile();

         hMmio = WinMMInterop.mmioOpen(waveFile, IntPtr.Zero, 
            WinMMInterop.MMIO_ALLOCBUF | WinMMInterop.MMIO_READWRITE |
             WinMMInterop.MMIO_CREATE);
         if (hMmio == IntPtr.Zero)
         {
            throw new IOException(
               String.Format("Could not open file {0}", waveFile));
         }

         CreateWaveFormatHeader();

      }

      private void CreateWaveFormatHeader()
      {
         int result = 0;

         // Set derived fields for PCM
         format.nBlockAlign =  (short) ((format.nChannels *
          format.wBitsPerSample) / 8);
         format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;

         // Write the WAVE header:
         mmckInfoParent = new WinMMInterop.MMCKINFO();
         mmckInfoParent.fccType = WinMMInterop.mmioStringToFOURCC("WAVE", 0);

         result = WinMMInterop.mmioCreateChunk(hMmio, ref mmckInfoParent, 
            WinMMInterop.MMIO_CREATERIFF);
         if (result != WinMMInterop.MMSYSERR_NOERROR)
         {
            CloseWaveFile();
            throw new WaveStreamException("Could not write the WAVE RIFF header
             chunk to the file.");
         }

         // Create the format chunk and write the format out:
         mmckInfoChild = new WinMMInterop.MMCKINFO();
         mmckInfoChild.ckid = WinMMInterop.mmioStringToFOURCC("fmt", 0);
         mmckInfoChild.ckSize =
          Marshal.SizeOf(typeof(WinMMInterop.WAVEFORMATEX));
      
         result = WinMMInterop.mmioCreateChunk(hMmio, ref mmckInfoChild, 0);
         if (result != WinMMInterop.MMSYSERR_NOERROR)
         {
            CloseWaveFile();
            throw new WaveStreamException("Could not write the 'fmt' header
             chunk to the file.");
         }

         int size = WinMMInterop.mmioWriteWaveFormat(hMmio, ref format,
          mmckInfoChild.ckSize);
         if (size != mmckInfoChild.ckSize)
         {
            CloseWaveFile();
            throw new WaveStreamException("Could not write the format
             information into the 'fmt' header chunk of the file.");
         }

         // Back out to the WAVE header:
         result = WinMMInterop.mmioAscend(hMmio, ref mmckInfoChild, 0);
         if (result != WinMMInterop.MMSYSERR_NOERROR)
         {
            CloseWaveFile();
            throw new WaveStreamException("Could not ascend out of 'fmt' header
             chunk.");
         }

         // Create the data chunk:
         mmckInfoChild.ckid = WinMMInterop.mmioStringToFOURCC("data", 0);
         result = WinMMInterop.mmioCreateChunk(hMmio, ref mmckInfoChild, 0);
         if (result != WinMMInterop.MMSYSERR_NOERROR)
         {
            CloseWaveFile();
            throw new WaveStreamException("Could not create the 'data' chunk
             for the audio data.");
         }
      
         // Stay in the data chunk for writing.
         dataOffset = WinMMInterop.mmioSeek(hMmio, 0, WinMMInterop.SEEK_CUR);
      }

      /// <summary>
      /// Closes the wave file.
      /// </summary>
      public void CloseWaveFile()
      {
         if (hMmio != IntPtr.Zero)
         {
            int result;

            // Ascend the output file out of the output chunk:
            result = WinMMInterop.mmioAscend(hMmio, ref mmckInfoChild, 0);
            // Ascend the output file out of the 'RIFF' chunk:
            result = WinMMInterop.mmioAscend(hMmio, ref mmckInfoParent, 0);

            // Close the file
            WinMMInterop.mmioClose(hMmio, 0);
            hMmio = IntPtr.Zero;
            audioLength = 0;
         }
      }
   }
}