カラオケを作りたーーーーい!

お疲れ様です!!(気さくな挨拶)

この記事は琉球大学知能情報アドベントカレンダー 2023 12/20 日の参加記事です!

12 月も終盤戦、カラオケっぽいものを作ってみたという記事をお送りします

TL;DR

  • Next.js でカラオケみたいに音符が流れてくるやつを作った。これ
  • AudioAnalyser をこねる
  • Next.js を GitHub Pages で静的デプロイするときのハマりポイントがある。

カラオケしたい

みなさん、カラオケはお好きですか? 私はたまに(occasionally)ヒトカラをしています。しないというみなさんも家で軽く歌ったりしますよね?
そんなときに気になるのは今歌った音程はあっているのか…?ずっと鼻歌で歌っていても、YouTube で音楽と一緒に歌っていても、カラオケに行くとまともに音程が取れないなんてこともザラにあります。

とうことで、曲の音程と自分がいまだしている音程が知りたいわけです。
つまり、カラオケを作りたいということですね。

カラオケを作るには、いくつかの必須の機能があります。

  • 声の音程を判定できること
  • 曲の音符(ノート)が表示されていること
  • 音符がいい感じに流れること

カラオケ(JoySound とか DAM とか)は判定ぐらいしかしてくれませんが、せっかくですし FFT の結果をそのまま使っても面白そうですね。 ということで作っていきましょう!

AudioAnalyser をこねこね

今回は練習ついでに Next.js (TS) を使用していきます。

マイクの音声はこんな感じでとれます。

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: false,
    noiseSuppression: false,
  },
});

echoCancellation と noiseSuppression はあんまり見慣れませんが、見ての通りノイズ抑制とかを無効化しています。 これが有効だとうまく声が入らなかったりしたので…

そして、JS にはなんと音声処理をできたりする AudioNode インスタンスがあって、その中でも AnalyserNode (MDN)を使えば音声の FFT ができます。

AnalyserNode の使い方を調べるとだいたい音楽データを流したりしていますがやっぱり声もやりたいですよね。 FFT した結果はこんな感じで取ることができます。

const fftSize = 8192;
analyser.fftSize = fftSize;
const bufferSize = analyser.frequencyBinCount;
const sampleRate = audioContext.sampleRate;
const buffer = new Uint8Array(bufferSize); // 結果が入るところ

audioContext = new AudioContext();
analyser = audioContext.createAnalyser();
const input = audioContext.createMediaStreamSource(stream);
input.connect(analyser);
analyser.getByteFrequencyData(buffer);

これでいい感じに周波数情報が取れたわけですが、これをビジュアライズするにはひと工夫必要です。 なんと、愚直にグラフ化すると、直感的なグラフにならないんですね~。
音程の周波数の差は高周波数の方ほど大きくなっていくので、思ったより変なグラフができます。

そのため、周波数側を log スケールの片対数グラフにするといい感じに直感的なグラフができます。 周波数のグラフ
この画像は下の方が低周波数、上に行くほど高周波数になるやつです。

ピッチを取りたい

周波数は取れたわけですが、ピッチ(音高)も取りたいです。なんでかというと、音高がないとどの周波数を音程として判定したらいいかわからなくなるからですね。
(今のところ判定はないんですが)

ピッチの判定とかで軽くググると機械学習を応用するーーとか書いてあるんですがそこまでの精度もいらないですし静的にデプロイできなさそうなので嫌です。JS(TS)でできるぐらいで十分なんです。

結局、周波数のピーク(山)を検出して、倍音を持っている山’s のうち低いものの周波数をピッチとして表示しています。
声からも n 倍音が出ているのでそれを手がかりにピッチを取っているわけです。

UST をこねこね

話は変わって、カラオケみたいなやつには音符を表示できる必要があります。カラオケに行けばノーツが表示されるアレです。
アレを作るには、まず使うデータを考えないといけません。音程の情報があって、ノーツの長さがあって、テンポの情報があって…そんなファイルを読み込めたら一番うれしいですね。
さて、ボーカロイド(広義)はそういうデータを扱っているはずですね。歌声合成ソフトには音程もノーツ長もテンポも必須です。
歌声合成ソフトは割りといっぱいって、フリーの UTAU,neutrino ,Synthesizer V Studio Basic(ボイスは有料) ,有料なら VOCALOID, Synthesizer V Studio Pro, Cevio AI… いっぱいあります。

保存形式としては、MIDI や UST ファイル,vsq ファイルとかがあるみたいでした。そのなかでパースが楽そうな保存ファイルとして、UTAU の.ust ファイルを使うことにしました。なんでかというと UTAU は UST を公開してくれている人が多かったり、平文で保存されていて、扱いやすそうだったからです。

UST ファイルの構成は@wikiにまとめてくれている方がました。大感謝!
使うのはとりあえずテンポと音階(MIDI Note 番号),ノートの長さ が必要なのでひとまずそれを読むパーサを書いて使います。

これをいい感じに Canvas に描画するとそれっぽい感じのグラフができました!! ノーツ (表示の UST はsm6752330で配布されているものをお借りしました)

ひとまず完成

とりあえず今できた部分を完成として出してしまいます。 ホントはノーツが表示範囲外になったらいい感じにずらすとか、一時停止できたりとかの機能がほしいところですが…

Zuckerberg がdone is better than perfectとか言っている有名な画像もあることですしね。

Next.js -> GitHubPages ハマりポイント

やったー!完成だーー!といって GitHubPages へデプロイして動作確認したら動かない!!Canvas が描画されてない!!

困りましたね。
Console にはスクリプトの 404 エラーが書いてありました。

うーーん?見覚えがありますね? Hugo を GitHubPages へ持っていったときにも見た記憶があります。そうです、ページのパスがサブディレクトリになっているときにルートへ読みに行って 404 を吐いてきていました。

ということで next.config.js へ次の設定を追加します

  basePath: "/karaoke",
  assetPrefix: "/karaoke/",

これで正しいパスへ読みに行ってくれるようになりました! ……あれー?まだ 404 です。パスはあっているんですが…

解決策を求めてネットの海をさまよったところ見つけました。みんなの味方 StackOverflow にありました!
js and css not loading when hosting next application on GitHub pages

ドキュメントのルートに.nojekyll という名前の空ファイルを置いておかないと GitHub がデプロイしてくれないそうですね!

これをつけることでやっと公開できました!めでたしめでたし

これで今日の記事は終わりです。 明日は 琉大つよつよ勢代表 @Yoshisaur さんの記事です お楽しみに!