Grani Engineering Blog

株式会社グラニはC#を中心として、ASP.NET、Unity、VR開発を行っています。

iOSで音楽を聴いているときにゲームの音楽を止める

こんにちは、開発部の@mayukiです。

今回はUnityでのゲーム音楽の再生とバックグラウンドアプリケーションによる音楽の再生のお話です。

ゲームの音楽とバックグラウンドの音楽、二つの音楽はなぜぶつかり合うのか

ゲームではゲーム固有の音楽が流れるのが一般的ですが、それとは別にスマートフォンを音楽プレイヤー代わりとして使うシーンが多々あります。iPhoneをお持ちの方は特にそういった使い方が多いでしょう。

ところがそれらの状況は同時に起こり、そのままでは両方の音楽が重なって結果どっちもまともに聞こえないという問題が発生することがしばしばです。特にスマートフォンのゲームは一回の滞在時間が短い場合もあり、そのような状況が発生しやすいでしょう。

対応方針

二つの音楽がぶつかり合ったとき、何とか救う方法はないかそもそもどうすればいいのか…を考えてみます。

  • 上記のとおり、スマートフォンのゲームは一時的に起動される機会が多い
  • 音楽を聴いているままゲームを起動したということは音楽を聴いていたいという可能性が高い
  • ゲームの音量設定を手動でミュートにするよりもバックグラウンドの音楽を止める操作のほうが簡単
    • プレイヤーがゲーム内の音量を上げたり下げたりするのはiOSのコントロールセンターから音楽を操作する簡単さには勝てない

といったことからゲーム側がバックグラウンドのアプリケーションによる音楽再生を尊重したほうがよさそうにおもえます。実際この時の挙動のおすすめはAppleのプログラミングガイドにも記載されています。

ユーザがアプリケーションを起動したとき、既に音声が再生中かも知れません。たとえばその時点で、「ミュージック(Music)」で音楽を再生していた、Safariで音声ストリーミングをしていた、などが考えられます。起動したのがゲームであれば、これは特に重要です。多くのゲームがサウンドトラックや効果音を活用しています。『iOS Human Interface Guidelines』の「Sound」で述べているように、ゲームの場合、その効果音は再生しつつ、起動前に再生していた音声もそのまま再生されるものとユーザは想定しています。 アプリケーションを起動する際、otherAudioPlayingプロパティで、音声が再生中であるかどうか調べてください。他の音声が再生中であれば、ゲーム側のサウンドトラックを無音にし、 AVAudioSessionCategorySoloAmbientカテゴリを指定します。カテゴリについて詳しくは、“カテゴリの取り扱い”(16 ページ)を参照してください。

「バックグラウンドのアプリケーションが音楽を流していたら、ゲーム中のBGMはミュートする(SEはそのまま)」のがよいということですね。ボイスやSEを止めるか止めないかはアプリ次第ですね。

なおこのパターンのデメリットとしてバックグラウンドの音楽とゲームの音楽を重ねられない点がありますが、その挙動で得するパターンはほとんどないので気にしなくてよいと思います。

実装

ではUnityで「バックグラウンドのアプリケーションが音楽を流していたら、ゲーム中のBGMはミュートする(SEはそのまま)」にするための実装をしましょう。

まずゲームBGMのミュートは大抵ゲームのシステムにある何らかの方法(マスターボリューム管理など)でできるようになっているかと思います。つまり「バックグラウンドのアプリケーションが音楽を流しているかどうか」さえ取得できれば、あとはそれに応じてボリュームをコントロールしてあげればよいわけです。

バックグラウンドで音楽が流れているかどうかに関しては、iOSでは先ほど引用したガイドにAVAudioSessionのotherAudioPlayingプロパティを確認してくださいと書いてありますが、iOS 8以降ではAVAudioSessionのsecondaryAudioShouldBeSilencedHintプロパティを利用することが推奨されています。secondaryAudioShouldBeSilencedHintプロパティはセカンダリオーディオ、つまり音楽再生アプリのようなプライマリではないもの(AVAudioSessionCategoryAmbient)はミュートになるべきかどうかを返します。ともあれこれが使えそうです。

Objective-C

チェックすべきものがわかったところで、iOSのAPIをUnityから直接呼び出すことはできないのでObjective-Cのコードを書いてあげます。

#import <AVFoundation/AVFoundation.h>

extern "C" {
    unsigned int avAudioSession_secondaryAudioShouldBeSilencedHint() {
        return [[AVAudioSession sharedInstance] secondaryAudioShouldBeSilencedHint];
    }
}

と言ってもAVAudioSessionsharedInstanceからsecondaryAudioShouldBeSilencedHintを取得したものをそのまま返すだけです。お手軽。AVAudioSession.mmなどの名前でPlugins/iOSなどに保存しておきます。

C# (Unity)

そしてC#での呼び出し側です。これも単純に先ほど書いた関数を呼び出すだけです。

using System;
using System.Runtime.InteropServices;

#if UNITY_IPHONE
public static class AVAudioSession
{
    [DllImport("__Internal", EntryPoint = "avAudioSession_secondaryAudioShouldBeSilencedHint")]
    private extern static int _SecondaryAudioShouldBeSilencedHint();

    /// <summary>
    /// セカンダリオーディオ(ゲームのBGMなど)をオフにすべきかどうかを返します。
    /// </summary>
    public static bool SecondaryAudioShouldBeSilencedHint
    {
        get { return _SecondaryAudioShouldBeSilencedHint() == 1; }
    }
}
#endif

あとはこれを監視して、プロパティの変更に応じて処理してあげればよいでしょう。毎フレーム見るほどでもないし呼び出し負荷ももったいないのでタイマーぐらいでちょうどいいかもしれません。ここはUniRxでちょちょいとやるとベンリですね。

Observable.Interval(TimeSpan.FromSecond(1))
    .Select(_ => AVAudioSession.SecondaryAudioShouldBeSilencedHint)
    .DistinctUntilChanged()
    .Subscribe(x =>
    {
        if (x)
        {
            // ミュートする処理をここに書く
        }
        else
        {
            // ミュート解除する処理をここに書く
        }
    })
    .AddTo(this);

まとめ

こういった普段スマートフォンを利用している中でゲームが邪魔にならない、ということはユーザーの体験に地味によい効果があると思うので実装をおすすめします。