マイクの入力レベルをdBで取得する

マイクの入力レベルを調べるのには、AVFoundationを使うのがお手軽です。下のコードは、マイクからの入力レベルをコンソール上に出力するごくごく簡単なサンプルです。

try!を使っていたりcanAddInput/Output()を使っていなかったりしていますので、その点は注意してください。

final class PowerLevel: NSObject {
    private let captureSession_ = AVCaptureSession()
    // オーディオデータを渡してくれるデリゲートメソッドが動くスレッド。
    /* ※データ順が狂ったりメインスレッドで処理落ちしないように
       「バックグラウンド」の「シリアルキュー」を必ず使うこと。*/
    private let serialQueue_ = dispatch_queue_create("PowerLevel.serialqueue.audio",
                                                    DISPATCH_QUEUE_SERIAL)
    init() {
        super.init()
        setupCaptureRoute()
    }

    func setupCaptureRoute() {
        // OSがデフォルトにしているマイクデバイスを選ぶ
        let audioDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio)

        // マイクデバイスをキャプチャセッションにつなぐ入力クラスを用意
        let audioInput = try! AVCaptureDeviceInput(device: audioDevice)

        // マイク入力の出力先(今回はデータをデリゲートメソッドに渡す)を用意
        let audioDataOut = AVCaptureAudioDataOutput()
        // デリゲートオブジェクトを設定
        audioDataOut.setSampleBufferDelegate(self, queue: serialQueue_)

        // キャプチャセッションに入出力を接続。これでいつでもstartできる
        captureSession_.addInput(audioInput)
        captureSession_.addOutput(audioDataOut)
    }

    func start() { captureSession_.startRunning() /* 開始 */ }
    func stop() { captureSession_.stopRunning() /* 停止 */ }
}

// デリゲートメソッドの用意
extension PowerLevel: AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(captureOutput: AVCaptureOutput!,
                             didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
                             fromConnection connection: AVCaptureConnection!) {

        // キャプチャコネクションからオーディオチャンネルを取得し、パワーレベル(dB)を入手
        let audioChannels = connection.audioChannels.filter{ $0 is AVCaptureAudioChannel }
        audioChannels.forEach {
            print("\($0.averagePowerLevel) / \($0.peakHoldLevel)")
        }
    }
}

このコードはこんなふうに使います。

let obj = PowerLevel()

obj.start() // コンソールにマイク入力のレベルをdB表示

obj.stop() // 処理停止

得られる値はdBで、デバイスの最大入力レベルを「0dB」とした値(つまり0に近いほど大きな値を表すマイナス値)になっています。

追記

オーディオチャンネルが複数あることも考慮すると、それぞれの平均をとったほうがよいかもです。つまり、デリゲートメソッドの中にある処理を、次のように変えます。

        let audioChannels = connection.audioChannels.filter{ $0 is AVCaptureAudioChannel }
        if !audioChannels.isEmpty {
            let averagePowerLevel = audioChannels.reduce(0.0){ $0 + $1.averagePowerLevel }
                / Float(audioChannels.count)
            let peakHoldLevel = audioChannels.reduce(0.0){ $0 + $1.peakHoldLevel }
                / Float(audioChannels.count)
            print("\(averagePowerLevel) / \(peakHoldLevel)")
        }

参考: stackoverflow.com

画面遷移したときに下部のタブバーを一時的に消す

ナビゲーションコントローラをタブバーと組み合わせて使っている時、プッシュ遷移した先の画面ではタブバーを消して全画面表示したいケースはあります。設定が終わるまでタブバーによる遷移を避けたいときなどがそうです。遷移先から戻ってきたときには、またタブバーを表示する動作とのセットです。

その場合、「遷移元のビューコントローラ」で、「自分のhidesBottomBarWhenPushedプロパティ」をtrueにセットします。これで

  • 遷移先に移動するときにタブバーが隠れる
  • 遷移先から戻ってくるときにタブバーが再表示される

がまとめて実現できます。prepareForSegue()で書いた場合の例を下に載せます。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    switch segue.identifier {
    case "ToDetailView"?:
        hidesBottomBarWhenPushed = true
        // tabBarController?.hidesBottomBarWhenPushed = true // NG。遷移先のviewがサイズ調整されない
    default:
        break
    }
}

ありがちなのは、遷移元ビューコントローラのhidesBottomBarWhenPushedプロパティを操作するのではなく、タブバーコントローラのプロパティを操作してしまうミスです。これだと、遷移先画面でベースのビューがサイズ調整されないまま表示され、使いづらくなってしまいます。

気をつけましょう(自分)

forkしたOSSライブラリをCocoaPodsで扱う

CocoaPodsを使った、既存のOSSライブラリをforkしたリポジトリをプロジェクトに組み込む方法です。

前ふり

CocoaPodsやCarthageなどのパッケージ管理システムを使うと、githubなどで公開されているOSSライブラリを簡単に使うことはよくあります。これらは簡単な定義ファイルとコマンドを使うだけで、依存関係などを含めてダウンロードしてプロジェクトに組み込めるすぐれものです。

けれども、公開されているライブラリをそのまま使うことはあまりなく、実際には少しだけ変更して使いたいことはしょっちゅうあります。開発者にプルリクエストを送ってマージしてもらうのがベストですが、実際にはそこまでするのは時間/機能的な問題で難しいことの方が多いでしょう。大抵、自分のリポジトリにforkして好みの変更を加えた上で使うことになります。

本記事では、CocoaPodsを使ったときの自分なりの方法を紹介します。「もっといい方法があるよ」という方はぜひコメントください。

ちなみに今回Carthageには触れません。 インストールしたライセンスのAcknowledgementをSettings.bundleに含める方法が便利なので・・・

OSSをforkして使うときの課題

あるOSSライブラリをgithub上でforkしたあと、自分のプロジェクトに組み込んで使うことを考えます。このとき、forkしたライブラリをCocoaPodsで使うには、いくつか課題が生じます。

  • forkしたライブラリなので、勝手にcocoapods.orgに登録することはできない
  • CocoaPodsがcloneする識別子になるリリースタグを勝手に打つのは憚られる
  • 〜.podspecファイルはできれば変えたくない(オリジナルを尊重したい)

勝手にforkしたライブラリなので、勝手にCocoaPods.orgに登録するのは絶対に避けなければいけません。それに、本家のリリースタグを無視した名前でタグを打つことは混乱のもとになります。そして、今後プルリクエストを送ることを考えると、masterブランチで〜.podspecを変更してしまうのは考えものです。

ようは、本家に迷惑をかけずに分かりやすく管理するにはどうしたらよいか、という問題です。

どうするか

この問題に対してどうするかということで、次のような方針を考えました。

  • forkしたライブラリなので、cocoapods.orgに登録することはできない Podfileでローカルのリポジトリを指定するようにします。

  • CocoaPodsがcloneする識別子になるリリースタグを勝手に打つのは憚られる リリース専用のブランチを作り、ひと目でforkしたものと分かるタグを打つようにします。

  • 〜.podspecファイルはできれば変えたくない(オリジナルを尊重したい) masterブランチはそのままで、リリース専用のブランチだけで.podspecを変更する

それぞれの詳細は次のとおりです。

具体的な方法

Podfileでローカルのリポジトリを指定

ここだけライブラリを使用する側の作業です。使用するライブラリを記述するPodfileで、本家のURLを書く代わりにforkしたリポジトリを記述します。

target 'MyApp' do
pod 'AwesomeOSS', :git => 'https://github.com/me/AwesomeOSS.git', :branch => 'local_release'
end

このときbranchキーの記述がポイントで、次で作るブランチを指定するようにしておきます。ここでは「local_release」という名前を指定しました。

次はforkしたリポジトリ側の作業です。

リリース専用ブランチを作り独自タグを打つ

まずリリース専用のブランチとして「local_release」を作ります。そしてそのブランチを次のルールで運用します。

  • 普段の変更はmasterや他のブランチで行う
  • 〜.podspecの変更はこのブランチの中でだけ行う
  • リリース時に、このリリース用ブランチに他所での修正をマージしてタグを打つ

git-flowのmasterブランチ(=リリースブランチ)に近いイメージです。プルリクエストに使える修正はどこでやってもいいですが、その中に〜.podspecの修正を含めないよう注意することで、あとあと管理しやすくのを狙っているわけです。

ブランチ 用途
リリース専用ブランチ(local_release) Podfileで参照するブランチ。リリース時に他ブランチをマージして、タグを打つために使う
他のブランチ(master, etc.) 自分の変更を入れるために使う。リリース時にlocal_releaseへマージ。ただし.podspecファイルやLICENSEなど、本家にプルリクエストを送る際に余計になるファイルは変更しないようにする
$ git co -b local_release

ブランチを作ったら、masterや他のブランチで必要な変更を加えていきます。そして必要な変更が終わったところでリリース専用ブランチにマージします。

マージ後は、リリース専用ブランチ上のコミットに対してリリースタグを打ちます。このとき気をつけなければいけないのは、タグに使う文字の並びです。CocoaPodsのマニュアルではSemantic Versioning形式で指定するようにとありますが、もし次のような文字列を指定してもpod lib lintコマンドで弾かれてしまい、実際にバージョン認識してくれませんでした。

ダメだったタグ
✕ foobar-v0.4.0
✕ foobar0.4.0
✕ foobar_0.4.0

いろいろと試してみた結果、次のようにバージョン番号の後ろに「-(ハイフン)」を付けて、その後に任意文字列を指定するのは大丈夫でした。

0.4.0-foobar-X.Y.Z

そこで、私は次のようにしてタグをつけています。

(a)    (b)     (c)
0.4.0-foobar-0.1.0

# (a): オリジナルのfork元バージョン番号
# (b): 自分のリポジトリ名
# (c): 自分のルールにおけるバージョン番号

最初にfork元のバージョン番号を示して、どの時点で派生したのかを分かるようにします。次にリポジトリが分かる名前を入れておき、最後に自分の管理下で付けるバージョン番号を付ける、というルールです。こうしておくと、clone元の由来がわかりつつ、独自のcloneであることを明確にできます。さらには自分用のバージョン番号を付けることで、独自部分の変遷も分かりやすく表すことができます。

$ git tag 0.4.0-foobar-0.1.0

.podspecはリリース専用のブランチだけで変更する

上でだいたい説明してしまいましたが、本家を尊重しなければならない変更は、極力masterなどの開発ブランチでは変えないようにしておき、リリース専用ブランチの上でだけ変更するようにします。

そうしておけば本家に敬意を表したままで、自分の変更を加えることができますし、なによりプルリクエストを作りやすくなるので、本家に取り入れてもらいやすくなるかもしれないという魂胆もあります。

作業は以上です。

まとめ

世の中に公開されているOSSライブラリに一手間加えたい場合、CocoaPodsのリポジトリURLを指定する方法が使えます。このときにブランチやタグのルールを守ることで、自分の変更も管理しやすくなる方法を紹介しました。

これよりももっといい方法があれば、ぜひコメントください。

パターンマッチング「func ~=」のデフォルト実装は「func ==」

TL;DR

パターンマッチングのルールを自分で定義するときは、~=演算子、つまり「func ~=」を定義することになるわけですが、未定義の場合のデフォルト実装は「func ==」を使うようになっている、という話です。そして、NSObjectのデフォルトはisEqual()が使われる、という話です。

Swiftのパターンマッチング

Swiftでオブジェクトを使ったパターンマッチングをする場合、次のように書きます。

struct A {}
let a = A()
let b = A()

switch a {
case a: print("a") // → a
case b: print("b")
defalut: print("default")
}

このときのパターンマッチングに使われるのは、「func ~=」なる演算子です。この演算子がBool型を返すことによってパターンマッチするかどうかが決まります。

それでは、目的の型が~=を用意していなかった場合にはどのようになるのでしょうか。この場合、デフォルト処理が使われることになり、「func ==」が使われます。==の結果を、~=の結果として利用しているのです。そして、もしユーザーやライブラリで独自の~=を定義すると、そちらが使われます。

ケース 使われる処理
何も定義していない デフォルト実装の~=を使う。実態は==
~=を定義 定義した~=を使う
==だけ定義 ~=のデフォルト実装から定義した==を使う

この振る舞いはドキュメントにもきちんと書いてあり、The Swift Programming Languageの[Language References]-[Expression Pattern]にて以下のように書かれています。

By default, the ~= operator compares two values of the same type using the == operator.

それでは実装はどうかというと、swiftソースの stdlib/public/core/Policy.swift を見ると次のように定義されていて、ドキュメントのとおりになっているとわかります。

//===----------------------------------------------------------------------===//
// Standard pattern matching forms
//===----------------------------------------------------------------------===//

// Equatable types can be matched in patterns by value equality.
@_transparent
@warn_unused_result
public func ~= <T : Equatable> (a: T, b: T) -> Bool {
  return a == b
}

以上です。

ちなみにこのstdlib/public/core/Policy.swift、他にもいろいろな定義がありとてもおもしろいソースファイルになっています。ぜひ一度読んでみることをおすすめします。

なぜ調べてみたのか

せっかくなので、なぜこんなことを調べたかも記録に残しておきます。

NSObjectを継承したクラスでパターンマッチングをしていたところ、マッチングが行われるたびにNSObjectisEqual()メソッドが呼ばれていることに気づきました。

class Foo: NSObject {
    override func isEqual(object: AnyObject?) -> Bool {
        return super.isEqual(object) // ←ここにbreakpointを貼ると止まる
    }
}
let a = Foo()
switch a {
case a: print("a") // a
default: break
}

これはなぜだろう、isEqual()~=とは、どのようにして繋がっているのだろうということが気になり、調べてみたというのがことの発端です。

stdlib/public/SDK/ObjectiveC/ObjectiveC.swift の末尾で、

// NSObject implements Equatable's == as -[NSObject isEqual:]
// NSObject implements Hashable's hashValue() as -[NSObject hash]
// FIXME: what about NSObjectProtocol?

extension NSObject : Equatable, Hashable {
  :
@warn_unused_result
public func == (lhs: NSObject, rhs: NSObject) -> Bool {
  return lhs.isEqual(rhs)
}
   :

となっていて、==isEqual() と呼ばれていました。

UIEdgeInsetsを(強引に)@IBInspectable化する

@IBInspectableで入力できる型は、User Defined Runtime Attributesで指定できる型と同じ、とXcodeのドキュメントには書かれています。つまり、Localized StringやNilを除く以下の型を指定できます。

値の型 オブジェクト エディタ上の状況
Boolean Bool チェックボックス。チェックするとtrue
Number Int, Float, CGFloat, etc. 数値入力フィールド。非数値を入力するとエラーダイアログを表示する
String String 文字列入力フィールド
Point CGPoint CGPointとしてxyそれぞれを入力するフィールド
Size CGSize CGSizeとしてwidthheightそれぞれを入力するフィールド
Rect CGRect CGRectとしてxywidthheightそれぞれを入力するフィールド
Range NSRange NSRangeとしてのlocationrangeを入力するフィールド。NSUInteger相当
Color CGColor カラーパレット
Image UIImage(iOS) / NSImage(OS X) 画像

しかしこの一覧にない型、たとえばUIEdgeInsetsなどは、扱う値の1つ1つが設定可能な型(CGFloat)ですが@IBInspectableの効果を得られません。

なんとかできないかなと、たとえばこんなコードを考えてみました。

public var trigerPadding: UIEdgeInsets = UIEdgeInsetsZero

@IBInspectable public var trigerPaddingTop: CGFloat {
    get { return trigerPadding.top }
    set { trigerPadding = UIEdgeInsets(top: newValue,
                                       left: trigerPadding.left,
                                       bottom: trigerPadding.bottom,
                                       right: trigerPadding.right) }
}

@IBInspectable public var trigerPaddingLeft: CGFloat {
    get { return trigerPadding.left }
    set { trigerPadding = UIEdgeInsets(top: trigerPadding.top,
                                       left: newValue,
                                       bottom: trigerPadding.bottom,
                                       right: trigerPadding.right) }
}

@IBInspectable public var trigerPaddingBottom: CGFloat {
    get { return trigerPadding.bottom }
    set { trigerPadding = UIEdgeInsets(top: trigerPadding.top,
                                       left: trigerPadding.left,
                                       bottom: newValue,
                                       right: trigerPadding.right) }
}

@IBInspectable public var trigerPaddingRight: CGFloat {
    get { return trigerPadding.right }
    set { trigerPadding = UIEdgeInsets(top: trigerPadding.top,
                                       left: trigerPadding.left,
                                       bottom: trigerPadding.bottom,
                                       right: newValue) }
}

すこし長いですがとても単純なことをしているだけで、単にUIEdgeInsetsの1つ1つの計算型プロパティを用意して、大元である変数trigerPaddingの値にアクセスしているのみです。

とはいえ、これで下のスクリーンショットのように設定できるようになります。これはこれでなかなか便利です。

f:id:swiftlife:20160327221435p:plain

RxSwiftを勉強していますが、なかなか理解が進まないシリーズです。

RxSwift / RxCocoaのバージョンは2.2を対象にしています。

学習の手始めに「UIButtonがタップされたら、画面上にあるラベルをhiddenにする」というごくごく簡単な処理を書いてみようと思ったのですが・・・。どうにか動くものはできたものの、まだまだ予断を許さない状況です。

  • ボタンがタップされたことを伝えるrx_tapに、ラベルを隠すrx_hiddenを繋ぐだけでいいんじゃない?…と思ったけれど、タップと隠すのは別のオブジェクトだ。どうやってつなぐんだろう。
  • どうやら別のObservableにつなぐ場合はbindTo()を使うと良さそうだ。でも、あれ?下のコードみたく書いてもエラーが出るだけだぞ?
button.rx_tap.bindTo(label.rx_hidden)
// -> error: cannot convert value of type 'AnyObserver<Bool>' to expected argument type 'Variable<Void>' (aka 'Variable<()>')
  • RxCocoaのサンプルを見るとbindTo()する前にmap()を挟んでいるのが多いけれど、これはどういう意味だろう

と紆余曲折の結果、次のコードで動くようになりました。

button.rx_tap.map { true }
    .bindTo(label.rx_hidden)
    .addDisposableTo(bag)

問題点はなんだったかというと、

  • rx_hiddenAnyObserver<Bool>という、Boolを型とするObservableでないといけなかった
  • map()trueを返しているのは、タップというUIControlイベントをtrue/falseに型変換するため

ということでした。深く考えずつないでいけるかと思いきや、型の安全さにともなう制限があったわけですね。

まだまだ先は長そうです。

Emacsで選択した領域を検索対象にする

EmacsC-sで検索するときに領域が選択されていたら、その領域を検索対象として検索するマクロを、自分の.emacsに追加しました。

  ;; 選択した領域を使ってインクリメンタル検索を実行
  (defadvice isearch-mode (around isearch-mode-default-string
                                  (forward &optional regexp op-fun recursive-edit word-p)
                                  activate)
  (if (and transient-mark-mode mark-active (not (eq (mark) (point))))
      (progn
        (isearch-update-ring (buffer-substring-no-properties (mark) (point)))
        (deactivate-mark)
        ad-do-it
        (if (not forward)
            (isearch-repeat-backward)
          (goto-char (mark))
          (isearch-repeat-forward)))
    ad-do-it))

C-s C-wで単語を検索対象にできますが、これだとカーソルよりも前にあった内容は選択されないので不便に感じていました。そもそも検索しようとしてキーワードを選ぶのではなく、キーワードに注目してから検索するケースは多いので、なかなか便利です。