OS Xでのマイク入力キャプチャ Swift版

More than 3 years have passed since last update.

OS X でのマイクの音声キャプチャ実装方法について、Swiftの勉強ついでに書いてみた。

構造がどうなっているのか気になったので、こちらも調べてメモ。

参考


Core Audio

iOS、OS Xプラットフォームでオーディオを処理を行うためのフレームワーク総称

録音や音声にエフェクトをかけるなどのオーディオ操作に関する API が内包されている。

具体的なAPIはObjective-C、Cで実装されている。


Core Audio アーキテクチャ


OS X Core Audioアーキテクチャ


  • OS XではHAL(Hardware Abstraction Layer)を通じてオーディオデバイスの設定を行う。
    alt


iOS Core Audio アーキテクチャ


  • iOS Core AudioはOS X Core Audioのサブセット版

  • iOSの場合はMIDIを扱うフレームワークがない

  • ハードウェアレイヤへのアクセスがAudio Sessionからに限定
    alt


ハードウェア抽象化

コードで直接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