はじめに
前回WAVE
ファイルからAudioClip
を動的に生成する記事を書きました。
今回はその逆でAudioCliip
からWAVE
ファイルを作ってみたいと思います。
WAVEファイルの形式
前述の記事でもWAVE
ファイルの仕組みを書いたのですが、今回はWAVE
ファイルを作る側なので一番シンプルな基本形だけ抑えておけばOKだと思います。
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/
おそらく元のAudioClip
のBitPerSample
が16
だったおかげだとは思います。
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)); } } }