はじめに
今回はWav
ファイルからAudioClip
を生成する方法について書きたいと思います。
https://docs.unity3d.com/ja/2018.4/ScriptReference/AudioClip.Create.html
https://docs.unity3d.com/ja/2018.4/ScriptReference/AudioClip.SetData.html
利用させていただくWavファイル
今回は実験として以下のサイトから.wav
ファイルを利用させていただきました。
サイト名:otosozai.com https://otosozai.com/
インポートした際の挙動
まず動的にAudioClip
を生成する前に、みなさんご存じかと思いますがUnityに.wav
をインポートするとAudioClip
に自動で変換してくれます。
ただしStreamingAssets
フォルダ以下に配置すると、生でデータを配置するためAudioClip
に変換は行われません。
https://docs.unity3d.com/ja/2022.1/Manual/StreamingAssets.html
他にも動的に.wav
を生成した後、再生をしたいとか等の場合に今回紹介する手法が活用できるはずです。
wavファイルの構造
WAVE
ファイルはRIFF
という形式で保存されているらしく、以下の構造をしています。
http://soundfile.sapp.org/doc/WaveFormat/
http://www13.plala.or.jp/kymats/study/MULTIMEDIA/load_wave.html
http://shopping2.gmobb.jp/htdmnr/www08/asp/wav.html
http://www.graffiti.jp/pc/p030506a.htm
基本系は一番上の記事の形みたいですが、色々変化系があるっぽいですね。またRIFF
,WAVE
,fmt
, fact
,data
といった文字列が識別のためにいくつかあるのですがそれらはビッグエンディアンで、他のデータはリトルエンディアンのようです。
Microsoft WAVE soundfile format
factチャンク無し(通常)
開始アドレス | 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) | 2 | 拡張部分のサイズ。リニアPCMなら存在しない |
(38) | n | 拡張部分。リニアPCMなら存在しない |
36 | 4 | "data" |
40 | 4 | 波形データのバイト数 |
44 | n | 波形データ |
factチャンク有り(特殊)
開始アドレス | 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) | 2 | 拡張部分のサイズ。リニアPCMなら存在しない |
(38) | n | 拡張部分。リニアPCMなら存在しない |
36 | 4 | "fact" |
40 | 4 | factチャンクのバイト数 |
44 | 4 | 全サンプル数 |
48 | 4 | "data" |
52 | 4 | 波形データのバイト数 |
56 | n | 波形データ |
ぶっちゃけfact
チャンク内の情報はあんまり重要じゃないぽい?。
WAVEファイルを読み取り、解析する
WAVE
ファイルの読み取りは各自好きにしてもらえればですが、サンプルとしてStreamingsAssets
からの読み取ってみます。
// Andriodの場合はUnityWebRequestを使うことに注意 var fileBytes = File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath, "se_sac08.wav"));
後はWave
ファイルを解析してみます。
using var memoryStream = new MemoryStream(fileBytes); // RIFF var riffBytes = new byte[4]; memoryStream.Read(riffBytes); if (riffBytes[0] != 0x52 || riffBytes[1] != 0x49 || riffBytes[2] != 0x46 || riffBytes[3] != 0x46) throw new ArgumentException("fileBytes is not the correct Wav file format."); // chunk size var chunkSizeBytes = new byte[4]; memoryStream.Read(chunkSizeBytes); var chunkSize = BitConverter.ToInt32(chunkSizeBytes); // WAVE var wavBytes = new byte[4]; memoryStream.Read(wavBytes); if (wavBytes[0] != 0x57 || wavBytes[1] != 0x41 || wavBytes[2] != 0x56 || wavBytes[3] != 0x45) throw new ArgumentException("fileBytes is not the correct Wav file format."); // fmt var fmtBytes = new byte[4]; memoryStream.Read(fmtBytes); if (fmtBytes[0] != 0x66 || fmtBytes[1] != 0x6d || fmtBytes[2] != 0x74 || fmtBytes[3] != 0x20) throw new ArgumentException("fileBytes is not the correct Wav file format."); // fmtSize var fmtSizeBytes = new byte[4]; memoryStream.Read(fmtSizeBytes); var fmtSize = BitConverter.ToInt32(fmtSizeBytes); // AudioFormat var audioFormatBytes = new byte[2]; memoryStream.Read(audioFormatBytes); var isPCM = audioFormatBytes[0] == 0x1 && audioFormatBytes[1] == 0x0; // NumChannels Mono = 1, Stereo = 2 var numChannelsBytes = new byte[2]; memoryStream.Read(numChannelsBytes); var channels = (int)BitConverter.ToUInt16(numChannelsBytes); // SampleRate var sampleRateBytes = new byte[4]; memoryStream.Read(sampleRateBytes); var sampleRate = BitConverter.ToInt32(sampleRateBytes); // ByteRate (=SampleRate * NumChannels * BitsPerSample/8) var byteRateBytes = new byte[4]; memoryStream.Read(byteRateBytes); // BlockAlign (=NumChannels * BitsPerSample/8) var blockAlignBytes = new byte[2]; memoryStream.Read(blockAlignBytes); // BitsPerSample var bitsPerSampleBytes = new byte[2]; memoryStream.Read(bitsPerSampleBytes); var bitPerSample = BitConverter.ToUInt16(bitsPerSampleBytes); // Discard Extra Parameters if(fmtSize > 16) memoryStream.Seek(fmtSize - 16, SeekOrigin.Current); // Data var subChunkIDBytes = new byte[4]; memoryStream.Read(subChunkIDBytes); // If fact exists, discard fact if (subChunkIDBytes[0] == 0x66 && subChunkIDBytes[1] == 0x61 && subChunkIDBytes[2] == 0x63 && subChunkIDBytes[3] == 0x74) { var factSizeBytes = new byte[4]; memoryStream.Read(factSizeBytes); var factSize = BitConverter.ToInt32(factSizeBytes); memoryStream.Seek(factSize, SeekOrigin.Current); memoryStream.Read(subChunkIDBytes); } if (subChunkIDBytes[0] != 0x64 || subChunkIDBytes[1] != 0x61 || subChunkIDBytes[2] != 0x74 || subChunkIDBytes[3] != 0x61) throw new ArgumentException("fileBytes is not the correct Wav file format."); // dataSize (=NumSamples * NumChannels * BitsPerSample/8) var dataSizeBytes = new byte[4]; memoryStream.Read(dataSizeBytes); var dataSize = BitConverter.ToInt32(dataSizeBytes); var data = new byte[dataSize]; memoryStream.Read(data);
MemoryStream
を利用せずにBitConverter
の引数で配列のインデックスを指定,もしくは該当箇所のSpan
を突っ込んであげた方が効率が良い?かもしれませんが、一応動作はします。
というかファイルから直接読み取るならFileStream
を使った方がメモリに直接結果を入れられるのでそれが一番だとは思います。気になるかたはそこだけちょいと直してみてください。
ただfmt
のExtraParams
の有無やfact
の有無の対処が必要なことには注意です。
AudioClipを生成する
後はAudioClip
を作成するためにデータの変換を少し行います。
private static AudioClip CreateAudioClip(byte[] data, int channels, int sampleRate, UInt16 bitPerSample, string audioClipName) { var audioClipData = bitPerSample switch { 8 => Create8BITAudioClipData(data), 16 => Create16BITAudioClipData(data), 32 => Create32BITAudioClipData(data), _ => throw new ArgumentException($"bitPerSample is not supported : bitPerSample = {bitPerSample}") }; var audioClip = AudioClip.Create(audioClipName, audioClipData.Length, channels, sampleRate, false); audioClip.SetData(audioClipData, 0); return audioClip; } private static float[] Create8BITAudioClipData(byte[] data) => data.Select((x, i) => (float) data[i] / sbyte.MaxValue).ToArray(); private static float[] Create16BITAudioClipData(byte[] data) { var audioClipData = new float[data.Length / 2]; var memoryStream = new MemoryStream(data); for(var i = 0;;i++) { var target = new byte[2]; var read = memoryStream.Read(target); if (read <= 0) break; audioClipData[i] = (float) BitConverter.ToInt16(target) / short.MaxValue; } return audioClipData; } private static float[] Create32BITAudioClipData(byte[] data) { var audioClipData = new float[data.Length / 4]; var memoryStream = new MemoryStream(data); for(var i = 0;;i++) { var target = new byte[4]; var read = memoryStream.Read(target); if (read <= 0) break; audioClipData[i] = (float) BitConverter.ToInt32(target) / int.MaxValue; } return audioClipData; }
bitPerSample
によってどれくらいのビット数でfloat
に変換していくかが変わっていきます。
bitPerSample | floatに変換するバイト単位 |
---|---|
8 | 1 |
16 | 2 |
32 | 4 |
それぞれsbyte
, short
, int
の最大値で割ってあげて、-1 ~ 1
の値にしてあげればOKです。
またAudioClip
を生成するのはAudioClip.Create
、AudioClip
にデータを詰め込むのはAudioClip.SetData
をすればできます。
https://docs.unity3d.com/ja/2018.4/ScriptReference/AudioClip.Create.html
https://docs.unity3d.com/ja/2018.4/ScriptReference/AudioClip.SetData.html