using System; using System.IO; namespace Audio { /// /// This class writes audio data to a .aif file on disk /// public class AiffFileWriter : Stream { private Stream outStream; private BinaryWriter writer; private long dataSizePos; private long commSampleCountPos; private int dataChunkSize = 8; private NAudio.Wave.WaveFormat format; private string filename; /// /// Creates an Aiff file by reading all the data from a WaveProvider /// BEWARE: the WaveProvider MUST return 0 from its Read method when it is finished, /// or the Aiff File will grow indefinitely. /// /// The filename to use /// The source WaveProvider public static void CreateAiffFile(string filename, NAudio.Wave.WaveStream sourceProvider) { using (AiffFileWriter writer = new AiffFileWriter(filename, sourceProvider.WaveFormat)) { byte[] buffer = new byte[16384]; while (sourceProvider.Position < sourceProvider.Length) { int count = Math.Min((int)(sourceProvider.Length - sourceProvider.Position), buffer.Length); int bytesRead = sourceProvider.Read(buffer, 0, count); if (bytesRead == 0) { // end of source provider break; } writer.Write(buffer, 0, bytesRead); } } } /// /// AiffFileWriter that actually writes to a stream /// /// Stream to be written to /// Wave format to use public AiffFileWriter(Stream outStream, NAudio.Wave.WaveFormat format) { this.outStream = outStream; this.format = format; this.writer = new BinaryWriter(outStream, System.Text.Encoding.ASCII); this.writer.Write(System.Text.Encoding.ASCII.GetBytes("FORM")); this.writer.Write((int)0); // placeholder this.writer.Write(System.Text.Encoding.ASCII.GetBytes("AIFF")); CreateCommChunk(); WriteSsndChunkHeader(); } /// /// Creates a new AiffFileWriter /// /// The filename to write to /// The Wave Format of the output data public AiffFileWriter(string filename, NAudio.Wave.WaveFormat format) : this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read), format) { this.filename = filename; } private void WriteSsndChunkHeader() { this.writer.Write(System.Text.Encoding.ASCII.GetBytes("SSND")); dataSizePos = this.outStream.Position; this.writer.Write((int)0); // placeholder this.writer.Write((int)0); // zero offset this.writer.Write(SwapEndian((int)format.BlockAlign)); } private byte[] SwapEndian(short n) { return new byte[] { (byte)(n >> 8), (byte)(n & 0xff) }; } private byte[] SwapEndian(int n) { return new byte[] { (byte)((n >> 24) & 0xff), (byte)((n >> 16) & 0xff), (byte)((n >> 8) & 0xff), (byte)(n & 0xff), }; } private void CreateCommChunk() { this.writer.Write(System.Text.Encoding.ASCII.GetBytes("COMM")); this.writer.Write(SwapEndian((int)18)); this.writer.Write(SwapEndian((short)format.Channels)); commSampleCountPos = this.outStream.Position; ; this.writer.Write((int)0); // placeholder for total number of samples this.writer.Write(SwapEndian((short)format.BitsPerSample)); this.writer.Write(IEEE.ConvertToIeeeExtended(format.SampleRate)); } /// /// The aiff file name or null if not applicable /// public string Filename { get { return filename; } } /// /// Number of bytes of audio in the data chunk /// public override long Length { get { return dataChunkSize; } } /// /// WaveFormat of this aiff file /// public NAudio.Wave.WaveFormat WaveFormat { get { return format; } } /// /// Returns false: Cannot read from a AiffFileWriter /// public override bool CanRead { get { return false; } } /// /// Returns true: Can write to a AiffFileWriter /// public override bool CanWrite { get { return true; } } /// /// Returns false: Cannot seek within a AiffFileWriter /// public override bool CanSeek { get { return false; } } /// /// Read is not supported for a AiffFileWriter /// public override int Read(byte[] buffer, int offset, int count) { throw new InvalidOperationException("Cannot read from an AiffFileWriter"); } /// /// Seek is not supported for a AiffFileWriter /// public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException("Cannot seek within an AiffFileWriter"); } /// /// SetLength is not supported for AiffFileWriter /// /// public override void SetLength(long value) { throw new InvalidOperationException("Cannot set length of an AiffFileWriter"); } /// /// Gets the Position in the AiffFile (i.e. number of bytes written so far) /// public override long Position { get { return dataChunkSize; } set { throw new InvalidOperationException("Repositioning an AiffFileWriter is not supported"); } } /// /// Appends bytes to the AiffFile (assumes they are already in the correct format) /// /// the buffer containing the wave data /// the offset from which to start writing /// the number of bytes to write public override void Write(byte[] data, int offset, int count) { byte[] swappedData = new byte[data.Length]; int align = format.BitsPerSample / 8; for (int i = 0; i < data.Length; i++) { int pos = (int)Math.Floor((double)i / align) * align + (align - (i % align) - 1); swappedData[i] = data[pos]; } outStream.Write(swappedData, offset, count); dataChunkSize += count; } private byte[] value24 = new byte[3]; // keep this around to save us creating it every time /// /// Writes a single sample to the Aiff file /// /// the sample to write (assumed floating point with 1.0f as max value) public void WriteSample(float sample) { if (WaveFormat.BitsPerSample == 16) { writer.Write(SwapEndian((Int16)(Int16.MaxValue * sample))); dataChunkSize += 2; } else if (WaveFormat.BitsPerSample == 24) { var value = BitConverter.GetBytes((Int32)(Int32.MaxValue * sample)); value24[2] = value[1]; value24[1] = value[2]; value24[0] = value[3]; writer.Write(value24); dataChunkSize += 3; } else if (WaveFormat.BitsPerSample == 32 && WaveFormat.Encoding == NAudio.Wave.WaveFormatEncoding.Extensible) { writer.Write(SwapEndian(UInt16.MaxValue * (Int32)sample)); dataChunkSize += 4; } else { throw new ApplicationException("Only 16, 24 or 32 bit PCM or IEEE float audio data supported"); } } /// /// Writes 32 bit floating point samples to the Aiff file /// They will be converted to the appropriate bit depth depending on the WaveFormat of the AIF file /// /// The buffer containing the floating point samples /// The offset from which to start writing /// The number of floating point samples to write public void WriteSamples(float[] samples, int offset, int count) { for (int n = 0; n < count; n++) { WriteSample(samples[offset + n]); } } /// /// Writes 16 bit samples to the Aiff file /// /// The buffer containing the 16 bit samples /// The offset from which to start writing /// The number of 16 bit samples to write public void WriteSamples(short[] samples, int offset, int count) { // 16 bit PCM data if (WaveFormat.BitsPerSample == 16) { for (int sample = 0; sample < count; sample++) { writer.Write(SwapEndian(samples[sample + offset])); } dataChunkSize += (count * 2); } // 24 bit PCM data else if (WaveFormat.BitsPerSample == 24) { byte[] value; for (int sample = 0; sample < count; sample++) { value = BitConverter.GetBytes(UInt16.MaxValue * (Int32)samples[sample + offset]); value24[2] = value[1]; value24[1] = value[2]; value24[0] = value[3]; writer.Write(value24); } dataChunkSize += (count * 3); } // 32 bit PCM data else if (WaveFormat.BitsPerSample == 32 && WaveFormat.Encoding == NAudio.Wave.WaveFormatEncoding.Extensible) { for (int sample = 0; sample < count; sample++) { writer.Write(SwapEndian(UInt16.MaxValue * (Int32)samples[sample + offset])); } dataChunkSize += (count * 4); } else { throw new ApplicationException("Only 16, 24 or 32 bit PCM audio data supported"); } } /// /// Ensures data is written to disk /// public override void Flush() { writer.Flush(); } #region IDisposable Members /// /// Actually performs the close,making sure the header contains the correct data /// /// True if called from Dispose protected override void Dispose(bool disposing) { if (disposing) { if (outStream != null) { try { UpdateHeader(writer); } finally { // in a finally block as we don't want the FileStream to run its disposer in // the GC thread if the code above caused an IOException (e.g. due to disk full) outStream.Close(); // will close the underlying base stream outStream = null; } } } } /// /// Updates the header with file size information /// protected virtual void UpdateHeader(BinaryWriter writer) { this.Flush(); writer.Seek(4, SeekOrigin.Begin); writer.Write(SwapEndian((int)(outStream.Length - 8))); UpdateCommChunk(writer); UpdateSsndChunk(writer); } private void UpdateCommChunk(BinaryWriter writer) { writer.Seek((int)commSampleCountPos, SeekOrigin.Begin); writer.Write(SwapEndian((int)(dataChunkSize * 8 / format.BitsPerSample / format.Channels))); } private void UpdateSsndChunk(BinaryWriter writer) { writer.Seek((int)dataSizePos, SeekOrigin.Begin); writer.Write(SwapEndian((int)dataChunkSize)); } /// /// Finaliser - should only be called if the user forgot to close this AiffFileWriter /// ~AiffFileWriter() { System.Diagnostics.Debug.Assert(false, "AiffFileWriter was not disposed"); Dispose(false); } #endregion } }