Voicy Tech Blog

Voicy公式Techブログ

Swift3でCore Audioを使用した音声ファイル変換

こんにちは!Voicy CTOの窪田です。

今回はSwift3でCore Audioを使って、WAVファイルをAACに変換する方法についてお話ししようと思います。

経緯

以前、Voicyの録音アプリでは音声を保存する際にはWAVファイルを使用していました。しかし無圧縮のWAVではサイズが大きいため、通信速度が遅い環境でアップロードに時間がかかってしまうという課題がありました。

そこで、録音が終わった後にWAVを圧縮形式のAACに変換すれば、WAVで録音している部分には手を加えなくて済むし良いのではないかと思い今回紹介するプログラムを作成しました。しかし、現実はそんなに甘くはなく、、、

Voicyでは最大10分間の録音ができるのですが、フルに録音したものをiPhone5AACに変換すると2分以上かかってしまったのです。iPhone6以降であれば問題なかったのですが、Voicyで活躍しているパーソナリティにはiPhone5を使用している方もいるので、録音するたびにそんな長時間待たせるわけにはいきません。

というわけで、最終的には録音しながらAACでリアルタイムに保存する対応を行うことになりました。そのため、Voicyのアプリでは今回紹介するフォーマットの変換処理は使っておりません。そして、せっかく作ったのにもったいないからブログのネタにしてやろうとか思ったわけでもありません(`・ω・´)キリッ

ちなみに、WAVファイルで録音していた時でもアップロード後にサーバー側で圧縮していたので、再生アプリでダウンロードする際の通信量は抑えられていました。サーバー側での圧縮処理にはAWSAmazon Elastic Transcoderを使用しています。

Core Audioを使用した変換

Core Audioと言っても処理によって複数のフレームワークやサービスに別れています。今回の変換処理ではAudio Toolboxフレームワークに含まれるExtended Audio File Serviceを使用します。昔のiOSではAACの変換は対応していなかったのですが、iOS 3.1あたりから対応したようです。10まで出ている現在では気にする必要はなさそうですね。ちなみにMP3への変換は未だ未対応です。

余談ですが、先日MP3のライセンスが終了というニュースがありました。未対応の理由がライセンスだったのであればこれを期に対応するのか、それともMP3はもう古いとしてずっと未対応なのかはちょっと気になるところではあります。個人的にAppleは後者ではないかと思っていますが。

WAVファイルを開く

それでは順番に変換処理を説明していきます。まずCore Audioを使用するにはAVFoundationをインポートする必要があります。

import AVFoundation

次に変換元となるWAVファイルを開きます。ファイルはプロジェクト内に元から存在するものとし、Bundle.mainでURLを取得してきています。

// 変換元のinput.wavを指すURL
let inFileUrl = URL(fileURLWithPath: Bundle.main.path(forResource: "input", ofType: "wav")!)

// WAVファイルを開く
var inFile: ExtAudioFileRef?
_ = ExtAudioFileOpenURL(inFileUrl as CFURL, &inFile)

// WAVファイルの情報を取得
var inASBDSize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
var inFormat = AudioStreamBasicDescription()
ExtAudioFileGetProperty(inFile!, kExtAudioFileProperty_FileDataFormat, &inASBDSize, &inFormat)

そんなに難しいところは無いと思います。最後の部分では変換元であるWAVファイルの情報を取得しています。サンプリングレートやビットレート、ステレオかモノラルかといった音声ファイルの基本的な情報が含まれており、Core AudioではASBD(Audio Stream Basic Description)と呼ばれる構造体で表されます。

AACファイルを開く

次に変換先となるAACファイルを開きます。ここではテンポラリフォルダにファイルを作成しています。

// 変換先のoutput.aacを指すURL
let toUrl = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent("output.aac"))

// AACファイル情報
var outASBDSize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
var aacFormat = AudioStreamBasicDescription()
aacFormat.mFormatID         = kAudioFormatMPEG4AAC
aacFormat.mSampleRate       = 44100.0
aacFormat.mChannelsPerFrame = 1
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, nil, &outASBDSize, &aacFormat)

// AACファイルを開く
_ = ExtAudioFileCreateWithURL(
    outFileUrl as CFURL,
    kAudioFileM4AType,
    &aacFormat,
    nil,
    AudioFileFlags.eraseFile.rawValue,
    &outFile)

先ほどのWAVファイルでは、ファイルからASBDを取得していましたが、AACの場合はまだファイルが存在しないため、書き込むASBDの値を自分で指定する必要があります。ここではAACであることと、サンプリングレートに44.1kHz、チャンネル数に1(モノラル) を指定しています。

ASBDには他にも情報を指定する必要があるのですが、AudioFormatGetPropertyにkAudioFormatProperty_FormatInfoを指定することでその他の情報を自動的に埋めてくれる便利機能があるのでそれを使用しています。

ファイルを開く際にもkAudioFileM4ATypeでAACであることを指定しています。同じAACを指すものでもASBDに指定していたkAudioFormatMPEG4AACとは違うので間違えないように気をつけてください。AudioFileFlags.eraseFileは既存のファイルがあった場合に上書きすることを表しています。

変換ルールを指定

これでWAV、AACの両ファイルを開けましたが、じゃあどこでデータの変換処理するの?となります。

答えはファイルを読み込む時、もしくは書き込む時で、どちらで行うかは自分で指定する必要があります。ここでは書き込む時に指定してみました。

// 書き込みプロパティ設定
ExtAudioFileSetProperty(
    outFile!,
    kExtAudioFileProperty_ClientDataFormat,
    inASBDSize,
    &inFormat)

AACファイル(outFile)に対して、WAVファイルの情報(inASBDSize, inFormat)を指定しています。これにより、WAVファイルのデータを渡すからAACに変換して保存してねという命令をしていることになります。

WAV -> AAC 変換処理

いよいよメインの処理です。とはいえ先ほど指定した変換ルールのおかげで難しいことは全て自動的にやってくれるため、プログラマーがやることとしては

  1. バッファを作成
  2. WAVファイルを順次読み込み
  3. AACファイル書き込み

これだけです。

まずはバッファを作成します。

// バッファ作成
var readFrameSize: UInt32 = 1024 // 一度に読み込むフレーム数
var bufferSize = readFrameSize * inFormat.mBytesPerPacket
var buffer: UnsafeMutableRawPointer = malloc(Int(bufferSize))
defer { free(buffer) }

var audioBuffer = AudioBufferList()
audioBuffer.mNumberBuffers = 1
audioBuffer.mBuffers.mNumberChannels = inFormat.mChannelsPerFrame
audioBuffer.mBuffers.mDataByteSize = bufferSize
audioBuffer.mBuffers.mData = buffer

var readFrameSize: UInt32 = 1024

フレーム数は必要に応じて変更してください。
ちなみにこの数字を増減しても処理時間はあまり変わりませんでした。

var bufferSize = readFrameSize * inFormat.mBytesPerPacket

WAVファイルの場合、フレーム数とパケット数が同じなので、バッファサイズは フレーム数 × パケットサイズ になります。

var buffer: UnsafeMutableRawPointer = malloc(Int(bufferSize))
defer { free(buffer) }

バッファ領域はmallocで確保しています。mallocで確保した場合は自動的に解放されないため、処理を抜ける時に解放されるようにするため、deferでfreeを行なっています。

バッファを確保できたら、次はファイルの読み込みと書き込みを行ないます。

// ファイルを読み込んで出力
while (true) {
    // WAVファイル読み込み
    ExtAudioFileRead(inFile, &readFrameSize, &audioBuffer)
    // 読み込むデータがなくなれば終了
    if readFrameSize <= 0 { break }
    // AACファイル書き込み
    ExtAudioFileWrite(outFile, readFrameSize, &audioBuffer)
}

バッファにはWAVファイルのデータが読み込まれ、それをExtAudioFileWriteへ渡した際に、先ほど登録した変換ルールが適用されてAACに変換して保存されます。想像つくかもしれませんが、この変換処理が一番重いです。とはいえ最近の端末に限定して良いのであればそこまで気にしなくてもいいのかもしれませんが。

ファイルを閉じる

// ファイルを閉じる
ExtAudioFileDispose(inFile!)
ExtAudioFileDispose(outFile!)

最後にファイルを閉じてオブジェクトを破棄します。Extended Audio File ServiceではExtAudioFileDisposeを呼ぶとファイルのクローズとオブジェクトの破棄を同時に行ってくれます。

終わりに

Core AudioはiPhoneで音声アプリをつくるなら避けては通れないものです。ポインタもあっちこっちで出てくるので最初はとっつきにくいかも知れませんが、今回紹介したExtended Audio File Serviceのように便利な関数も多数用意されており、知れば知るほど楽しくなってきます。VoicyではこれからもCore Audioを使いこなし、音声技術のスペシャリスト集団になっていきたいと思います!

Voicyのメンバーがどういった思いでサービスを作っているのか、ぜひともこちらのインタビューも合わせてお読みください!