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

WiiUにUSB記録メディアとしてSSDを増設する

WiiUでゼルダの伝説 ブレスオブザワイルドをやってみたくなったのですがダウンロードするには記憶容量が10G足りません。

任天堂の公式HPでは保存メモリー容量を拡張する(動作確認済みUSB記録メディア)としてUSB-HDDを増設するように指示してあります。32GBとかのUSBメモリーをつなぐというのが手早そうですが、頻繁にデータを保存するので耐久性に問題があるようです。USB-HDDは古いのが余っていますが、繋いでみると途中でストップしてしまいます。やはり任天堂の指示のとおりY字USBケーブルで繋がないと供給電力不足になるようです。昔持っていたはずですが、まだあったかな・・と押し入れを探してみましたがY字ケーブルは見つかりません。

パーツ屋さんに買いに行きましたが、Y字ケーブルのみは売っていません。そもそも手持ちのHDDはUSB3なのでそんなケーブルはそもそもないだろう・・・セルフパワータイプのHDDをつないでもいいのですが、WiiUの電源を切ったときも回り続けるのはどうも・・・と思っていたところに120GBのSSDが5000円で売っていました。これを古いUSB3-HDDの箱に入れて繋げば、供給電力不足にはならないと思われるので理屈上は動くはずです。

ケースは昔に買った玄人志向の外付けHDDケースです。SSDは本日購入したTranscend SSD220S(120GB)を入れて、繋いでみると・・・無事動きました。すくなくとも2,3時間プレイしたところではセーブなども問題ないようです。

USB2接続なのでなんとももったいない使用法ですが、いまさらY字ケーブルの古いHDDを探すくらいならこういう方法もあるということで、人柱としての報告でした。

 

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

MDX Player for iOS ver 2を更に高音質化するHiDef版のガイド

MDX Player ver2で再生周波数を変更できるようになりました(22,44,48,62.5KHz)
62.5KHzでは音質が良いのですが、他の周波数では内部で62.5KHzでエミュレートした上でダウンサンプリングをしていて、そのせいで音質が悪化します。

変更点

エミュレートエンジンであるGAMDXを改造して、
1) 指定周波数でのエミュレートを行いそのまま出力する(GAMDXの内部処理を変更)
2)ダウンサンプリングが必要なときは、高音質のリサンプラーを通すようにする
3) ADPCMは15.6KHzからアップサンプリングしているので、そこもリサンプラーを通す
リサンプラーはBSDライセンスのspeexライブラリを使用しました。2),3)はコンパイル時に使用・不使用を設定できるようにしました(後述)。iOS版では2)はメリットが殆ど無いのでデフォルトではオフにしてあります

4)また、究極の高音質化として、62.5KHz のMDX出力をダウンサンプルすること無くアップサンプルして出力できるよう、192KHzまでのHiDef DAC(外付け)に対応しました(実際に鳴っているところをまだ確認できていませんが

5)内部処理と、出力の周波数が違うので、周波数ボタンを2段にして上が内部処理、下が外部出力周波数とします

6)MDXを聞きながらポケモンGOをしていて気づいたのですが、一部のMDXファイルではボリュームが小さめのものがあります。ボリュームスイッチで音量を上げると、ポケモンGOの効果音が非常に大きくなってしまいます。せっかく内部のボリュームバーが付いたので、ボリュームを0〜1ではなく2まであげられるようにしてみました。

7)ボリュームを内部処理で上げると、16bitPCMの範囲からオーバーフローすることがあります(実はこれまでも一部の曲ではオーバーフローしていることはあった)。オーバーフローした時は自動的にボリュームを下げる機能をつけました(Automatic Gain Control)。ただ、次の曲に移ったときもそのボリュームが維持されるのが難点ですが。

8)周波数を変更したときにハングすることが一度だけ確認できました(何処かでMutexが開放されていないため?)。曲選択でエラーになったときにMutexが開放されていませんでしたので直しました。またしばらくMutex確保できないときにはタイム・アウトするようにしてみました。

使用法

https://github.com/sinn246/MDXPlayer/tree/HiDef においてあります。

フォルダ GAMDX > jni > speex というのが新たに導入したリサンプラーのファイル群です。
そのなかで speex_MDX.h というヘッダファイルにコンパイル時設定をまとめました。

#define USE_SPEEX
これを設定するとspeexルーチンが導入されます。設定しなければもとのままですが、リサンプラーはどうなるかというと、AppleがCoreAudioの中で勝手にリサンプルしてくれます

#define USE_SPEEX_FOR_ADPCM_UPSAMPLING
これを設定すると、ADPCMのアップサンプリングにspeexを使います。アップサンプリングにはわずかにディレイが生じるので、微妙にADPCMだけ音が遅れることにつながりますが、私にはほとんどわかりません・・・

// #define USE_SPEEX_FOR_DOWNSAMPLING
62.5KHzからダウンサンプルする必要があるとき、CoreAudioに任せずspeexライブラリを使用します。iPhoneで使う分には特に音質が上がるわけではなく(CoreAudioも音質は悪くなくさらに最適化されていると考えられるので)、研究的なもの(Androidなどに移植するときに使えるかも)として一応ソース上は残してあります(が、オフにしてあります

MDXPlayer高音質化番外編:192KHzのHiDefで出力

MDXPlayerの高音質化を色々やっていたわけですが、62.5KHzのFM音源の出力を48KHzにダウンサンプルすると必ず音質は落ちるはずです(人間の耳ではほとんど違いはないはずですが)。いっぽうで、手元にはMacに繋いでiPhoneの出力を取り込んで分析するために購入したUSB-DACがあります。これをiPhoneに繋いで192KHzで出力することができれば、62.5KHzの約3倍になるので限りなく原音に近い出力が得られるに違いない!と考えました。いわゆるハイレゾ音源です。

iOS 7で実現! iPhoneからハイレゾ出力する方法を徹底ナビ

AppleがDTM正式サポート! Lightning-USB3カメラアダプタを使ってみた

などを参考にして、まずLightning – USB 3カメラアダプタを購入します・・¥4,200か・・・と思ったらヨドバシのポイントカードで買えました。早速繋いでみると、DACの電源ランプがつきません。USBバスパワー駆動なので、電源供給できるUSBハブを経由すると動く可能性があるようですが、むかしそんなのが家にあったような気がするもわざわざ購入するのも・・・動く保証もないし・・・と思いながら数週間が経過。しかしUSB3アダプターまで買ったのだからと一念発起して電源供給できるハブを購入しました。約1300円なり。

繋いでみると・・・ランプが点灯して認識されました!

img_6413

イヤホンをDACに入れてさて聞いてみようとすると・・・音が出ていません。iOS10からサウンドの出力先の設定がわかりやすくなっているので、画面に出してみると・・・・

img_6414

SPDIF出力になっています。私が使っているUSB-DACはMacでは出力先をドライバなどで設定する形になってますが(下の図でOutputを選ぶとUSB-DACのイヤホン端子から音が出る)iOSではどうすればいいのか・・・

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-11-11-16-17-51

しばらく色々やってみましたがだめぽいです。

ただ、192KHzでの出力自体はできているのか確認してみましょう。MDXPlayerをちょっといじってみて、192KHzでの出力を要求するようにして、実際に出力されている周波数も表示するようにしてみます

    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setPreferredSampleRate:192000.0 error:nil];    // 希望する周波数
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    [session setActive:YES error:nil];
    float sr = [session sampleRate];  // この返り値が実際の出力周波数

USB-DAC繋いでいないとき:周波数の欄は2段になっていて、上がアプリ内部の周波数、下が実際の周波数です。48KHzにダウンサンプルされていることが確認できます
img_6416

 

USB-DACを繋いだとき:192KHzで実際に出力されているようです!

聞けないけど。

img_6415

ということで、手持ちのSD-U2DAC-HPLはiPhoneでは出力できないっぽいというレポートでした。どなたかiPhoneで使えるDACをお持ちでしたら試してみてください。ソースはGitHub https://github.com/sinn246/MDXPlayer/tree/HiDef に置いておきます。

 

GAMDX改造:ADPCMアップサンプリングをSpeexで

このシリーズもそろそろ終わりです。

もとのGAMDXではADPCMはもともと15625Hzなのを62500Hzにアップサンプリング(ちょうど4倍なので4回同じデータを読み出す形でアップサンプリングして、ローパスフィルターを2回通している)して、FM音源と合成して、その後ダウンサンプリングしています。ここまでのところで、ダウンサンプラーを入れ替えることで音質向上になることがわかりました。

せっかくリサンプラーをいれたので、ADPCMをアップサンプリングするところもSpeexライブラリでやってみてはどうかと考えました。

1)もとのADPCMアップサンプラー(私が改造したものを使っているのでローパスフィルターを1回しか通っていないです。したがってやや高音のノイズが多い)+Speexダウンサンプラー

defaultupsampler

2)ADPCMにSpeexアップサンプラーを使用+Speexダウンサンプラー

speexupsampler

殆ど変わりません。アップサンプリングが15625->62500とちょうど4倍なので比較的ノイズが乗りにくいようです。そこで、内部44.1KHzにしたときを比較してみましょう

3)もとのアップサンプラーで44.1KHzにアップサンプリングして、44.1KHzのFM音源と合成、そのまま出力したもの

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-19-18-14-18

4)Speexでアップサンプリングして44.1KHzのFM音源と合成、そのまま出力したもの

speex441

4)は1),2)によく似ています。ということはアップサンプリングがうまく行っているということでしょう。非整数倍にアップサンプリングするときにはちゃんとした?リサンプラーを使ったほうが良さそうです。なお、このアップサンプリングはダウンサンプリングに比べてCPU時間が半分くらいですんでいます。

GAMDX改造:リサンプラーのCPU使用率

ここまでの流れで、GAMDX音質向上を図ってみた結果もとのGAMDXのリサンプラーはあまり音質が良くないようなので差し替えてみることとしました。iOSでは比較的性能がよくおそらく最適化されているであろうAppleのリサンプラーが利用できますが、他のプラットフォームでも還元できるように、リサンプラーをfree sourceのSpeexプロジェクトから利用させてもらうようにしました。

当然、CPUに負担がかかると思いますので、使用率を比べてみることとしました。

同じ曲をiPhone5sで再生したときのCPU使用率をプロファイリングしてみました。

1)GAMDX内蔵のダウンサンプラー

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-16-17-11-21

アプリのCPU時間のうち1%も使っていません。

2)Speexリサンプラー Quality=4

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-16-12-14-51

Qualityを0-10のうち4にしたときはCPU時間のうち60%程がリサンプラーに使われてしまいます。これはやりすぎか・・・

Qualityを2にするとFM音源エミュレートと同じくらいのCPUパワーがリサンプラーに使われて、0だとエミュレートの半分くらいのCPUパワーが使われるようです。Appleのリサンプラーを使ったときはアプリの外側でリサンプラーが動くので直接比較できません(が、CPUパワーの負担は軽いと思われます)。

AudacityでiPhoneの出力を取り込んで音質を比較してみたところではあまり変わりないようですし、Qualityは0〜2で十分に思われます。

GAMDX改造:ADPCM変更の効果は?

このところGAMDX改造の話題が続きましたがそろそろ終わりになります。

もとのGAMDXではADPCMの15.6KHzの音源を内部周波数の62.5KHzにアップサンプリング、その際にローパスフィルターを通していますが何故かpcm8.cppおよびX68pcm8.cppという2つのルーチンで2重にローパスフィルターを通っています。ローパスフィルターのパラメータは固定なので、アップサンプリングする周波数が変わってしまうと問題が生じそうです。また、2回LPFを通るので音の歪みなどが必要以上に乗ってしまう可能性はあるかもしれません。追記:LPFの効果を強めたい場合、2つのLPFを直列につなぐのは「あり」のようですので、音質の劣化はあまり心配しなくていいかもしれません。

この処理を少し変えて、GAMDX内部での周波数(可変にしてみました)に合わせてアップサンプリング、そのあとADPCMが15.6KHzなのでその半分の7KHz以上の成分はノイズと考えられるのでアップサンプリングした周波数に合わせてローパスフィルタを通す、という風にしてあります。

フィルタが変わったことで音質の比較のため内蔵の「X68030のテーマ」のボイスパートを使ってみました。ADPCMの比較をするために、FM音源のところはプログラム上でカットしてあります。

1)もとのGAMDX(MDX player 2.0, 62.5KHz出力を使用。したがってダウンサンプラーはAppleのものが使われます)

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-48-27

出だし約10秒間を詳しく周波数分析したもの

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-48-42

2)改造版、ダウンサンプラーはAppleの物を使用

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-41-01

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-41-15

3)改造版、ダウンサンプラーはSpeex(Quality=2)を使用

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-43-27

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-43-57

4)改造版、Speexリサンプラー(Quality=4)

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-56-30

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-13-17-56-57

 

SpeexリサンプラーはQuality=2と4でほぼ同じ結果のようです。あとでソースを読んで見る必要あり。AppleよりもSpeexのほうがローパスな傾向があるのと、私が改造したADPCMルーチンは8KHz以上の本来はカットされるべき部分が結構残っているようなのでLPFのパラメータはいじる余地がありそうです。

GAMDXのリサンプラーをいれかえてみる

前回に引き続きGAMDXの改造を継続。

まずADPCMをいじっているときに、内部で使用しているSample型が16bitに設定されていたのでPCMデータがオーバーフローする状況に遭遇。ローパスフィルターを整理するとともに極力データのロスがないように、原則データロスにつながる操作をしないようにしてみた結果なのですが。ということでSample 型は32bitにしてあります。

そしてもともとのダウンサンプラーの性能が良くないと指摘してAppleの(iOS組み込みの)ダウンサンプラーを使っていたのですが、他のOSに移植することもあるかもしれませんし、ダウンサンプラーを差し替えてみようと考えました。

ダウンサンプラーとアップサンプラーをあわせてリサンプラーというのですがそのものを総合的に解説したHPがあります:Digital Audio Resampling Home Page

色々解説が書いてありますがこれを勉強して一から書くのは相当大変というか無理です。ということでこのHPのFree Resampling Softwaresというところでフリーの実装を物色してみます。SoXというのが有名らしいがかなりボリュームが多いしGPLだ・・と思っていたら一番下の方でSpeexライブラリがBSDライセンス(商用利用も可能)となっています。Speex自体はVoIPで音声を圧縮して通話することが主目的のライブラリですが、リサンプラーも半ば独立したものがあるみたい(Speex自体は開発終了していてOpusという新しいプロジェクトに移っているようですが、リサンプラーはあまり変わらないようです)。通話目的のプロジェクトなので、速度も重視されているようです。

ということで秋の夜長、Speexのresampler.cを移植してみました。1.2rc3をダウンロードして、そのものズバリresampler.cというのを入れてみて、コンパイルエラーが起こらないまでにヘッダーを持ち込みます。.cファイルは一つだけというのが好感が持てます。リサンプルの精度は0-10で選べます。ARMのNEON命令セットに対応しているようですが、今はうまく動かないようです。

GAMDXの内部にはMXDRVG_MakeResampler,  MXDRVG_ClearResamplerという2つの関数を追加しただけで、他はほぼ透過的にリサンプルする・しないが選べます。もとのGAMDXではMXDRVG_Startという関数で出力周波数を指定しますが、ここでは内部処理の周波数を指定して、出力周波数を決めるのはResamplerでという風にしたのでフレキシブルになっていると思います。

手元にUSB-DAC/ADCがないのでリサンプラーごとの性能比較が今はできないので後日。

先述の通りSpeexのリサンプラーは精度が0-10で選べます。デフォルトは4になっていて,0では線形補間のレベルと書いてあります(が、ソースの中をちゃんと読んでいないので後の課題にしておきましょう)。5以上のレベルはCPUにかかる負荷が重くなりそうなので、レベル4,2,0について調べてみました。62.5KHzの内部周波数で、48KHzにダウンサンプリングして出力したものです。

レベル4

level4

レベル2

level2

レベル0

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-11-16-26-00

もとのGAMDXリサンプラーと直接比べられるように同じ曲を調べてみました。ノイズは明らかに減っていて、Appleのリサンプラーとあまり違いないレベルでしょうか。レベルごとの違いについて、実際に耳で聞いたところでもあまり変わらないようですが、CPU使用率は結構違います。iPhone5sではレベル4でCPUの使用率30%程度、レベル0で20%程度となります。レベル2くらいでいいのかもしれません。

なお、前回の48KHzの出力で1)GAMDX内部は62500Hz、Appleのリサンプラーで48KHzに変換 3)GAMDX内部から48KHzで処理、を比較してみる話ですが、手元にあったVeyrlen氏のSonic Boom Stage 1 (高校生の時に松山銀天街のセガでやったのを思い出す・・・)はあきらかに1)のほうが良いみたいです。ほかの曲では大きな違いがない様に聞こえるのですが。

ということで現時点での結論としてiOS についてはベストの音質は62.5KHzにApple resamplerをかましたもの、となりver2.0からはあまり大きな改善は見込めなさそうです。他のOSについてはSpeex resamplerがうまくいけば音質向上になると思います。