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 において置きました。