15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?