OS X でのマイクの音声キャプチャ実装方法について、Swiftの勉強ついでに書いてみた。
構造がどうなっているのか気になったので、こちらも調べてメモ。
参考
- Core Audio Overview
- Technical Note TN2091 Device input using the HAL Output Audio Unit
- http://stackoverflow.com/questions/24838106/using-audiobufferlist-with-swift
Core Audio
iOS、OS Xプラットフォームでオーディオを処理を行うためのフレームワーク総称
録音や音声にエフェクトをかけるなどのオーディオ操作に関する API が内包されている。
具体的なAPIはObjective-C、Cで実装されている。
Core Audio アーキテクチャ##
OS X Core Audioアーキテクチャ###
iOS Core Audio アーキテクチャ###
ハードウェア抽象化##
コードで直接HALとやりとりする必要はない。
AUHALユニット(OS X)およびAURemoteIOユニット(iOS)と呼ばれる特殊なオーディオユニットが提供されており、これらを経由してオーディオデータを他のオーディオユニットに渡すことができる。
実際のハードウェアとのやりとりはI/OKitがドライバーとアクセスして実行される。OS Xではこれらの接続インターフェースとしてAUHALユニットが提供されている。
AudioHardware.hヘッダファイルで定義されている。
プログラム#
デフォルトのマイクの音声をキャプチャするプログラム
main.swift##
lang:main.swift
import Foundation
let micInput = MicInput.sharedInstance
micInput.setUpAudioDevice()
micInput.startAudio()
// Enterで終了
getchar()
micInput.stopAudio()
MicInput.swift##
lang:MicInput.swift
import Foundation
import CoreAudio
import AudioUnit
class MicInput {
    class var sharedInstance: MicInput
    {
        struct Static
        {
            static let instance : MicInput = MicInput()
        }
        return Static.instance
    }
    
    var inputUnit = AudioUnit()
    
    let inputProc : AURenderCallback = { (
        inRefCon: UnsafeMutablePointer<Void>,
        ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
        inTimeStamp: UnsafePointer<AudioTimeStamp>,
        inBufNumber: UInt32,
        inNumberFrames: UInt32,
        ioData: UnsafeMutablePointer<AudioBufferList>) -> OSStatus in
        
        var err :OSStatus? = nil
        
        let buffer = MicInput.sharedInstance.allocateAudioBuffer(2 ,size: inNumberFrames)
        var bufferList = AudioBufferList.init(mNumberBuffers: 1, mBuffers: buffer)
        
        err = AudioUnitRender(MicInput.sharedInstance.inputUnit, ioActionFlags, inTimeStamp, inBufNumber, inNumberFrames, &bufferList)
        
        if err == noErr
        {
            let data=UnsafePointer<Int16>(bufferList.mBuffers.mData)
        
            let dataArray = UnsafeBufferPointer<Int16>(start:data, count: Int(bufferList.mBuffers.mDataByteSize)/sizeof(Int16))
            for i in 0...dataArray.count-1
            {
                print(dataArray[i])
            }
        }
        return err!
    }
    
    func allocateAudioBuffer(let numChannel: UInt32, let size: UInt32) -> AudioBuffer
    {
        let dataSize = UInt32(numChannel * UInt32(sizeof(Float64)) * size)
        let data = malloc(Int(dataSize))
        let buffer = AudioBuffer.init(mNumberChannels: numChannel, mDataByteSize: dataSize, mData: data)
        
        return buffer
    }
    
    private func setUpAudioHAL() -> OSStatus
    {
        // AudioUnitを作成する
        var desc = AudioComponentDescription()
        var comp:AudioComponent?
        
        desc.componentType = kAudioUnitType_Output
        desc.componentSubType = kAudioUnitSubType_HALOutput
        
        desc.componentManufacturer = kAudioUnitManufacturer_Apple
        desc.componentFlags = 0
        desc.componentFlagsMask = 0
        
        comp = AudioComponentFindNext(nil, &desc)
        if comp == nil
        {
            return -1
        }
        
        let err = AudioComponentInstanceNew(comp!, &self.inputUnit)
        return err
    }
    
    private func setUpEnableIO() -> OSStatus
    {
        // AudioUnitの入力を有効化、出力を無効化する。
        // デフォルトは出力有効設定
        var enableIO: UInt32 = 1
        var disableIO: UInt32 = 0
        var err: OSStatus?
        
        err = AudioUnitSetProperty(self.inputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &enableIO, UInt32(sizeof(UInt32)))
        
        if err == noErr
        {
            err = AudioUnitSetProperty(self.inputUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &disableIO, UInt32(sizeof(UInt32)))
        }
        
        return err!
    }
    
    private func setUpMicInput() -> OSStatus
    {
        // 入力デバイスを設定
        
        var inputDeviceId = AudioDeviceID()
        var address =  AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster)
        var size = UInt32(sizeof(AudioDeviceID))
        
        var err = AudioObjectGetPropertyData(UInt32(kAudioObjectSystemObject), &address, 0, nil, &size, &inputDeviceId)
        // デフォルトの入力デバイスを取得
        
        if err == noErr
        {
            err = AudioUnitSetProperty(self.inputUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &inputDeviceId, size)
            // AudioUnitにデバイスを設定
        }
        
        // 確認用
        print("DeviceName:",self.deviceName(inputDeviceId))
        print("BufferSize:",self.bufferSize(inputDeviceId))
        
        return err
    }
    
    private func deviceName(let devID: AudioDeviceID) -> String
    {
        // 名前確認
        var address =  AudioObjectPropertyAddress(mSelector: kAudioObjectPropertyName, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster)
        var name: CFStringRef?
        var stringsize = UInt32(sizeof(CFStringRef))
        
        AudioObjectGetPropertyData(devID, &address, 0, nil, &stringsize, &name)
        
        let string = String(name)
        return string
        
    }
    
    private func bufferSize(let devID: AudioDeviceID) -> UInt32
    {
        // バッファサイズ確認
        var address =  AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyBufferFrameSize, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster)
        var buf: UInt32 = 0
        var bufSize = UInt32(sizeof(UInt32))
        
        AudioObjectGetPropertyData(devID, &address, 0, nil, &bufSize, &buf)
        
        return buf
    }
    
    private func setUpInputFormat() -> OSStatus
    {
        // サンプリングレートやデータビット数、データフォーマットなどを設定
        var audioFormat = AudioStreamBasicDescription()
        audioFormat.mBitsPerChannel = 16
        audioFormat.mBytesPerFrame = 4
        audioFormat.mBytesPerPacket = 4
        audioFormat.mChannelsPerFrame = 2
        audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
        audioFormat.mFormatID = kAudioFormatLinearPCM
        audioFormat.mFramesPerPacket = 1
        audioFormat.mSampleRate = 44100.00
        
        let size = UInt32(sizeof(AudioStreamBasicDescription))
        let err = AudioUnitSetProperty(self.inputUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &audioFormat, size)
        
        return err
    }
    
    private func setUpCallback() -> OSStatus
    {
        // サンプリング用コールバックを設定
        var input = AURenderCallbackStruct(inputProc: self.inputProc, inputProcRefCon: nil)
        
        let size = UInt32(sizeof(AURenderCallbackStruct))
        let err = AudioUnitSetProperty(self.inputUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &input, size)
        
        return err
    }
    
    func setUpAudioDevice()
    {
        if self.setUpAudioHAL() != noErr
        {
            exit(-1)
        }
        
        if self.setUpEnableIO() != noErr
        {
            exit(-1)
        }
        
        if self.setUpMicInput() != noErr
        {
            exit(-1)
        }
        
        if self.setUpInputFormat() != noErr
        {
            exit(-1)
        }
        
        if self.setUpCallback() != noErr
        {
            exit(-1)
        }
        
        if AudioUnitInitialize(self.inputUnit) != noErr
        {
            exit(-1)
        }
    }
    
    func startAudio()
    {
        if AudioOutputUnitStart(self.inputUnit) != noErr
        {
            exit(-1)
        }
    }
    
    func stopAudio()
    {
        if AudioOutputUnitStop(self.inputUnit) != noErr
        {
            exit(-1)
        }
    }
}
まとめ#
・デバイスドライバへの意識をまったくせずに実装できたのは楽だった。
・最初コールバック関数の引数である "ioDate" からキャプチャしたデータを引っ張れると思いこんでいて少しハマった。。。AudioRenderCallback()関数を呼び出してデータを引っ張ってくる必要があった。Audio Unit Hosting Fundamentals

