タコさんブログ

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

Swiftでちょっとだけモナド

Monads for functional programming - Philip Wadler セクション2のメモ。セクション2では、シンプルな評価器を構成することにより、モナドを導入し、修正に強い評価器を構成するのにモナドが使用できることが説明されている。

環境

準備

  • forget inpure world

バリエーション0:基本的な評価器

項を定数 con(Int) か 項の商 div(Term, Term) で定義する。

enum Term {
    case con(Int)
    indirect case div(Term, Term)
}

ログのために CustomStringConvertible を準拠させる。

extension Term: CustomStringConvertible {
    var description: String {
        switch self {
        case let .con(a):
            return "con(\(a))"
        case let .div(t, u):
            return "div(\(t), \(u))"
        }
    }
}

評価器(eval関数)は項(Term)に作用する。
基本的な評価機(eval関数)は至って簡単。
(話を分かりやすくするために、簡単にしてある。)

func eval(_ term: Term) -> Int {
    switch term {
    case let .con(a):
        return a
    case let .div(t, u):
        return eval(t) / eval(u)
    }
}

以下の項 answererror は他の例でも用いる。

// (1972 / 2) / 23
let answer: Term = .div(.div(.con(1972), .con(2)), .con(23))
// 1 / 0
let error: Term = .div(.con(1), .con(0))

eval(answer) の評価は 42 になる。この評価器には例外処理は組み込まれていないので、eval(error)の評価はexecution errorになる。

eval(answer)  // 出力:42
eval(error)   // error

バリエーション1:例外

上記の例 eval(error) でエラーメッセージを返すようにエラー判定を加える。 例外処理は例外を投げる可能性がある計算を表す型を導入する。

enum M<A> {
    typealias `exception` = String
    case raise(`exception`)
    case ret(A)  // Haskellのreturn
}

型M<A> は、 .raise(e) の形式(eはexception)か、.ret(a) の形式(aは型Aの値)をした値をとる。

評価器をこの型に適合させるのは簡単だが、退屈である。

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return .ret(a)
    case let .div(t, u):
        switch eval(t) {
        case let .raise(e):
            return .raise(e)
        case let .ret(a):
            switch eval(u) {
            case let .raise(e):
                return .raise(e)
            case let .ret(b):
                if b == 0 {
                    return .raise("divided by zero")
                } else {
                    return .ret(a / b)
                }
            }
        }
    }
}

各評価器の呼び出し時に、結果の形式を確認する必要がある。
例外が投げられた場合は、再度例外が投げられ、値の場合は、処理される。

eval(answer) // 出力:ret(42)
eval(error)  // 出力:raise("divided by zero")

バリエーション2:状態(State)

評価中の除法(割り算)の回数をカウントすることを考える。

状態は状態に作用する計算を表す型を導入する。

typealias State = Int
typealias M<A> = (State) -> (A, State)

型M<A> の値は初期状態を取り、最終状態とペアになった計算の値を戻り値にする関数である。

再び、評価器をこの型に適合させるのは簡単だが、退屈である。

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return { x in (a, x) }
    case let .div(t, u):
        return { x in
            let (a, y) = eval(t)(x)
            let (b, z) = eval(u)(y)
            return (a / b, z + 1)
        }
    }
}

eval(answer)(0) の評価は (42, 2) になる。よって、初期状態 0 の時、答えは 42 で、最終状態は、除法が2回行われたことを示す 2 である。

eval(answer)(0) // 出力:42 2

バリエーション3:出力

実行のトレースを出力することを考える。

出力は出力を生成する計算を表現する型を導入する。

typealias Output = String
typealias M<A> = (Output, A)

型M<A> の値は出力を生成したペアと計算結果の値で構成される。

又々、評価器をこの型に適合させるのは簡単だが、退屈である。

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return (line(term)(a), a)
    case let .div(t, u):
        let (x, a) = eval(t)
        let (y, b) = eval(u)
        return (x + y + line(term)(a / b), a / b)
    }
}

func line(_ term: Term) -> (Int) -> Output {
    return { "eval \(term) <= \($0)\n" }
}

eval(answer) のoutputは計算のトレースを表し、出力は以下のようになる。

eval con(1972) <= 1972
eval con(2) <= 2
eval div(con(1972), con(2)) <= 986
eval con(23) <= 23
eval div(div(con(1972), con(2)), con(23)) <= 42

Monadic評価器

各バリエーションで、計算の種類を見た。それぞれMは、例外を発生させ、状態に作用し、出力を生成する計算を表した。最初の評価器はTerm -> Intの型を持っていたが、各バリエーションでは、Term -> M<Int>の型になった。一般に、A -> Bの関数の型からA -> M<B>の関数の型に替わった。これは、型Aを引数に取り、型Bの結果とMによって得られた付加効果を返す関数と解釈できる。

それぞれの例から分かるように、型Mに必要なオペレーションは以下の2つ:

func unit(_ a: A) -> M<A>
func *<A, B>(x: M<A>, f:(A) -> M<B>) -> M<B>

型構成子Mと2つのオペレーションで構成する3つの組み(M, unit, *)モナドと言う。これらのオペレーションはセクション3で説明されている3つの法則を満たさなければならない。

これらの抽象化の観点から、評価器は以下のように書き換えられる:

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return unit(a)
    case let .div(t, u):
        return eval(t) * { a in eval(u) * { b in
                unit(a / b)
            }
        }
    }
}

この評価器は最初のものよりは複雑だが、柔軟である。各バリエーションで見た変更は、M, unit, * の定義と局所的な変更をするだけで良い。評価器全体を書き換える必要がない。

バリエーション0再考:基本的な評価器

最も単純なモナドでは、計算は値と何ら変わりはない。

typealias M<A> = A

func unit<A>(_ a: A) -> M<A> {
    return a
}

func *<A, B>(a: A, f: ((A) -> M<B>)) -> M<B> {
    return f(a)
}

これを恒等モナド(Identity Monad)という。

バリエーション1再考:例外

例外モナドでは、計算は例外を発生させるか、値を返す。

enum M<A> {
    typealias `exception` = String
    case raise(`exception`)
    case ret(A)
}

func unit<A>(_ a: A) -> M<A> {
    return .ret(a)
}

func *<A, B>(a: M<A>, k:(A) -> M<B>) -> M<B> {
    switch a {
    case let .ret(a):
        return k(a)
    case let .raise(e):
        return .raise(e)
    }
}

func raise<A>(_ e: M<A>.exception) -> M<A> {
    return .raise(e)
}

unit(a) は単純に値aを返す。
m * k はmが例外の場合、再度例外を発生させ、その他の場合、関数kを戻された値に適応する。
最後に関数raiseは例外を発生させる。

例外処理をmonadic評価器に加えるには、上記のモナドと、 unit(a/b) を以下のように変更すれば良い。

if b == 0 { return raise("divide by zero") } 
else { return unit(a / b) }

評価器は以下のようになる:

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return unit(a)
    case let .div(t, u):
        return eval(t) * { a in eval(u) * { b in
                if b == 0 { return raise("divided by zero")}
                else { return unit(a / b) }
            }
        }
    }
}

バリエーション2再考:状態

状態モナドでは、計算は初期状態を取り、値とペアになる最終状態を返す。

typealias State = Int
typealias M<A> = (State) -> (A, State)

func unit<A>(_ a: A) -> M<A> {
    return { x in (a, x) }
}

func *<A, B>(m: @escaping M<A>, k: @escaping ((A) -> M<B>)) -> M<B> {
    return { x in
        let (a, y) = m(x)
        let (b, z) = k(a)(y)
        return (b, z)
    }
}

let tick: M<()> = { x in ((), x + 1) }

unit(a) は初期状態xと値aと最終状態xをとる計算を返す。つまり、unit(a)はaをとり状態を変更させない。
m * k は初期状態xの計算mを実行し、値aと中間状態yを引き起こし、状態yの計算k(a)を実行し、値bと最終状態zを返す。
tick は状態を増加させ、空の値()を返す。

monadic評価器に実行回数をカウントする機能を追加するには、上記のモナドとunit(a/b) を以下に変更すれば良い。

 tick * { unit(a / b) }

評価器は以下のようになる:

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        return unit(a)
    case let .div(t, u):
        return eval(t) * { a in eval(u) * { b in
                tick * { unit(a / b) }
            }
        }
    }
}

バリエーション3再考:出力

出力モナドでは、計算は生成された出力とペアになる値を返す。

typealias Output = String
typealias M<A> = (Output, A)

func unit<A>(_ a: A) -> M<A> {
    return ("", a)
}

func *<A, B>(m: M<A>, k:(A) -> M<B>) -> M<B> {
    let (x, a) = m
    let (y, b) = k(a)
    return (x + y, b)
}

func out(_ x: Output) -> M<()> {
    return (x, ())
}

unit(a) は出力なしとペアになるaを返す。 m * k は出力xと計算mの値aを抽出し、出力yと計算k(a)の値bを抽出し、xとyの結合からなる出力とペアになる値bを返す。 out(x) は出力xと空の値()の計算を返す。

実行のトレースをmonadic評価器に適応すると以下のようになる。

func eval(_ term: Term) -> M<Int> {
    switch term {
    case let .con(a):
        let output = out(line(term)(a))
        return output * { unit(a) }
    case let .div(t, u):
        return eval(t) * { a in eval(u) * { b in
                let output = out(line(.div(t, u))(a / b))
                return output * { unit(a / b) }
            }
        }
    }
}

参考URL

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'

Swift vDSP で 実FFT

C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理 ページ33 で取り上げられているサイン波を vDSP(vectorized digital signal processing) ライブラリを使用して、FFTにより振幅スペクトルを求める(ここでは簡単のために窓関数を用いずにFFTを実行する)。

サイン波は次式のように表せられる。

x(n) = Amp * sin(2 * PI * f0 * n / fs), (0<=n<N) 

ここで、Ampは振幅、f0は基本周波数、fsは標本化周波数、nは標本化数

FFTを実行する音データとして、以下の値をとるサイン波を考える。

  • 振幅(Amp) 0.25
  • 基本周波数(f0) 250 Hz
  • 標本化周波数(fs) 8k Hz
  • 標本化数(n) 64

このとき、サイン波は次のようになる。

 x(n) = 0.25 * sin(PI * n / 16.0), (0 <= n < 64) (式1)

環境

準備

Accelerateフレームワークをインポート。

import Accelerate

サンプルデータの用意

振幅(Amp)を 0.25、 基本周波数(f0)を 250 Hz、 標本化周波数(fs)を 8k Hz、 標本化数を 64 としたときのサイン波(式1)のデータの配列 samples を用意する。

// 標本化数
let numSamples = 64
// サンプルデータ
var samples = [Float](count: numSamples, repeatedValue: 0)

for n in 0..<numSamples {
    samples[n] = 0.25 * sinf(Float(M_PI) * Float(n) / 16.0)
}

サンプルデータをDSPSplitComplex型に変換する

まず、vDSPで実FFTを実行する前に、vDSP_ctoz関数を使用してDSPSplitComplex型にパック(変換)する必要がある。 vDS_ctozを 実数データの配列 A = {A[0], A[1], … A[n]} に適用すると even-odd 配列 AEvenOdd = {A[0], A[2], …, A[n-1], A[1], A[3], … A[n]} のように変換される。

var reals = [Float](count: numSamples/2, repeatedValue: 0)
var imgs  = [Float](count: numSamples/2, repeatedValue: 0)
var splitComplex = DSPSplitComplex(realp: &reals, imagp: &imgs)
let samplesPtr = UnsafePointer<DSPComplex>(samples)
vDSP_ctoz(samplesPtr, 2, &splitComplex, 1, vDSP_Length(numSamples/2))

FFT ウェイト配列(FFT Weights Arrays)

FFTのウェイト配列は vDSP_create_fftsetup 関数を使用することによって生成する。 vDSP_create_fftsetupの第1引数 __Log2n には、底を2とする標本化数の対数(i.e. log2(number_of_samples))以上を指定する。第2引数 __Radix には、基数オプションを指定する。基数オプションには、ビット演算ORでFFT_RADIX2、FFT_RADIX3、FFT_RADIX5の任意の組み合わせが使用できる。

//  Create FFT setup
// __Log2nは log2(64) = 6 より、6 を指定
let setup = vDSP_create_fftsetup(6, FFTRadix(FFT_RADIX2))

FFTの実行・出力

vDSP_fft_zrip 関数を使用してFFTを実行する。

// Perform FFT
vDSP_fft_zrip(setup, &splitComplex, 1, 6, FFTDirection(FFT_FORWARD))

vDSP Programming Guide に記載されている通り、C_imp = 2 * C_math となっている。C_math を求めるには 1/2倍する必要がある。vDSP_vsmul 関数を使用して各配列要素の値を1/2倍する。また、splitComplex.realp[0], splitComplex.imagp[0] には DC成分ナイキスト成分 がそれぞれ入っている。音データの場合は無視できるので、インデックスは1から出力する。

// splitComplex.realp, splitComplex.imagpの各要素を1/2倍する
var scale:Float = 1 / 2
vDSP_vsmul(splitComplex.realp, 1, &scale, splitComplex.realp, 1, vDSP_Length(numSamples/2))
vDSP_vsmul(splitComplex.imagp, 1, &scale, splitComplex.imagp, 1, vDSP_Length(numSamples/2))
// 複素数の実部と虚部を取得する
let r = Array(UnsafeBufferPointer(start: splitComplex.realp, count: numSamples/2))
let i = Array(UnsafeBufferPointer(start: splitComplex.imagp, count: numSamples/2))
for n in 1..<numSamples/2 {
  let rel = r[n]
  let img = i[n]
  let mag = sqrtf(rel * rel + img * img)
  let log = "[%02d]: Mag: %5.2f, Rel: %5.2f, Img: %5.2f"
  print(String(format: log, n, mag, rel, img))
}

// setupを解放
vDSP_destroy_fftsetup(setup)

出力結果は以下のようになる。

[01]: Mag:  0.00, Rel: -0.00, Img: -0.00
[02]: Mag:  8.00, Rel:  0.00, Img: -8.00
[03]: Mag:  0.00, Rel:  0.00, Img:  0.00
以下0が続く
 .
 .
 .

C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理 に記載されている値が出力されていることがわかる。

参考URL

Swift AVAudioEngine でサイン波を鳴らす

AVAudioEngine を使用して、440Hzのサイン波(ラ音)をPCMバッファーに設定し、鳴らす。 AVAudioEngineの基本的な使い方は Swift AVAudioEngine の基本 - タコさんブログ を参照。

環境

準備

AVFoundation をインポートする

import AVFoundation

サイン波

周波数fのサイン波は以下の式で与えられる。

 x(n) = sin(2 * PI * f * n/fs)

ここで、
 n = 0, 1, 2, ...
 fs はサンプリング周波数

この式を使用すると、サンプリング周波数 44.1k Hz, 440 Hzのサイン波は以下のようになる。

for n in 0..<some_buffer_length {
  samples[n] = sinf(Float(2.0 * M_PI) * 440.0 * Float(n) / sampleRate)
}

PCMバッファーの生成と設定

2 ch, 44.1k Hz, 非インターリーブドのオーディオフォーマット(audioFormat)に対し、PCMバッファーを以下のように生成する。

let buffer = AVAudioPCMBuffer(PCMFormat: audioFormat, frameCapacity:UInt32(44100))
buffer.frameLength = UInt32(44100)

上記のバッファー変数(buffer)に対して、サイン波の値を設定する。

// オーディオフォーマットからオーディオのチャンネル数を取得
let channels = Int(audioFormat.channelCount)
for ch in (0..<channels) {
  let samples = buffer.floatChannelData[ch]
  for n in 0..<Int(buffer.frameLength) {
    samples[n] = sinf(Float(2.0 * M_PI) * 440.0 * Float(n) / sampleRate)
  }
}

このバッファーをAVAudioPlayerNode.scheduleBuffer()でスケジュールし、再生すればサイン波が鳴る。

AVAudioEngine, AVAudioPlayerNode をインスタンス変数として宣言する。

// エンジンの生成
let audioEngine = AVAudioEngine()
// ソースノードの生成
let player = AVAudioPlayerNode()

playSineWave関数でオーディオエンジン・バッファーを設定し、サイン波を再生する。

func playSineWave() {
  // プレイヤーノードからオーディオフォーマットを取得
  let audioFormat = player.outputFormatForBus(0)
  // サンプリング周波数: 44.1K Hz
  let sampleRate = Float(audioFormat.sampleRate)
  // 3秒間鳴らすフレームの長さ
  let length = 3.0 * sampleRate
  // PCMバッファーを生成
  let buffer = AVAudioPCMBuffer(PCMFormat: audioFormat, frameCapacity:UInt32(length))
  // frameLength を設定することで mDataByteSize が更新される
  buffer.frameLength = UInt32(length)
  // オーディオのチャンネル数
  let channels = Int(audioFormat.channelCount)
  for ch in (0..<channels) {
    let samples = buffer.floatChannelData[ch]
    for n in 0..<Int(buffer.frameLength) {
      samples[n] = sinf(Float(2.0 * M_PI) * 440.0 * Float(n) / sampleRate)
    }
  }
      
  // オーディオエンジンにプレイヤーをアタッチ
  audioEngine.attachNode(player)
  let mixer = audioEngine.mainMixerNode
  // プレイヤーノードとミキサーノードを接続
  audioEngine.connect(player, to: mixer, format: audioFormat)
  // 再生の開始を設定
  player.scheduleBuffer(buffer) {
      print("Play completed")
  }
    
  do {
    // エンジンを開始
    try audioEngine.start()
    // 再生
    player.play()
  } catch let error {
    print(error)
  }
}

参考URL

Swift AVAudioEngine の基本

環境

準備

AVFoundation をインポートする。

import AVFoundation

AVAudioEngine

AVAudioEngine は 接続された audio node(AVAudioNode) のグループを定義する。 Audio node を使用してオーディオシグナルを生成、それらの処理、オーディオ入力・出力を実行する。

Audioエンジンを使用する方法は、 Audio node をそれぞれ生成し、audioエンジンへアタッチする。

エンジンの利用手順

  1. エンジンの生成
  2. ノードの生成
  3. エンジンにノードをアタッチ
  4. ノード同士を接続
  5. エンジンをスタート

AVAudioNode

AVAudioNodeはオーディオの生成・処理・I/Oブロックの抽象クラス。 ノードは入力・出力バス(接続ポイントのようなもの)を持っている。ミキサーは複数の入力バスがあり、1つの出力バスがある。 全てのバスにはバスと関連してフォーマットがあり、AVAudioMixerNode と AVAudioOutputNode 以外はノードどうしを接続するときフォーマットが同じである必要がある。

ノードの種類

  • Source (ex. Player, microphone)
  • Process (ex. Mixer, effect)
  • Destination (ex. Speaker)

Source ノードと Destination ノードが接続されている状態をActive Chainと言う。Active Chainでない状態をInactive Chainと言う。

Active Chain 例)

 Source Node (Player) - connect -> Destination Node
 Source Node (Player) - connect -> Processing Node - connect -> Destination Node

出力ノード (Output Node)

エンジンには暗黙に出力ノード(Output node)と呼ばれる Desitination Node があり、出力ノードはオーディオデータを出力ハードウェアに提供する。

ミキサーノード (Mixer Node)

ミキサーノードはN個の入力をミックスし、1つの出力へ出力する。 エンジンには暗黙的にミキサーノードがあり、追加のミキサーノードを生成し、エンジンにアタッチすることができる。ミキサーのインプットには異なるオーディオフォーマットを使用することができる。

 Input 1 -> | ---------------- |
 Input 2 -> |                  |
 Input 3 -> | AVAudioMixerNode |->
   .     -> |                  |
 Input N -> | ---------------- |

以下のようにSub-Mixingをすることも可能。

 Input 1 -> | ------------------ |
   .     -> |  AVAudioMixerNode  | -> | ---------------------------- |
 Input N -> | ------------------ |
                                      | AVAudioMixerNode (Main Mixer)|->
 Input 1 -> | ------------------ |
   .     -> |  AVAudioMixerNode  | -> | ---------------------------- |
 Input M -> | ------------------ |

プレイヤーノード(Player Node)

プレイヤーノード(AVPlayerNode)はファイル(AVAudioFile)またはバッファ(AVAudioBuffer)からデータをスケジュール(特定の時間に)し再生する。

エンジンとノードの生成・ノードのアタッチ

以下のようにして、エンジンとソースノード(Playerノード)を生成し、エンジンにノードをアタッチする。

// エンジンの生成
let engine = AVAudioEngine()
// ソースノードの生成
let player = AVAudioPlayerNode()
// エンジンにノードをアタッチ
engine.attachNode(player)

プレイヤーの設定(ファイル)

ファイルからプレイヤーをスケジュールする例。

do {
  // オーディオファイルの取得
  let audioFile = try AVAudioFile(forReading: url)
  // エンジンからメインミキサを取得
  let mainMixer = engine.mainMixerNode
  // プレイヤーノードとメインミキサーを接続
  engine.connect(player, to: mainMixer, format: audioFile.processingFormat)
  // プレイヤーをすぐに開始するようにスケジュール(atTimeをnil)
  player.scheduleFile(audioFile, atTime:nil, completionHandler:nil)
} catch let error {
  print(error)
}

プレイヤーの設定(バッファ)

バッファからプレイヤーをスケジュールする例。

// バッファを生成または取得
let buffer: AVAudioPCMBuffer = ...
// メインミキサの取得
let mainMixer = engine.mainMixerNode
// プレイヤーノードとメインミキサーを接続
engine.connect(player, to: mainMixer, format: buffer.format)
// プレイヤのスケジュール
player.scheduleBuffer(buffer, atTime:nil, completionHandler:nil)

エンジンの開始とプレイヤーの再生

do {
  // エンジンの開始
  try engine.start()
  // プレイヤーの再生
  player.start
} catch let error {
  print(error)
}

プレイヤーのスケジュールオプション

AVPlayer.scheduleFile(audioFile,atTime,options,completionHandler) のoptionsに設定する。

設定できる options

  • Loops (ループ再生)
  • Interrupts (再生中のオーディオをすぐに停止して、再生)
  • InterruptsAtLoop (ループ再生が終了してから再生)

ループ再生オプションの例。

player.scheduleFile(audioFile,
                    atTime:nil,
                    Loops,
                    completionHandler:nil)

再生の開始時間を設定する

10秒後に再生する例。

let tenSecInTheFuture = AVAudioTime.timeWithSampleTime(10 * buffer.format.sampleRate), atRate: buffer.format.sampleRate)
player.scheduleFile(audioFile, atTime:tenSecInTheFuture, completionHandler:nil)
player.play()

Node Tap

Node Tapを使用すればデータをプルすることができる。

Outputバス(インデックス0)にタップをインストールする例。

let format = mixier.outputFormatForBus(0)
mixier.installTapOnBuss(0,
                        bufferSize:4096,
                        format:format) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
  // Process buffer
}

ノードを非接続にする

入力ノードを非接続にする例。

engine.disconnectNodeOutput(input)

オーディオの再生例

// インスタンス変数
// エンジンの生成
let audioEngine = AVAudioEngine()
// Playerノードの生成
let player = AVAudioPlayerNode()

func play() {
  if let url = NSBundle.mainBundle().URLForResource("sound-file", withExtension: "wav") {
    do {
      // オーディオファイルの取得
      let audioFile = try AVAudioFile(forReading: url)
      // エンジンにノードをアタッチ
      audioEngine.attachNode(player)
      // メインミキサの取得
      let mixer = audioEngine.mainMixerNode
      // Playerノードとメインミキサーを接続
      audioEngine.connect(player,
                          to: mixer,
                          format: audioFile.processingFormat)
      // プレイヤのスケジュール
      player.scheduleFile(audioFile, atTime: nil) {
          print("complete")
      }
      // エンジンの開始
      try audioEngine.start()
      // オーディオの再生
      player.play()
    } catch let error {
      print(error)
    }
  } else {
    print("File not found")
  }
}

参考URL

Swift で Audio Queue Services を使って再生する

Audio Queue Services Programming Guide - Playing Audio を参考にSwiftでAudio Queue Serviceを使用する。

環境

準備

AudioToolbox をインポートする。

import AudioToolbox

Audio Queue Service を使用する手順

  1. 状態・フォーマット・パス情報を管理する構造体を定義する
  2. 実際の再生を行うオーディオキューコールバックを作成する
  3. オーディオキューバッファのサイズを決定するコードを書く
  4. プレイバックするオーディオファイルを開き、オーディオデータのフォーマットを取得する
  5. プレイバックオーディオキューを作成し、プレイバックに設定する
  6. オーディオキューバッファを生成し、エンキューする
  7. リソースの解放

状態・フォーマット・パス情報を管理するクラス(構造体)を定義

オーディオフォーマットとオーディオキューの情報を管理するクラスを定義する。 状態の更新を行うのでクラス(参照型)で定義する。

let kNumberBuffers = 3

class AQPlayerState {
  var mDataFormat: AudioStreamBasicDescription
  var mQueue: AudioQueueRef
  var mBuffers: [AudioQueueBufferRef]
  var mAudioFile: AudioFileID
  var bufferByteSize: UInt32
  var mCurrentPacket: Int64
  var mNumPacketsToRead: UInt32
  var mPacketDescs: UnsafeMutablePointer<AudioStreamPacketDescription>
  var mIsRunning: Bool
  // 見やすさのためinit内で初期化する
  init() {
      mDataFormat = AudioStreamBasicDescription()
      mQueue = nil
      mBuffers = [AudioQueueBufferRef](count: kNumberBuffers, repeatedValue: nil)
      mAudioFile = nil
      bufferByteSize = 0
      mCurrentPacket = 0
      mNumPacketsToRead = 0
      mPacketDescs = nil
      mIsRunning = false
  }
}

オーディオキューコールバックの作成

このコールバック関数は主に以下の3つのことをする

  1. 指定された分のデータをオーディオファイルから読み込み、それをオーディオキューバッファに入れる
  2. バッファキューにオーディオバッファキューをエンキューする
  3. 読み込むデータがない場合にオーディオキューに停止するように伝える

以下がサンプルに示されている基本的なコールバック関数になる。

func HandleOutputBuffer(aqData: UnsafeMutablePointer<Void>, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) {
  // AQPlayerStateにキャスト
  let aqdata = UnsafeMutablePointer<AQPlayerState>(aqData).memory
  // 再生中でない場合は何もしない
  guard aqdata.mIsRunning else {
      return
  }
  // AudioFileReadPackets deprecated の変更により
  // numBytesReadFromFile を設定する
  var numBytesReadFromFile = aqdata.bufferByteSize
  var numPackets = aqdata.mNumPacketsToRead
  // オーディオファイルからデータを読み込む
  AudioFileReadPacketData(aqdata.mAudioFile,
                          false,
                          &numBytesReadFromFile,
                          aqdata.mPacketDescs,
                          aqdata.mCurrentPacket,
                          &numPackets,
                          inBuffer.memory.mAudioData)
  if (numPackets > 0) {
      inBuffer.memory.mAudioDataByteSize = numBytesReadFromFile
      // オーディオキューバッファをエンキューする
      AudioQueueEnqueueBuffer(aqdata.mQueue,
                              inBuffer,
                              (aqdata.mPacketDescs != nil ? numPackets : 0),
                              aqdata.mPacketDescs)
      aqdata.mCurrentPacket += Int64(numPackets)
  } else {
      // オーディオキューを停止する
      AudioQueueStop(aqdata.mQueue, false)
      aqdata.mIsRunning = false
  }
}

再生するオーディオキューバッファのサイズを決定する

func DeriveBufferSize(ASBDesc: AudioStreamBasicDescription, maxPacketSize: UInt32, seconds: Float64, outBufferSize: UnsafeMutablePointer<UInt32>, outNumPacketsToRead: UnsafeMutablePointer<UInt32>) {
  let maxBufferSize: UInt32 = 0x50000 // 320 KB
  let minBufferSize: UInt32 = 0x4000  // 16 KB

  if ASBDesc.mFramesPerPacket != 0 {
      let numPacketsForTime = ASBDesc.mSampleRate / Float64(ASBDesc.mFramesPerPacket) * seconds
      outBufferSize.memory = UInt32(numPacketsForTime) * maxPacketSize
  } else {
      outBufferSize.memory = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize
  }

  if outBufferSize.memory > maxBufferSize && outBufferSize.memory > maxPacketSize {
      outBufferSize.memory = maxBufferSize
  } else {
      if outBufferSize.memory < minBufferSize {
          outBufferSize.memory = minBufferSize
      }
  }

  outNumPacketsToRead.memory = outBufferSize.memory / maxPacketSize
}

再生するオーディオファイルを開く

let filePath = "music file path"
let url: CFURL = NSURL(fileURLWithPath: filePath)
let result = AudioFileOpenURL(url, .ReadPermission, 0, &aqData.mAudioFile)
assert(result == noErr, "AudioFileOpenURL Error: \(result)")

オーディオデータのフォーマットを取得

let result = AudioFileGetProperty(aqData.mAudioFile,
                                  kAudioFilePropertyDataFormat,
                                  &dataFormatSize,
                                  &aqData.mDataFormat)
assert(result == noErr, "AudioFileGetProperty Error: \(result)")

プレイバックオーディオキューの生成

let result = AudioQueueNewOutput(&aqData.mDataFormat,
                                 HandleOutputBuffer,
                                 &aqData,
                                 CFRunLoopGetCurrent(),
                                 kCFRunLoopCommonModes,
                                 0,  // アップルに予約されている。0でなければならない
                                 &aqData.mQueue)

assert(result == noErr, "AudioQueueNewOutput Error: \(result)")

プレイバックオーディオキューのサイズを設定

var maxPacketSize: UInt32 = 0
var propertySize = UInt32(sizeof(UInt32.self))
let result = AudioFileGetProperty(aqData.mAudioFile,
                                  kAudioFilePropertyPacketSizeUpperBound,
                                  &propertySize,
                                  &maxPacketSize)

DeriveBufferSize(aqData.mDataFormat, maxPacketSize: maxPacketSize, seconds: 0.5, outBufferSize: &aqData.bufferByteSize, outNumPacketsToRead: &aqData.mNumPacketsToRead)

Packet Description配列のメモリを割りあてる

// Allocating Memory for a Packet Descriptions Array
let isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 ||
                  aqData.mDataFormat.mFramesPerPacket == 0

if isFormatVBR {
    let size = Int(aqData.mNumPacketsToRead * UInt32(sizeof(AudioStreamPacketDescription.self)))
    aqData.mPacketDescs = UnsafeMutablePointer<AudioStreamPacketDescription>.alloc(size)
} else {
    aqData.mPacketDescs = nil
}

プレイバックオーディオキューのMagic Cookieを設定

var cookieSize = UInt32(sizeof(UInt32.self))

let result = AudioFileGetPropertyInfo(aqData.mAudioFile,
                                      kAudioFilePropertyMagicCookieData,
                                      &cookieSize,
                                      nil)

if result != noErr {
    let magicCookie = UnsafeMutablePointer<CChar>.alloc(Int(cookieSize))
    AudioFileGetProperty(aqData.mAudioFile,
                         kAudioFilePropertyMagicCookieData,
                         &cookieSize,
                         magicCookie)

    AudioQueueSetProperty(aqData.mQueue,
                          kAudioQueueProperty_MagicCookie,
                          magicCookie,
                          cookieSize)
    free(magicCookie)
}

最初に再生するオーディオキューバッファを用意する

aqData.mIsRunning = true
aqData.mCurrentPacket = 0
for i in (0..<kNumberBuffers) {
    AudioQueueAllocateBuffer(aqData.mQueue,
                             aqData.bufferByteSize,
                             &aqData.mBuffers[i])

    HandleOutputBuffer(&aqData,
                       inAQ: aqData.mQueue,
                       inBuffer: aqData.mBuffers[i])
}

オーディオキューを開始する

AudioQueueStart(aqData.mQueue, nil)

Command Lineで実行する場合は以下も必要。iOSの場合は不要。

repeat {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode,
                       0.25,
                       false)
} while aqData.mIsRunning

CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, false)

再生後の処理

AudioQueueDispose(aqData.mQueue, true) // Dispose audio queue
AudioFileClose(aqData.mAudioFile)      // Close audio file
free(aqData.mPacketDescs)              // Release the packet description

参考URL

デリゲートメソッドをRxSwift対応させる方法

この例ではUIWebViewDelegateメソッドをRxSwift対応させる。

環境

準備

RxCocoa、RxSwiftをインポート。

import RxCocoa
import RxSwift

今のところ以下のキャスト時に使用する関数はパブリックになっていないので、RxCocoaからコピーしてくる。

@noreturn func rxFatalError(lastMessage: String) {
  fatalError(lastMessage)
}

func castOptionalOrFatalError<T>(value: AnyObject?) -> T? {
  if value == nil {
    return nil
  }
  let v: T = castOrFatalError(value)
  return v
}

func castOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T {
  guard let returnValue = object as? T else {
    throw RxCocoaError.CastingError(object: object, targetType: resultType)
  }
  return returnValue
}

func castOptionalOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T? {
  if NSNull().isEqual(object) {
    return nil
  }
  guard let returnValue = object as? T else {
    throw RxCocoaError.CastingError(object: object, targetType: resultType)
  }
  return returnValue
}

func castOrFatalError<T>(value: AnyObject!, message: String) -> T {
  let maybeResult: T? = value as? T
  guard let result = maybeResult else {
    rxFatalError(message)
  }
  return result
}

func castOrFatalError<T>(value: Any!) -> T {
  let maybeResult: T? = value as? T
  guard let result = maybeResult else {
    rxFatalError("Failure converting from \(value) to \(T.self)")
  }
  return result
}

DelegateProxyクラスを作成

対応させるデリゲート(UIWebViewDelegate)のプロキシークラスを作成する。

class RxUIWebViewDelegateProxy: DelegateProxy, UIWebViewDelegate, DelegateProxyType {

  class func currentDelegateFor(object: AnyObject) -> AnyObject? {
    let webView: UIWebView = castOrFatalError(object)
    return webView.delegate
  }

  class func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) {
    let webView: UIWebView = castOrFatalError(object)
    webView.delegate = castOptionalOrFatalError(delegate)
  }
}

Reactive ラッパー

UIWebViewのエクステンションとして、Reactiveラッパーを用意する。今回は以下のUIWebViewDelegateメソッドをラップする。

  • webViewDidStartLoad(webView: UIWebView)
  • webViewDidFinishLoad(webView: UIWebView)
  • webView(webView: UIWebView, didFailLoadWithError error: NSError?)
extension UIWebView {
  //  Reactive wrapper for `delegate`.
  public var rx_delegate: DelegateProxy {
    return proxyForObject(RxUIWebViewDelegateProxy.self, self)
  }
  //  Reactive wrapper for webViewDidStartLoad(_:)
  public var rx_webViewDidStartLoad: Observable<UIWebView> {
    let sel = #selector(UIWebViewDelegate.webViewDidStartLoad(_:))
    let o = rx_delegate
      .observe(sel)
      .map { a in
        return try castOrThrow(UIWebView.self, a[0])
    }
    return o
  }
  //  Reactive wrapper for webViewDidFinishLoad(_:)
  public var rx_webViewDidFinishLoad: Observable<UIWebView> {
    let sel = #selector(UIWebViewDelegate.webViewDidFinishLoad(_:))
    let o = rx_delegate
      .observe(sel)
      .map { a in
        return try castOrThrow(UIWebView.self, a[0])
    }
    return o
  }
  //  Reactive wrapper for webView(_:didFailLoadWithError:)
  public var rx_webViewDidFailLoadError: Observable<(UIWebView, NSError?)> {
    let sel = #selector(UIWebViewDelegate.webView(_:didFailLoadWithError:))
    let o = rx_delegate
      .observe(sel)
      .map { a -> (UIWebView, NSError?) in
        let webview = try castOrThrow(UIWebView.self, a[0])
        let error = try castOptionalOrThrow(NSError.self, a[1])
        return (webview, error)
    }
    return o
  }
}

戻り値のあるデリゲートメソッドの場合

DelegateProxy.setForwardToDelegate(_:retainDelegate:) を使用して通常と同じように扱う。

使用例

class ExampleViewController: UIViewController, UIWebViewDelegate {
  @IBOutlet weak var webView: UIWebView!
  let disposeBag = DisposeBag()

  override func viewDidLoad() {
    webView
      .rx_webViewDidStartLoad
      .subscribeNext { webview in
        print("webViewDidStartLoad", webview)
      }.addDisposableTo(disposeBag)

    webView
      .rx_webViewDidFinishLoad
      .subscribeNext { webview in
        print("webViewDidFinishLoad", webview)
      }.addDisposableTo(disposeBag)

    webView
      .rx_webViewDidFailLoadError
      .subscribeNext { (webview, error) in
        print("webViewDidFailLoadError", webview, error)
      }.addDisposableTo(disposeBag)

    webView.rx_delegate.setForwardToDelegate(self, retainDelegate: false)

    let url = NSURL(string: "https://xxx")!
    let request = NSURLRequest(URL:url)
    webView.loadRequest(request)
  }

  func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    print(request.URL?.absoluteString,)
    return true
  }
}

参考URL