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)
  }
}

 

広告

PolyHexane 六角形迷路生成のアルゴリズム

しばらく前にやねうらおさんというコンピュータ将棋ソフト「やねうら王」などを作られている方のブログで迷路生成アルゴリズムについて読んだ。

古くて新しい自動迷路生成アルゴリズム

そっからのリンクでクラスタリングによる迷路作成アルゴリズム

その後しばらくそのことも忘れていたが、ふとした時に六角形の迷路で同じアルゴリズムを使えるか考えてみた。

仮に4x4の迷路を作るとして、図のように番号を振ることとする

ルーチンcreateで横X 縦Yの迷路を作成する。

部屋の間の壁を考える(struct Wall)。部屋Aと部屋Bの間の壁で、向きは{縦・右が上・左が上}の3種類がある。すべての壁を列挙する。配列Poolに入れておく
→本当は一番ここが面倒なところ。縦棒の壁はすごくわかりやすいが、斜めの壁はYが奇数・偶数で変わってくるなどあり、多少の場合分けが必要。ソースをご参照ください。

初めはすべての部屋が別のクラスタに属するので、部屋一つずつに対応する配列を作って別々のクラスタ番号を割り振る。

最終的な迷路に残る壁を入れる配列としてwalls
各部屋から隣接して移動できる部屋の配列としてlink という配列の配列を用意する

配列Poolが空になるまでループ:
ランダムにPoolから壁を一つ取り出す この壁は部屋Aと部屋Bの間にあるとする
部屋A,Bのクラスタ番号を比較
同じクラスタ:この壁は残す 配列wallsに追加
違うクラスタ:この壁は取り払う 両側の部屋のクラスタ番号を同じにする
部屋A,Bが繋がったのでlink[A]にBを追加、link[B]にAを追加

これで迷路は完成しているが、スタート地点からの距離を調べておくと便利なのでここで計算しておく distanceという各部屋に対応した配列を作り-1で初期化
部屋pについて link[p]で隣接している部屋のうち、distanceが-1のものは自分の距離+1に設定した上で、その部屋について再帰的に適用する。
部屋0からスタート

というアルゴリズムをSwiftで書いたのが以下のソースです。PolyHexaneの内部では対戦モード向けに反転した迷路を作らなければいけないのでもうちょっと余計な処理が増えています。


class Maze{

  enum WallDirection{
    case None
    case StraightUp
    case RightUp
    case LeftUp
  }

  struct Wall {
    var A:Int
    var B:Int
    var Direction:WallDirection
    init(a:Int, b:Int, dir:WallDirection){
      A = a
      B = b
      Direction = dir
    }
  }

  private var Pool = [Wall]()
  private var clusterNumber = [Int]()

  var walls = [Wall]()
  var link = [[Int]]()
  var distance = [Int]()

  func create(X:Int, Y :Int){
    walls = []
    Pool = []
    clusterNumber = []
    for i in 0...X*Y-1{
      clusterNumber.append(i)
    }
    link = Array(repeating: [], count: X*Y)

    for y in 0...Y-1{
      for x in 0...X-2 {
        Pool.append(Wall(a: y*X+x, b: y*X+x+1, dir: .StraightUp))
      }
      if y > 0{
        if y % 2 == 1{
          for x in 0...X-1 {
            Pool.append(Wall(a: y*X-X + x, b: y*X + x, dir: .RightUp))
          }
          for x in 0...X-2 {
            Pool.append(Wall(a: y*X-X + x+1, b: y*X + x, dir: .LeftUp))
          }
        }else{
          for x in 0...X-1 {
            Pool.append(Wall(a: y*X-X + x, b: y*X + x, dir: .LeftUp))
          }
          for x in 1...X-1 {
            Pool.append(Wall(a: y*X-X + x-1, b: y*X + x, dir: .RightUp))
          }
        }
      }
    }

    while !Pool.isEmpty {
      var n = Int(drand48() * Double(Pool.count))
      let a = clusterNumber[Pool[n].A]
      let b = clusterNumber[Pool[n].B]
      if a == b {
        // this wall stands within a cluster so add it to remaining 'walls'
        walls.append(Pool[n])
      }else{
        // cluster B is merged to A
        link[Pool[n].A].append(Pool[n].B)
        link[Pool[n].B].append(Pool[n].A)
        for i in 0...X*Y-1{
          if clusterNumber[i] == b {
            clusterNumber[i] = a
          }
        }
      }
      Pool.remove(at: n)
    }

    distance = Array(repeating: -1, count: X*Y)
    func dig(_ p:Int){
      for i in link[p]{
        if distance[i] == -1{
          distance[i] = distance[p]+1
          dig(i)
        }
      }
    }
    distance[0] = 0
    dig(0)
  }
}