2015年3月20日金曜日

効果音を鳴らす

正解したら「ピンポ〜ン」、不正解なら「ブッ!」といった簡単なSEを鳴らしたい場合。
21世紀にもなったのに、やり方がちょっととっても面倒なのでまとめる。
なお、Swift 2.0もわかった点だけ後述

鳴らすためのサウンドファイルはどっかから用意すること。GarageBandあたりで作ってもよし。

ファイルをXcodeにドラッグしてプロジェクトに登録するまでは画像なんかと一緒。
しかし次が重要
ドラッグして追加した場合、Build PhasesのCopy Bundle Resourcesで、「+」ボタンを押してサウンドファイルを指定してやらないと、コード中でpathが掴めなくてエラーになってしまう。
ただし、ファイルメニューの「Add Files to "プロジェクト名"」で追加した場合は自動的にCopy Bundle Resourcesに追加されるので、不要のようだ。こっちの方が楽だな。

サウンドファイルを選んでAddを押す。

これでちゃんとサウンドファイルが認識されるようになる。

必要なFrameworkはAVFoundation.frameworkなので、インポートしとくこと。


ヘッダーファイルのinterface部でAVAudioPlayerDelegateの設定が必要。
@interface GameViewController : UIViewController <AVAudioPlayerDelegate>

実装ファイルの方でも工夫が必要。
サウンド用のインスタンス変数を用意。(後述)
NSMutableArray *soundArray; //SEのインスタンスを一時的にしまっておく
AVAudioPlayer *soundPlayer; //SE再生用

AVAudioPlayerってので再生するのだが、ローカル変数では、ARCで動かすせいで、メソッドを抜けるとすぐに変数が解放されてしまうため、音を鳴らすところまでいってくれないのだ。
ということでサウンドのインスタンスはインスタンス変数にしまって保持するのだが、一つのインスタンス変数だけでは、今度は前の音が終了するまで次の音が鳴らなくなってしまう。ギギギ…(-_-;)
そこで、NSMutableArrayに同時に鳴らす数だけのインスタンス変数をしまって保持するようにする。
そして音が鳴り終わった時に呼ばれるありがたいdelegateメソッドによって、不要なインスタンス変数をNSMutableArrayから消すのだ。

//サウンドの再生
- (void)playSoundWithResource:(NSString *)resource ofType:(NSString *)type{
    NSString *path = [[NSBundle mainBundle]pathForResource:resource ofType:type];
    
    soundPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL fileURLWithPath:path] error:nil];
    soundPlayer.delegate = self;
    soundPlayer.volume = 0.4;
    soundPlayer.numberOfLoops = 0; //連続再生回数。0で1回、1で2回…、-1で無限ループ
    [soundPlayer prepareToPlay]; //あらかじめメモリに展開するなど、すぐ再生できるよう準備
    [soundPlayer play];
    [soundArray insertObject:soundPlayer atIndex:0]; //ARCでメソッド終了後に消されないようにインスタンス変数の配列に格納
}

//鳴り終わったら呼ばれるdelegateメソッド
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    //鳴り終わったSEのインスタンスを消す
    [soundArray removeObject:player];
}

上記のplaySoundWithResource:〜メソッドは自作なので、実際に音を鳴らす際は、どっかから以下のように呼び出す。
[self playSoundWithResource:@"correct" ofType:@"m4a"];

いちいちめんどくせえだろ? 8ビットパソコンでBEEPとだけ書けば良かったものが、この手間の数。世の中間違っとるよね。Swiftではどうなのか知らんけど。


BGMを鳴らす場合

ゲームなんかでBGMを鳴らしっぱなしにする際は、
soundPlayer.numberOfLoops = -1;
にしてやりゃいい。

BGMをGarageBandで作ると無音部分が追加される

共有でiTunesに書き出して、そのファイルを使うことになるわけだけど、共有メニューからiTunesやディスクに書き出すと、曲の最後に1秒程度の無音部分が勝手に追加されてしまう。
おかげで、BGM2巡目に入る前に音が途切れることとなり甚だよろしくない。
音の切り貼りができるソフト(PRO版のQuickTime7 Playerとか)を使って残響音のところを消してしまえば問題はないが、めんどくさいし、QTPlayerだと波形が見られないからちょうどいいところで削除できなかったり、AIFFかWaveなどでしか保存できないみたい。

40秒という制限はあるが、GarageBandの共有から着信音として書き出すと、連続再生に向いた、無音部分のないものが書き出せる。

なお、Xcodeにとっかえひっかえ曲のファイルを入れて試していたら、新しく追加した無音部分なしファイルが認識されず、差し替えたはずの無音部分付きファイルが再生されることがあって、やっぱりダメなのかと迷ったことがあった。
プロダクトのクリーン、Xcode再起動で直ったが、こういうことはXcodeではよくあるね。

Swift 2.0の場合

・AVFoundation.frameworkのインポートは必要
import AVFoundation

・ヘッダー部分でのAVAudioPlayerDelegateの記述は必要
class ViewController: UIViewController, AVAudioPlayerDelegate {

・インスタンス変数を定義しておくのも必要
    var soundPlayer:AVAudioPlayer? //SE再生用

・Swift 2.0の仕様変更でエラー処理の仕方が変わったため、AVAudioPlayer使用時に
do 〜 catch内にtry文を書く必要がある
よく知らんのだけど、try後に書いたメソッドでエラーが発生したら、catch後の処理を実行するのだね。
    func playSoundWithResource(resource:String, type:String) {
        let path = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(resource, ofType: type)!)
        do {
            soundPlayer = try AVAudioPlayer(contentsOfURL: path)
            soundPlayer!.delegate = self
            soundPlayer!.volume = 0.4
            soundPlayer!.numberOfLoops = 0 //0で1回再生
            soundPlayer!.prepareToPlay()
            soundPlayer!.play()
        } catch {
            print("sound再生でエラー発生")
        } 
    }

tryのところは以下でもいいみたい。
try soundPlayer = AVAudioPlayer(contentsOfURL: path)

まあ後はテキトーに調べて書こう。

0 件のコメント:

コメントを投稿