Pebble Coding

ソフトウェアエンジニアによるIT技術、数学の備忘録

swiftでwavファイル書き込みしてみる(AVAudioFIle)

iOSでWAVファイルを書き出す場合、
以前はAudioToolKitにあるAudioFileCreateWithURLを使っていましたが、
ここでは試しにiOS8,macOS10.10で追加されたAVFoundationにあるAVAudioFileを使ってみます。
まず、AVAudioFileのクラス定義をみてみます。

open class AVAudioFile : NSObject {
    public init(forReading fileURL: URL) throws
    public init(forReading fileURL: URL, commonFormat format: AVAudioCommonFormat, interleaved: Bool) throws
    public init(forWriting fileURL: URL, settings: [String : Any]) throws
    public init(forWriting fileURL: URL, settings: [String : Any], commonFormat format: AVAudioCommonFormat, interleaved: Bool) throws
    open func read(into buffer: AVAudioPCMBuffer) throws
    open func read(into buffer: AVAudioPCMBuffer, frameCount frames: AVAudioFrameCount) throws
    open func write(from buffer: AVAudioPCMBuffer) throws
    open var url: URL { get }
    open var fileFormat: AVAudioFormat { get }
    open var processingFormat: AVAudioFormat { get }
    open var length: AVAudioFramePosition { get }
    open var framePosition: AVAudioFramePosition
}

forWritingというのを使いwriteを呼び出してクラスのメモリを解放すれば良さそうです。
AVAudioPCMBufferの定義を見て見ます。

open class AVAudioPCMBuffer : AVAudioBuffer {
    public init(pcmFormat format: AVAudioFormat, frameCapacity: AVAudioFrameCount)
    open var frameCapacity: AVAudioFrameCount { get }
    open var frameLength: AVAudioFrameCount
    open var stride: Int { get }
    open var floatChannelData: UnsafePointer<UnsafeMutablePointer<Float>>? { get }
    open var int16ChannelData: UnsafePointer<UnsafeMutablePointer<Int16>>? { get }
    open var int32ChannelData: UnsafePointer<UnsafeMutablePointer<Int32>>? { get }
}

フォーマットとキャパシティ(=サンプル数)を指定してインスタンスを作り、データを詰めれば良さそうです。

書き込むフォーマットはWindowsで標準的なInt16、サンプルレート44100,チャネル数2,インターリーブであるWAVファイル形式とします。
書き込むデータはLには440Hzのサイン波、Rには523Hzのサイン波を1024サンプルだけ出力することにします。
音の長さは約0.023秒となります。
以下、実装です。

if let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
    let path = documentsPath.appending("/test.wav")
    let format:AVAudioFormat = AVAudioFormat(commonFormat: AVAudioCommonFormat.pcmFormatInt16,
                                             sampleRate: 44100,
                                             channels: 2,
                                             interleaved: true)
    if let url = URL(string:path) {
        do {
            // WAVファイルのフォーマットを指定する
            let file:AVAudioFile = try AVAudioFile(forWriting: url,
                                               settings: format.settings, 
                                               commonFormat: format.commonFormat,
                                               interleaved: true)
            // データ書き込みフォーマットを指定する 
            // ここではWAVファイルと同じフォーマットを使う
            let buffer:AVAudioPCMBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)
            if let p:UnsafePointer<UnsafeMutablePointer<Int16>> = buffer.int16ChannelData {
                let q:UnsafeMutablePointer<Int16> = p.pointee
                var x:Float = 0
                var y:Float = 0
                let delta_x:Float = Float(440 * 2 * M_PI / 44100) // ラ(440Hz)
                let delta_y:Float = Float(523 * 2 * M_PI / 44100) // ド(523Hz)
                for i:Int in stride(from:0, to:1024*2, by: 2) {
                    q[i]   = Int16(sin(x) * 32767.0) // L
                    q[i+1] = Int16(sin(y) * 32767.0) // R
                    x += delta_x
                    y += delta_y
                }
            } else {
                assert(false)
            }
            // メモリに書き込んだサンプル数をセット
            buffer.frameLength = 1024
            // WAVファイルへ書き込み
            try file.write(from: buffer)
        } catch {
            assert(false)
        }
    }
}

ここでは、WAVファイルのフォーマットとデータを書き込む時のフォーマットを同一にしています。
同じにしておくとコンバートが走らないので効率が良いですが、異なるフォーマットでもある程度の範囲内なら
AVAudioFileが勝手にコンバートしてくれたりします。
経験的にはInterleavedやチャネル数が同じでないと動かないです。

interleaved:trueにした場合AVAudioPCMBufferのInt16の領域が1つになりますのでint16ChannelDataから
サンプル数分LRの順序で詰めていけば良いです。 buffer.frameLengthに書き込む値を毎回セットする必要があります。

AudioToolKitで書くよりソースコードが長くなっている気もしますが、
swiftは型安全性を何より優先しますので、こんな感じになります。
ついでに波形画像も載せておきます。

f:id:pebble8888:20170122004906p:plain

GitHub - pebble8888/AudioFileCreateSample