PolyHexane サウンドルーチンもSwiftで

音を鳴らすアプリは色々作ってきましたが、Swiftでは生のポインタをいじったりするのに色々制限があり、Obj-CあるいはほぼCで書くのが良いと思っていました(今でも基本的にはそう思っています)。

が、SwiftでもCoreAudio周りのルーチンが書けるという記事を読んだので試してみました。結論から言うとデバッグモードでは配列のチェックなどはいるようでかなり遅いですが、まあ動きます。見通しの良いコードが書けるのは良い点かもしれません。

PolyHexaneのサウンドルーチンでは正弦波を鳴らす機能を作りました。単に鳴らすだけではなく減衰するのと、複数の周波数の音を重ねて鳴らすようにしてあります。数字を音にする、ということで与えられた数nを因数分解して、その因数分の1の周波数の音を出すことで、元の周波数とある周期で共鳴する音をだすようになっています。

一つの音はしばらくすると減衰してバッファから取り除かれます。

音を鳴らすルーチンはどのスレッドからでも呼べるようになっています。音を鳴らすコマンドはnotes_to_addという配列にストアされます。AudioUnitを鳴らす方のコールバックで、notes_to_addをチェックして、mutexで同時アクセスになっていないことを確認した上で現在なっている音の集合であるnotesに追加します。同時アクセスになった場合は、他のルーチンがアクセスを終わるのを待ってもいいのですが、コールバック関数でタイム・アウトすると音が途切れてしまいますので、この周期では新たな音を追加しないようにしています。

あらたに音を鳴らすコマンドであるplay関数からnotesに直接アクセスすると、コールバック関数との間で衝突が生じやすくなるのと、衝突したときに音のデータが全部読めなくなって不都合が生じるので、notes_to_addに衝突しうる処理を追い出して音の乱れが起こりにくいようにしてあります。

import Foundation
import AudioUnit

let sampleRate = 44100.0

class SNPlayer {

  struct Note {
    let start_time:Double
    let tones:[Double]
    let decay_rate:Double
    init(time:Double, note:[Double], decay:Double){
      start_time = time
      tones = note
      decay_rate = decay
    }
  }

  let delta:Double = 1.0 / sampleRate
  var audioUnit: AudioComponentInstance?
  var mlock:pthread_mutex_t = pthread_mutex_t()
  var notes:[Note] = []
  var elapsed_time:Double = 0
  var notes_to_add:[Note] = [] // 新たに鳴らす音は次の割り込み周期で処理。 mutexで保護

  let callback: AURenderCallback = {
    (inRefCon: UnsafeMutableRawPointer,
    ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
    inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrames: UInt32,
    ioData: UnsafeMutablePointer<AudioBufferList>?) in

    let player:SNPlayer = Unmanaged<SNPlayer>.fromOpaque(inRefCon).takeUnretainedValue()
    return player.render(ioActionFlags: ioActionFlags,
      inTimeStamp: inTimeStamp,
      inBusNumber: inBusNumber,
      inNumberFrames: inNumberFrames,
      ioData: ioData)
  }

  init() {
    var acd = AudioComponentDescription();
    acd.componentType = kAudioUnitType_Output;
    acd.componentSubType = kAudioUnitSubType_RemoteIO;
    acd.componentManufacturer = kAudioUnitManufacturer_Apple;
    acd.componentFlags = 0;
    acd.componentFlagsMask = 0;
    let ac = AudioComponentFindNext(nil, &acd);
    AudioComponentInstanceNew(ac!, &audioUnit);
    if audioUnit != nil{
      var asbd = AudioStreamBasicDescription(mSampleRate: sampleRate, mFormatID: kAudioFormatLinearPCM,
        mFormatFlags: kAudioFormatFlagsNativeFloatPacked|kAudioFormatFlagIsNonInterleaved,
        mBytesPerPacket: 4, mFramesPerPacket: 1, mBytesPerFrame: 4, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0)
      AudioUnitSetProperty(audioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &asbd, UInt32(MemoryLayout.size(ofValue: asbd)))

      let ref: UnsafeMutableRawPointer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
      var callbackstruct:AURenderCallbackStruct = AURenderCallbackStruct(inputProc: callback, inputProcRefCon: ref)
      AudioUnitSetProperty(audioUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackstruct, UInt32(MemoryLayout.size(ofValue: callbackstruct)))
      AudioUnitInitialize(audioUnit!)
      AudioOutputUnitStart(audioUnit!)
      pthread_mutex_init(&mlock, nil)
    }else{
      print("Failed to init AudioUnit")
    }
  }

  func render(ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus {
    guard let abl = UnsafeMutableAudioBufferListPointer(ioData) else {
      return noErr
    }
    let bufL:UnsafeMutablePointer<Float> = (abl[0].mData?.bindMemory(to: Float.self, capacity: Int(inNumberFrames)))!
    let bufR:UnsafeMutablePointer<Float> = (abl[1].mData?.bindMemory(to: Float.self, capacity: Int(inNumberFrames)))!

    if pthread_mutex_trylock(&mlock) == 0{
      notes.append(contentsOf: notes_to_add)
      notes_to_add = []
      pthread_mutex_unlock(&mlock)
    }

    for i in 0..<inNumberFrames{
      var out:Double = 0
      for n in notes{
        let dt = elapsed_time - n.start_time
        var decay = 0.2 * exp(dt * n.decay_rate) / Double(4 + n.tones.count)
        if dt < 0.01 {
          decay *= (dt/0.01)
        }
        for f in n.tones{
          out += sin(2 * .pi * f * dt) * decay
        }
      }
      if out > 1.0 {out = 1.0}
      if out < -1.0 {out = -1.0}
      bufL[Int(i)] = Float(out)
      bufR[Int(i)] = Float(out)
      elapsed_time += delta
    }

    notes = notes.filter {elapsed_time - $0.start_time < 3 / (-$0.decay_rate)}
    return noErr
  }

  func play(_ n:Int){
    func fact(_ N:Int)->Int{
      for i in 2..<N{
        if N % i == 0 { return i }
      }
      return N
    }
    var x = n
    let basetone = 440.0
    var k = 0.0
    var tones = [basetone]
    while x > 1{
      let f = fact(x)
      tones.append(basetone * (k + 4.0) / Double(f))
      x /= f
      k += 1.0
    }
    let note = Note(time:elapsed_time,note:tones, decay:-3)
    pthread_mutex_lock(&mlock)
    notes_to_add.append(note)
    pthread_mutex_unlock(&mlock)
  }
}

 

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中