はじめに
今回はOggVorbisEncoder
というライブラリを用いたWav
からOgg
へのファイル変換の仕方について紹介したいと思います。
一応Mac
での動作確認はできたので、Windows
でもいけると思います。(是非調べてみてください)
概要
A .NET Core Ogg Vorbis audio encoding library written entirely in managed code.
// DeepL翻訳
.NET Core Ogg Vorbis オーディオエンコードライブラリ。すべてマネージドコードで書かれています。
GitHub - SteveLillis/.NET-Ogg-Vorbis-Encoder: Ogg Vorbis audio encoding library written in C#
全てManaged code
、つまりはC#
により記述されているのでおそらく大抵のプラットフォームで動くはずです。
NAudio
なども試してはみたのですが、結局OSの機能を用いていたりでWindows
でしか動作しないものが大半でした。
(マルチプラットフォームに対応していない)ネイティブプラグインがないというだけで、かなり価値はあると思います。
インストール
今回はnuget.org
からdll
を拾ってきます。
依存先のライブラリを含めて以下を入れれば動作しました。
- OggVorbisEncoder.dll (v1.2.1)
- System.Runtime.CompilerServices.Unsafe.dll (6.0.0)
コード
どうやらこのdll
を入れただけでは簡単にOgg
に変換できないっぽいので公式のサンプルコードからコードを引っ張ってきます。
private static readonly int WriteBufferSize = 512; private static readonly int[] SampleRates = {8000, 11025, 16000, 22050, 32000, 44100}; private static byte[] ConvertRawPCMFile(int outputSampleRate, int outputChannels, byte[] pcmSamples, PcmSample pcmSampleSize, int pcmSampleRate, int pcmChannels) { int numPcmSamples = (pcmSamples.Length / (int) pcmSampleSize / pcmChannels); float pcmDuraton = numPcmSamples / (float) pcmSampleRate; int numOutputSamples = (int) (pcmDuraton * outputSampleRate); //Ensure that samble buffer is aligned to write chunk size numOutputSamples = (numOutputSamples / WriteBufferSize) * WriteBufferSize; float[][] outSamples = new float[outputChannels][]; for (int ch = 0; ch < outputChannels; ch++) { outSamples[ch] = new float[numOutputSamples]; } for (int sampleNumber = 0; sampleNumber < numOutputSamples; sampleNumber++) { float rawSample = 0.0f; for (int ch = 0; ch < outputChannels; ch++) { int sampleIndex = (sampleNumber * pcmChannels) * (int) pcmSampleSize; if (ch < pcmChannels) sampleIndex += (ch * (int) pcmSampleSize); switch (pcmSampleSize) { case PcmSample.EightBit: rawSample = ByteToSample(pcmSamples[sampleIndex]); break; case PcmSample.SixteenBit: rawSample = ShortToSample((short) (pcmSamples[sampleIndex + 1] << 8 | pcmSamples[sampleIndex])); break; } outSamples[ch][sampleNumber] = rawSample; } } return GenerateFile(outSamples, outputSampleRate, outputChannels); } private static byte[] GenerateSineWaveFile(int outputSampleRate, int outputChannels, int frequency, float durationSeconds, float volume = 0.2f) { float[][] outSamples = new float[outputChannels][]; int numOutputSamples = (int) (durationSeconds * outputSampleRate); //Ensure that samble buffer is aligned to write chunk size numOutputSamples = (numOutputSamples / WriteBufferSize) * WriteBufferSize; for (int ch = 0; ch < outputChannels; ch++) { outSamples[ch] = new float[numOutputSamples]; } for (int i = 0; i < numOutputSamples; i++) { var sample = volume * SineSample(i, frequency, outputSampleRate); for (int ch = 0; ch < outputChannels; ch++) outSamples[ch][i] = sample; } return GenerateFile(outSamples, outputSampleRate, outputChannels); } private static byte[] GenerateFile(float[][] floatSamples, int sampleRate, int channels) { using MemoryStream outputData = new MemoryStream(); // Stores all the static vorbis bitstream settings var info = VorbisInfo.InitVariableBitRate(channels, sampleRate, 0.5f); // set up our packet->stream encoder var serial = new System.Random().Next(); var oggStream = new OggStream(serial); // ========================================================= // HEADER // ========================================================= // Vorbis streams begin with three headers; the initial header (with // most of the codec setup parameters) which is mandated by the Ogg // bitstream spec. The second header holds any comment fields. The // third header holds the bitstream codebook. var comments = new Comments(); comments.AddTag("ARTIST", "TEST"); var infoPacket = HeaderPacketBuilder.BuildInfoPacket(info); var commentsPacket = HeaderPacketBuilder.BuildCommentsPacket(comments); var booksPacket = HeaderPacketBuilder.BuildBooksPacket(info); oggStream.PacketIn(infoPacket); oggStream.PacketIn(commentsPacket); oggStream.PacketIn(booksPacket); // Flush to force audio data onto its own page per the spec FlushPages(oggStream, outputData, true); // ========================================================= // BODY (Audio Data) // ========================================================= var processingState = ProcessingState.Create(info); for (int readIndex = 0; readIndex <= floatSamples[0].Length; readIndex += WriteBufferSize) { if (readIndex == floatSamples[0].Length) { processingState.WriteEndOfStream(); } else { processingState.WriteData(floatSamples, WriteBufferSize, readIndex); } while (!oggStream.Finished && processingState.PacketOut(out OggPacket packet)) { oggStream.PacketIn(packet); FlushPages(oggStream, outputData, false); } } FlushPages(oggStream, outputData, true); return outputData.ToArray(); } private static void FlushPages(OggStream oggStream, Stream output, bool force) { while (oggStream.PageOut(out OggPage page, force)) { output.Write(page.Header, 0, page.Header.Length); output.Write(page.Body, 0, page.Body.Length); } } private static float SineSample(int sample, float frequency, int sampleRate) { float sampleT = ((float) sample) / sampleRate; return (float) Math.Sin(sampleT * Math.PI * 2.0f * frequency); } private static float ByteToSample(short pcmValue) { return pcmValue / 128f; } private static float ShortToSample(short pcmValue) { return pcmValue / 32768f; } /// <summary> /// We cheat on the WAV header; we just bypass the header and never /// verify that it matches 16bit/stereo/44.1kHz.This is just an /// example, after all. /// </summary> private static void StripWavHeader(BinaryReader stdin) { var tempBuffer = new byte[6]; for (var i = 0; (i < 30) && (stdin.Read(tempBuffer, 0, 2) > 0); i++) { if ((tempBuffer[0] == 'd') && (tempBuffer[1] == 'a')) { stdin.Read(tempBuffer, 0, 6); break; } } } enum PcmSample : int { EightBit = 1, SixteenBit = 2 }
.NET-Ogg-Vorbis-Encoder/Encoder.cs at master · SteveLillis/.NET-Ogg-Vorbis-Encoder · GitHub
一部コードを書き換えましたが、このコードのConvertRawPCMFile
メソッドを利用することでWav
からOgg
への変換ができます。
Wavファイルから情報を抜き取る
先程のConvertRawPCMFile
メソッドを利用するには以下の情報が必要になります。
- outputSampleRate
- outputChannels
- byte[] pcmSamples,
- pcmSampleSize
- pcmSampleRate
- pcmChannels
ひとまずoutputSampleRate
とpcmSampleRate
、outputChannels
とpcmChannels
は同じものを使うとして、これらの情報はWav
の先頭に情報が書き込まれています。
Microsoft WAVE soundfile format
細かいことは昔書いた記事でも言及しているので良ければ見てみてください。
【Unity】WAVファイルからAudioClipを動的に生成する(WAVEのファイル構造解析) - はなちるのマイノート
ここから情報を抜き取れば良いので、コードを書いていきます。
/// <summary> /// Wavファイルを読み取る /// </summary> /// <param name="stream">Stream</param> /// <returns>Wavファイルに書き込まれているデータ</returns> private static WavFileFormat ReadWavFile(Stream stream) { // RIFF var riffBytes = new byte[4]; stream.Read(riffBytes, 0, riffBytes.Length); 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]; stream.Read(chunkSizeBytes, 0, chunkSizeBytes.Length); var chunkSize = BitConverter.ToInt32(chunkSizeBytes, 0); // WAVE var wavBytes = new byte[4]; stream.Read(wavBytes, 0, wavBytes.Length); 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]; stream.Read(fmtBytes, 0, fmtBytes.Length); 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]; stream.Read(fmtSizeBytes, 0, fmtSizeBytes.Length); var fmtSize = BitConverter.ToInt32(fmtSizeBytes, 0); // AudioFormat var audioFormatBytes = new byte[2]; stream.Read(audioFormatBytes, 0, audioFormatBytes.Length); var isPCM = audioFormatBytes[0] == 0x1 && audioFormatBytes[1] == 0x0; // NumChannels Mono = 1, Stereo = 2 var numChannelsBytes = new byte[2]; stream.Read(numChannelsBytes, 0, numChannelsBytes.Length); var channels = (int) BitConverter.ToUInt16(numChannelsBytes, 0); // SampleRate var sampleRateBytes = new byte[4]; stream.Read(sampleRateBytes, 0, sampleRateBytes.Length); var sampleRate = BitConverter.ToInt32(sampleRateBytes, 0); // ByteRate (=SampleRate * NumChannels * BitsPerSample/8) var byteRateBytes = new byte[4]; stream.Read(byteRateBytes, 0, byteRateBytes.Length); // BlockAlign (=NumChannels * BitsPerSample/8) var blockAlignBytes = new byte[2]; stream.Read(blockAlignBytes, 0, blockAlignBytes.Length); // BitsPerSample var bitsPerSampleBytes = new byte[2]; stream.Read(bitsPerSampleBytes, 0, bitsPerSampleBytes.Length); var bitPerSample = BitConverter.ToUInt16(bitsPerSampleBytes, 0); // Discard Extra Parameters if (fmtSize > 16) stream.Seek(fmtSize - 16, SeekOrigin.Current); // Data var subChunkIDBytes = new byte[4]; stream.Read(subChunkIDBytes, 0, subChunkIDBytes.Length); // If fact exists, discard fact if (subChunkIDBytes[0] == 0x66 && subChunkIDBytes[1] == 0x61 && subChunkIDBytes[2] == 0x63 && subChunkIDBytes[3] == 0x74) { var factSizeBytes = new byte[4]; stream.Read(factSizeBytes, 0, factSizeBytes.Length); var factSize = BitConverter.ToInt32(factSizeBytes, 0); stream.Seek(factSize, SeekOrigin.Current); stream.Read(subChunkIDBytes, 0, subChunkIDBytes.Length); } 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]; stream.Read(dataSizeBytes, 0, dataSizeBytes.Length); var dataSize = BitConverter.ToInt32(dataSizeBytes, 0); var data = new byte[dataSize]; stream.Read(data, 0, data.Length); return new WavFileFormat() { IsPCM = isPCM, NumChannels = channels, SampleRate = sampleRate, BitPerSample = bitPerSample, Data = data }; } /// <summary> /// Wavファイルに書き込まれているデータ /// </summary> public class WavFileFormat { public bool IsPCM; public int NumChannels; public int SampleRate; public ushort BitPerSample; public byte[] Data; }
たまにfact
チャンクがあったり、fmt
チャンクにExtra parameters
があったりするのでそれに対応する処理が含まれています。
Oggへ変換する
これまでのコード達を使って変換を行っていきます。
/// <summary> /// WavファイルからOggに変換をする /// </summary> /// <param name="wavStream">Wavファイル</param> /// <returns>Ogg</returns> public static byte[] ConvertWav2Ogg(Stream wavStream) { var wav = ReadWavFile(wavStream); var pcmSample = wav.BitPerSample switch { 8 => PcmSample.EightBit, 16 => PcmSample.SixteenBit, _ => throw new InvalidOperationException() }; var ogg = ConvertRawPCMFile(wav.SampleRate, wav.NumChannels, wav.Data, pcmSample, wav.SampleRate, wav.NumChannels); return ogg; }
これで一通り完成ですね。
利用するサンプルコード
実際にこれらのコードを利用するサンプルを書いてみます。
var wavPath = Application.streamingAssetsPath + "/output.wav"; // 今回はストレージから直接読み取るサンプル // メモリから読み取るならMemoryStreamを利用すると良い using var wav = new FileStream(wavPath, FileMode.Open); // WavからOggに変換 (OggUtilsは今までのコードが格納されているクラス) var ogg = OggUtils.ConvertWav2Ogg(wav); // ファイル出力する var outputPath = Application.streamingAssetsPath + "/output.ogg"; using var writer = new FileStream(outputPath, FileMode.Create); writer.Write(ogg, 0, ogg.Length); // エディタを更新する #if UNITY_EDITOR AssetDatabase.Refresh(); #endif
Path
とかOggUtils
(今までのコードを格納しているクラス)とかは適宜置き換えてもらって、このコードでいけるはずです。
ストレージから直接読み取る場合はFileStream
を利用した方が効率が良いですが、メモリから読み取る場合はMemoryStream
を使いましょう。