Swiftでラ◯イトセーバーぽいの作る


今日は2015年12月17日！！つまり明日は2015年12月18日！！そう！STAR WARS EP7がついに公開する！！！！！予告、ハン・ソロの「戻ってきたぜ」にはしびれましたね・・・STAR WARSと言えばライトセーバーですが、なんで青とか緑とか赤とか色が違うのかなって不思議ですよね。あれ実はライトセーバーを作るときに使うアデガン・クリスタルという石に依存していて、ジェダイたちは青や緑の天然の石を使うんですが、シスは人工の石を使ってかつ邪悪な気を込めて作るから赤色になるらしいですよ！ちなみにライトセーバーでの戦闘には７つのフォームがありましt（ry

# 概要

というわけで今回は振るとブォンッと音がなるライトセーバーっぽいものを作っていきます。加速度を取得するCoreMotionや音を再生するAVFoundationを使ってiPhoneの動きに合わせて音がする機能を作っていきます。
完成がこちら
https://github.com/kentarohorie/HikariSaber

# 手順

1. __準備__
2. __加速度を取得してiPhoneの動きを受け取る__
3. __音の出力の実装__
4. __音を連続で出せるようにする__
5. __ボタンを押した時のブォォンッ__

# 実装

さっそく作っていきましょう！

## Xcode新規プロジェクトを作成する

今回はSingle View Applicationで作っていきます。

## 事前準備

デフォルトのViewControllerファイルにCoreMotion, AVFoundationを導入し、適当なボタンを作ります。
以下、ViewController.swiftファイルで作業...

```swift:ViewController.swift
import UIKit
import CoreMotion   
import AVFoundation 

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func setUp() {
		setButton()
    }
    
    func setButton() {
        let button = UIButton()
        button.frame.size = CGSize(44, 44)
        button.center = view.center
        button.setTitle("Tap", forState: .Normal)
        button.setTitleColor(UIColor.blueColor(), forState: .Normal)
        button.setTitleColor(UIColor.redColor(), forState: .Selected)
        button.addTarget(self, action: "tapButton:", forControlEvents: .TouchUpInside)
        view.addSubview(button)
    }
    
    func tapButton(sender: UIButton) {
    	sender.selected = true
        print("tapButton")
    }
    
}
```

## 加速度を取得してみる

さて、まずはiPhoneを振ったというアクションを受け取るために加速度を取得してデバックエリアに表示してみましょう。

### CMMotionManagerのインスタンスを生成

```swift
    let motionManager = CMMotionManager()
```

_※インスタンスをメソッド内で生成するとうまく機能しません_
 
> IMPORTANT
> An app should create only a single instance of the CMMotionManager class. Multiple instances of this class can affect the rate at which data is received from the accelerometer and gyroscope.

https://developer.apple.com/library/ios/documentation/CoreMotion/Reference/CMMotionManager_Class/

### 加速度の取得＆出力

第１引数に作業するキューを指定します。今回はmainQueueと指定します(他に処理がある場合など、一般的にこういった作業をmainQueueに渡すことは推奨されていません）このクロージャ内の第１引数でx軸、y軸、z軸の加速度を、第２引数で加速度を取得している際のエラーを受け取ります。

```swift
    func getAccelerometer() {
        motionManager.accelerometerUpdateInterval = 1 / 100
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) { (accelerometerData: CMAccelerometerData?, error: NSError?) in
            print(accelerometerData)
        }
    } 
```

データを取得更新する頻度を指定します。

```swift
motionManager.accelerometerUpdateInterval = 1 / 100
```

どうでしょうか・・・デバックエリアがものすごい勢いでスクロールしていることでしょう・・・

## 合成加速度を取得

今回はこの合成加速度がある値以上になった場合に音を出すような仕組みを作ります。これによりiPhoneを動かす速さによって音を出す機能が実現できます。またiPhoneを振る速さで場合分け・・・なんて考えたらいろんなアイデアが思い浮かびますね！

合成加速度は

![ds_synthe_equ1.gif](https://qiita-image-store.s3.amazonaws.com/0/77478/a5a20916-1748-fff0-3d67-989ee21bac69.gif "ds_synthe_equ1.gif")

上記の式で求められますが、今回は簡単にこの式の平方根を除いた式を使います。
また、これにより求められる値が5以上になったときに音を出すとします

```swift
func getAccelerometer() {
        motionManager.accelerometerUpdateInterval = 1 / 30
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) { (accelerometerData: CMAccelerometerData?, error: NSError?) in
        
        	guard error == nil else {
                return
            }
            
            let x = accelerometerData!.acceleration.x	//　x軸の加速度
            let y = accelerometerData!.acceleration.y	//　y軸の加速度
            let z = accelerometerData!.acceleration.z	//　z軸の加速度
            let synthetic = (x * x) + (y * y) + (z * z)	//　合成加速度
            
            if synthetic >= 5 {
                print("sound")
            }
        }
    }
```

## 音を出力する

次に音を出力してみましょう。
まずはAVAudioPlayerのインスタンスを生成しましょう。メソッドをまたいで使用するため、メソッドの外に定義しましょう。

```swift
 var audioPlayer = AVAudioPlayer()
```

次はいよいよ音のセットです。今回は[こちら](http://taira-komori.jpn.org/sf01.html)から音をダウンロードして使いました。setSoundメソッドはviewDidLoadで呼び出しましょう。

```swift
func setSound() {
        let soundData = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("light_saber3", ofType: "mp3")!)	//NSBundleを使用して音声ファイルを取得
        audioPlayer = try! AVAudioPlayer(contentsOfURL: soundData)
        audioPlayer.prepareToPlay()
    }
```

では音を実際に出す処理を書きましょう！

```swift
func getAccelerometer() {
        motionManager.accelerometerUpdateInterval = 1 / 100
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) { (accelerometerData: CMAccelerometerData?, error: NSError?) in
        
        	guard error == nil else {
                return
            }
            
            let x = accelerometerData!.acceleration.x
            let y = accelerometerData!.acceleration.y
            let z = accelerometerData!.acceleration.z
            let synthetic = (x * x) + (y * y) + (z * z)
            
            if synthetic >= 5 {
                self.audioPlayer.play()
            }
        }
    }
```



試しにiPhoneを振ってみましょう！
どうでしょう・・・ジェダイの気分を味わえましたか・・・

## 音を連続で出力する

今のままでは一定のリズムでしか音が出ません。僕らはリズミカルセーバーを作ろうとしてるのではありません。僕らが目指してるのは・・・
さて、振る度に音を出すためにgetAccelerometerメソッドを以下のように書き換えます。

```swift
func getAccelerometer() {
        var preBool = false
        var postBool = false
        motionManager.accelerometerUpdateInterval = 1 / 5
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()) { (accelerometerData: CMAccelerometerData?, error: NSError?) in
        
            guard error == nil else {
                return
            }
            
            let x = accelerometerData!.acceleration.x
            let y = accelerometerData!.acceleration.y
            let z = accelerometerData!.acceleration.z
            let synthetic = (x * x) + (y * y) + (z * z)

            if preBool {
                postBool = true
            }
            
            if !postBool && synthetic >= 6 {
                self.audioPlayer.currentTime = 0	// 再生中の音を止める
                self.audioPlayer.play()

                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))	//バイブ
                
                preBool = true
            }
            
            if postBool && synthetic >= 6 {
                self.audioPlayer.currentTime = 0	// 再生中の音を止める
                self.audioPlayer.play()
                
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))	//バイブ
                
                postBool = false
                preBool = false
            }
        }
    }

```

この二つの真偽値を使って”振った”という動作を判断しています。

```swift
var preBool = false
var postBool = false
```

ついでに振る度に振動させてみましょう。
AudioToolboxを導入し・・・

```swift
import AudioToolbox
```
       
たったこれだけ！

```swift
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
```

そして更新周期、音出力の閾値を微調整して・・・

かなりいい感触になってきたはずです・・・！

## ボタンを押した時の効果音

さてそれでは臥龍点睛といきましょう・・・
音の出力と同じ要領で実装します。
新たにAVAudioPlayerのインスタンスを生成し、音データをセットしボタンのアクションに追加しましょう！

```swift
var startAudioPlayer = AVAudioPlayer()
```

```swift
func setStartSound() {
        let soundData = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("electric_chain", ofType: "mp3")!)
        startAudioPlayer = try! AVAudioPlayer(contentsOfURL: soundData)
        startAudioPlayer.prepareToPlay()

```

ちなみに新たにインスタンスを生成せず、初めに生成したAVAudioPlayerインスタンスの中身を入れ替える形で実装するとうまくいきませんでした。おそらく音を出す処理の直後にインスタンスの中身が入れ替わっているからか・・・

```swift
// 以下のようにすると失敗する
    func tapButton(sender: UIButton) {
        let soundData = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("electric_chain", ofType: "mp3")!)
        audioPlayer = try! AVAudioPlayer(contentsOfURL: soundData)
        audioPlayer.play()
        setSound()
        getAccelerometer()
    }
```

以上で完成です！！音量をMAXにして（サイレント解除も忘れず）振り回しましょう！！！！
<hr>

さて・・・あなたの手元にあるサーベルは何色か・・・



__*May the force be with you*__