【入門】iOS アプリ開発　#3【Sound を再生する】

# Sound

今回はゲーム中のサウンドを流す処理を作成したい。SpriteKit は簡単なサウンド制御も提供している。「パックマン」程度であれば、この範囲で実現することができそうだ。

サウンドは効果音(Sound Effect) と BGM(BackGround Music) がある。この２種類を実現するクラスを作成する。

# Sound Manager class の作成

効果音を再生する API は、

```swift
playSE(.EatDot)
``` 

とする。
 
BGMを再生する API は、
 
```swift
playBGM(.BgmPower)
``` 
 
とする。

このような API を持つ、次の CgSoundManager クラスを作成する。

#CgSoundManager class

```swift
/// Sound management class plays sound with SpriteKit.
class CgSoundManager {

    // Kind of sound items to play back.
    enum EnKindOfSound: Int {
        case EatDot = 0
        case EatFruit
        // ・・・略・・・
        case Intermission
    }

    // List of sound files to load.
    private let table_urls: [[(resourceName: String, typeName: String, interval: Int)]] = [
        [ ("16_pacman_eatdot_256ms", "wav", 256) ],
        [ ("16_pacman_eatfruit_438ms", "wav", 438) ],
        // ・・・略・・・
        [ ("16_pacman_intermission_5200ms", "wav", 5200) ]
    ]
    private var view: SKScene?
    private var actions: [SKAction] = []
    private var table_playingTime: [Int] = []

    // Adjustment time for processing to play sound.
    private let triggerThresholdTime: Int = 16 //ms

    /// Create and initialize a sound manager object.
    /// - Parameters:
    ///   - view: SKScene object that organizes all of the active SpriteKit content.
    init(view: SKScene) {
        self.view = view
        table_playingTime = Array<Int>(repeating: 0, count: table_urls.count)

        for t in table_urls {
            appendSoundResource(resourceName: t[0].resourceName, typeName: t[0].typeName)
        }
        // ・・・略・・・
    }
    
    /// Append sound resources to SpriteKit.
    /// - Parameters:
    ///   - resourceName: File name for sound resource.
    ///   - typeName: Type name for sound resource.
    private func appendSoundResource(resourceName: String, typeName: String) {
        let fileName = resourceName+"."+typeName
        let sound: SKAction = SKAction.playSoundFileNamed(fileName, waitForCompletion: false)
        actions.append(sound)
    }

    /// Play back a specified sound.
    /// If the specified item is playing back, it will not be played back.
    /// - Parameter number: Kind of sound items to play back.
    func playSE(_ number: EnKindOfSound) {
        guard soundEnabled && number.rawValue < actions.count else { return }

        let _number = number.rawValue
        if table_playingTime[_number] <= triggerThresholdTime {
            let table = table_urls[_number]
            table_playingTime[_number] = table[0].interval
            view?.run(actions[_number])
        }
    }
```

enum で定義された値と、再生するサウンド・ファイルの table_urls テーブルを対応させておく。

クラス初期化時に、これらのファイルを SKActionのオブジェクトとして生成しておく。

またサウンド・ファイルの再生時間を値として管理しておく。これは同じ効果音が複数重ならないようにするためで、ある効果音が再生中に同じものを再生する場合は再生しないようにする。

また BGM を再生するときには、指定したサウンド・ファイルの再生が終了したことを知り、同じものを繰り返し再生するために使用する。自動で開始するためのトリガーは、updateメソッド内で行う。

#サウンド・ファイル

今回のパックマンに使うサウンド・ファイルは 15個で、フォーマットは WAV形式、16bit、モノラル、サンプリング・レート 22050Hz で作成した。

8bit だと、少し音がこもる感じになるので、16bit にして処理の重さを気にしてサンプリング・レートを 22050Hzへ落とした。

再生でプチプチ・ノイズが出ないように、先頭は FadeIn、終端は FadeOut処理をしておく。

サウンド・ファイルの再生時間は編集ツールで確認しておく。

<img src="https://github.com/Kikutada/0002_SoundTest/blob/master/images/Wave01.png?raw=true" width=480>

[Sound Files]
* 16_pacman_eatdot_256ms.wav    11,372bytes
* 16_pacman_eatfruit_438ms.wav	 19,360bytes
* 16_pacman_eatghost_544ms.wav	 24,040bytes
* 16_pacman_miss_1536ms.wav	 67,788bytes
* 16_pacman_extrapac_1952ms.wav	 86,166bytes
* 16_credit_224ms.wav	  9,634bytes
* 16_BGM_normal_400ms.wav	 17,852bytes
* 16_BGM_power_400ms.wav	 17,750bytes
* 16_BGM_return_528ms.wav	23,366bytes
* 16_BGM_spurt1_352ms.wav	 15,592bytes
* 16_BGM_spurt2_320ms.wav	 14,212bytes
* 16_BGM_spurt3_592ms.wav	 26,272bytes
* 16_BGM_spurt4_512ms.wav	 23,018bytes
* 16_pacman_beginning_4224ms.wav	187,054bytes
* 16_pacman_intermission_5200ms.wav	229,388bytes

#テスト・プログラム

[GitHub](https://github.com/Kikutada/0002_SoundTest) に公開しているテスト・プログラムを実行すると、以下のような画面が表示され、BGMが5秒毎に切り替わる。スクリーンをタッチすると効果音が再生される。

<img src="https://github.com/Kikutada/0002_SoundTest/blob/master/images/0002_soundTest.png?raw=true" width=288>

Sound Manager クラスは、SoundManager.swift ファイルにコーディングしている。コメント入れて 200行未満となった。

```swift
class GameScene: SKScene {
    
    private var sound: CgSoundManager!
    
    override func didMove(to view: SKView) {

        // Create and reset sound object.
        sound = CgSoundManager(view: self)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        sound.playSE(.EatDot)
    }
    
    private let bgm: [CgSoundManager.EnKindOfSound] = [.BgmNormal, .BgmSpurt1, .BgmSpurt2, .BgmPower, .BgmReturn]
    private var bgmIndex: Int = 0
    private var bgmTime: Int = 0

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered

        // Play back BGM.
        if bgmTime == 0 {
            bgmTime = 16*60*5  // 5s
            sound.playBGM(bgm[bgmIndex])
            bgmIndex += 1
            if bgmIndex >= bgm.count {
                bgmIndex = 0
            }
        } else {
            bgmTime -= 16
        }

        // Update sound manager.
        sound.update(interval: 16 /* ms */)
    }
}
```

CgSoundManagerクラスは、SKViewオブジェクトをパラメータとして、オブジェクトを生成する。

SKScene からオーバーライドした touchesEndedイベントのメソッドで、SEを再生する。

また、同様に updateイベントのメソッドで、5秒毎にBGMを切り替えて再生する。

#参考

* [iOS Swift Game Development Cookbook: Simple Solutions for Game Development Problems](https://www.amazon.co.jp/dp/B07HYGFFW8/ref=cm_sw_em_r_mt_dp_IhRmFb5SYAG9Q)
* [【Swift】SpriteKitの使い方。バックミュージックや効果音を鳴らす。(Swift 2.2、Xcode 7.3](https://hajihaji-lemon.com/swift/spritekit-playsoundfilenamed/)

#次の記事
[【入門】iOS アプリ開発　#4【アーキテクチャの設計】]
(https://qiita.com/KIKU_CHU/items/7e273653e95296485c3f)