Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Creating RF64 and BWF WAV Files with NAudio
using System;
namespace NAudioUtils
{
// https://tech.ebu.ch/docs/tech/tech3285.pdf
class BextChunkInfo
{
public BextChunkInfo()
{
//UniqueMaterialIdentifier = Guid.NewGuid().ToString();
Reserved = new byte[190];
}
public string Description { get; set; } // max 256 chars
public string Originator { get; set; } // max 32 chars
public string OriginatorReference { get; set; } // max 32 chars
public DateTime OriginationDateTime { get; set; }
public string OriginationDate { get { return OriginationDateTime.ToString("yyyy-MM-dd"); } }
public string OriginationTime { get { return OriginationDateTime.ToString("HH:mm:ss"); } }
public long TimeReference { get; set; } // first sample count since midnight
public ushort Version { get { return 1; } } // version 2 has loudness stuff which we don't know so using version 1
public string UniqueMaterialIdentifier { get; set; } // 64 bytes http://en.wikipedia.org/wiki/UMID
public byte[] Reserved { get; private set; } // for version 2 = 180 bytes (10 before are loudness values), using version 1 = 190 bytes
public string CodingHistory { get; set; } // arbitrary length string at end of structure
// http://www.ebu.ch/CMSimages/fr/tec_text_r98-1999_tcm7-4709.pdf
//A=PCM,F=48000,W=16,M=stereo,T=original,CR/LF
}
}
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using NAudio.Wave;
namespace NAudioUtils
{
/// <summary>
/// Broadcast WAVE File Writer
/// </summary>
class BwfWriter : IDisposable
{
private readonly WaveFormat format;
private readonly BinaryWriter writer;
private readonly long dataChunkSizePosition;
private long dataLength;
private bool isDisposed;
public BwfWriter(string filename, WaveFormat format, BextChunkInfo bextChunkInfo)
{
this.format = format;
writer = new BinaryWriter(File.OpenWrite(filename));
writer.Write(Encoding.UTF8.GetBytes("RIFF")); // will be updated to RF64 if large
writer.Write(0); // placeholder
writer.Write(Encoding.UTF8.GetBytes("WAVE"));
writer.Write(Encoding.UTF8.GetBytes("JUNK")); // ds64
writer.Write(28); // ds64 size
writer.Write(0L); // RIFF size
writer.Write(0L); // data size
writer.Write(0L); // sampleCount size
writer.Write(0); // table length
// TABLE appears here - to store the sizes of other huge chunks other than
// write the broadcast audio extension
writer.Write(Encoding.UTF8.GetBytes("bext"));
var codingHistory = Encoding.ASCII.GetBytes(bextChunkInfo.CodingHistory ?? "");
var bextLength = 602 + codingHistory.Length;
if (bextLength%2 != 0)
bextLength++;
writer.Write(bextLength); // bext size
var bextStart = writer.BaseStream.Position;
writer.Write(GetAsBytes(bextChunkInfo.Description, 256));
writer.Write(GetAsBytes(bextChunkInfo.Originator, 32));
writer.Write(GetAsBytes(bextChunkInfo.OriginatorReference, 32));
writer.Write(GetAsBytes(bextChunkInfo.OriginationDate, 10));
writer.Write(GetAsBytes(bextChunkInfo.OriginationTime, 8));
writer.Write(bextChunkInfo.TimeReference); // 8 bytes long
writer.Write(bextChunkInfo.Version); // 2 bytes long
writer.Write(GetAsBytes(bextChunkInfo.UniqueMaterialIdentifier, 64));
writer.Write(bextChunkInfo.Reserved); // for version 1 this is 190 bytes
writer.Write(codingHistory);
if (codingHistory.Length%2 != 0)
writer.Write((byte) 0);
Debug.Assert(writer.BaseStream.Position == bextStart + bextLength, "Invalid bext chunk size");
// write the format chunk
writer.Write(Encoding.UTF8.GetBytes("fmt "));
format.Serialize(writer);
writer.Write(Encoding.UTF8.GetBytes("data"));
dataChunkSizePosition = writer.BaseStream.Position;
writer.Write(-1); // will be overwritten unless this is RF64
// now finally the data chunk
}
public void Write(byte[] buffer, int offset, int count)
{
if (isDisposed) throw new ObjectDisposedException("This BWF Writer already disposed");
writer.Write(buffer,offset,count);
dataLength += count;
}
public void Flush()
{
if (isDisposed) throw new ObjectDisposedException("This BWF Writer already disposed");
writer.Flush();
// could do FixUpChunkSizes(true) here to ensure WAV file created is always playable after Flush
}
private void FixUpChunkSizes(bool restorePosition)
{
var pos = writer.BaseStream.Position;
var isLarge = dataLength > Int32.MaxValue;
var riffSize = writer.BaseStream.Length - 8;
if (isLarge)
{
var bytesPerSample = (format.BitsPerSample / 8) * format.Channels;
writer.BaseStream.Position = 0;
writer.Write(Encoding.UTF8.GetBytes("RF64"));
writer.Write(-1);
writer.BaseStream.Position += 4; // skip over WAVE
writer.Write(Encoding.UTF8.GetBytes("ds64"));
writer.BaseStream.Position += 4; // skip over ds64 chunk size
writer.Write(riffSize);
writer.Write(dataLength);
writer.Write(dataLength / bytesPerSample);
// data chunk size can stay as -1
}
else
{
// fix up the RIFF size
writer.BaseStream.Position = 4;
writer.Write((uint)riffSize);
// fix up the data chunk size
writer.BaseStream.Position = dataChunkSizePosition;
writer.Write((uint)dataLength);
}
if (restorePosition)
{
writer.BaseStream.Position = pos;
}
}
public void Dispose()
{
if (!isDisposed)
{
FixUpChunkSizes(false);
writer.Dispose();
isDisposed = true;
}
}
private static byte[] GetAsBytes(string message, int byteSize)
{
var outputBuffer = new byte[byteSize];
var encoded = Encoding.ASCII.GetBytes(message ?? "");
Array.Copy(encoded, outputBuffer, Math.Min(encoded.Length, byteSize));
return outputBuffer;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.