LoginSignup
15
10

More than 5 years have passed since last update.

AUAudioUnit の使い方

Last updated at Posted at 2017-11-16

iOS 9, macOS 10.11 から AudioUnit のラッパーである AUAudioUnit が用意されていて、これを使えば AudioUnit を素のまま使うのに比べ、かなり楽に書くことができます。ネット上にもあまりサンプルがなくて、どう使うか結構試行錯誤して大変だったので、忘備録を兼ねて使い方の骨格を紹介しておきます。
(実際にアプリを作るにあたっては、AVAudioSession の設定とか、info.plist で privacy - microphone の設定とかエラー処理とかが必要ですが、今回は骨格を示すのが目的なので、省いてあります)

Pass Through

まず、マイクの入力をスピーカーにそのまま渡すコードは以下のようにすればいいようです。

import UIKit

@UIApplicationMain class
AppDelegate: UIResponder, UIApplicationDelegate {
    var
    window: UIWindow?
}

class
PassThroughVC: UIViewController {

    var
    m = try! PassThrough()

    @IBAction func
    ToggleA( _: Any? ) {
        try! m.Toggle()
    }
}

import AudioUnit
import AVFoundation

class
PassThrough {

    var
    auau: AUAudioUnit

    var
    isRunning = false

    var
    abl: UnsafeMutablePointer<AudioBufferList>?

    deinit {
        auau.deallocateRenderResources()
    }

    init( _ sampleRate: Int = 44100, _ numChannels: AVAudioChannelCount = 2 ) throws {

        auau = try AUAudioUnit(
            componentDescription: AudioComponentDescription(
                componentType           : kAudioUnitType_Output
            ,   componentSubType        : kAudioUnitSubType_RemoteIO
            ,   componentManufacturer   : kAudioUnitManufacturer_Apple
            ,   componentFlags          : 0
            ,   componentFlagsMask      : 0
            )
        )

        try auau.inputBusses[ 0 ].setFormat(
            AVAudioFormat( standardFormatWithSampleRate: Double( sampleRate ), channels: numChannels )!
        )
        try auau.outputBusses[ 1 ].setFormat( auau.inputBusses[ 0 ].format )

        auau.isInputEnabled = true
        auau.inputHandler = {                           // ------------------------ (2)
            ( actionFlags, timestamp, numberFrames, busNumber ) in
            guard let wABL = self.abl else { return }   // ------------------------ (4)
            let _ = self.auau.renderBlock(              // ------------------------ (3)
                actionFlags
            ,   timestamp
            ,   numberFrames
            ,   busNumber
            ,   wABL
            ,   nil
            )
        }

        auau.outputProvider = {                         // ------------------------ (1)
            ( actionFlags, timestamp, numberFrames, busNumber, data ) -> AUAudioUnitStatus in
            self.abl = data
            return 0
        }

        try auau.allocateRenderResources()
    }

    func
    Toggle() throws {
        if isRunning    { auau.stopHardware() }
        else            { try auau.startHardware() }
        isRunning = !isRunning
    }
}

出力モジュール(要するにスピーカーとか)が出力するべきデータが必要になると、(1)の outputProvider が呼ばれます。
引数 data が AudioUnit がアロケートした AudioBufferList なので、そのアドレスを保持しておきます。

入力データが揃うと、(2)の inputHandler が呼ばれます。このとき(1)の outputProvider で保持した AudioBufferList のアドレスで示される AudioUnit によってアロケートされた AudioBufferList を使用して、(3)で入力データを読み込みます。
(4)の guard は、inputHandler が outputProvider より先に呼ばれる場合に備えているものです。
(outputProvidert と inputHandler は同じスレッドで排他的に呼ばれています。)

Echo

入力を時間差で出力したい場合は、次のようなコードにすればいいようです。

昔の実際のエコー装置は、循環する磁気テープに1つの書き込みヘッドで書き込み、遅延する時間に相当する長さ分の複数の読み込みヘッドから読み込んでいます。読み込みヘッドが一つしかないエコー装置をプログラムでシミュレートしてみます。

下のプログラムでは、サンプルレートの2倍(要するに2秒分)の音データを保持できるバッファを確保して、
書き込みヘッド(writeHead)の1秒後の位置に読み込みヘッド(readHead)を設定しています。

import UIKit

@UIApplicationMain class
AppDelegate: UIResponder, UIApplicationDelegate {
    var
    window: UIWindow?
}

class
NSecVC: UIViewController {

    var
    m = try! NSec()

    @IBAction func
    ToggleA( _: Any? ) {
        try! m.Toggle()
    }
}

import AudioUnit
import AVFoundation

class
NSec {

    var
    auau: AUAudioUnit

    var
    isRunning = false

    var
    abl: UnsafeMutablePointer<AudioBufferList>?

    var
    buffer = [ [ Float ] ]()

    var
    writeHead: Int

    var
    readHead: Int

    deinit {
        auau.deallocateRenderResources()
    }

    init( _ sampleRate: Int = 44100, _ numChannels: AVAudioChannelCount = 2 ) throws {

        let 
        wBufferLength = sampleRate * 2

        writeHead = 0
        readHead = sampleRate

        for _ in 0 ..< numChannels {
            buffer.append( Array( repeating: Float( 0.0 ), count: wBufferLength ) )
        }

        try auau = AUAudioUnit(
            componentDescription: AudioComponentDescription(
                componentType           : kAudioUnitType_Output
            ,   componentSubType        : kAudioUnitSubType_RemoteIO
            ,   componentManufacturer   : kAudioUnitManufacturer_Apple
            ,   componentFlags          : 0
            ,   componentFlagsMask      : 0
            )
        )

        try auau.inputBusses[ 0 ].setFormat(
            AVAudioFormat( standardFormatWithSampleRate: Double( sampleRate ), channels: numChannels )!
        )
        try auau.outputBusses[ 1 ].setFormat( auau.inputBusses[ 0 ].format )

        auau.isInputEnabled = true
        auau.inputHandler = {
            ( actionFlags, timestamp, numberFrames, busNumber ) in
            guard let wABL = self.abl else { return }
            if self.auau.renderBlock(
                actionFlags
            ,   timestamp
            ,   numberFrames
            ,   busNumber
            ,   wABL
            ,   nil
            ) == 0 {
                let wABLP = UnsafeMutableAudioBufferListPointer( wABL )
                var w = [ UnsafePointer<Float> ]()
                for i in 0 ..< wABLP.count { w.append( UnsafePointer<Float>( OpaquePointer( wABLP[ i ].mData! ) ) ) }
                for j in 0 ..< Int( numberFrames ) {
                    for i in 0 ..< wABLP.count { self.buffer[ i ][ self.writeHead ] = w[ i ][ j ] }
                    self.writeHead += 1
                    if self.writeHead == wBufferLength { self.writeHead = 0 }
                }
            }
        }

        auau.outputProvider = {
            ( actionFlags, timestamp, numberFrames, busNumber, data ) -> AUAudioUnitStatus in
            self.abl = data
            let wABLP = UnsafeMutableAudioBufferListPointer( data )
            var w = [ UnsafeMutablePointer<Float> ]()
            for i in 0 ..< wABLP.count { w.append( UnsafeMutablePointer<Float>( OpaquePointer( wABLP[ i ].mData! ) ) ) }
            for j in 0 ..< Int( numberFrames ) {
                for i in 0 ..< wABLP.count { w[ i ][ j ] = self.buffer[ i ][ self.readHead ] }
                self.readHead += 1
                if self.readHead == wBufferLength { self.readHead = 0 }
            }
            return 0
        }

        try auau.allocateRenderResources()
    }

    func
    Toggle() throws {
        if isRunning    { auau.stopHardware() }
        else            { try auau.startHardware() }
        isRunning = !isRunning
    }
}

Synthesizer

音を生成するようなプログラムは下のようにすればいいようです。

outputProvider に音を生成させる AURenderPullInputBlock 型の関数を渡します。
以下のプログラムを動作させると、通常の環境であれば、左チャンネルから A(440Hz)、右チャンネルから E(660Hz)の正弦波とか矩形波を鳴らすことができます。

import UIKit

@UIApplicationMain class
AppDelegate: UIResponder, UIApplicationDelegate {
    var
    window: UIWindow?
}

class
GeneratorVC: UIViewController {

    var
    m = try! Generator()

    func
    Sin(
        actionFlags : UnsafeMutablePointer<AudioUnitRenderActionFlags>
    ,   timestamp   : UnsafePointer<AudioTimeStamp>
    ,   numberFrames: AUAudioFrameCount
    ,   busNumber   : Int
    ,   data        : UnsafeMutablePointer<AudioBufferList>
    ) -> AUAudioUnitStatus {
        let wABLP = UnsafeMutableAudioBufferListPointer( data )
        var w: [ UnsafeMutablePointer<Float> ] = []
        for i in 0 ..< wABLP.count { w.append( UnsafeMutablePointer<Float>( OpaquePointer( wABLP[ i ].mData! ) ) ) }
        let wSR = Float( self.m.sampleRate )
        for j in 0 ..< Int( numberFrames ) {
            let wPhase = Float( ( Int( timestamp.pointee.mSampleTime ) + j ) * 2 ) * Float.pi / wSR
            for i in 0 ..< wABLP.count { w[ i ][ j ] = sin( wPhase * Float( 220 + ( 220 * ( i + 1 ) ) ) ) }
        }
        return 0
    }

    func
    Square(
        actionFlags : UnsafeMutablePointer<AudioUnitRenderActionFlags>
    ,   timestamp   : UnsafePointer<AudioTimeStamp>
    ,   numberFrames: AUAudioFrameCount
    ,   busNumber   : Int
    ,   data        : UnsafeMutablePointer<AudioBufferList>
    ) -> AUAudioUnitStatus {
        let wABLP = UnsafeMutableAudioBufferListPointer( data )
        var w: [ UnsafeMutablePointer<Float> ] = []
        for i in 0 ..< wABLP.count { w.append( UnsafeMutablePointer<Float>( OpaquePointer( wABLP[ i ].mData! ) ) ) }
        let wSR = Float( self.m.sampleRate )
        for j in 0 ..< Int( numberFrames ) {
            let wTimeX2 = Float( ( Int( timestamp.pointee.mSampleTime ) + j ) * 2 ) / wSR
            for i in 0 ..< wABLP.count {
                w[ i ][ j ] = Int( wTimeX2 * Float( 220 + ( 220 * ( i + 1 ) ) ) ) % 2 == 0 ? 1 : -1
            }
        }
        return 0
    }

    @IBAction func
    ToggleA( _: Any? ) {
        try! m.Toggle()
    }
    @IBAction func
    SinA( _: Any? ) {
        m.auau.outputProvider = Sin
    }
    @IBAction func
    SquareA( _: Any? ) {
        m.auau.outputProvider = Square
    }
}

import AudioUnit
import AVFoundation

class
Generator {

    var
    auau: AUAudioUnit

    var
    sampleRate: Int

    var
    isRunning = false

    deinit {
        auau.deallocateRenderResources()
    }

    init( _ sampleRate: Int = 44100, _ numChannels: AVAudioChannelCount = 2 ) throws {

        self.sampleRate = sampleRate

        try auau = AUAudioUnit(
            componentDescription: AudioComponentDescription(
                componentType           : kAudioUnitType_Output
            ,   componentSubType        : kAudioUnitSubType_RemoteIO
            ,   componentManufacturer   : kAudioUnitManufacturer_Apple
            ,   componentFlags          : 0
            ,   componentFlagsMask      : 0
            )
        )

        try auau.inputBusses[ 0 ].setFormat(
            AVAudioFormat( standardFormatWithSampleRate: Double( sampleRate ), channels: numChannels )!
        )

        auau.isInputEnabled = true  // ------------------------------- (1)

        try auau.allocateRenderResources()
    }

    func
    Toggle() throws {
        if isRunning    { auau.stopHardware() }
        else            { try auau.startHardware() }
        isRunning = !isRunning
    }
}

バグ?

(1)の auau.isInputEnabled = true は必要ないと思われるし、なくてもシミュレータでは音がでるのですが、少なくとも私の実機(iPhone7)ではこの行がないと音が出ないという現象がありました。

レポジトリ

上記のプログラムを GitHub において置きました。

15
10
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
15
10