Instantly share code, notes, and snippets.

Embed
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();
}
}
@rejahanwer

This comment has been minimized.

rejahanwer commented Mar 12, 2013

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

@PhilGal

This comment has been minimized.

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.

@harmonius

This comment has been minimized.

harmonius commented Aug 13, 2013

@ PhilGal,

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

@chulini

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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?

@Harteroider

This comment has been minimized.

Harteroider commented Aug 31, 2014

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

@eyalfx

This comment has been minimized.

eyalfx commented Oct 3, 2014

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

@GMAlfonso

This comment has been minimized.

GMAlfonso commented Jan 28, 2015

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?

@mitay-walle

This comment has been minimized.

mitay-walle commented Feb 8, 2015

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

This comment has been minimized.

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

This comment has been minimized.

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?

@Jakhongir91

This comment has been minimized.

Jakhongir91 commented May 11, 2015

.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 ();
@Jakhongir91

This comment has been minimized.

Jakhongir91 commented May 11, 2015

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

@Lomyo

This comment has been minimized.

Lomyo commented Jun 11, 2015

NIce,Its very helpful,Thx

@nsathish16

This comment has been minimized.

nsathish16 commented Sep 29, 2015

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.

@MrAnonymous110

This comment has been minimized.

MrAnonymous110 commented Mar 28, 2016

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

@DaganBog2fast

This comment has been minimized.

DaganBog2fast commented Jun 7, 2016

Is there a nice way to reduce file size?

@BeatUpir

This comment has been minimized.

BeatUpir commented Nov 20, 2016

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();
	}
}
@BeatUpir

This comment has been minimized.

BeatUpir commented Nov 20, 2016

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

This comment has been minimized.

CanisLupus commented Jan 20, 2017

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.

@tammyhuang

This comment has been minimized.

tammyhuang commented Jan 31, 2017

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!

@infinitypbr

This comment has been minimized.

infinitypbr commented Feb 7, 2017

@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.

@infinitypbr

This comment has been minimized.

infinitypbr commented Feb 7, 2017

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

This comment has been minimized.

sadajpe commented Aug 17, 2017

hi
Can I load saved audio into my audio clip?

@Eldoir

This comment has been minimized.

Eldoir commented Jan 11, 2018

Really good script.
However, I recommend you set the return type of the Save method to void, as that method currently always returns true, which is absolutely non-informative.
There is also an extra empty line in the method, which is useless to me.
And as a C# developer, I virtually die when I read the var keyword. C# is a strongly-typed language, so go ahead and use it the way it was designed for! (I'm always open to discuss further on this, though)
I would put the comment of CreateDirectory() on the same line as the method call, to prevent leaving the useless comment alone if ever its associated code was moved around, or copied in another place.
Finally, you probably want the recording file to be not longer than the actual recorded audio, e.g. if you declared the clip to be 120s long with Microphone.Start(deviceName, false, 120, sampleRate);, the recording file will be 120s long no matter what, even if you only recorded actual audio, let's say, the first 6 seconds. You need to use the TrimSilence() method in order to cut the recording file right after no audio is detected.
Thus, to fulfill my needs, the Save() method becomes (I also removed the Debug.Log):

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

    string filepath = Path.Combine(Application.persistentDataPath, filename);

    Directory.CreateDirectory(Path.GetDirectoryName(filepath)); // Make sure directory exists if user is saving to sub dir.

    if (makeClipShort)
    {
        clip = TrimSilence(clip, 0);
    }

    using (var fileStream = CreateEmpty(filepath))
    {
        ConvertAndWrite(fileStream, clip);

        WriteHeader(fileStream, clip);
    }
}

I'd be happy if this code sample saved you some time.
Cheers!

@XY01

This comment has been minimized.

XY01 commented Apr 29, 2018

Hey, thanks for the script. I put together a little repo using this script along with the ability to record and load files if anyone is interested.

https://github.com/EXP-Productions/Unity-Audio_Recording_and_serialization_example/

@kwalkerk

This comment has been minimized.

kwalkerk commented May 30, 2018

I'm using the script and it works quite well with one glitch for me. It truncates the files. I can't see anything in the code which limits the file size of the clip. Am I missing something?

@angappanprakash

This comment has been minimized.

angappanprakash commented Aug 1, 2018

Sorry to open this old thread, It records only for 1 sec, if anyone has any idea?

@retinize

This comment has been minimized.

retinize commented Sep 1, 2018

Could this easily be translated to 48/24 wavs? I can change the sample rate easily, but changing the bit-rate to 24 seems more of a challenge?

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