【CRI ADX2】タイムストレッチ機能を使った音声の倍速再生


この記事は、[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap) 12/11の記事です。
昨日の記事は、[@kojima_akira](https://qiita.com/kojima_akira)さんによる「億単位のレコード数のテーブルを含むDBをAWS Database Migration Serviceを使って移行した際のTIPS」でした。

# 概要
Unityで音声の再生速度を音の高さは変えずに変化させたい場合、どうすればよいでしょうか。
AudioSourceの[Pitch](https://docs.unity3d.com/ja/current/Manual/class-AudioSource.html)プロパティの値を変えることで再生速度は変えられますが、音の高さも変わってしまいます。
[PitchShifter](https://docs.unity3d.com/ja/current/Manual/class-AudioPitchShifterEffect.html)を併用することで、元の音の高さで再生することはできますが、スクリプトから制御するとなるとひと手間必要になります。[^1]
ＣＲＩ・ミドルウェア社が開発した音声再生ミドルウェア[CRI ADX2](https://game.criware.jp/products/adx/)のタイムストレッチ[^2]機能を使うと、比較的簡単にかつクオリティ高く実現できたので、今回はその方法と注意点などを紹介していければと思います。

# 実装
では、早速実装を見ていきましょう。

## プレイヤーの設定
タイムストレッチ機能は、[ボイス](https://game.criware.jp/manual/unity_plugin/latest/contents/atom4u_keys_voice_overview.html)（音の再生を行うオブジェクト）を割り当てるボイスプールというものに対して付けることになり、タイムストレッチ機能がアタッチされたボイスプールと音声の再生制御を行うプレイヤーである[CriAtomExPlayer](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExPlayer.html#details)を紐付けることで、そのプレイヤーでタイムストレッチ機能による倍速再生を行えるようになります。

実装手順は、以下のようになります。

1. 0以外の任意の識別子IDを持ったボイスプール[CriAtomExStandardVoicePool](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExStandardVoicePool.html)を生成する  
   ∟デフォルトだとプレイヤーは識別子IDが0のデフォルトのボイスプールを使って再生してしまう
2. ボイスプールにタイムストレッチ機能をアタッチする
3. タイムストレッチ機能をアタッチしたボイスプールとプレイヤーを紐付ける




```c#
public class SoundPlayer
{
    /// <summary>
    /// プレイヤー
    /// </summary>
    private CriAtomExPlayer _player;

    /// <summary>
    /// ボイスプール
    /// </summary>
    private CriAtomExStandardVoicePool _voicePool;
    
    /// <summary>
    /// 初期化
    /// </summary>
    public void Initialize()
    {
        _player = new CriAtomExPlayer();

        // 1.
        _voicePool = new CriAtomExStandardVoicePool(1, 2, 96000, true, 2104);
        // 2.
        _voicePool.AttachDspTimeStretch();

        // 3.
        _player.SetVoicePoolIdentifier(_voicePool.identifier);
    }
}
```

## 倍速再生
プレイヤーの再生前に、CriAtomExPlayerクラスの[SetDspTimeStretchRatioメソッド](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExPlayer.html#a904aaf1bbebb3857cc22bc7b76288650)で、再生時間の倍率を設定します。再生速度を指定する場合は、再生速度の倍率の逆数を指定します。
例えば、再生速度を2倍にしたい場合は、メソッドの引数に1/2 = 0.5を指定します。
設定できる値には制約があって、設定可能な値の範囲は 0.5f ～ 2.0f までとなっています。

```c#
public class SoundPlayer
{
    /// <summary>
    /// プレイヤー
    /// </summary>
    private CriAtomExPlayer _player;

    /// <summary>
    /// ボイスプール
    /// </summary>
    private CriAtomExStandardVoicePool _voicePool;

   /// <summary>
   /// 初期化
   /// </summary>
   public void Initialize()
   {
        // （略）
   }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play(CriAtomExAcb acb, int cueId, float playSpeed = 1f)
    {
        _player.SetCue(acb, cueId);
        _player.SetDspTimeStretchRatio(1 / playSpeed);

        _player.Start();
    }
}
```

# 注意点
## ボイスプールが破棄されないようにする
タイムストレッチ機能を使う間は、タイムストレッチ機能がアタッチされたボイスプールの参照を持ち続ける必要があります。
ボイスプールが破棄されてしまうと、再生用リソースの実態であるボイス自体も破棄されてしまうため、音が再生されなくなってしまいます。

### ダメな実装例
プレイヤーの設定でダメな実装の例を挙げます。

```c#
public class SoundPlayer
{
    /// <summary>
    /// プレイヤー
    /// </summary>
    private CriAtomExPlayer _player;
    
    /// <summary>
    /// 初期化
    /// </summary>
    public void Initialize()
    {
        _player = new CriAtomExPlayer();

        var voicePool = new CriAtomExStandardVoicePool(1, 2, 96000, true, 2104);
        voicePool.AttachDspTimeStretch();

        _player.SetVoicePoolIdentifier(voicePool.identifier);
    }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play(CriAtomExAcb acb, int cueId, float playSpeed = 1f)
    {
        _player.SetCue(acb, cueId);
        _player.SetDspTimeStretchRatio(1 / playSpeed);

        _player.Start();
    }
}
```

この例では、ボイスプールをローカル変数として定義してしまっており、メソッドのスコープを抜けるとボイスプールは破棄されてしまいます。
ボイスプールのインスタンスは、仮に他のメソッドで参照しないとしても、フィールド変数で定義して、倍速再生機能を使う間は参照を持ち続けるようにしましょう。


# 聴こえ方
CRI ADX2のタイムストレッチ機能では、[PSOLA](https://ja.wikipedia.org/wiki/PSOLA)という技術が使われている[^3]ようなのですが、PSOLAの性質上、再生速度の倍率が1倍から離れるにしたがって音の品質は落ちていきます。
特に、再生速度の倍率を小さくしすぎると、本来鳴ってないはずの音が多く合成されてしまうためか、全体的にノイズが目立つようになります。
人の声の場合、再生速度の倍率は±30％の範囲で変えるにとどめておくと音の品質的にそこまで問題にならないらしいです。


# ここが良い！
## リアルタイムに再生速度を変更できる
CriAtomExPlayerクラスの[SetDspTimeStretchRatioメソッド](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExPlayer.html#a904aaf1bbebb3857cc22bc7b76288650)で再生速度の倍率を指定した上で、CriAtomExPlayerクラスの[Updateメソッド](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExPlayer.html#a008b3db7cbbea5aa6589758394d8783a)もしくは[UpdateAllメソッド](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExPlayer.html#a05f11acbbc38d8321cc3ba719c57cbf4)を実行することで、
再生中の音に対しても、タイムストレッチの倍率を変えることができます。

```c#
public class SoundPlayer
{
    /// <summary>
    /// プレイヤー
    /// </summary>
    private CriAtomExPlayer _player;

    /// <summary>
    /// ボイスプール
    /// </summary>
    private CriAtomExStandardVoicePool _voicePool;

   /// <summary>
   /// 初期化
   /// </summary>
   public void Initialize()
   {
        // （略）
   }

    /// <summary>
    /// 再生速度を変更する
    /// </summary>
    public void UpdateSpeed(float speed)
    {
        _player.SetDspTimeStretchRatio(1f / speed);
        _player.UpdateAll();
    }
}
```

例えば、バトルシーンのBGMの再生速度を戦況に応じて変えることで、臨場感を引き出すといった使い方もできるかと思います。

## 音声出力解析機能と併用できる
CRI ADX2には、音声出力をリアルタイムに解析する機能があり、再生中の音声の振幅や周波数の情報をリアルタイムに取得することができます。キャラクターのボイスのRMS (Root Mean Square) 値を取得し加工することで、リップシンクに使えたりします。
タイムストレッチ機能で倍速再生している音声に対して解析を行うと、倍速が反映されたデータが取得できます。

音声解析機能を使うには、解析対象のプレイヤーに[CriAtomExOutputAnalyzer](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExOutputAnalyzer.html#af67e3fefd94694b81987678c9a09860d)をアタッチしてやります。
RMS値の取得は、CriAtomExOutputAnalyzerクラスの[GetRmsメソッド](https://game.criware.jp/manual/unity_plugin/latest/contents/classCriAtomExOutputAnalyzer.html#af67e3fefd94694b81987678c9a09860d)で行います。

```c#
public class SoundPlayer
{
    /// <summary>
    /// プレイヤー
    /// </summary>
    private CriAtomExPlayer _player;

    /// <summary>
    /// 音声出力のアナライザー
    /// </summary>
    private CriAtomExOutputAnalyzer _analyzer;

    
    /// <summary>
    /// 初期化
    /// </summary>
    public void Initialize()
    {
        _player = new CriAtomExPlayer();

        // アナライザの生成
        _analyzer = new CriAtomExOutputAnalyzer(
            new CriAtomExOutputAnalyzer.Config
            {
                // RMSレベルの取得を有効にする
                enableLevelmeter = true,
            });

        // アナライザーにプレイヤーをアタッチする
        _analyzer.AttachExPlayer(_player);
    }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play(CriAtomExAcb acb, int cueId)
    {
        _player.SetCue(acb, cueId);

        _player.Start();
    }

    /// <summary>
    /// RMS値を取得
    /// </summary>
    public float GetRms()
    {
        return _analyzer.GetRms(0);
    }

    /// <summary>
    /// アナライザをデタッチ
    /// AtomExプレーヤを破棄する前に実行する。
    /// </summary>
    public void DetachAnalizer()
    {
        _analyzer.DetachExPlayer();
    }
}
```

#まとめ
本記事では、CRI ADX2のタイムストレッチ機能による倍速再生について紹介しました。
手軽に倍速再生を実現できるので、CRI ADX2を使われている方やこれから使うことを考えている方の参考になれば幸いです。

明日は、[@shirai_suguru](https://qiita.com/shirai_suguru)さんの記事です。

# 注釈
[^1]: https://qiita.com/hotaro/items/c3c93866a6509e65e79f
[^2]: タイムストレッチとは、音声処理において、音の高さを変えずにテンポを変える処理のことです。逆に、テンポを変えずに音の高さを変える処理をピッチシフト (またはピッチスケーリング) と言います。
[^3]: https://game.criware.jp/learn/tips/soundhi_soundlo/