Recording In-Game Audio in Unity
Recently I began doing a second pass on my synthesizers in the Google Play store. I think the core of each of those synths is pretty solid, but they are still missing some key features. For example, if you want to record a performance, you must record the output of the headphone jack.
So I just finished writing a class that renders a Unity audio stream to a wave file, and I wanted to share it here.
The class is called AudioRenderer. It's a MonoBehaviour that uses the OnAudioFilterRead method to write chunks of data to a stream. When the performance ends, the Save method is used to save to a canonical wav file.
The full AudioRenderer class is pasted here. As written it will only work on 16bit/44kHz audio streams, but it should be easily adaptable.
using UnityEngine;
using System;
using System.IO;
public class AudioRenderer : MonoBehaviour
{
#region Fields, Properties, and Inner Classes
// constants for the wave file header
private const int HEADER_SIZE = 44;
private const short BITS_PER_SAMPLE = 16;
private const int SAMPLE_RATE = 44100;
// the number of audio channels in the output file
private int channels = 2;
// the audio stream instance
private MemoryStream outputStream;
private BinaryWriter outputWriter;
// should this object be rendering to the output stream?
public bool Rendering = false;
/// The status of a render
public enum Status
{
UNKNOWN,
SUCCESS,
FAIL,
ASYNC
}
/// The result of a render.
public class Result
{
public Status State;
public string Message;
public Result(Status newState = Status.UNKNOWN, string newMessage = "")
{
this.State = newState;
this.Message = newMessage;
}
}
#endregion
public AudioRenderer()
{
this.Clear();
}
// reset the renderer
public void Clear()
{
this.outputStream = new MemoryStream();
this.outputWriter = new BinaryWriter(outputStream);
}
/// Write a chunk of data to the output stream.
public void Write(float[] audioData)
{
// Convert numeric audio data to bytes
for (int i = 0; i < audioData.Length; i++)
{
// write the short to the stream
this.outputWriter.Write((short)(audioData[i] * (float)Int16.MaxValue));
}
}
// write the incoming audio to the output string
void OnAudioFilterRead(float[] data, int channels)
{
if( this.Rendering )
{
// store the number of channels we are rendering
this.channels = channels;
// store the data stream
this.Write(data);
}
}
#region File I/O
public AudioRenderer.Result Save(string filename)
{
Result result = new AudioRenderer.Result();
if (outputStream.Length > 0)
{
// add a header to the file so we can send it to the SoundPlayer
this.AddHeader();
// if a filename was passed in
if (filename.Length > 0)
{
// Save to a file. Print a warning if overwriting a file.
if (File.Exists(filename))
Debug.LogWarning("Overwriting " + filename + "...");
// reset the stream pointer to the beginning of the stream
outputStream.Position = 0;
// write the stream to a file
FileStream fs = File.OpenWrite(filename);
this.outputStream.WriteTo(fs);
fs.Close();
// for debugging only
Debug.Log("Finished saving to " + filename + ".");
}
result.State = Status.SUCCESS;
}
else
{
Debug.LogWarning("There is no audio data to save!");
result.State = Status.FAIL;
result.Message = "There is no audio data to save!";
}
return result;
}
/// This generates a simple header for a canonical wave file,
/// which is the simplest practical audio file format. It
/// writes the header and the audio file to a new stream, then
/// moves the reference to that stream.
///
/// See this page for details on canonical wave files:
/// http://www.lightlink.com/tjweber/StripWav/Canon.html
private void AddHeader()
{
// reset the output stream
outputStream.Position = 0;
// calculate the number of samples in the data chunk
long numberOfSamples = outputStream.Length / (BITS_PER_SAMPLE / 8);
// create a new MemoryStream that will have both the audio data AND the header
MemoryStream newOutputStream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(newOutputStream);
writer.Write(0x46464952); // "RIFF" in ASCII
// write the number of bytes in the entire file
writer.Write((int)(HEADER_SIZE + (numberOfSamples * BITS_PER_SAMPLE * channels / 8)) - 8);
writer.Write(0x45564157); // "WAVE" in ASCII
writer.Write(0x20746d66); // "fmt " in ASCII
writer.Write(16);
// write the format tag. 1 = PCM
writer.Write((short)1);
// write the number of channels.
writer.Write((short)channels);
// write the sample rate. 44100 in this case. The number of audio samples per second
writer.Write(SAMPLE_RATE);
writer.Write(SAMPLE_RATE * channels * (BITS_PER_SAMPLE / 8));
writer.Write((short)(channels * (BITS_PER_SAMPLE / 8)));
// 16 bits per sample
writer.Write(BITS_PER_SAMPLE);
// "data" in ASCII. Start the data chunk.
writer.Write(0x61746164);
// write the number of bytes in the data portion
writer.Write((int)(numberOfSamples * BITS_PER_SAMPLE * channels / 8));
// copy over the actual audio data
this.outputStream.WriteTo(newOutputStream);
// move the reference to the new stream
this.outputStream = newOutputStream;
}
#endregion
}