ヘッドバンギングで音量と再生速度が変わる音楽プレーヤーを作ってみた

この記事は [JINS MEME Advent Calendar 2015](http://qiita.com/advent-calendar/2015/jinsmeme) 25日目の記事です。

![meme.gif](https://qiita-image-store.s3.amazonaws.com/0/3752/5c6043cc-eef6-bf4d-7f0d-fba3a726add5.gif "meme.gif")



JINS MEMEはセンサとBlueToothを内蔵したメガネです。
以下のセンサーで目の動きや頭の傾きなどがBluetooth経由で取得できます。

* 3点式眼電位センサー
* 3軸加速度センサー
* 3軸ジャイロセンサー

詳細はAdvent Calendarの各記事をご覧ください！

## 何か作りたい

JINS MEMEはの良いところはTHE ウェアラブルデバイス、という感じがしなくて、日常生活で違和感なく使えるところだと思います。見た目や重さは僕が普段使っているメガネとほとんど同じ感じです（！）
※ 左側がJINS MEMEです
![IMG_3995.png](https://qiita-image-store.s3.amazonaws.com/0/3752/138ba4f6-1343-a8e2-ee53-9f0cf9faa2d8.png "IMG_3995.png")  

以前参加したJINSのハッカソンで、音楽に合わせて首を振る動きでライブとかのノリの可視化みたいなのをやろうとしましたが、完成できなかったので今回は同じようなものを作ってみようと思いました。

でも、音楽にのってるかどうか、というのを判定するのはちょっと簡単に作れそうになかったので、、、
歩く速さに合わせて、曲の再生速度が変化するオーディオプレイヤーアプリ（Rendow）があったので、それと同じ要領で頭の振り（ヘッドバンギング）に応じて再生速度や音量が変化するアプリを作ってみることにしました。  

アイデアの参考にさせていただきました。 
Rendow
https://itunes.apple.com/jp/app/rendow/id1029061338?mt=8&ign-mpt=uo%3D4

AVAudioPlayerの使い方も開発者の方のブログを参考にしました。
http://nackpan.net/blog/2015/09/20/ios-wift-avaudioplayer-playback-rate-changer/

## JINS MEME SDK
* iOS用のSDKとサンプルソースが公開されています。  
開発者用アカウントを取得してSDKをダウンロードし、アプリ登録をするとアプリID、Secretキーが発行されるので、これをAppDelegateで設定するとSDKを動かせます。  
https://developers.jins.com/ja/resource/docs/startup_guide/ios/
* Jins MemeのサンプルはObjective-Cなので、こちらのSwiftで書き変えたソースをベースに実装しました。  
https://github.com/manchan/JinsMeme-Swift-Sample

## 音楽の再生、再生速度と音量の変更

音楽の再生にはAVAudioPlayerクラスを使用します。

・AVAudioPlayerクラス

```
var audioPlayer: AVAudioPlayer?

audioPlayer = try? AVAudioPlayer(contentsOfURL: url)
```

* 再生速度：rateプロパティ

```
player.enableRate = true
player.rate = 2.0 // 0〜2.0
```

* 音量：volumeプロパティ

```
player.volume = 1.0 // 0〜1.0
```

※ AVAudioPlayerでは、クラウドアイテムやApple Musicで「マイミュージックに追加」したアイテムなどは再生できないようです。iTunes Store経由で購入した曲やCDからインポートした曲でないと再生できませんでした。

* 曲の再生、一時停止、停止はAVAudioPlayerの以下の各メソッドで実装できます。

```
player.play()
player.pause()
player.stop()
```


曲の選択はMPMediaPickerControllerを使うとミュージックライブラリの選択画面と同じようなUIが簡単に実装できます。

```MusicPlayerViewController.swift
    // MARK: Media Picker
    @IBAction func pick(sender: AnyObject) {
        let picker = MPMediaPickerController()
        picker.delegate = self
        picker.allowsPickingMultipleItems = false

        presentViewController(picker, animated: true, completion: nil)
    }
    
    // 曲を選択したときに呼ばれる
    func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        // このfunctionを抜けるときにピッカーを閉じる
        defer {
            mediaPicker.dismissViewControllerAnimated(true, completion: nil)
        }
        
        let items = mediaItemCollection.items
        if items.count < 1 {
            songTitleLabel.text = "曲 名"
            return
        }
        
        let item = items[0]
        if let url = item.assetURL {
            audioPlayer = try? AVAudioPlayer(contentsOfURL: url)
            audioPlayer?.delegate = self
            
            if audioPlayer == nil {
                // 再生できません
                return
            }
            
            // 再生開始
            if let player = audioPlayer {

                songTitleLabel.text = item.title ?? ""
                
                player.enableRate = true
                player.rate = playbackRateSlider.value
                player.volume = volumeSlider.value
                
                player.play()
            }
            
        } else {
            audioPlayer = nil
        }
    }
    
    // 選択がキャンセルされた場合に呼ばれる
    func mediaPickerDidCancel(mediaPicker: MPMediaPickerController) {
        mediaPicker.dismissViewControllerAnimated(true, completion: nil)
    }
```

## JINS MEMEから頭の振りを取得する
MEMELibDelegateクラスのmemeRealTimeModeDataReceivedメソッドから取得したデータを使用します。
リアルタイムモードではこのメソッドを通して、約20Hzでデータの取得ができるとのこと。

頭の振りは姿勢を表す角度のうちのpitch要素（頭を縦に振る向きの傾き）、頭の上下移動の頻度の算出には加速度のy軸の値を使用しました。 
参考（roll, pitch, yaw）
http://qiita.com/mito_log/items/020a87996ed2b9d793e6#roll-pitch-yaw


```MusicPlayerViewController.swift
func memeRealTimeModeDataReceived(data: MEMERealTimeData) {
        
	self.blinkIndicator()
	self.reloadData(data)
	self.delegate?.memeRealTimeModeDataReceivedPitch(data.pitch)
}

func reloadData(data: MEMERealTimeData) {
	if data as MEMERealTimeData? != nil {
    	// pitchの値を頭の振りの判定に、accYを頭の上下動の判定に使う
   		self.pitch.text =  NSString(format: "pitch: %.2f", (data.pitch)) as String
		self.accY.text = NSString(format: "accY: %d", (data.accY)) as String
            
		self.pitchValue = data.pitch
		self.accYValue = data.accY
	}
}
```

1秒間あたりの上下の頭の振りが大きいほど音量が大きく、頭の上下移動の頻度（加速度のプラス/マイナス切り替わり回数）が高いほど再生速度が早くなるようにしてみました。

音量 = |pitchの最小値の正弦値| + |pitchの最大値の正弦値| * 1/2
		
```
	self.audioPlayer!.volume = ceilf(
	((abs(sin(self.minPitchValue * Float(M_PI/180))) + abs(sin(self.maxPitchValue * Float(M_PI/180)))) * 0.5)/volumeUnit
	) * volumeUnit
```

再生速度 += 速度増加幅 * （プラス/マイナスの切り替わり回数の1秒前との差分）

```
self.audioPlayer?.rate += dRate * Float(newSignChangeCount - signChangeCount)
```


pitchデータ、accYデータを格納するプロパティの値を監視し（willSet）、頭の振りによる前回の値との差分をみて一定値以上になるとモーダルビューが表示され、曲が一時停止している場合は再開するようにしています。振りが止まると曲も一時停止します。

```MusicPlayerViewController.swift
var minPitchValue: Float = 0
var maxPitchValue: Float = 0
var count : Int = 1
var pitchValue:Float = 0 {
    willSet {
            
        // 曲の開始/一時停止
        if pitchValue > 0 && abs(newValue - pitchValue) > 2 && self.audioPlayer?.prepareToPlay() == true {
            // 曲が選択されているか、一時停止状態で頭を振ると曲が開始する
            self.didStartSwingingHead()
                
            if self.audioPlayer?.playing == false {
                self.audioPlayer?.play()
            }
                
        } else if self.modalView != nil && abs(newValue - pitchValue) < 0.02 {
            // Swingが止まったら、モーダルビューを閉じる
            self.modalView!.dismissViewControllerAnimated(true, completion: {self.modalView = nil})
            self.audioPlayer?.pause()
                
            self.rateSlider.value = (self.audioPlayer?.rate)!
            self.volumeSlider.value = (self.audioPlayer?.volume)!
                
        }
            
        // 音量の変更
        if count >= realTimeDataFrequency {
            // 1秒間（20Hz）の最大値/最小値から音量を決める（0〜1.0の間で0.125ずつの10段階）
            if self.audioPlayer != nil {
                self.audioPlayer!.volume = ceilf(((abs(sin(self.minPitchValue * Float(M_PI/180))) + abs(sin(self.maxPitchValue * Float(M_PI/180)))) * 0.5)/volumeUnit) * volumeUnit
//                    print(NSString(format: "Volume: %0.3f", self.audioPlayer!.volume))
                    
                self.maxPitchValue = 0
                self.minPitchValue = 0
                count = 1
            }
        }
            
        if newValue > self.maxPitchValue {
            self.maxPitchValue = newValue
            
        } else if newValue < self.minPitchValue {
            self.minPitchValue = newValue
                
        }
        count += 1
            
    }
}
    
var dRate: Float = 0.1
var signChangeCount: Int = 0
var newSignChangeCount: Int = 0
var accYValue: Int8 = 0 {
    willSet {
        // 1秒間(20Hz)の上下往復の頻度を計測する
        if count >= realTimeDataFrequency {
            if self.audioPlayer != nil {
                
                // 往復頻度（加速度 > 0 <-> 加速度 < 0 への切り替わり回数）が増えていたら再生速度を早くする
                if newSignChangeCount > signChangeCount {
                    self.audioPlayer?.rate += dRate * Float(newSignChangeCount - signChangeCount)
                    
                } else if newSignChangeCount < signChangeCount {
                    self.audioPlayer?.rate -= dRate * Float(signChangeCount - newSignChangeCount)
                        
                }
//                    print(NSString(format: "Rate: %0.3f", self.audioPlayer!.rate))
                signChangeCount = newSignChangeCount
                newSignChangeCount = 0
            }
        }
            
        if isPlus(newValue) != isPlus(accYValue) {
            newSignChangeCount += 1
        }
    }
}
    
func isPlus(value : Int8) -> Bool {
    var r: Bool
        
    if value > 0 {
        r = true
        
    } else {
        r = false
        
    }
    return r
}
```


モーダルビューでは表示した顔のイラストをpitchの角度に応じて上下に動かしてみました。

```SwingingHeadViewController.swift
func memeRealTimeModeDataReceivedPitch(dataPitch: Float) {
        
        // 角度に応じて顔の位置を上下に動かす（角度：-90〜90度ぐらい 最大20pt）
        let d: Float = 20
        self.swingingHead.center.y = self.view.center.y + CGFloat(d * sin(dataPitch * Float(M_PI/180)))
    }
```

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/3752/aad02c84-ca8b-fe7e-c43d-20d047b14ef8.gif "sample.gif")



## github
https://github.com/takaishota/MEME-HeadBanging

## 課題
連続的なセンサの値を使う際には異常値を除外したりして、ユーザが意図していない入力値を反映しないような工夫が必要だなと思いました。頭の振りが止まったときに音楽も止まるようにしてるのですが、現状では頭は動かしてるつもりでも一瞬音楽が止まったりします。そういうのがあると自然な操作感が途切れてしまって、機械に合わせる感が出てしまうなあ、と思いました。

## 感想

加速度センサーやジャイロセンサーはスマホにもついていますが、メガネに組み込まれていることによって両手が空くので、動きや姿勢を自然に取得できるのがいいなあと思っています。
Advent Calendarの他の方の作っていたのもそうですが、普段の生活の動きを生かしたアプリなどができるとおもしろいのではないかと思います。

新しいデバイスが普及するには用途や自然な操作感以外にも文化的な側面も大きいと思うので、エンジニアやデザイナーなどが率先して使ってみて面白いものが広がりやすい状況にしたいですね！　以上です。  
![HEAD+G.png](https://qiita-image-store.s3.amazonaws.com/0/3752/397f8c3b-9493-b15c-e0ef-458f8ac206a8.png "HEAD+G.png")