STYLYにマイク入力機能を実装したかった話

先日、社内で久し振りにハッカソンをやったんですよ。
全体としては特にテーマはなく自分でコンセプトや目標を掲げて良い、という比較的緩いレギュレーションでした。
よしそれなら以前から実装したいと思っていた機能を実装するか。
と奮起して取り組んでみたものの、最終的には目標達成できなかった話をします。

# はじめに

私は個人の活動として、AudioVisualライブをやったりしています。
主に[hydra](https://hydra.ojack.xyz/)を使ったLiveCodingで映像を作りつつ、シンセで即興的にマシンライブやったりしてます。

https://www.instagram.com/reel/C1guaSZSGwA/

<iframe width="1497" height="354" src="https://www.youtube.com/embed/75ueEjCAHbI" title="AudioVisual Live@poi3" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

そんな活動をしているので、せっかくならSTYLYも使ったAudioVisualライブとかやってみたいと思っているんですね。
良い意味での公私混同というか、自分が仕事で開発しているプラットフォームを個人の活動にも絡ませられると、仕事のモチベーションも上がるしシナジーあるかな。
と普段から考えていて、作ったものをSTYLY上で[公開](https://gallery.styly.cc/artist/hacha/scenes)してたりします。

具体的にどうやるかは幾つか道筋はあります。
かねてからアプリ内機能で画面録画した際にマイク音源も含めて欲しいという要望もあるので、ちょっとマイク入力を扱えるようにしようと考えました。
マイク入力を起点にオーディオリアクティブでAR演出とかできれば、まぁそれなりの表現はできそうだなという見通しもあります。

# どのようにマイク入力を取り込むか？

そもそもは要望に応じるため、画面録画にどうマイク音源を混ぜ込むか、という方向で考えてました。
普通に考えると画面録画とマイク音源の録音を同時に行い、録画終了時に混ぜ合わせる方法かなと思います。
う〜ん、これはなかなか鬼門な感じがしますね。
ちょっと低レベルなレイヤーを触らないといけなさそうなのと、iOS/Androidの違いやOSバージョンの違いなど色々な障害がありそうな予感がします。
なんとか面倒な道を歩まずに済まないものか……

そんなことを考えていたある日、閃きました。
単純にマイク入力を、Unityシーン内で鳴らし直すのはどうだろうか。
アプリ内機能の画面録画ではAudioListenerの捉えたものがそのまま収録されるので、厄介なレイヤーに手を付けずにいけるのでは。
また画面録画だけでなく、シーン内で鳴っているならAudioSpectrumを取れるのでオーディオリアクティブな演出も作れます。
STYLYでのオーディオリアクティブのやり方については、STYLY MAGAZINEに記事があります。

https://styly.cc/ja/tips/audiovisualizer_discont_playmaker/

調べてみると、ちょっと古い記事ですがマイク入力をAudioClipとして扱う方法が見つかりました。
```cs
using UnityEngine;
using System.Collections;

// 空の Audio Source を作って置く
[RequireComponent (typeof (AudioSource))]
public class MicrophoneSource : MonoBehaviour 
{
    void Start() 
    {
        // 空の Audio Sourceを取得
        var audio = GetComponent<AudioSource>();
        // Audio Source の Audio Clip をマイク入力に設定
        // 引数は、デバイス名（null ならデフォルト）、ループ、何秒取るか、サンプリング周波数
        audio.clip = Microphone.Start(null, false, 10, 44100);
        // マイクが Ready になるまで待機（一瞬）
        while (Microphone.GetPosition(null) <= 0) {}
        // 再生開始（録った先から再生、スピーカーから出力するとハウリングします）
        audio.Play();
    }
}
```
参考記事：[Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた](https://tips.hecomi.com/entry/2014/11/11/021147)

サンプルコードを書いてみた感じではうまくいきそう。

# 早速STYLYに組み込んでみたが

STYLYではプログラマブルな処理は基本的にPlaymakerでアクセスします。
こんな感じのカスタムアクションを定義してみました。

![スクリーンショット 2024-01-30 22.32.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5671/b456c90d-1b15-fde9-30a8-137ed3c87e7e.png)

まぁ[UnityEngine.Microphone.Start](https://docs.unity3d.com/ja/2020.3/ScriptReference/Microphone.Start.html)のラッパーみたいなものなのでシンプルですね。
再生に使うAudioSourceをAudioMixer通すようにしたりすると取り回しも良さそう。

が、実際に挙動をチェックしていて幾つかの問題が見つかりました。
これを実装した開発ビルドは残念ながら公開できないので動画で説明します。

ちょっと余談ですが。
実際どれぐらい遅延したりトラブルがあるものなのか、適当にYouTubeでMVとか流した画面を撮りながらチェックしてました。
ただそういう動画を使うのも問題かなと考え、ちょうど勉強中の[strudel](https://strudel.cc/workshop/getting-started/)でシンプルに音が鳴るタイミングで画面が光るようなものを作ってみました。

https://strudel.cc/?9hOq7v8zHbbV

上記を端末機能で動画撮影してみたものがこれです。

<iframe width="391" height="295" src="https://www.youtube.com/embed/tyoit_EjsuA" title="検証動画（通常の動画撮影）" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

そして開発ビルドで動画撮影してみたものがこれです。

<iframe width="391" height="295" src="https://www.youtube.com/embed/v1HuEP_5EiM" title="検証動画（遅延）" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

思った以上に音が遅延しているように見えます。
そして遅延以外にも幾つかの問題があります。

# 問題点１「ループバック」

端末のスピーカーから大きい音で鳴らすと、それをマイクが拾ってループバックしてしまいます。
まぁこれは元より想定内ではありますが、実際そこまで大きなボリュームでなければ致命的な問題でもないのかなと思いました。
理屈ではマナーモードにするなどして端末スピーカーから音を出していなければ起きない問題だと思うのですが、ちょっと後述の問題に絡んでいる気がします。

# 問題点２「AirPods接続時の障害」

<iframe width="391" height="295" src="https://www.youtube.com/embed/fn7DA59bd10" title="検証動画（AirPods接続時）" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

iPhone12Pro(iOS 17.2.1)にAirPods Proを接続すると、マイク入力を開始したタイミングで謎のフリーズが発生します。
エラーログにも何も出ず、ちょっと調べてみたもののさっぱり原因不明。
外部マイクが問題かと考え、マイク付き有線イヤホンを繋いでみたもののこちらは発生せず。
AirPods以外のBluetoothイヤホンで発生するのかは検証できていません。

# 問題点３「なんか音がガビガビする」

<iframe width="391" height="295" src="https://www.youtube.com/embed/zQ1bsNCTGu8" title="検証動画（音がガビガビする）" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

音がガビガビになった上にループバックしています。
こうなる時とならない時の切り分けができていません。
マナーモードにして端末スピーカーからは音を出していない状態なので、マイクが音を拾っているというわけではありません。
少し不思議なのは、こちらの動画では映像と音のタイミングは遅延していないんですよね。

端末の負荷状況も関係するかなと端末再起動も試してみたものの、変わらず。
Microphone.Start()のfrequencyパラメータも変えてみて色々試したものの、下げたからと言って解決するわけではありませんでした。

これまたさっぱり原因が分かりません。

# 総括

流石にこれではまともに使えない機能なのでリリースはできません。
ちゃんと原因を特定して使えるようにしたいのですが、どうもUnityでのマイク入力周りは公式マニュアルも含めあまり情報が転がってない印象です。
う〜ん、本当はこの機能を実装した上で、それを使ったかっちょいいコンテンツを自分が真っ先に発表する思惑だったのですが残念です。
誰か……助けて………

# 宣伝

STYLYではUnityエンジニア・サーバーサイドエンジニアを募集しています！！
我こそは問題解決と実装のスペシャリストぞ！という方も、チョットデキル方もご応募お待ちしております！

https://www.wantedly.com/projects/1360135

https://www.wantedly.com/projects/1360123