Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Unity3D: script to save an AudioClip as a .wav file.
// Copyright (c) 2012 Calvin Rien
// http://the.darktable.com
//
// This software is provided 'as-is', without any express or implied warranty. In
// no event will the authors be held liable for any damages arising from the use
// of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim
// that you wrote the original software. If you use this software in a product,
// an acknowledgment in the product documentation would be appreciated but is not
// required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
// =============================================================================
//
// derived from Gregorio Zanon's script
// http://forum.unity3d.com/threads/119295-Writing-AudioListener.GetOutputData-to-wav-problem?p=806734&viewfull=1#post806734
using System;
using System.IO;
using UnityEngine;
using System.Collections.Generic;
public static class SavWav {
const int HEADER_SIZE = 44;
public static bool Save(string filename, AudioClip clip) {
if (!filename.ToLower().EndsWith(".wav")) {
filename += ".wav";
}
var filepath = Path.Combine(Application.persistentDataPath, filename);
Debug.Log(filepath);
// Make sure directory exists if user is saving to sub dir.
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
using (var fileStream = CreateEmpty(filepath)) {
ConvertAndWrite(fileStream, clip);
WriteHeader(fileStream, clip);
}
return true; // TODO: return false if there's a failure saving the file
}
public static AudioClip TrimSilence(AudioClip clip, float min) {
var samples = new float[clip.samples];
clip.GetData(samples, 0);
return TrimSilence(new List<float>(samples), min, clip.channels, clip.frequency);
}
public static AudioClip TrimSilence(List<float> samples, float min, int channels, int hz) {
return TrimSilence(samples, min, channels, hz, false, false);
}
public static AudioClip TrimSilence(List<float> samples, float min, int channels, int hz, bool _3D, bool stream) {
int i;
for (i=0; i<samples.Count; i++) {
if (Mathf.Abs(samples[i]) > min) {
break;
}
}
samples.RemoveRange(0, i);
for (i=samples.Count - 1; i>0; i--) {
if (Mathf.Abs(samples[i]) > min) {
break;
}
}
samples.RemoveRange(i, samples.Count - i);
var clip = AudioClip.Create("TempClip", samples.Count, channels, hz, _3D, stream);
clip.SetData(samples.ToArray(), 0);
return clip;
}
static FileStream CreateEmpty(string filepath) {
var fileStream = new FileStream(filepath, FileMode.Create);
byte emptyByte = new byte();
for(int i = 0; i < HEADER_SIZE; i++) //preparing the header
{
fileStream.WriteByte(emptyByte);
}
return fileStream;
}
static void ConvertAndWrite(FileStream fileStream, AudioClip clip) {
var samples = new float[clip.samples];
clip.GetData(samples, 0);
Int16[] intData = new Int16[samples.Length];
//converting in 2 float[] steps to Int16[], //then Int16[] to Byte[]
Byte[] bytesData = new Byte[samples.Length * 2];
//bytesData array is twice the size of
//dataSource array because a float converted in Int16 is 2 bytes.
int rescaleFactor = 32767; //to convert float to Int16
for (int i = 0; i<samples.Length; i++) {
intData[i] = (short) (samples[i] * rescaleFactor);
Byte[] byteArr = new Byte[2];
byteArr = BitConverter.GetBytes(intData[i]);
byteArr.CopyTo(bytesData, i * 2);
}
fileStream.Write(bytesData, 0, bytesData.Length);
}
static void WriteHeader(FileStream fileStream, AudioClip clip) {
var hz = clip.frequency;
var channels = clip.channels;
var samples = clip.samples;
fileStream.Seek(0, SeekOrigin.Begin);
Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF");
fileStream.Write(riff, 0, 4);
Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8);
fileStream.Write(chunkSize, 0, 4);
Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE");
fileStream.Write(wave, 0, 4);
Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt ");
fileStream.Write(fmt, 0, 4);
Byte[] subChunk1 = BitConverter.GetBytes(16);
fileStream.Write(subChunk1, 0, 4);
UInt16 two = 2;
UInt16 one = 1;
Byte[] audioFormat = BitConverter.GetBytes(one);
fileStream.Write(audioFormat, 0, 2);
Byte[] numChannels = BitConverter.GetBytes(channels);
fileStream.Write(numChannels, 0, 2);
Byte[] sampleRate = BitConverter.GetBytes(hz);
fileStream.Write(sampleRate, 0, 4);
Byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2
fileStream.Write(byteRate, 0, 4);
UInt16 blockAlign = (ushort) (channels * 2);
fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2);
UInt16 bps = 16;
Byte[] bitsPerSample = BitConverter.GetBytes(bps);
fileStream.Write(bitsPerSample, 0, 2);
Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data");
fileStream.Write(datastring, 0, 4);
Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2);
fileStream.Write(subChunk2, 0, 4);
// fileStream.Close();
}
}

nice work. a note of thanks to u and gregzo for this.

PhilGal commented Jun 25, 2013

Good job! However I have an optimization tip to suggest.

I tried to apply this class in an Android app where ConvertAndWrite() worked incredibly slow (approx 5sec for a ~1mb file). I came up with the following implementation that does its job in less then a second:

static void ConvertAndWrite(MemoryStream memStream, ClipData clipData)
    {

        var samples = clipData.samples;

        Int16[] intData = new Int16[samples.Length];

        Byte[] bytesData = new Byte[samples.Length * 2];

        const int rescaleFactor = 32767; //to convert float to Int16

        for (int i = 0; i < samples.Length; i++)
        {
            intData[i] = (short)(samples[i] * rescaleFactor);
        }
        Buffer.BlockCopy(intData, 0, bytesData, 0, bytesData.Length);
        memStream.Write(bytesData, 0, bytesData.Length);
    }

ClipData is a struct replacement for the AudioClip class, so we can run the method in a separate thread.

@ PhilGal,

Can you explain a bit more about ClipData. Is this part of unity?

chulini commented Sep 26, 2013

First of all, thanks for this. Saved me lot of work.

Second, I've figure that the conversion only works for mono (1 channel) AudioClips.
To make this work for multichannel AudioClips (like stereo or sorround) I've replaced (line 110):

var samples = new float[clip.samples];

for this:

var samples = new float[clip.samples*clip.channels];

5argon commented Oct 10, 2013

Hi, I have tried this and it's working OK but experienced some audio quality loss (The saved .wav file sounds like compressed 128 kbps mp3 file.) which can be fixed by replacing :

int rescaleFactor = 32767; //to convert float to Int16

with this

float rescaleFactor = 32767;

tqnst commented Aug 19, 2014

Hi everybody,

Thank you for excellent code to record audio, but I have a problem : possible to save an AudioClip as a .mp4 file? If possible, can anyone suggest me a solution?

Thank you for a great work!
Would be cool to add Windows Store Apps support though (FileStream doesn't work on that platform).

eyalfx commented Oct 3, 2014

Yeah, a compress format would be awesome. but either way, this is a kick ass class.
Thank you.

thanks!
but I have a problem. is it possible to trip/cut the audiosoucre.audio.clip when i record it with the microphone?
cause when I start to record with the mic. Microphone.Start(null, true, 10, maxFreq);
then stopped after 5 sec. it saves the audio but its a 10 second audioclip.
is it possible to save at runtime?

Hi all, I've recreated couple of function with PhilGal Threading.

struct ClipData
{
    public int samples;
}
    private Thread WritingThread;

    void ConvertAndWrite(MemoryStream memStream, ClipData clipData)
    {
        float[] samples = new float[clipData.samples];

        Int16[] intData = new Int16[samples.Length];

        Byte[] bytesData = new Byte[samples.Length * 2];

        const float rescaleFactor = 32767; //to convert float to Int16

        for (int i = 0; i < samples.Length; i++)
        {
            intData[i] = (short)(samples[i] * rescaleFactor);
        }
        Buffer.BlockCopy(intData, 0, bytesData, 0, bytesData.Length);
        memStream.Write(bytesData, 0, bytesData.Length);
    }

    public AudioClip Save(string filename, AudioClip clip)
    {
        if (!filename.ToLower().EndsWith(".wav"))
            filename += ".wav";
        string filepath = Path.Combine(Application.persistentDataPath, filename);
        Debug.Log(filepath);
        Directory.CreateDirectory(Path.GetDirectoryName(filepath));
        ClipData clipData = new ClipData();
        clipData.samples = clip.samples;
        using (FileStream fileStream = CreateEmpty(filepath))
        {
            MemoryStream memStream = new MemoryStream();
            WritingThread = new Thread(() => ConvertAndWrite(memStream, clipData));
            memStream.WriteTo(fileStream);
            WriteHeader(fileStream, clip);
        }
        return clip;
    }

worla commented Mar 15, 2015

hi, i already have a script that records sound from a microphone and plays back. I am wondering how i can incorporate this in my code so i just don't play back but also save as a wav file.

Here is my code:

using UnityEngine;  
using System.Collections;  

   [RequireComponent (typeof (AudioSource))]  

   public class VoiceChat : Photon.MonoBehaviour   
   {  
//A boolean that flags whether there's a connected microphone  
private bool micConnected = false;  

//The maximum and minimum available recording frequencies  
private int minFreq;  
private int maxFreq;  

//A handle to the attached AudioSource  
private AudioSource goAudioSource;  

//Use this for initialization  
void Start()   
{  
    //Check if there is at least one microphone connected  
    if(Microphone.devices.Length <= 0)  
    {  
        //Throw a warning message at the console if there isn't  
        Debug.LogWarning("Microphone not connected!");  
    }  
    else //At least one microphone is present  
    {  
        //Set 'micConnected' to true  
        micConnected = true;  

        //Get the default microphone recording capabilities  
        Microphone.GetDeviceCaps(null, out minFreq, out maxFreq);  

        //According to the documentation, if minFreq and maxFreq are zero, the microphone supports any frequency...  
        if(minFreq == 0 && maxFreq == 0)  
        {  
            //...meaning 44100 Hz can be used as the recording sampling rate  
            maxFreq = 44100;  
        }  

        //Get the attached AudioSource component  
        goAudioSource = this.GetComponent<AudioSource>();  
    }  
}  

void OnGUI()   
{  
    //If there is a microphone  
    if(micConnected)  
    {  
        //If the audio from any microphone isn't being captured  
        if(!Microphone.IsRecording(null))  
        {  
            //Case the 'Record' button gets pressed  
            if(GUI.Button(new Rect(Screen.width/2-100, Screen.height/2-25, 200, 50), "Record"))  
            {  
                //Start recording and store the audio captured from the microphone at the AudioClip in the AudioSource  
                goAudioSource.clip = Microphone.Start(null, true, 20, maxFreq);  
            }  
        }  
        else //Recording is in progress  
        {  
            //Case the 'Stop and Play' button gets pressed  
            if(GUI.Button(new Rect(Screen.width/2-100, Screen.height/2-25, 200, 50), "Stop and Play!"))  
            {  
                Microphone.End(null); //Stop the audio recording  
                goAudioSource.Play(); //Playback the recorded audio  
            }  

            GUI.Label(new Rect(Screen.width/2-100, Screen.height/2+25, 200, 50), "Recording in progress...");  
        }  
    }  
    else // No microphone  
    {  
        //Print a red "Microphone not connected!" message at the center of the screen  
        GUI.contentColor = Color.red;  
        GUI.Label(new Rect(Screen.width/2-100, Screen.height/2-25, 200, 50), "Microphone not connected!");  
    }  

}  

}

ChiefLD commented May 1, 2015

i know im late to this party, but, is it possible to then load the .wav file back as an audioclip during runtime?

.wav files can be loaded as an audioClip in several ways, this is how I do it

        AudioClip audioClip = Resources.Load ("voiceRecord") as AudioClip;
        audioSource.audio.clip = audioClip;
audioSource.audio.Play ();

can anyone help me to rewright the same script to save audioClips as .mp3

Lomyo commented Jun 11, 2015

NIce,Its very helpful,Thx

I have used this script to save audio wav file. The above code records a wav file and saves but when we try to play it back the audio quality and modulation of recorded voice is changed but it plays the recorded one.

I need to convert this wav file to base 64 and pass it it to ispeech api but it says as bad audio data. so we contacted the support team of ispeech they said us to set sample rate = 16khz bit rate = 16 bit channel = mono

How can I set these values while writing a wav.

Why in function WriteHeader have comment out:
// fileStream.Close();
can it make memory leak, can you explain for me, thanks you

Is there a nice way to reduce file size?

this code does the job in seconds!

using System;
using System.IO;
using UnityEngine;
using System.Collections.Generic;
using System.Threading;

public class SavWav {

	const int HEADER_SIZE = 44;
	struct ClipData{

		public  int samples;
		public  int channels;
		public float[] samplesData;

	}

	public bool Save(string filename, AudioClip clip) {
		if (!filename.ToLower().EndsWith(".wav")) {
			filename += ".wav";
		}

		var filepath = filename;

		Debug.Log(filepath);

		// Make sure directory exists if user is saving to sub dir.
		Directory.CreateDirectory(Path.GetDirectoryName(filepath));
		ClipData clipdata = new ClipData ();
		clipdata.samples = clip.samples;
		clipdata.channels = clip.channels;
		float[] dataFloat = new float[clip.samples*clip.channels];
		clip.GetData (dataFloat, 0);
		clipdata.samplesData = dataFloat;
		using (var fileStream = CreateEmpty(filepath)) {
			MemoryStream memstrm = new MemoryStream ();
			ConvertAndWrite(memstrm, clipdata);
			memstrm.WriteTo (fileStream);
			WriteHeader(fileStream, clip);
		}

		return true; // TODO: return false if there's a failure saving the file
	}

	public AudioClip TrimSilence(AudioClip clip, float min) {
		var samples = new float[clip.samples];

		clip.GetData(samples, 0);

		return TrimSilence(new List<float>(samples), min, clip.channels, clip.frequency);
	}

	public AudioClip TrimSilence(List<float> samples, float min, int channels, int hz) {
		return TrimSilence(samples, min, channels, hz, false, false);
	}

	public AudioClip TrimSilence(List<float> samples, float min, int channels, int hz, bool _3D, bool stream) {
		int i;

		for (i=0; i<samples.Count; i++) {
			if (Mathf.Abs(samples[i]) > min) {
				break;
			}
		}

		samples.RemoveRange(0, i);

		for (i=samples.Count - 1; i>0; i--) {
			if (Mathf.Abs(samples[i]) > min) {
				break;
			}
		}

		samples.RemoveRange(i, samples.Count - i);

		var clip = AudioClip.Create("TempClip", samples.Count, channels, hz, _3D, stream);

		clip.SetData(samples.ToArray(), 0);

		return clip;
	}

	 FileStream CreateEmpty(string filepath) {
		var fileStream = new FileStream(filepath, FileMode.Create);
	    byte emptyByte = new byte();

	    for(int i = 0; i < HEADER_SIZE; i++) //preparing the header
	    {
	        fileStream.WriteByte(emptyByte);
	    }

		return fileStream;
	}

	void ConvertAndWrite(MemoryStream memStream, ClipData clipData)
	{
		float[] samples = new float[clipData.samples*clipData.channels];

		samples = clipData.samplesData;

		Int16[] intData = new Int16[samples.Length];

		Byte[] bytesData = new Byte[samples.Length * 2];

		const float rescaleFactor = 32767; //to convert float to Int16

		for (int i = 0; i < samples.Length; i++)
		{
			intData[i] = (short)(samples[i] * rescaleFactor);
			//Debug.Log (samples [i]);
		}
		Buffer.BlockCopy(intData, 0, bytesData, 0, bytesData.Length);
		memStream.Write(bytesData, 0, bytesData.Length);
	}

	 void WriteHeader(FileStream fileStream, AudioClip clip) {

		var hz = clip.frequency;
		var channels = clip.channels;
		var samples = clip.samples;

		fileStream.Seek(0, SeekOrigin.Begin);

		Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF");
		fileStream.Write(riff, 0, 4);

		Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8);
		fileStream.Write(chunkSize, 0, 4);

		Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE");
		fileStream.Write(wave, 0, 4);

		Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt ");
		fileStream.Write(fmt, 0, 4);

		Byte[] subChunk1 = BitConverter.GetBytes(16);
		fileStream.Write(subChunk1, 0, 4);

		UInt16 two = 2;
		UInt16 one = 1;

		Byte[] audioFormat = BitConverter.GetBytes(one);
		fileStream.Write(audioFormat, 0, 2);

		Byte[] numChannels = BitConverter.GetBytes(channels);
		fileStream.Write(numChannels, 0, 2);

		Byte[] sampleRate = BitConverter.GetBytes(hz);
		fileStream.Write(sampleRate, 0, 4);

		Byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2
		fileStream.Write(byteRate, 0, 4);

		UInt16 blockAlign = (ushort) (channels * 2);
		fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2);

		UInt16 bps = 16;
		Byte[] bitsPerSample = BitConverter.GetBytes(bps);
		fileStream.Write(bitsPerSample, 0, 2);

		Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data");
		fileStream.Write(datastring, 0, 4);

		Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2);
		fileStream.Write(subChunk2, 0, 4);

//		fileStream.Close();
	}
}

also I have gathered some libraries and made a package to save audioclip to mp3!

you can find it here: https://github.com/BeatUpir/Unity3D-save-audioClip-to-MP3

CanisLupus commented Jan 20, 2017 edited

Just noting that Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); is incorrect. The variable samples comes from clip.samples, which is the total number of samples with all channels already included. This means that multiplying by channels will effectively double the expected number of samples if your clip has 2 channels, which is wrong. It should simply be samples * 2.

The result of this bug is that some audio players will report the wav file as being double its real duration, as they may read only this value and not the size of the data section itself (i.e. the samples). Most of them will stop the clip "midway", when the samples themselves end, because there are no more samples to play.

I have two questions after tried the original code in this post. It worked with saving a WAV file onto my disk. However, I can't trim the silence at the beginning and the end of the wav file. Each wav file saved is with 30 seconds (if Byte[] subChunk2 = BitConverter.GetBytes(samples * channels)), or 1 minute (if Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2)). How to solve this problem?

Is there a modified code to convert the source AudioClip into FLAC format? I can only use FLAC format for my application.

Thanks and hope to get help from you guys!

@CanisLupus

Correct, I recently found this as well. iTunes would play it normal, but Unity would play it twice as long, with the 2nd half silent.

Question: Has anyone had issues with volume being low? I have a version that combines arrays of audio clips:

https://github.com/infinitypbr/AudioClipArrayCombiner

Basically if each "Layer" has 5 clips, the script will export 125 variations, every possible combination of them. It has options for volume & delay as well. It works great with small clips.

But, when I try to combine some song layers, basically 2 minutes each, the result sounds right, but is much quieter than it should be. Any thoughts?

sadajpe commented Aug 17, 2017

hi
Can I load saved audio into my audio clip?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment