【swift5】音楽の同時再生を簡単に実装する方法

# 音楽の同時再生をスマートに記述する方法

### 環境　
swift5.0.1
xcode 11.0

### 目次
[1. 前書き](#1前書き)
[2. 同時再生実現までの道のり](#2同時再生実現までの道のり)
　[音楽再生機能の付け足し](#音楽再生機能の付け足し) 
　[同時再生機能の付け足し](#同時再生機能の付け足し) 
　[同時再生機能の問題点](#同時再生機能の問題点) 
[3. まとめ](#3まとめ)

### 1.　前書き
アプリ上でBGMを流しながら特定の操作が行われた時に効果音を流すプログラムを作りたかった。
初期段階において、効果音が流れる際にBGMの再生が止まってしまう、という問題が浮上した。
その解決策として、複数の音楽ファイルを再生する方法が見つかったのだがコードの記述が美しくなかった。
そのため、よりスマートに当該機能を実現するべく、試行錯誤を繰り返した。

#### 目標
アプリ上でBGMを流しながら、特定の操作が行われた場合に効果音を流す。

#### 本投稿の意図
・同時再生を実現する方法の備忘録
・より詳しい人からより良い方法があればご教授願いたいな、、、と。


### 2.　同時再生実現までの道のり
元々のコードは、以下。
ここから、始める。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

```

#### 音楽再生機能の付け足し
まずは、兎にも角にも再生機能をつけなければ何も始まらない。
ググった結果、AVFoundationを用いることで音楽再生が可能となることがわかった。


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

class ViewController: UIViewController {
    // 音楽鳴らす用のインスタンスを複数生成
    var audioPlayer: AVAudioPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 音楽を流し始める
        playSound(name: "音楽ファイル名")
    }
}
// 以下、コピペ
extension ViewController: AVAudioPlayerDelegate {
    func playSound(name: String) {
        guard let path = Bundle.main.path(forResource: name, ofType: "mp3") else {
            print("音源ファイルが見つかりません")
            return
        }
        do {
            // AVAudioPlayerのインスタンス化
            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
            // AVAudioPlayerのデリゲートをセット
            audioPlayer.delegate = self
            // 音声の再生
            audioPlayer.play()
        } catch {
        }
    }
}


```

これにより、音楽再生機能が付け足された。

#### 同時再生機能の付け足し

上記のコードで、単純にplaySound()をもう一つ書くだけだと、2つ目の音楽の再生開始時に1つ目の音楽の再生が止まってしまう。

```swift:ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // 音楽を流し始める
        playSound(name: "音楽ファイル名")
        playSound(name: "音楽ファイル名その２")//この二つ目の音楽再生に伴い、一つ目の音楽の再生が停止されてしまう。
    }
       
```

調べたところによると、
・拡張子がcafでないと同時再生できない
・ファイル名にアンダーンバーがダメ

等々の結果が出てきたが、どれを試しても駄目であった。

そこでplaySound関数内をよく見てみると、AVAudioPlayerのインスタンスを一つ作っているだけだったので、これを複数作ればいいのではないか、と思い立った。
そこで、以下のようにextension内を変更し、関数の呼び出しも次のように変更した。　

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

class ViewController: UIViewController {
    // 音楽鳴らす用のインスタンスを複数生成
    var player1: AVAudioPlayer!
    var player2: AVAudioPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 音楽を流し始める
        // 別々のインスタンスを渡して実行する
        playSound(name: "音楽ファイル名",player: player1)
        playSound(name: "音楽ファイル名2",player: player2)
    }
}
// 以下、コピペ
extension ViewController: AVAudioPlayerDelegate {
    // インスタンスを引数とすることで、別々のインスタンスに対して音楽再生を実行できるのではないかと考えた
    func playSound(name: String, player: AVAudioPlayer!) {
        var audioPlayer:AVAudioPlayer! = player
        guard let path = Bundle.main.path(forResource: name, ofType: "mp3") else {
            print("音源ファイルが見つかりません")
            return
        }
        do {
            // AVAudioPlayerのインスタンス化
            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
            // AVAudioPlayerのデリゲートをセット
            audioPlayer.delegate = self
            // 音声の再生
            audioPlayer.play()
        } catch {
        }
    }
}

```

この書き方でxcode上でエラーは出なかったが、実行してみると一つ目の音楽が再生されずに動作が止まってしまう。
原因はわからないが、audioPlayerを関数内で定義する変数としてしまったからなのかもしれない。


#### 同時再生機能の問題点

しかしながら、関数外で定義したAVAudioPlayerのインスタンスに対して関数内で操作を行おうとすると、複数のインスタンスに対して操作を行う場合に、各インスタンスに対してplaySound関数で行ったことと同等の作業をする必要が出てくる。
一つの関数内でこれを達成しようとした場合、**任意のタイミングで効果音を鳴らすことはできなく**なる。
つまり、同時に**鳴り得る効果音全てに対して一つ一つ名前を変えて関数を定義**しなければいけなくなり、コードが煩雑になる。これは美しくない。
そこで、**新たに音楽再生クラスを作り**、クラスインスタンスを作るコードを加えるだけで同時再生を可能にできないか考えた。

```swift:Viewcontroller.swift

import AVFoundation
import UIKit

class ViewController: UIViewController, AVAudioPlayerDelegate {
    // 効果音鳴らす用のクラスのインスタンス
    var player1 =  player()
    var player2 =  player()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // クラスの関数を呼び出す
        player1.playSound(name: "音楽ファイル名")
        player2.playSound(name: "音楽ファイル名2")
        
        
}

// 同時に音楽を再生するために、関数と変数のセットを複数作れるよう、クラスとして定義する。
class player{
    var audioPlayer: AVAudioPlayer!
    func playSound(name: String) {
        guard let path = Bundle.main.path(forResource: name, ofType: "mp3") else {
            print("音源ファイルが見つかりません")
            return
        }
        do {
            // AVAudioPlayerのインスタンス化
            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
            // AVAudioPlayerのデリゲートをセット
            audioPlayer.delegate = self as? AVAudioPlayerDelegate
            // 音声の再生
            audioPlayer.play()
        } catch {
        }
    }
}


```


### 3.　まとめ

音楽再生機能を付け足し、任意のタイミングで複数の音楽を同時に鳴らす方法を考えた。
ググって出てきたやり方だと、作るAVAudioPlayerのインスタンスの数だけ音楽再生関数を作る必要があると思い、流石にそれはダサいだろうと。
そこでAVAudioPlayerインスタンスとplaySound関数をまとめたクラスを作り、クラスインスタンスを作るだけで任意のタイミングで同時に音を鳴らせる機能を実装した。

初めてAVFoundationを触ったので、全然わかってないです。より良い同時再生方法があればご教授いただけると嬉しいなと思い、記事を書かせてもらいました。