LoginSignup
24
28

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-01-24

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

24
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
28