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

swift アクセス制御

open どこからでもサブクラスの作成、上書き定義可能
public このクラスを定義したモジュール内部でのみサブクラスの作成、上書き定義可能
internal (省略時) 定義を含むソースファイルと同じモジュール内部からアクセス可能
private クラスや構造体の定義単位の内部からのみ利用可能
fileprivate 同一ソースファイル内からのみ利用可能

詳説 AudioStreamBasicDescription / AVAudioFormat

CoreAudioでよく使われるAudioStreamBasicDescription これはC言語の構造体で使いづらいです。
macOS10.10, iOS8以降ではAVAudioFormatと相互変換できますので、これを経由するとコーディングが簡単になります。

まずは実例を見ていきます。
よく使われる.wavファイルのフォーマットを作ってみます。

let fmt = AVAudioFormat(commonFormat:.pcmFormatInt16,
                        sampleRate:44100,
                        channels:2,
                        interleaved:true)
print("\(fmt)")
// <AVAudioFormat 0x100b06650:  2 ch,  44100 Hz, Int16, inter>
let asbd:AudioStreamBasicDescription = fmt.streamDescription.pointee
// AudioStreamBasicDescription(mSampleRate: 44100.0, mFormatID: 1819304813, mFormatFlags: 12, mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 16, mReserved: 0)
print("\(asbd)")

1819304813(=0x6c70636d)というのは kAudioFormatLinearPCM = 'lpcm'の10進値で、線形PCM形式であることを示しています。
フォーマットフラグの12は
12=8(=kAudioFormatFlagIsPacked)+4(kAudioFormatFlagIsSignedInteger)
を意味します。

例をもう一つ見て見ましょう。

import AVFoundation
let fmt = AVAudioFormat(standardFormatWithSampleRate:44100, channels:2)
//<AVAudioFormat 0x100a00bb0:  2 ch,  44100 Hz, Float32, non-inter>
let asbd:AudioStreamBasicDescription = fmt.streamDescription.pointee
print("\(asbd)")
//AudioStreamBasicDescription( mSampleRate: 44100.0, mFormatID: 1819304813, mFormatFlags: 41, mBytesPerPacket: 4, 
//mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0)

標準フォーマットとはfloat32のnon-interleavedの事だとわかります。
non-interleaveとはステレオの場合に、
LRの音データをLLL..., RRR...
のように別領域に割り当てることを指します。
interleaveの場合LRLR...のように交互に割り当てます。
フォーマットフラグの41は
41=32(kAudioFormatFlagsIsNonInterleaved)+8(kAudioFormatFlagsIsPacked)+1(kAudioFormatFlagIsFloat)
を意味します。

クラス構造を見てきます。

open class AVAudioFormat : NSObject, NSSecureCoding {
    public init(streamDescription asbd: UnsafePointer<AudioStreamBasicDescription>)
    public init(streamDescription asbd: UnsafePointer<AudioStreamBasicDescription>, channelLayout layout: AVAudioChannelLayout?)
    public init(standardFormatWithSampleRate sampleRate: Double, channels: AVAudioChannelCount)
    public init(standardFormatWithSampleRate sampleRate: Double, channelLayout layout: AVAudioChannelLayout)
    public init(commonFormat format: AVAudioCommonFormat, sampleRate: Double, channels: AVAudioChannelCount, interleaved: Bool)
    public init(commonFormat format: AVAudioCommonFormat, sampleRate: Double, interleaved: Bool, channelLayout layout: AVAudioChannelLayout)
    public init(settings: [String : Any])
    public init(cmAudioFormatDescription formatDescription: CMAudioFormatDescription)
    open func isEqual(_ object: Any) -> Bool
    open var isStandard: Bool { get }
    open var commonFormat: AVAudioCommonFormat { get }
    open var channelCount: AVAudioChannelCount { get }
    open var sampleRate: Double { get }
    open var isInterleaved: Bool { get }
    open var streamDescription: UnsafePointer<AudioStreamBasicDescription> { get }
    open var channelLayout: AVAudioChannelLayout? { get }
    open var magicCookie: Data?
    open var settings: [String : Any] { get }
    open var formatDescription: CMAudioFormatDescription { get }
}

// モノラルなら1,ステレオなら2の値です
public typealias AVAudioChannelCount = UInt32

// よく使うのはpcmFormatFloat32とpcmFormatInt16でしょう
public enum AVAudioCommonFormat : UInt {
    case otherFormat
    case pcmFormatFloat32
    case pcmFormatFloat64
    case pcmFormatInt16
    case pcmFormatInt32
}

formatDescriptionプロパティとsettingsプロパティに何が入っているのかも見てみましょう。

let fmt = AVAudioFormat(commonFormat:.pcmFormatInt16,
                        sampleRate:44100,
                        channels:2,
                        interleaved:true)
print("\(fmt.formatDescription)")
print("\(fmt.settings)")

<CMAudioFormatDescription 0x100a075a0 [0x7fffd7221d80]> {
    mediaType:'soun' 
    mediaSubType:'lpcm' 
    mediaSpecific: {
        ASBD: {
            mSampleRate: 44100.000000 
            mFormatID: 'lpcm' 
            mFormatFlags: 0xc 
            mBytesPerPacket: 4 
            mFramesPerPacket: 1 
            mBytesPerFrame: 4 
            mChannelsPerFrame: 2 
            mBitsPerChannel: 16    } 
        cookie: {(null)} 
        ACL: {(null)}
        FormatList Array: {(null)} 
    } 
    extensions: {(null)}
}
["AVLinearPCMIsFloatKey": 0, "AVLinearPCMIsNonInterleaved": 0, "AVNumberOfChannelsKey": 2, "AVSampleRateKey": 44100, "AVLinearPCMBitDepthKey": 16, "AVLinearPCMIsBigEndianKey": 0, "AVFormatIDKey": 1819304813]

settingsには文字列キーのDictionaryが入っているようです。