[MR Dev Tips #7] Unity Microphone クラスで正確な録音時間の AudioClip を生成する方法

# はじめに
Unity の Microphone API ``Microphone.Start()`` を利用して AudioClip を作成する際、録音の途中で ``Microphone.End()`` を実行しても、AudioClip が ``Microphone.Start()`` メソッドの第3引数で指定した lengthSec (録音するオーディオクリップの長さ) の秒数で生成されていることに気が付きました。

```Microphone.cs
class Microphone
{
   public static AudioClip Start (string deviceName, bool loop, int lengthSec, int frequency) 
   {
     ..
   };
}
```

| 引数  | 型 | 概要 |
| --- | --- | --- |
| deviceName | string | 録音するデバイス名 (マイク) |
| loop | bool | ループ録音を行うかどうか、``true`` の場合は ``lengthSec`` に到達した時にクリップの最初に戻って録音を継続する |
| lengthSec | int | 録音するオーディオクリップの長さ |
| frequency | int | 録音するオーディオクリップの周波数 |

``Microphone.End`` をコールすることで録音自体は停止するのですが、AudioClip は ``Microphone.Start()`` メソッドコール時の第三引数の秒数 (int) で生成されてしまっているようです。

例えば ``Microphone.Start(deviceName, false, 60, 44100)`` を実行した後、10秒後に ``Microphone.End()`` を実行すると、60秒の AudioClip が生成され、0～10秒間の間のみ、録音されており、11秒～60秒までは無音の状態が続くといったような状況です。

今回は録音されている部分のみを新たな AudioClip として新規作成する方法をご紹介したいと思います。

# 基礎知識
まず最初に Microphone クラス や AudioClip クラスを扱う上で、確実に理解しておきたい用語のご紹介をしたいと思います。

| 用語 | 解説 |
| --- | --- |
| サンプリング周波数 (frequency) | 人の声などのアナログの音声信号をデジタル信号へ変換する際に、1秒間にサンプリング(標本化)を行う頻度のこと。1秒間に実行されるサンプリング回数はヘルツ (Hz) の単位で表すことができます。サンプリング周波数が 44100 Hz の場合は、1秒間に 44100 回サンプリングが行われることを示します。|
| サンプル数 (samples) | サンプリング周波数に応じて取得された標本の数。サンプリング周波数 (Hz) × 音声データの秒数 (sec) = サンプル数 (標本数) となります。 |
| チャンネル (channels) | **音の出力先となるスピーカー**のこと。チャンネル数は、スピーカーの数を表しており、「スピーカー◯本分の音を収録している」と同時に「この音声を完全に再生するためには◯本のスピーカーが必要」ということでもある。 |

# 録音した部分だけを AudioClip として切り出す方法

```RecorderSample.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.MixedReality.Toolkit.UI;

/// <summary>
/// HoloLens 2 で音声録音を行うサンプル
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class RecorderSample : MonoBehaviour
{
  // オーディオクリップの再生を行うための AudioSource
  private AudioSource audioSource;

  // 録音したデータを保存する AudioClip
  private AudioClip audioClip;

  // 録音したデータをトリミングした AudioClip
  private AudioClip audioClip2;

  // 録音を終了した際のサンプル位置
  private int position;

  [Header("Buttons")]
  [SerializeField]
  private Interactable startAndStopButton;

  void Start()
  {
    // 同一オブジェクトにアタッチされている AudioSource を取得
    audioSource = GetComponent<AudioSource>();
    startAndStopButton.OnClick.AddListener( delegate { recording(); });
  }

  private void recording()
  {
    if (!Microphone.IsRecording(Microphone.devices[0]))
    {
      startRecord();
    }
    else
    {
       stopRecord();
    }
  }


  /// <summary>
  /// レコーディングを開始する
  /// </summary>
  private void startRecord()
  {
    // 録音の開始
    audioClip = Microphone.Start(Microphone.devices[0], false, 60, 44100);
  }

  /// <summary>
  /// レコーディングを終了する
  /// </summary>
  private void stopRecord()
  {
    // 録音を終了した際のサンプリングの数 (終了位置)
    position = Microphone.GetPosition(Microphone.devices[0]);
    Microphone.End(Microphone.devices[0]);

    // Microphone.Start で録音したデータのサンプルを取得する
    // まずサンプル数と同じ要素数のfloat配列を初期化する
    // サンプルのデータ数は audioClip.samples * audioClip.channels で計算 
    float[] audioData = new float[audioClip.samples * audioClip.channels];
    // audioClip.GetData で audioData の配列要素数だけサンプルを取得する
    // 第二引数はオフセットサンプル数
    // (1秒後から取得したい場合は、サンプリング周波数×1秒で 44100 が第二引数となる)
    audioClip.GetData(audioData, 0);

    // audioData から録音終了時までのサンプルデータを抜き出す
    // まずサンプルデータを保存する配列を初期化する
    float[] audioData2 = new float[position * audioClip.channels];
    
    // audioData2 配列の要素数だけ audioData のデータをコピーする
    for (var i = 0; i < audioData2.Length; i++)
    {
      audioData2[i] = audioData[i];
    }

    // 新しい AudioClip を生成する
    audioClip2 = AudioClip.Create("audioClip2", position, audioClip.channels, 44100, false);
    // 生成した AudioClip にサンプルデータを格納する
    audioClip2.SetData(audioData2, 0);

    // 生成した AudioClip2 を再生する
    audioSource.clip = audioClip2;
    Play();
  }

  /// <summary>
  /// AudioSource にセットされている AudioClip を再生する
  private void Play()
  {
     audioSource.Play();
  }
}
```

空の GameObject を追加して、``RecorderSample.cs`` をアタッチします。
また、録音の開始/終了をするためのボタンを追加します。
今回は MRTK Prefabs の ``PressableButtonHoloLens2Toggle_48×48`` を使用しています。
インスペクタービューで RecorderSample コンポーネントの ``StartAndStopButton`` プロパティにボタンの GameObject をドラッグ＆ドロップで追加したら、完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180432/bffacb1d-909e-fc5b-0afa-0d3007f0e759.png)

実際に HoloLens 2 で動かしてみた動画は以下からご確認ください。<br/>

<iframe width="560" height="315" src="https://www.youtube.com/embed/sbUBTXhsTp8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


# Refs
本記事を作成するにあたり、参考にさせていただいた Web サイト一覧です。
あわせてご覧いただけると理解が深まると思います。

- [サンプリング周波数とは？](https://www.freshvoice.net/knowledge/word/6703/#:~:text=%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E5%91%A8%E6%B3%A2%E6%95%B0%E3%81%A8%E3%81%AF%E3%80%81%E4%BA%BA,%E3%82%82%E5%A2%97%E3%81%88%E3%81%A6%E3%81%84%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82)
- [世界一やさしい「音声仕様」の見方](https://audio-renaissance.com/know-how/video-audio-spec#:~:text=%E9%9F%B3%E5%A3%B0%E4%BB%95%E6%A7%98%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%80%8C%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB%EF%BC%8FChannel,%E5%BF%85%E8%A6%81%E3%80%8D%E3%81%A8%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%A7%E3%82%82%E3%81%82%E3%82%8B%E3%80%82)
- [[Unity] AudioClipの解析について](https://westhillapps.blog.jp/archives/42569321.html)
- [Unity : record dynamic length from microphone](https://discussions.unity.com/t/record-dynamic-length-from-microphone/82740)