タコさんブログ

プログラミングメモと小言

Swift AVAudioEngine でマイク入力を録音

AVAudioEngine を使用して、マイクからの入力を録音する。
AVAudioEngineの基本的な使い方は Swift AVAudioEngine の基本 - タコさんブログ を参照。
簡単のため、マイクアクセスの許可、オーディオセッションの管理などのハンドリングは考慮していない。

環境

準備

AVFoundation をインポートする。

import AVFoundation

録音

オーディオフォーマットは以下:

  • オーディオファイルフォーマット: PCM
  • サンプリング周波数: 44.1 kHz
  • ビットレート: 32 bit
  • チャンネル数: 1
  • Interleaved: true

※ Non-interleaved を指定すると、Audio files cannot be non-interleaved とエラーがコンソールに表示され、自動的に Interleaved に変更されているので注意

AVAudioEngineは 暗黙の入力ノード(inputNode) を持ち、この入力ノードはマイク(入力ハードウェア)からオーディオデータを受け取る。inputNodeの出力を別のノードに接続するか、タップをインストールすれば、inputNodeから入力を受け取ることができる。

録音するには AVAudioNode.installTap(onBus:bufferSize:format:block:) を使用してノードからの出力を監視し、バッファを逐次保存する。

// オーディオエンジンをインスタンス変数として宣言
let engine = AVAudioEngine()

func record() {
  do {
    // 保存する場所: 今回はDocumentディレクトリにファイル名"sample.caf"で保存
    let documentDir = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!
    let filePath = NSURL(fileURLWithPath: documentDir + "/sample.caf")
    // オーディオフォーマット
    let format = AVAudioFormat(commonFormat: .PCMFormatFloat32  , sampleRate: 44100, channels: 1 , interleaved: true)
    // オーディオファイル
    let audioFile = try AVAudioFile(forWriting: filePath, settings: format.settings)
    // inputNodeの出力バス(インデックス0)にタップをインストール
    // installTapOnBusの引数formatにnilを指定するとタップをインストールしたノードの出力バスのフォーマットを使用する
    // (この例だとフォーマットに inputNode.outputFormatForBus(0) を指定するのと同じ)
    // tapBlockはメインスレッドで実行されるとは限らないので注意
    let inputNode = engine.inputNode!  // 端末にマイクがあると仮定する
    inputNode.installTapOnBus(0, bufferSize: 4096, format: nil) { (buffer, when) in
      do {
        // audioFileにバッファを書き込む
        try audioFile.writeFromBuffer(buffer)
      } catch let error {
        print("audioFile.writeFromBuffer error:", error)
      }
    }

    do {
      // エンジンを開始
      try engine.start()
    } catch let error {
      print("engine.start() error:", error)
    }
  } catch let error {
    print("AVAudioFile error:", error)
  }
}

エフェクトをかけて録音

ディレイ効果をかけて、エフェクトがかかった音を録音する。

func record() {
  do {
    // 保存するファイルパス
    let documentDir = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!
    let filePath = NSURL(fileURLWithPath: documentDir + "/sample.caf")
    // オーディオフォーマット
    let format = AVAudioFormat(commonFormat: .PCMFormatFloat32  , sampleRate: 44100, channels: 1 , interleaved: true)
    // オーディオファイル
    let audioFile = try AVAudioFile(forWriting: filePath, settings: format.settings)
    // ディレイ
    let delay = AVAudioUnitDelay()
    delay.delayTime = 0.25
    // オーディオエンジンにディレイをアタッチ
    engine.attachNode(delay)
    // ノード同士を接続 (inputNode -> delay)
    let inputNode = engine.inputNode!
    engine.connect(inputNode, to: delay, format: format)
    // ディレイの出力バスにタップをインストール
    delay.installTapOnBus(0, bufferSize: 4096, format: format) { (buffer, when) in
      do {
        try audioFile.writeFromBuffer(buffer)
      } catch let error {
        print("audioFile.writeFromBuffer error:", error)
      }
    }

    do {
      // エンジンを開始
      try engine.start()
    } catch let error {
      print("engine.start error:", error)
    }
  } catch let error {
    print("AVAudioFile error:", error)
  }
}

上記の Audio Graph は以下のようになっている:

// --sound--> inputNode(mic) --audio source--> delay --audio source-->
//                                                   ^
//                                                   |
//                                        installTapOnBus(...) { 録音 }

タップの削除

AVAudioNode.removeTapOnBus() でインストールしたオーディオタップを削除できる。

例)

engine.inputNode!.removeTapOnBus(0)

Note

例えば、引数にフォーマットを指定するとき、間違ったフォーマットを指定しまっていると訳の分からない例外が投げられたりする。エフェクトをかけて録音の delay.installTapOnBus(0, bufferSize: 4096, format: format) { ... }delay.installTapOnBus(0, bufferSize: 4096, format: nil) { ... } とすると、以下のようなエラーがでて、クラッシュする。

ERROR: [0x1a0a06000] AVAudioEngineGraph.mm:2510: PerformCommand: error -10868
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error -10868'