ガンズターン 公式サイト

楽しいことに、まじめです。 ——ガンズターンアプリ研究所公式サイト

AVFoundationで音を鳴らす方法(Objective-C、iOS)

Pocket

なんだかんだでサボり気味

毎日書くといっておきながら、1週間も放置してしまいました。
そこで気づいたのですが、そもそも毎日1記事は今のわたしにはちょっときびしい……。
中身のない記事であればいくらでも書けるのですが、そうするとただの日記になってしまいそうなんですよね。

それでも書き続けるのが大事なのかもしれないと思い、ネタ記事でもいいから何か書こう、と考えた日もありました。
ただ、そもそもなんでブログを始めたか、という時の自分の気持ちを思い出してみるに、ただの日記をここに公開するというのは目指すべき姿ではないな、と考え直しまして。

そりゃ、たまにはなんの意味もないネタ記事も書きたくなりますが、何事もバランスが大事、というわけで。

今後は、週に1回程度、きちんとテーマを決めた記事を、自分なりに情報を整理したうえで公開していければな、と思っているしだいです。

ブログ更新を減らすことで空いた時間(具体的には1日2〜3時間)は、すべてアプリ開発のほうに向けていきたいと思います。

てなわけで、前置きが長くなりました。
お久しぶりです。ガンズターンのRyosukeです。

本日は、iPhoneアプリで音を鳴らす際にわたしが利用している「AVFoundation.framework」について、使用方法等で気づいたことを記していきたいと思います。

1. AVFoundation.frameworkの導入方法

ここは、とくに注意すべき点はありませんね。
他のライブラリ導入と同じやり方で導入できます。
一応、手順のメモを書いておきます

  1. Xcodeの画面で、左ペインの最上段にあるプロジェクト名をクリック。
  2. 続いて真ん中のペインの最上段で「Build Phases」のタブをクリック。
  3. 「Link Binary With Libraries」を展開すると、最下段に「+」「−」の表示があるので、そこの「+」をクリック。
  4. 追加するライブラリを選ぶダイアログが出てくるので、「Search」となっているところに「AVFoundation」と入力。
  5. 「AVFoundation.framework」が表示されたら、それを選択して「Add」を押す。

上記手順で、プロジェクトに「AVFoundation.framework」が導入できました。
続いて、音声ファイルの再生方法を見ていきましょう。

2. m4aファイルの準備(任意)

(なんらかのこだわりがあって、m4a以外の形式(例えばwavのままなど)で再生したいという場合は、この手順を飛ばしてください)

個人的に、iPhoneアプリで使用する音声ファイルについては「m4a」ファイル1択だと考えています。
容量、音質、そして権利関係もろもろのめんどくささなどを考えると、m4aファイルが一番便利だからです。(mp3は、エンコード等に使用するライブラリによって権利関係がグレーな場合があるようです)
なんといっても、m4aファイルはwavファイルからの変換が楽チンです。
Appleが提供している(Macに標準装備されてる)「afconvert」というツールを使うと、ターミナルからのコマンド1行で圧縮できちゃうんです。

> cd (圧縮したいファイルの置かれたディレクトリパス)
> afconvert -f m4af (変換したいファイル名).wav

上記コマンドを、Mac上で起動したターミナルで叩くことで、元のwavファイルから10分の1ぐらいのサイズまで圧縮されたm4aファイルが出来上がります。
「afconvert」には他にも様々なオプションがあって、実に細やかな設定が可能です。
ただし、それらを使いこなすには音声圧縮に関する知識が別途必要とされてきますので、とくに音質にこだわらないゲームアプリを作るのであれば上の1コマンドを覚えておくだけで十分です。

m4aファイルの準備が完了したら、Xcodeの「ファイル追加」の手順で、作成した「m4a」ファイルをプロジェクトに追加します。

3. 実際に音を鳴らしてみる

任意のプロジェクトに「AVFoundation.framework」を利用した「音を鳴らす仕組み」を追加することを考えます。
(「1.」で説明したフレームワークの追加はすでに完了している前提)

まずしなければならないのは、「AVFoundation/AVFoundation.h」のインポートです。
以下の一文を、音を鳴らしたいクラスの実装部の先頭に追加してください。

#import <AVFoundation/AVFoundation.h>

インポート文を追加し終えたら、次にするのは、インスタンスの確保です。
音を鳴らしたい処理の最中に、以下のコードを加えてください。

// 再生したい楽曲を示すファイルURLを取得
// 「 ofType:@"m4a" 」の部分は再生したいファイル形式によって変わります
NSURL *file_url = [NSURL fileURLWithPath:
   [[NSBundle mainBundle] pathForResource:@"再生したいファイル名"
                                   ofType:@"m4a"]];

// AVAudioPlayer インスタンスの確保(この時点で音声データが読み込まれる)
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc]
                    initWithContentsOfURL:file_url
                                    error:&error];

if(! error) { // error == nil なら音声データの読み取り成功
    [player play];
}

以上のコードで、先ほど準備したm4aファイルを再生することができると思います。
ただ楽曲を再生したいだけなら、上記処理だけで問題ありません。
ただしゲームで利用する場合など、再生するたびにインスタンスを改めて確保していたのでは、実行速度に不安が残ります。

そのような場合には、以下のようにすると実行速度の面で有利になるはずです。(簡略化のためエラー処理は省略)

//AVAudioPlayerインスタンスをstatic変数として定義
static AVAudioPlayer *player = nil;

- (void) init {
    if(self = [super init]) {

        /* なんらかの、クラス自体の初期化処理 */

        /* クラス自体の初期化処理後にplayerの初期化 */
        NSURL *file_url = [NSURL fileURLWithPath:
                           [[NSBundle mainBundle]
                            pathForResource:@"再生したいファイル名"
                                     ofType:@"m4a"]];

        NSError *error = nil;
        player = [[AVAudioPlayer alloc] 
                  initWithContentsOfURL:file_url
                                  error:&error];
    }
}

//実際に音声を鳴らす処理
- (void) playSound {
    [player play];
}

上記のように、初期化処理内であらかじめインスタンスの確保をしておけば、実際に音を鳴らすときは「 [player play] 」の一行だけで済むようになります。
ここでは表記を簡略化するためにstaticな変数を用いましたが、もちろんクラスのメンバ変数に代入しても問題ないです。

4. メモリリークに注意しましょう

基本的なことですが、わたし自身がよく理解していなかったので、戒めを込めて記事に書きます。

手動でのメモリカウンタ方式をとっている場合に、以下のようなプログラムを書くと即メモリリークにつながります。

AVAudioPlayer *player = nil;
NSError *error = nil;

//効果音1(file_url_1)の読み込み
player = [[[AVAudioPlayer alloc] 
          initWithContentsOfURL:file_url_1
                          error:&error] retain];

// 効果音1の再生
[player play];

//効果音1の再生を終えたのち、メモリ解放
[player release];

//効果音2(file_url_2)の読み込み
player = [[[AVAudioPlayer alloc] 
          initWithContentsOfURL:file_url_2
                          error:&error] retain];

// 効果音2の再生
[player play];

//効果音2の再生を終えたのち、メモリ解放
[player release];

どこがよくないのか、すぐにわかりますか?

普通に考えると、同じ変数に対して2回も「init」メソッドを使って代入しているのがよくない気がしますが、それにしても代入する直前に「release」しているので問題なさそうな気がします。

けれど、これが実は問題大有りなんですね。

「 [initWithContentsOfURL: error:] 」というメソッド。
これ、どうやら内部的にretainカウントを+1した状態でオブジェクトを返却してきてるみたいなんですよ。
それに対して、自分がさらに外側で「retain」しちゃってるものだから、インスタンス生成した時点ですでにretainカウントが+2されてしまっていたんです。

けれど、対応する「release」は1回きりなので、効果音2の読み込みが終わった時点においても効果音1のために確保したメモリは解放されずに残ってしまっているという……。

こいつはよろしくないですね。

じゃあどうするか。

ようは「retainカウント」を余計にしなければいいんです。
(「release」を2回呼び出すのはめちゃくちゃ気持ち悪いので)

結果、現在のところ自分は以下のように処理しています。

AVAudioPlayer *player = nil;
NSError *error = nil;

//効果音1(file_url_1)の読み込み(retainしない!)
player = [[AVAudioPlayer alloc] 
          initWithContentsOfURL:file_url_1
                          error:&error];

// 効果音1の再生
[player play];

//効果音1の再生を終えたのち、メモリ解放
[player release];
player = nil; //念のためnil代入

//効果音2(file_url_2)の読み込み(retainしない!)
player = [[AVAudioPlayer alloc] 
          initWithContentsOfURL:file_url_2
                          error:&error];

// 効果音2の再生
[player play];

//効果音2の再生を終えたのち、メモリ解放
[player release];
player = nil; //念のためnil代入

ゲームにおけるBGM再生など、場面によって再生したいファイルが異なる時に、同じ変数名を何度も使いまわして、音声ファイルだけ差し替えることがあると思います。
そんな時は、以下のことに注意するとメモリリークが防げると考えます。

  1. 「 [initWithContentsOfURL: error:] 」を使った時は「retain」はしない。
  2. 新しい音声ファイルに差し替える前に「release」して「nil」代入する。

例えば、与えられたファイル名の音声ファイルを再生する処理は以下のようになります。

- (void) playSound:(NSString*)filePath {
    static AVAudioPlayer *player = nil;
    NSURL *file_url = [NSURL fileURLWithPath:
                           [[NSBundle mainBundle]
                            pathForResource:filePath
                                     ofType:@"m4a"]];
    // init する前にすでにメモリが確保されてるか確認
    // 確保されていたらメモリを解放してから init を行う
    if(player) {
        [player release];
        player = nil;
    }

    player = [[AVAudioPlayer alloc] 
          initWithContentsOfURL:file_url
                          error:&error];

    [player play];
}

このまんま利用すると同一ファイルの繰り返し再生時にもインスタンスが改めて確保されてしまい実行速度に影響が出ますが、そのあたりは各自で工夫してください。(丸投げ笑)

んでは、今日はこの辺で。
ちょっと的を絞った記事にしすぎたかな、と書き終えてから反省しているRyosukeでした! m(_ _)m

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

トラックバックURL: http://gunsturn.com/2015/05/26/objective-c_use_avfoundation/trackback/