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

HenPitsu 4.0 – Swift 3.0で書き直し

今年の正月は実家に帰省せず自宅にいました。大晦日にランニングしたところ数日筋肉痛がひどく自宅からなかなか出られなかったこともありSwiftが3.0になってどのような変更があったのかをみていました。
[iOS 10] UIGraphicsImageRenderer について というのが見通しの良いコードが描けそうなので、HenPitsuをSwiftで書き直してみることにしました。

一番の変更点は、背景の写真と文字のレイヤーを分けて、表示・エクスポート時に合成するようにすることです。これにより、後から自分の書いた画だけあるいは背景だけ消したりすることが出来るようになりました。さらにはこれまでおざなりにしていた書き味の細かいチューニングもやってみました。

一部でバグを見つけたり、今でも何故動くのかわからない部分もありながらなんとか完成しました。あと、AppleがiADから撤退したせいでアプリからの広告収入がなくなっていたのですが、GoogleのAdMobをCocoapods経由で組み込んでみました。

ということでver4.0ができたのですが、UIGraphicsImageRendererを使うにはそのためだけに対応OSがver10以上となってしまいます。ver9.0以上で動くようにするために自力で擬似的なものを作ってみました。本当は内部バッファを自分で確保して内部ではcgImageを保持して必要なときにUIImageを生成するようにしてみたのですが、座標軸が上下反転する問題に巻き込まれてかえってややこしくなってしまったので、馬鹿みたいに簡単なものになりました。指定通りのサイズのビットマップを作るのか、Retina対応でスケール対応したビットマップを作るのかで関数を変えています。

なお起動に時間がかかるようになったのはAdMobのせいだと思います。

< SNGraphicsImageRenderer.swift >
import UIKit
class SNGraphicsImageRenderer{
    var sz:CGSize
    init(size: CGSize){
        sz = size
    }
    func image(actions: (CGContext) -> Void) -> UIImage?{
        UIGraphicsBeginImageContext(sz)
        if let con = UIGraphicsGetCurrentContext(){
            actions(con)
            let img = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return img
        }
        return nil
    }
    
    func imageScaled(actions: (CGContext) -> Void) -> UIImage?{
        UIGraphicsBeginImageContextWithOptions(sz, false, 0.0)
        if let con = UIGraphicsGetCurrentContext(){
            actions(con)
            let img = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return img
        }
        return nil
    }
}

右か左かをランダムに表示するだけのiPadアプリを作ってみた(Swift)

このところプログラムを作っていなかったのですが、GIGAZINEというサイトでこんな記事を見ました。

右か左かをランダムに表示するだけのiPadアプリに合計1億5000万円以上が支払われていた

ビデオで見るとたしかに左右の矢印を出すだけのようですが、タップした後数秒で元の画面に戻るのが特徴でしょうか。ひまだったので作ってみたところ、意外と楽しいです。AppStoreで公開するには微妙すぎるので、この場で作り方を公開します(注:自分で作った時はInterfaceBuilderを使って作りましたが、そこの操作を説明するのは面倒なのでプログラム的に画面を作るようにしました。そのため使用途中で画面を回転すると変になります)

手順1 XCode立ち上げ次の画面で新規プロジェクト作成をえらぶ

手順2 アプリの種類はSingle View Applicationをえらび、Randomizerという名前でSwiftのUniversalアプリ(iPhone/iPad兼用アプリ)を作成。

手順3 矢印を出すViewを作ります。XCodeのメニューのFile > New > File… を選び、Cocoa Touch Classのテンプレートを選択、ArrowViewという名前で親クラスにはUIView、言語はSwiftを選ぶ。

手順4 ArrowView.swiftの中身を以下のようにする

import UIKit

class ArrowView: UIView {
    var left:Bool = true
 
    init(frame:CGRect, directionLeft:Bool){
        // 矢印の向きはView作成時に指定される
        super.init(frame: frame)
        left = directionLeft
        backgroundColor = .black
    }
 
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
 
    override func draw(_ rect: CGRect) {
        // Drawing code
        if let c = UIGraphicsGetCurrentContext(){
            c.translateBy(x: frame.width/2, y: frame.height/2)
            // 座標系の原点をフレームの中心に変更
            if left{
                c.rotate(by: CGFloat(M_PI))
 // 以下のルーチンでは右向きの矢印を描きますが、左向きの時は座標系を180度回転しておく
 // これだと一つのコードで左右どちらも描けます
            }
            let w:CGFloat = max(frame.height,frame.width)/4
 // 矢印の太さは画面の長辺の4分の1
            c.move(to:CGPoint(x:frame.width/2, y:0))
            c.addLine(to: CGPoint(x:frame.width/2-w, y:-w))
            c.addLine(to: CGPoint(x:frame.width/2-w, y:-w/2))
            c.addLine(to: CGPoint(x:-frame.width/2, y:-w/2))
            c.addLine(to: CGPoint(x:-frame.width/2, y:w/2))
            c.addLine(to: CGPoint(x:frame.width/2-w, y:w/2))
            c.addLine(to: CGPoint(x:frame.width/2-w, y:w))
            c.closePath()
            c.setFillColor(UIColor.white.cgColor)
            c.drawPath(using: .eoFill)
        }
    }
}

手順5 初めからできているViewController.swiftの中身を以下のようにする

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        view.backgroundColor = .blue
        let button = UIButton(frame: view.frame)
        button.setTitle("Tap here", for: .normal)
        button.addTarget(nil, action: #selector(tapped), for: .touchUpInside)
        view.addSubview(button)
        // 画面と同じサイズのボタンを作って、現在のviewの上に貼り付ける
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
 
    func tapped() {
        let left:Bool = (arc4random() % 2 == 0)
        // 向きはここで決める。乱数が偶数なら左向き
        let av = ArrowView(frame: view.frame, directionLeft: left)
        view.addSubview(av)
        // ArrowViewを現在のviewの上に乗っけます
        UIView.animate(withDuration:1.0, delay: 1.0, options: .curveEaseIn
        // 1秒後から、1秒間かけてアニメーションを行う
                      ,animations: {av.alpha = 0.0}
        // アニメーションの内容は、ArrowViewが透明になっていく
                      ,completion: {_ in av.removeFromSuperview()})
        // アニメーションが終了したら、ArrowViewを取り除く
    }
}

あとはRunすればシミュレーターや実機で動かせると思います。

2016.9.28追記:Swift 3に合わせて変更しました

帝國圖書館 AppStoreで公開 & 境界認識アルゴリズム

1月ごろから作り始めて、1ヶ月以上前に完成していたものの、公開するか迷っていたアプリ「帝國圖書館」ですが無事ダウンロードできるようになりました。使い慣れた?Objective-CではなくSwiftで100%書いてみました(そもそも練習のために作ったようなアプリです)。Swiftは使いやすい面が多々ありますが、Swift ver1.1から1.2への移行でも色々と変更がありまだ言語仕様が安定していません。NSString と String, NSArray と Array型などの互換性があるのかないのか微妙です。それでも、次のアプリもできればSwiftで開発してみたいと思います。SAARTのようにCoreAudioをガンガン使っている場合は、そこだけObj-Cにしなければいけなさそうですが。


さて、帝国図書館では狭いiPhoneの画面をなるべく有効活用するように、本の余白を認識してなるべく本文のみを表示するようにしてみました。いわゆる画像認識の領域ですが、まじめに組もうと思ったらものすごく大変なところです。アプリの性質上、サクサク動くことが条件なので「ほぼ瞬時に」計算できることが必要条件です。

また、認識する境界について、図の赤枠が本来の「本」の境界ですが、画面を有効利用するために本文の周囲の枠(図の青枠)を認識することを目指しました。

名称未設定 2

1)まず、画像全体を縮小します。文字の一画一画などを認識するのではなく、行単位でおおまかに見れれば良いので、とりあえず64×64に縮小することにしました。ビットマップ縮小のAPIを使ったので、おそらくいろんな最適化がなされていると思いますしほとんど時間はかかりません。ついでにここで白黒にしておきます。

名称未設定 5

2)境界を認識するために隣り合ったピクセルの明るさの差をとります。横方向で認識する際には、本文は画面の上1/3, 下1/3にはないことが多いので真ん中2分の1だけを取って、縦の差分は全部加算しておきます。縦方向の境界認識でも同様の操作をしています。ページの真ん中の線など白と黒のコントラストが強いところに引っ張られすぎないように、閾値以上のものは一定の値にしておきます。

3)単純にピクセルの明るさの差が大きいところを境界と認識すると、ページの真ん中の線などに強く影響されていまいます。また、本文が1,2行しかなく残りが余白になっているようなページでおかしくなることがあります。一方で、ページのレイアウトは大体の本で大きな変わりはなく、どのへんに余白が来るかなどはある程度固定しています。そこで、「このへんに本文の境界がありそうだ」という元々の知識(=ベイズ統計学でいう事前情報)を確率関数化して、先ほどのピクセルの明るさの差に掛けあわせます。確率関数は本文の上下左右にそれぞれ1つずつの4つ用意します。こうすると、認識結果が多少外れたとしても大ハズレにはならない?

4)確率関数を掛けあわせた明暗の変化が最も大きい点が境界として選ばれます。

プログラム上ではおおよそ100行程度ですんでいます。縦、横ともにだいたい同様の計算をして境界を見つけていますが、レイアウトの特殊な本などではうまくいかないと思います。確率関数も、ある程度チューニングしましたが最適とはとても言えないと思います。もし認識に大きな不具合があるようでしたら、ここのコメントでも良いのでお知らせください。なるべく対処します。