はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】AudioClipからWAVEファイルを生成する

はじめに

前回WAVEファイルからAudioClipを動的に生成する記事を書きました。

www.hanachiru-blog.com

今回はその逆でAudioCliipからWAVEファイルを作ってみたいと思います。

WAVEファイルの形式

前述の記事でもWAVEファイルの仕組みを書いたのですが、今回はWAVEファイルを作る側なので一番シンプルな基本形だけ抑えておけばOKだと思います。

WAVEファイルの構造

Microsoft WAVE soundfile format

開始アドレス byte データ内容
0 4 "RIFF"
4 4 ファイルのバイトサイズ - 8byte
8 4 "WAVE"
12 4 "fmt"
16 4 fmtチャンクのバイト数。リニアPCMなら16
20 2 フォーマットID。リニアPCMなら1
22 2 チャンネル数。モノラルなら1, ステレオなら2
24 4 サンプリングレート
28 4 データ速度(byte/sec)
32 2 ブロックサイズ(byte/sample*チャンネル数)
34 2 サンプルあたりのビット数(bit/sample)
36 4 "data"
40 4 波形データのバイト数
44 n 波形データ
構造

AudioClipとの対応

WAVEファイルの構造はこうだと言われても、AudioClipのどの値が対応してるんだとなるかと思います。

私が調べた結果以下で合っていると思うのですが、間違っている可能性もありますのでご注意ください。(もし間違っていた場合はコメント等で教えていただけると嬉しいです)

また名前は先ほど貼った図と一致しているのでそちらを見ながらチェックしてみてください。
Microsoft WAVE soundfile format

名前
ChunkSize 44 + AudioClip.samples * AudioClip.channels * BitsPerSample / 8
NumChannels AudioClip.channels
SampleRate AudioClip.frequency
ByteRate AudioClip.samples * AudioClip.channels * BitsPerSample / 8
BlockAlign AudioClip.channels * BitsPerSample / 8
Subchuk2Size AudioClip.samples * AudioClip.channels * BitsPerSample / 8
BitsPerSample AudioClipの情報から算出できるはずだが、やり方が分からなかった。情報求む

コード

BitsPerSampleの出し方が分からなかったので、ひとまず16に決め打ちしちゃってます。

一応以下のサイトのse_saa01.wavで試してみたところできました。
サイト名:otosozai.com https://otosozai.com/

おそらく元のAudioClipBitPerSample16だったおかげだとは思います。

実験で用いたWavファイル
using System;
using System.IO;
using System.Text;
using UnityEngine;

public static class Wav
{
    private const int BitsPerSample = 16;
    private const int AudioFormat = 1;

    /// <summary>
    /// Wavファイルのデータ構造に変換する
    /// </summary>
    /// <param name="audioClip"></param>
    /// <returns></returns>
    public static byte[] ToWav(this AudioClip audioClip)
    {
        using var stream = new MemoryStream();
        
        WriteRiffChunk(audioClip, stream);
        WriteFmtChunk(audioClip, stream);
        WriteDataChunk(audioClip, stream);

        return stream.ToArray();
    }

    /// <summary>
    /// Wavファイルをpathに出力する
    /// </summary>
    /// <param name="audioClip"></param>
    /// <param name="path"></param>
    public static void ExportWav(this AudioClip audioClip, string path)
    {
        using var stream = new FileStream(path, FileMode.Create);
        
        WriteRiffChunk(audioClip, stream);
        WriteFmtChunk(audioClip, stream);
        WriteDataChunk(audioClip, stream);
    }

    private static void WriteRiffChunk(AudioClip audioClip, Stream stream)
    {
        // ChunkID RIFF
        stream.Write(Encoding.ASCII.GetBytes("RIFF"));

        // ChunkSize
        const int headerByteSize = 44;
        var chunkSize = BitConverter.GetBytes((UInt32)(headerByteSize + audioClip.samples  * audioClip.channels * BitsPerSample / 8));
        stream.Write(chunkSize);
        
        // Format WAVE
        stream.Write(Encoding.ASCII.GetBytes("WAVE"));
    }

    private static void WriteFmtChunk(AudioClip audioClip, Stream stream)
    {
        // Subchunk1ID fmt
        stream.Write(Encoding.ASCII.GetBytes("fmt "));

        // Subchunk1Size (16 for PCM)
        stream.Write(BitConverter.GetBytes((UInt32)16));
        
        // AudioFormat (PCM=1)
        stream.Write(BitConverter.GetBytes((UInt16)AudioFormat));
        
        // NumChannels (Mono = 1, Stereo = 2, etc.)
        stream.Write(BitConverter.GetBytes((UInt16)audioClip.channels));
        
        // SampleRate (audioClip.sampleではなくaudioClip.frequencyのはず)
        stream.Write(BitConverter.GetBytes((UInt32)audioClip.frequency));
        
        // ByteRate (=SampleRate * NumChannels * BitsPerSample/8)
        stream.Write(BitConverter.GetBytes((UInt32)(audioClip.samples * audioClip.channels * BitsPerSample / 8)));
        
        // BlockAlign (=NumChannels * BitsPerSample/8)
        stream.Write(BitConverter.GetBytes((UInt16)(audioClip.channels * BitsPerSample / 8)));
        
        // BitsPerSample
        stream.Write(BitConverter.GetBytes((UInt16)BitsPerSample));
    }

    private static void WriteDataChunk(AudioClip audioClip, Stream stream)
    {
        // Subchunk2ID data
        stream.Write(Encoding.ASCII.GetBytes("data"));
        
        // Subchuk2Size
        stream.Write(BitConverter.GetBytes((UInt32)(audioClip.samples * audioClip.channels * BitsPerSample / 8)));
        
        // Data
        var floatData = new float[audioClip.samples * audioClip.channels];
        audioClip.GetData(floatData, 0);

        switch (BitsPerSample)
        {
            case 8:
                foreach (var f in floatData) stream.Write(BitConverter.GetBytes((sbyte) (f * sbyte.MaxValue)));
                break;
            case 16:
                foreach (var f in floatData) stream.Write(BitConverter.GetBytes((short)(f * short.MaxValue)));
                break;
            case 32:
                foreach (var f in floatData) stream.Write(BitConverter.GetBytes((int)(f * int.MaxValue)));
                break;
            case 64:
                foreach (var f in floatData) stream.Write(BitConverter.GetBytes((float)(f * float.MaxValue)));
            default:
                throw new NotSupportedException(nameof(BitsPerSample));
        }
    }
}