UnityからiOS/Android端末の音量設定を操作する方法

Unity側から端末側の音量を取得/設定する方法を試してみました。
[今回のサンプル実装はこちら](https://github.com/satanabe1/soundvolumeplugin-sample/tree/main/Assets/Plugins) にコミットしてあります。

# はじめに
2023年現在、Unity側ではスマートフォン端末の端末音量を操作するC# APIが提供されていません。
したがって自前でネイティブの処理を実装する必要があります。
この記事では、iOSとAndroidのネイティブプラグイン側の実装例と、そのプラグインとC#との繋ぎ込み方法を紹介します。

# iOS(obj-c)の場合

Unityプロジェクト内に.mmや.hを配置し、インポート設定からiOSにチェックを入れることでiOS用のPluginとして扱われます。

![スクリーンショット 2023-12-04 午後1.53.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5973/8f116f94-2973-6caa-1c36-571846375d23.png)
このインポート設定では依存Frameworkを有効にすることもできます。ここで有効にしておくことで、自前でPostprocessでxcode projectを改変するような処理を用意する必要がなくなります。
![スクリーンショット 2023-12-04 午後1.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5973/983d052d-4f4b-d3e1-b0cc-32b58b2f05ae.png)

今回、音量設定Pluginということで、Rarely used frameworkdsからMediaPlayerを有効にします。
![スクリーンショット 2023-12-04 午後1.53.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5973/e500008b-4b92-eb3c-e9f8-36f9f07ef75e.png)


## 音量の取得
音量の取得は単純で、MediaPlayer.hをimportしておけば、呼ぶべきAPI自体は `AVAudioSession.sharedInstance.outputVolume` の1行で済みます。
もしくは、`[MPMusicPlayerController applicationMusicPlayer].volume` でもほぼ同等のものが取得できます。
以下にサンプルを載せます。

```objc
#import <MediaPlayer/MediaPlayer.h>

// unityから呼べるようにするための宣言
extern "C" {
    float SoundVolumePlugin_getVolume(); // 音量取得用
    void SoundVolumePlugin_setVolume(const float volume); // 音量設定用
}

float SoundVolumePlugin_getVolume() {
    return AVAudioSession.sharedInstance.outputVolume;
    // return [MPMusicPlayerController applicationMusicPlayer].volume; でも可
}
```

## 音量の設定
音量設定についても、
- `[[MPMusicPlayerController applicationMusicPlayer] setVolume:volume];`
- `[[MPMusicPlayerController systemMusicPlayer] setValue:@(volume) forKey:@"volumePrivate"];`

など1行で実装することもできますが、前者は非推奨APIであり、後者は現状では非推奨でこそありませんが、privateアクセスが必要など、お行儀の良い実装ではありません。
非推奨APIやprivateを避ける実装方法の一つとして、以下のようなMPVolumeViewを経由する方法があります。

```objc
void SoundVolumePlugin_setVolume(const float volume) {
    // 一番単純な方法。しかし、非推奨API
    // [[MPMusicPlayerController applicationMusicPlayer] setVolume:volume];
    // 割と単純でかつ、コンパイル時、非推奨警告が出なくなる方法。しかしprivateアクセス
    // [[MPMusicPlayerController systemMusicPlayer] setValue:@(volume) forKey:@"volumePrivate"];
    // そこそこ正当だけど一番ごちゃつく
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    for(UIView *view in [volumeView subviews])
    {
        if([NSStringFromClass(view.class) isEqualToString:@"MPVolumeSlider"] )
        {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                UISlider *volumeSlider = (UISlider *)view;
                volumeSlider.value = MIN(MAX(0, volume), 1);
            });
            return;
        }
    }
}
```

# Android
Androidの場合、Unityプロジェクト内に.javaや.ktを配置することでネイティブプラグインとして扱うことができます。

![スクリーンショット 2023-12-04 午後2.09.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5973/d4fa003f-066e-1f23-fa5c-2b69d3149a8e.png)

## 音量の取得
Androidの端末音量はAudioManagerから取得できます。
ただしこの音量は0.0~1.0の値ではなく、0~15などの整数値になっています。そのため、最大音量をAudioManagerから取得して、0.0~1.0の間に正規化するなどした方が扱いやすくなります。


```java
// AudioManagerを拾ってくる
private static AudioManager getAudioManager() {
    return (AudioManager) UnityPlayer.currentActivity.getSystemService(Context.AUDIO_SERVICE);
}

// 音量を取得する
public static float getVolume() {
    AudioManager audioManager = getAudioManager();
    // 現在音量(getStreamVolume)を最大音量(getStreamMaxVolume)で割る
    return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
            / (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
}
```

## 音量の設定
音量の設定についても、取得時と同様に0.0~1.0の範囲の指定ではないため、最大音量を取得した上で、適切な整数値を算出する必要があります。
```java
// 音量を設定
public static void setVolume(float volume) {
    AudioManager audioManager = getAudioManager();
    volume = Math.min(1.0f, Math.max(0.0f, volume));
    // 0.0~1.0に正規化されている音量(volume)に最大音量(getStreamMaxVolume)を掛ける
    volume = volume * audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, Math.round(volume), 0);
}
```

# Unityとの繋ぎこみ
iOS用とAndroid用の固有の実装は `UNITY_IOS` や `UNITY_ANDROID` で囲むのが鉄板です。
それぞれの実装例を示します。

## iOS
iOSのネイティブ呼び出しは、C#側とobjc側の名前を合わせた上で、DllImport属性とextern修飾子をつけることで実現できます。

```c#
#if UNITY_IOS
using System.Runtime.InteropServices;

partial class SoundVolumePlugin
{
    private const string DllName = "__Internal";

    [DllImport(DllName)]
    private static extern void SoundVolumePlugin_setVolume(float volume);

    [DllImport(DllName)]
    private static extern float SoundVolumePlugin_getVolume();
}
#endif
```

## Android
Androidのネイティブ呼び出しはUnityが用意しているAndroidJavaClassを利用することで実現できます。

```c#
#if UNITY_ANDROID
using UnityEngine;

partial class SoundVolumePlugin
{
    private const string PluginClassName = "jp.qualiarts.soundvolumeplugin.SoundVolumePlugin";

    private static void SoundVolumePlugin_setVolume(float volume)
    {
        using var plugin = new AndroidJavaClass(PluginClassName);
        plugin.CallStatic("setVolume", volume);
    }

    private static float SoundVolumePlugin_getVolume()
    {
        using var plugin = new AndroidJavaClass(PluginClassName);
        return plugin.CallStatic<float>("getVolume");
    }
}
#endif
```

## iOS/Android用API呼び出しの共通化
partialを利用することで、iOSとAndroidの実装を分けつつ、一つのクラスとして扱えるので、汚れがちなプラグイン周りの実装の整理に役立つかもしれません。

```c#
public static partial class SoundVolumePlugin
{
    public static void SetVolume(float volume)
    {
        SoundVolumePlugin_setVolume(volume);
    }

    public static float GetVolume()
    {
        return SoundVolumePlugin_getVolume();
    }
}
```

## 使い方

実際のプロジェクト側からの呼び出し方は以下のような感じになります。

```c#
using UnityEngine;
using UnityEngine.UI;

public class SampleVolumeController : MonoBehaviour
{
    public Slider volumeSlider;
    void Start() => volumeSlider.onValueChanged.AddListener(SoundVolumePlugin.SetVolume);
    void Update() => volumeSlider.SetValueWithoutNotify(SoundVolumePlugin.GetVolume());
}
```

以上、UnityからiOS/Android端末の音量設定を操作する方法でした。