0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

音ゲーの譜面作りを楽にしたい ― 音源分離・ビート解析・ピッチ解析を組み合わせた制作支援PoC

0
Last updated at Posted at 2026-09-12

キラメキ☆音楽神殿への挑戦.png

著作権情報 
アイドルマスター シンデレラガールズ スターライトステージ THE IDOLM@STER™& ©Bandai Namco Entertainment Inc.
hololive Dreams(ホロライブ ドリームス)© COVER / © QualiArts, Inc.
プロジェクトセカイ カラフルステージ! feat. 初音ミク © SEGA / © Colorful Palette Inc. / © Crypton Future Media, INC. www.piapro.net piaproAll rights reserved.

キラメキ創作譜面_完成版.mp4_snapshot_02.25_[2026-09-13_12.01.54].jpg

スクリーンショット 2026-09-13 000952.png

概要

音ゲーの譜面を自動生成するのではなく、音源分離・ビート解析・ピッチ解析などを組み合わせて「譜面を作るためのガイド」を自動生成し、人間による譜面制作を支援できないかを検証したPoCです。最終的にはAnalyzer、譜面Editor、Playerまで実装し、実際の1曲を解析して創作譜面を制作し、最後までプレイできるところまで通しました。

そもそもの出発点

最初に感じていた問題は、音ゲーの創作譜面を作るときの大変さでした。

譜面制作では、単にノーツを配置するだけではなく、

「ここでドラムが鳴った」
「ここからボーカルの音が伸びている」
「ここでベースのアタックがある」
「このフレーズはこのタイミングまで続く」

といったことを、何度も曲を聴き直しながら人間が探す必要があります。

以前、Demucsでドラムを分離し、太鼓の達人のような譜面を機械的に生成するPoCが成立することは確認していました。そこで今回はもう一段踏み込み、

譜面そのものを完全自動生成する必要はない。
曲の中で「何の音が、いつ始まり、いつ終わったか」を機械的に抽出して、譜面制作者にガイドとして提示できればいいのではないか。

という発想から始まりました。

ここは今回の記事でかなり重要なポイントです。目標は自動採譜ではなく、人間による譜面デザインを残したまま、耳でタイミングを探す単純作業を減らすことでした。

そこで作ったもの

この考えを検証するため、システムを最初から一枚岩にはせず、

Analyzer → Analysis JSON → Editor → Chart JSON → Player

という独立したコンポーネントに分けました。

Analyzer、Editor、Playerが同じ言語やフレームワークである必要はありません。標準化したJSONを境界にしておけば、それぞれに適した技術を自由に選べます。

実際、現在はおおむね次の構成になっています。

  • Analyzer:Python。Demucs、Beat This!、librosa、torchcrepeなどを組み合わせて楽曲を解析
  • Analysis JSON:beat、downbeat、各stemのevent、開始・終了時刻、pitch、confidenceなどを保持
  • Editor:Tauri + React + TypeScript + Rust。解析結果をガイドとして表示し、人間が実際の譜面を制作
  • Chart JSON:Single / Long / Slide / Flickなどの実際のゲーム譜面を保存
  • Player:Editorとは独立したPlayer。現在のPoCではNode.jsの小さなローカルサーバーとブラウザのES Modulesで実装

つまり、音響解析はあくまで「譜面を書くための情報」を作り、最終的な譜面は人間がEditorで作るという構造です。

楽曲解析では何をしたのか

Analyzerも単一のAIモデルではなく、複数の技術を役割分担させています。

Demucsでは楽曲を vocals / drums / bass / guitar / piano / other などに分離します。

ただし、Demucsは「音源を分離する」ものであって、「いつノーツを置くべきか」を直接教えてくれるものではありません。

そこで、Beat This!を使って楽曲全体からbeatやdownbeatを検出し、librosaで各stemのonsetや音量変化を解析し、さらにtorchcrepeでpitch情報を取得します。

特に設計上重視したのは、単なる「発音時刻」だけではなく、

startSec / endSec

を持つことでした。

音ゲーでは瞬間的なタップだけでなく、LongやSlideのように「どこまで音が続いているか」も重要だからです。

実際の曲で試した

PoCでは実際に U149「キラメキ☆」 を使って一連の処理を検証しました。

約201秒の楽曲から、461 beats、117 downbeatsが検出され、代表的なテンポは約142.857 BPMとなりました。

Demucsによる分離だけでなく、beat、onset、pitch、event生成、JSON化まで含めた解析チェーン全体を実音源で通すことができ、処理時間も約52.9秒でした。

つまり約3分21秒の楽曲を、1分未満で解析できています。

精度については当然まだ改善余地があります。曲冒頭のbeat誤検出、otherとdrumsのイベント重複、分離由来のartifact、confidenceの扱いなど、音響解析として解くべき問題は残っています。

それでも、

「曲を解析して、譜面制作に利用できるタイミング情報を大量に生成する」

というPoCそのものは成立しました。

解析結果を「使える」Editorへ

次に重要だったのが、解析精度だけを追いかけるのではなく、実際の譜面制作で役に立つUIにすることでした。

Editorでは解析イベント、beat grid、既存ノーツなどをタイムライン上に表示し、Single / Long / Slide / Flickを配置できるようにしました。

さらに制作を進める中で、かなり本格的な編集機能に発展しています。

Select / Placeの分離、Longの編集、複数waypointを持つSlide、End Flick、Flick Chain、Undo / Redo、横スクロール、Zoom、再生速度変更、stem個別再生、ノーツ配置時のauditionなどを追加しました。

image.png

特に今回の目的に直結しているのがSnap / Magnetです。

ノーツを移動すると、beatだけでなく、解析で得られたボーカルなどのイベント開始・終了位置や、他のノーツのタイミングにも吸着します。

Premiere Proなどの編集ソフトでクリップ同士が磁石のように揃う感覚を、譜面Editorに持ち込んだ形です。

ここまで来ると、音響解析は単にグラフを表示するためのものではなく、

「ここに音がある」という解析結果そのものがEditorの操作支援になる

ようになりました。

最後にPlayerまで作った

image.png

さらにPoCを「解析結果がそれっぽく見えました」で終わらせず、作成したChart JSONを実際に遊べるPlayerまで実装しました。

PlayerはEditorのコードには依存せず、共通のChart JSON schemaだけを読みます。

Single、Long、Slide、Flick、End Flick、Flick Chainなどを表示し、判定、コンボ、スコア、結果画面、キーボード・マウス・タッチ入力、autoplayなどを実装しました。

そして「キラメキ☆」の実際の創作譜面を読み込ませ、約3分21秒を最後まで再生。

autoplayによる検証では、

693 / 693 Perfect、Full Combo

まで通りました。

つまり今回、

楽曲

音源分離・ビート・ピッチ・イベント解析

解析結果を使って譜面制作

Chart JSON

実際に遊べるPlayer

という経路が一本につながりました。

1. PoC全体の構成

今回のPoCでは、最初から「1つの巨大なアプリケーション」として作るのではなく、役割ごとに大きく3つのコンポーネントへ分離しました。

  • Analyzer
  • Editor
  • Player

全体の流れは次のようになります。

Audio
  ↓
Analyzer
  ↓
Analysis JSON
  ↓
Editor
  ↓
Chart JSON
  ↓
Player

この構成にした理由は単純で、音響解析、譜面編集、ゲームプレイでは、それぞれ求められる技術がかなり違うからです。

音響解析ではPythonのエコシステムが非常に強く、PyTorchを中心にDemucsやBeat This!、torchcrepe、librosaなどを利用できます。

一方、譜面Editorでは、タイムライン表示、ズーム、スクロール、ドラッグ操作、複数選択など、一般的なGUIアプリケーションに近い操作性が必要です。

さらにPlayerでは、音声再生と描画を同期させながら、リアルタイムでノーツの位置を計算し、入力判定を行う必要があります。

これらを無理に同じ言語やフレームワークへ統一するメリットはあまりないと考えました。

そこで今回のPoCでは、各コンポーネントをJSON Schemaで接続する方針を取りました。

Analyzer

Analyzerの役割は、楽曲を解析して「譜面制作に利用できそうな情報」を生成することです。

入力は音声ファイルです。

ここで音源分離、beat解析、各stemのイベント検出、pitch解析などを行い、結果をAnalysis JSONとして出力します。

重要なのは、Analyzerが直接ゲームのノーツを作るわけではないという点です。

Analyzerが出力するのは、

この時刻にbeatがある
この時刻にドラムの音が鳴った
この区間でボーカルが発音している
この付近にギターのアタックがある

といった、あくまで譜面制作のためのガイド情報です。

最終的にどの音をノーツとして採用するかは、人間が判断します。

Analysis JSON

AnalyzerとEditorの間では、解析結果をJSONとして受け渡します。

例えば、

beat
downbeat
vocals
drums
bass
guitar
piano
pitch
confidence
startSec
endSec

といった情報を格納します。

ここでJSON Schemaを定義しておくことで、Analyzer側の実装を変更しても、Editor側が期待する形式さえ維持すれば独立して開発できます。

逆にEditorを完全に作り直しても、Analysis JSONが同じであればAnalyzerを変更する必要はありません。

今回のPoCでは、この「コンポーネント間をJSONでつなぐ」という考え方がかなり有効でした。

Editor

Editorの役割は、Analyzerが生成した情報を実際の譜面制作に利用できる形で表示することです。

解析結果を単に一覧表示するだけではなく、楽曲のタイムライン上に、

beat
vocals
drums
bass
guitar
piano

などのガイドレイヤーとして表示します。

そして、その上に実際のゲーム用ノーツを配置していきます。

現在のPoCでは、主に以下のノーツを扱えるようにしました。

Single
Long
Slide
Flick

さらにSlideの中間点、LongやSlide終端のFlick、連続Flickなども表現できます。

EditorはTauri + React + TypeScriptを中心に構築しています。

ここで特に重視したのが、解析結果を「見る」だけでなく、編集操作そのものに利用することです。

例えばノーツを配置・移動するときに、beatや解析イベントのタイミングへ磁石のように吸着させるSnap機能を実装しました。

これにより、

解析結果を見る
↓
時刻を確認する
↓
ノーツを手動で同じ位置へ合わせる

という操作ではなく、

ノーツを近くまで移動する
↓
解析イベントへ自動的に吸着する

という操作が可能になります。

このSnap機能は、今回のPoCにおいてかなり重要な要素になりました。

Chart JSON

Editorで作成した最終的な譜面は、Chart JSONとして保存します。

Analysis JSONとChart JSONは別物です。

Analysis JSONは、

楽曲には何が含まれているか

を表します。

Chart JSONは、

ゲームとして何を叩かせるか

を表します。

例えばドラムの音が1000個検出されたとしても、その1000個をすべてノーツにする必要はありません。

音楽として重要な部分や、ゲームとして気持ち良い部分を人間が選択し、Chart JSONへ落とし込みます。

ここを分離したことで、解析結果に引っ張られすぎず、人間が自由に譜面をデザインできます。

Player

最後に、Chart JSONを読み込んで実際に遊べるPlayerを作りました。

PlayerもEditorから独立しています。

Editorの内部状態やReactコンポーネントを流用するのではなく、Playerが知っているのは基本的にChart JSONの仕様だけです。

現在のPoCでは、PlayerはNode.jsの小さなローカルサーバーと、ブラウザ上のES Modulesを使った比較的シンプルな構成になっています。

ここでも、

Editor
↓
Chart JSON
↓
Player

という境界を明確にしています。

この構成にしたことで、将来的にPlayerだけをAndroidアプリへ置き換えることもできます。

AnalyzerがPythonのままでも、EditorがTauriのままでも問題ありません。

各コンポーネントを独立させた

今回の構成で最も重要だったのは、

Analyzer、Editor、Playerを同じ技術体系に統一しなかったこと

です。

それぞれの役割に向いている技術を選び、JSON Schemaだけを共通の契約として持たせています。

結果として、

Pythonによる音響解析
        ↓
    Analysis JSON
        ↓
Tauri / Reactによる譜面制作
        ↓
      Chart JSON
        ↓
独立したPlayer

という形になりました。

PoCでは途中で解析アルゴリズムやEditorの仕様を何度も変更しましたが、この境界を最初に決めていたことで、ある部分の変更がシステム全体へ波及することをかなり抑えられました。

今回の目的は「どのフレームワークで音ゲーを作るか」ではありません。

音響解析を使うことで、実際に譜面制作という作業を楽にできるのか。

それを検証するための構成として、Analyzer / Editor / Playerを分離する設計を採用しました。

2. 開発・検証環境

今回のPoCは、音響解析・譜面編集・Playerで必要な技術が異なるため、それぞれ別の技術スタックを採用しています。

主な検証環境はWindows 11上で、GPUにはNVIDIA GeForce RTX 3060 12GBを使用しました。

Analyzer

AnalyzerはPythonで実装しています。

主な環境は以下です。

Python 3.11
uv
PyTorch
torchaudio
Demucs
Beat This!
torchcrepe
librosa

Pythonの依存関係管理にはuvを使用しました。

音響解析ではPyTorchベースのライブラリが多く、CUDAを利用することでGPU上で処理しています。

検証時の主要バージョンはおおむね以下の構成でした。

Python      3.11
uv          0.11.x
PyTorch     CUDA対応版
torchaudio  CUDA対応版
Demucs      4.x
Beat This!  1.x
torchcrepe  0.0.x
librosa     0.11.x

PoCではライブラリの新しさを優先するよりも、同じ入力から同じ解析結果を再現できることを重視しました。

特にDemucsについては、使用するモデルやcheckpointを固定し、解析結果の再現性を確保しています。

GPU

音源分離やpitch解析ではGPUを利用しました。

使用したGPUは、

NVIDIA GeForce RTX 3060
VRAM 12GB

です。

今回の目的は専用の研究用GPUで最高性能を出すことではなく、比較的一般的なコンシューマ向けGPUでも実用的な処理速度になるかを確認することでした。

実際、約3分21秒の楽曲を対象にした一連の解析も、PoC用途として十分現実的な時間で完了しています。

この点はかなり重要で、譜面を編集するたびに数十分待つような仕組みでは、制作支援ツールとして使いにくくなります。

解析を制作工程に組み込む以上、

「精度」だけでなく「解析結果が返ってくるまでの時間」

も重要な評価対象としました。

Editor

譜面EditorにはTauriを採用しました。

構成は、

Tauri
React
TypeScript
Vite
Rust

です。

EditorはWebブラウザだけで完結するツールではなく、ローカルの音声ファイルやプロジェクトファイルを扱うデスクトップアプリとして使いたかったため、Tauriを選択しました。

UI部分はReact + TypeScriptで実装し、必要なネイティブ処理をTauri / Rust側へ持たせています。

PoCを進める中でEditorには、

タイムライン表示
横スクロール
ズーム
再生位置追従
ノーツ配置
ノーツ選択
ドラッグ移動
範囲選択
Undo / Redo
Snap
stem別ガイド表示
stem個別再生
再生速度変更

など、多くの編集機能が追加されました。

最初は単純なノーツ配置画面でしたが、実際に1曲の譜面を作ろうとすると、一般的な動画編集ソフトやDAWに近い操作性が必要になることが分かりました。

AnalyzerとEditorは完全に別プロセス

AnalyzerとEditorを直接結合していない点も、今回の構成の特徴です。

例えば、

EditorからPythonを直接呼び出す

といった構成にはしていません。

Analyzerは音声ファイルを解析してJSONを出力します。

Editorは、そのJSONを読み込みます。

そのため、解析だけを別PCや別環境で実行することも可能です。

また将来、

AnalyzerだけをLinuxサーバーへ移す
EditorだけをWindowsで使う

といった構成にもできます。

PoC段階では、この疎結合な構成にしておいたことで、解析アルゴリズムを頻繁に変更してもEditorへの影響を小さくできました。

Player

Playerについては、さらに別の構成を試しました。

PoCのPlayerでは、

Node.js
Browser
JavaScript ES Modules
HTML / CSS

を利用しています。

ビルドシステムや大きなフレームワークは使わず、かなり小さな構成です。

Playerの役割は、

Chart JSONを読み込む
音声を再生する
ノーツを描画する
現在時刻に応じてノーツを移動する
入力を判定する
スコアを計算する

ことです。

Editorと同じReactコンポーネントを流用することもできましたが、あえて独立して実装しました。

これは、

Chart JSONだけで本当にゲームとして成立するか

を確認する意味もありました。

Editorの内部実装に依存したPlayerでは、JSON Schemaが本当に十分な情報を持っているのか判断しにくくなります。

完全に別実装のPlayerがChart JSONだけを読んで動けば、データ形式そのものがEditorとPlayerの契約として成立していることを確認できます。

Windowsで一連の制作作業を完結

今回のPoCでは、

楽曲解析
↓
解析結果確認
↓
譜面編集
↓
譜面保存
↓
Playerで確認

までをWindows PC上で完結できる状態にしました。

Analyzerについても、コマンド一つで解析できる形にしています。

概念的には、

python -m analyzer input.flac --output-dir output

のように音声ファイルを渡すと、解析結果と必要なstemが生成されます。

EditorについてもWindowsから起動しやすいように起動スクリプトを用意し、開発環境のセットアップを毎回意識しなくても使えるようにしました。

開発環境もPoCの評価対象

今回のPoCでは、解析アルゴリズムそのものだけでなく、

「実際に譜面を作る人が繰り返し使えるか」

も重視しています。

1回だけ解析結果を出して成功するだけなら、研究用Notebookでも十分です。

しかし今回確認したかったのは、

解析する
↓
Editorで確認する
↓
譜面を修正する
↓
必要なら再解析する
↓
Playerで確認する

という試行錯誤を繰り返せる制作環境です。

そのため、Analyzer、Editor、Playerのそれぞれを独立させながらも、ローカルPC上で一連の作業が簡単に回せる構成にしました。

この環境を土台として、次章から実際に試した音響解析技術について紹介します。

3. 音源分離 ― Demucs

今回のPoCで最初に行ったのが、楽曲をパートごとに分離することです。

使用したのはDemucsです。

音ゲーの譜面を作るとき、完成された2mixだけを見ても、

ドラム
ベース
ボーカル
ギター
ピアノ

といった音がすべて重なっています。

人間であればある程度聞き分けることができますが、機械的にイベントを抽出しようとすると、この混ざった状態が大きな問題になります。

例えば曲中で急激な音量変化を検出したとしても、それが、

スネアなのか
ボーカルの子音なのか
ギターのアタックなのか

を判別するのは簡単ではありません。

そこで最初に、楽曲をstemへ分離しました。

使用したモデル

今回のPoCでは、Demucsのhtdemucs_6sを使用しました。

このモデルでは、楽曲を次の6つのstemへ分離できます。

vocals
drums
bass
guitar
piano
other

一般的な4stem分離では、

vocals
drums
bass
other

程度になるため、ギターやピアノはotherへ混ざります。

今回の目的は単なるカラオケ音源の生成ではなく、譜面制作のガイドとして各楽器の発音タイミングを使うことでした。

そのため、guitarとpianoを個別に取得できる6stemモデルを採用しました。

なぜstem分離が必要だったのか

stem分離の目的は、分離された音源そのものを聞くことだけではありません。

本当に欲しかったのは、

それぞれの楽器に対して別々の解析をかけられる状態にすること

です。

例えばdrumsだけを取り出せれば、

キック
スネア
ハイハット

などの瞬間的なアタックを検出しやすくなります。

一方でvocalsだけを取り出せれば、伴奏に邪魔されず、

歌い始め
音の伸び
フレーズの切れ目
pitch

などを解析できます。

guitarであればピッキングのタイミング、pianoであれば打鍵に近い音量変化を検出する、といったように、stemごとに適した解析方法を選択できるようになります。

この点が今回のPoCでは非常に重要でした。

Demucsの出力をそのままノーツにはしない

ここで注意したいのは、Demucsそのものが譜面候補を生成してくれるわけではないことです。

Demucsの役割はあくまで、

完成音源
↓
各パートに分けた音声ファイル

までです。

例えばguitar.wavを得たとしても、

どの時刻でギターが鳴ったか
どの音をノーツとして採用するか
どこからどこまで音が続いているか

といった情報は、さらに別の処理で抽出する必要があります。

そのため今回の構成では、

Audio
↓
Demucs
↓
Stem Audio
↓
Event Detection / Pitch Analysis

という形にしました。

Demucsは解析パイプラインの最初の前処理として位置付けています。

stemごとに聞けること自体が便利だった

PoCを進めて分かったのですが、stem分離にはもう一つ大きなメリットがありました。

image.png

それは、

譜面制作者が各パートを単独で聞けること

です。

例えば完成音源では聞き取りにくかったベースラインも、bass stemだけを再生すればかなり追いやすくなります。

同様に、

ボーカルだけ聞く
ドラムだけ聞く
ギターだけ聞く
ピアノだけ聞く

という操作ができるだけでも、譜面制作はかなり楽になります。

そのため後にEditor側でも、各stemを個別に再生できる機能を追加しました。

つまりDemucsは、

機械解析の前処理

としてだけでなく、

人間が曲を聞き分けるための支援

としても役立ちました。

実際に分離して分かった問題

もちろん、stem分離は完全ではありません。

実際に使うと、

ボーカル成分がotherへ漏れる
ギターの一部がotherにも残る
ドラムのアタックが他stemへ混ざる
分離時のartifactが発生する

といった現象が起きます。

これは後段のイベント検出にも影響します。

例えばdrums stemからイベントを検出し、other stemからもイベントを検出すると、同じ音が両方に含まれているため、ほぼ同時刻に重複イベントが生成されることがあります。

つまり、

stemを分離した
=
楽器を完全に独立して取得できた

というわけではありません。

分離精度より「ガイドとして使えるか」を重視

今回のPoCでは、DemucsのSource Separation精度そのものを評価することが目的ではありませんでした。

最終的に重要なのは、

Editorで譜面を作るときに役に立つ情報が取れるか

です。

多少別の楽器が混ざっていても、

ここでギターのフレーズが始まった
ここでドラムが強く鳴った
ここからボーカルが入った

といったタイミングが分かれば、ガイドとしては十分使える場合があります。

逆に、音源分離として非常に綺麗でも、発音タイミングをうまく抽出できなければ譜面制作支援としての価値は低くなります。

そのため今回の評価軸は、

分離音源がどれだけ綺麗か

ではなく、

後段の解析やEditor操作にどれだけ役立つか

としました。

Demucs単体では解決しなかった

最初の実験では、

Demucsで分離すれば、そのまま譜面候補を作れるのではないか

という期待もありました。

しかし実際には、音源分離だけでは不十分でした。

必要だったのは、

音源分離
+
beat解析
+
発音イベント検出
+
音の開始・終了推定
+
pitch解析

といった複数の解析の組み合わせです。

Demucsは非常に重要な土台でしたが、今回のPoCで実感したのは、

音源分離はスタート地点であって、譜面制作支援そのものではない

ということでした。

次は、楽曲全体の時間的な基準を作るために試した、beatとdownbeatの解析について紹介します。

4. ビート・ダウンビート解析 ― Beat This!

Demucsで楽曲をstemへ分離した次に必要だったのが、曲全体の時間的な基準です。

譜面制作では、

どこが拍なのか
どこが小節頭なのか
BPMはいくつなのか

という情報が非常に重要です。

もちろん、人間が曲を聞きながらBPMを調べてグリッドを設定することもできます。

しかし今回のPoCでは、できるだけ「曲を聞き込んで手作業でタイミングを探す」という工程そのものを減らしたかったため、beatとdownbeatについても自動解析することにしました。

そこで使用したのが Beat This! です。

beatとdownbeat

今回の解析では、大きく2種類の情報を取得しています。

beat
downbeat

beatは楽曲の拍です。

一方、downbeatは小節の先頭となる拍を表します。

例えば4拍子の曲であれば、概念的には、

1  2  3  4 | 1  2  3  4 | 1  2  3  4
^            ^            ^
downbeat

のようになります。

譜面制作では単に一定間隔の線があればいいわけではなく、小節の区切りが分かることも重要です。

強いノーツを小節頭へ置いたり、フレーズのまとまりを確認したりするときに、downbeatはかなり便利でした。

なぜ単純なBPM指定ではなく解析するのか

一定BPMの楽曲だけを対象にするのであれば、

BPM = 140
開始位置 = 0.52秒

のように人間が設定し、数学的にbeat gridを生成する方法もあります。

しかし実際には、問題になるのはBPMそのものより、

「曲のどの時点からbeatが始まっているか」

です。

例えばBPMが正しくても、グリッドの開始位置が100msずれていれば、曲全体のノーツ配置も100msずれた状態になります。

さらに楽曲によっては、

無音のイントロ
SE
ルバート的な導入
テンポ変化
生演奏由来の揺れ

なども考えられます。

そのためPoCでは、BPMという単一値だけではなく、実際の音声から

beatが発生した時刻の列

を取得する方式にしました。

image.png

特に楽曲の始まりと終わりはBPMが変化しやすい。キラメキ☆の場合、曲の終わりがリタルダンドで曲のテンポが遅くなる。

Beat This!の出力を時間軸へ載せる

解析結果は、例えば概念的には次のような時刻列になります。

beat:
0.84
1.26
1.68
2.10
...

downbeat:
0.84
2.52
4.20
...

Editorでは、この時刻をそのままタイムライン上のガイドとして描画できます。

つまり、

Audio
  ↓
Beat This!
  ↓
beat / downbeat timestamps
  ↓
Analysis JSON
  ↓
EditorのBeat Grid

という流れです。

この方法にしたことで、Editor側はBPM解析のロジックを持つ必要がありません。

Analyzerが出力した時刻を描画するだけです。

「キラメキ☆」での解析結果

今回PoCの実データとして使用した「キラメキ☆」では、約201秒の音源から、

beats      461
downbeats  117

が検出されました。

代表的なテンポは約、

142.857 BPM

となりました。

117 downbeats × 4拍と考えると468拍なので、検出された461 beatsともかなり近く、この曲が基本的に4拍子の安定したテンポで進行していることとも整合します。

この結果だけを見ると、beat tracking自体はかなりうまく動いています。

BPMはbeat列から求められる

今回の構成では、BPMを解析の中心データにはしていません。

中心になるのはあくまで、

beatの時刻列

です。

隣接するbeatの時間差が分かれば、BPMは計算できます。

例えばbeat間隔が0.42秒なら、

60 / 0.42 ≒ 142.86 BPM

です。

実際のEditorでは、固定されたBPM値から線を生成するよりも、解析済みbeat timestampを直接使う方が柔軟です。

将来的にテンポ変化のある曲を扱う場合でも、

beat[0]
beat[1]
beat[2]
...

をそのまま扱えば、途中でBPMが変化しても対応できます。

Beat GridをSnap先として使う

beat解析が本当に役立ったのは、線を表示したときだけではありません。

Editorではbeatを Snap対象 として利用しました。

例えばSingleノーツをbeatの近くへドラッグすると、

現在位置  10.482 sec
beat      10.500 sec

のように十分近い場合、

10.500 sec

へ吸着させます。

これは一般的なDAWや動画編集ソフトにある磁石のような操作感です。

音ゲーの譜面では、すべてのノーツがbeat上に存在するわけではありません。

それでも、

四分音符
八分音符
十六分音符

などを基準に配置するケースは非常に多いため、beat gridは最も基本的なSnap sourceになりました。

解析したbeatから細分化したグリッドを作る

実際の譜面制作では、四分音符だけでは足りません。

そこでEditor側ではbeat間をさらに分割し、

1/2
1/4
1/8
1/16

などの位置をSnap候補として扱えるようにしました。

考え方としては、

beat A -------- beat B

という2点があれば、その間を等分できます。

例えば16分相当なら、

A
|
|--- 1
|--- 2
|--- 3
|
B

のような中間点を生成できます。

つまりBeat This!が直接16分音符を検出しているわけではありません。

Beat This!によって得た基準beatをもとに、Editor側で制作に使いやすいグリッドへ展開しています。

誤検出もあった

もちろん、beat解析も完全ではありません。

実際の楽曲では、特に、

曲の冒頭
音数の少ない区間
リズムが曖昧な区間
強いSEが入る場所

などで、beatの取り方が人間の感覚と一致しないことがあります。

PoCでも曲冒頭付近には、後から見ると不要と思われるbeatや、開始位置として不自然な結果が含まれるケースがありました。

これは完全自動化を考える場合には問題になります。

しかし今回の目的は、

解析されたbeatを絶対的な正解として使用する

ことではありません。

あくまで、

譜面制作時の候補・ガイドとして利用する

ことです。

多少誤った線が混ざっていたとしても、人間がEditor上で見れば、

これは使える
これは無視する

という判断ができます。

100%正確でなくても十分役に立つ

このPoCを進めていく中で分かった重要な点の一つが、

制作支援では解析精度が100%でなくても価値がある

ということです。

完全自動採譜を目的にする場合、

1つbeatを間違えた
=
生成譜面も間違える

ことになります。

しかしHuman-in-the-loopの制作支援であれば、

9割程度は正しい位置が示される
↓
人間は残りだけ確認する

という使い方ができます。

何もない状態から曲を何度も聞き直してbeatを探すのと、ほぼ正しいグリッドが最初から表示されているのでは、作業量が大きく違います。

beatだけでもまだ足りない

一方、beat解析まで実装した段階で別の問題もはっきりしました。

beat gridが分かっても、

そのbeatで何の音が鳴っているのか

は分かりません。

例えば同じbeat上でも、

ドラムが鳴っている
ボーカルが始まっている
ギターが鳴っている
何も特徴的な音はない

という違いがあります。

また、音ゲーのノーツは必ずしもbeat上だけに存在するわけではありません。

ボーカルの細かな発音やギターのフレーズは、beatの中間に存在することも多くあります。

image.png

この個所のボーカル抽出は的場梨沙Pにはぜひ聞いてもらいたい。悶絶間違いなしである。

そこで次に必要になったのが、

各stemの中から実際に「音が発生したタイミング」を検出すること

でした。

次章では、librosaや独自のイベント検出処理を使って、各stemから譜面制作の候補となる発音タイミングを抽出した方法について紹介します。

5. 発音タイミング解析 ― onset / event detection

beatとdownbeatが取れるようになると、曲全体の時間的な基準はかなり見えるようになります。

ただし、beat gridだけでは譜面制作にはまだ足りません。

例えばある拍の中で、

ボーカルが入る
ギターが鳴る
スネアが鳴る
ベースが動く

といった個別のイベントまでは分かりません。

そこで次に行ったのが、Demucsで分離した各stemから、

「実際に音が鳴ったタイミング」

を抽出する処理です。

onsetだけではなく「event」として扱う

最初に考えやすいのは、onset detectionです。

onsetは概念的には、

音が立ち上がった瞬間

を表します。

例えばドラムであれば、

キック
スネア
ハイハット

などの瞬間的なアタックを検出しやすいです。

音ゲーのSingleノーツ候補を探すだけなら、onset detectionはかなり相性が良いです。

しかし実際に譜面を作ってみると、単純なonsetだけでは不足しました。

その理由は、譜面にはSingleだけではなく、

Long
Slide

のような「時間を持つノーツ」があるからです。

そこで今回のPoCでは、単なるonsetの一覧ではなく、

startSec
endSec
confidence

などを持つ event として解析結果を扱う方向へ発展させました。

stemごとにイベントを抽出する

解析対象は、Demucsで分離した各stemです。

主に、

vocals
drums
bass
guitar
piano
other

を対象にしました。

stemごとに分ける最大のメリットは、

同じアルゴリズムでも、完成音源に対して実行するより意味のあるイベントを取りやすい

ことです。

例えば完成音源に対して音量変化を見ると、

ボーカル
ドラム
ギター
ベース

の変化がすべて重なります。

一方、guitar stemだけを見れば、

ギターの局所的な立ち上がり

をかなり明確に検出できます。

librosaを使った特徴量解析

PoCではlibrosaも利用し、音量やonsetに関連する特徴量を取りました。

基本的な発想は、

音声波形
↓
短時間フレームへ分割
↓
各フレームの特徴量を計算
↓
局所的に大きな変化を探す

というものです。

例えば、

RMS
spectral energy
onset strength

のような時間方向の特徴を見ることで、

この瞬間に急に音が強くなった

という場所を候補として抽出できます。

ただし、単純に閾値を超えた場所をすべてイベントにすると、大量の誤検出が発生します。

単純な閾値判定では多すぎる

例えば、

energy > 0.5

という固定閾値だけでイベントを検出すると、

曲の大きな区間では大量に検出され、静かな区間では何も検出されない、といった問題が起こります。

楽曲の音量は時間とともに大きく変化します。

さらにstemごとにも、

drumsは瞬間的に大きい
vocalsは比較的持続する
guitarは曲によって差が大きい
bassはアタックが目立たない場合がある

という違いがあります。

そのため、固定値だけでは安定しませんでした。

局所的な背景との差を見る

そこでguitarなどの解析では、

rolling medianを背景レベルとして使い、そこからどれだけ突出しているかを見る

方式も試しました。

概念的には、

現在のエネルギー
-
周辺区間の中央値

を計算します。

例えば、

周辺の平均的な強さ      0.25
現在フレームの強さ      0.62
差                     0.37

であれば、局所的に強いイベントである可能性があります。

絶対値ではなく、

その場所の周囲と比べてどれだけ目立つか

を見ることで、

曲の静かな区間と大きな区間の両方に対応しやすくなります。

local peakをイベント候補にする

さらに、単に閾値を超えたフレーム全部を採用するのではなく、

局所ピーク

だけを候補にしました。

例えば特徴量が、

0.1
0.2
0.4
0.8
0.5
0.3

と変化した場合、

0.8

の位置だけをイベント候補として扱います。

これにより、1回の発音に対して複数イベントが生成される問題を減らせます。

minimum spacing

それでも実際の音源では、1つの発音の周辺に複数ピークが出ることがあります。

そこでイベント同士の最小間隔も設定しました。

guitarの検証では、例えば、

40ms

程度のminimum spacingを設けました。

つまり、非常に近い位置に複数候補があった場合は、

より強いものだけを残す

方向にします。

この処理は、イベント数の爆発を防ぐのにかなり有効でした。

confidenceを持たせる

PoCを進める中で、イベントを、

ある / ない

だけで扱うのは不便だと感じました。

解析結果には必ず曖昧さがあります。

そこでeventには、

confidence

を持たせるようにしました。

例えば、

0.95  非常に強いイベント
0.72  かなり有力
0.40  弱い候補

のような値です。

Editor側では、将来的にconfidenceを使って、

高confidenceだけ表示
弱い候補は薄く表示
thresholdをUIから変更

といったこともできます。

ここでも重要なのは、

Analyzerが正解を決めるのではなく、Editor側で人間が選べる情報を残す

ことです。

dynamic range gate

イベント検出を改善するために、dynamic rangeによるgateも試しました。

例えば、ほとんど無音に近いstemでは、小さなartifactでも局所ピークとして検出されることがあります。

そこで、

そのstemに十分なダイナミックレンジが存在するか

を確認し、情報量が少なすぎる場合はイベントを抑制します。

これにより、

分離artifactだけを大量にイベント化する

ケースを減らしました。

presence-vs-mix gate

もう一つ試したのが、

stem単体で強く見えていても、元のmixに対して本当に存在感があるか

を見るgateです。

Demucsで分離されたstemには、元音源ではほとんど聞こえない成分が強調されて残ることがあります。

そのため、

stemではピークがある
↓
しかしmix全体ではほぼ存在感がない

場合は、譜面制作ガイドとしての価値が低い可能性があります。

そこで、stem内の強さだけでなく、

元mixに対するpresence

も判断材料にしました。

この処理は、分離artifact由来の誤検出を減らすためのものです。

guitarでは約1500イベント

「キラメキ☆」でguitar stemを対象にした検証では、

約201秒
約1500 events

程度のイベントが検出されました。

これは当然、

1500個のギターノーツを置く

という意味ではありません。

むしろ、

ギターの中で音響的に変化が起きた候補

をかなり細かく拾っていると考えています。

Editor側では、その中から人間が必要なものだけを使います。

stemによって理想的な検出方法が違う

PoCで分かったことの一つが、

すべてのstemに同じイベント検出アルゴリズムを使うのは難しい

ということです。

例えばdrumsは、

瞬間的なアタック

が非常に重要です。

一方、vocalsでは、

発音開始
持続時間
音程変化
フレーズ

が重要になります。

bassでは、音によってはアタックが弱く、

onsetだけでは拾いにくい

ことがあります。

guitarも、

ストローク
単音
歪んだ持続音
アルペジオ

で特徴が大きく異なります。

そのため最終的には、

stem
↓
そのstemに適した特徴抽出
↓
共通のevent形式へ変換

という設計が良いと考えるようになりました。

最終的には共通eventへ正規化する

内部の検出方法が違っていても、Editorから見れば共通の形式で扱えた方が便利です。

そこでAnalysis JSONでは、概念的に、

{
  "startSec": 12.34,
  "endSec": 12.61,
  "confidence": 0.82,
  "source": "guitar"
}

のような形へ正規化します。

Editorは、

このイベントがlibrosa由来なのか
rolling median由来なのか
別のモデル由来なのか

を知らなくても利用できます。

これもAnalyzerとEditorを分離したメリットです。

誤検出はなくならない

イベント検出を改善していくと精度は上がりましたが、誤検出を完全になくすことはできません。

主な原因は、

Demucsのstem leakage
分離artifact
残響
同じ音の倍音
複数楽器の重なり
非常に短い装飾音

などです。

例えば1つのスネアが、

drums
other

の両方に少しずつ含まれ、それぞれでイベントとして検出されることがあります。

このためAnalysis JSONには、時間的にほぼ同一のイベントが複数存在するケースもあります。

誤検出を消すよりEditorで使いやすくする

ここでも、完全自動採譜と制作支援の違いが重要になります。

完全自動でノーツへ変換するなら、

誤検出
=
誤ったノーツ

です。

しかし制作支援では、

多少余計なガイドがある

程度で済みます。

実際にEditor上で、

vocals
drums
bass
guitar
piano

をレイヤーとして分けて表示すれば、不要なイベントは無視できます。

逆に必要なイベントが1つも表示されない方が困ります。

そのためPoCでは、

多少多めに候補を出し、confidenceやレイヤー表示で人間が選別する

方針を取りました。

onset detectionから「譜面制作ガイド」へ

この段階で、Analyzerはかなり譜面制作支援らしい形になってきました。

最初は単純に、

音が鳴った瞬間を取る

という発想でした。

しかし実際に使っていくと、

どのstemか
どの時刻から始まるか
どこまで続くか
どれくらい信頼できるか

まで持ったeventの方が、Editorでは圧倒的に使いやすいことが分かりました。

特に、

どこまで続くか

という情報はLongやSlideの制作に直結します。

次章では、このendSecをどのように扱い、単なる発音点ではなく「発音区間」を譜面制作ガイドとして利用するようになったかを紹介します。

image.png

この制作支援がなければ最後のギターストローク風フリック譜面作成は心が折れていたであろう。

6. 音の終了タイミングを推定する

onset detectionによって「音が鳴り始めた瞬間」はかなり取れるようになりました。

ただ、実際に譜面を作るとすぐに別の問題にぶつかりました。

開始時刻だけでは、LongやSlideを作るには足りないという問題です。

Singleノーツであれば、

この時刻に音が鳴った

だけでも十分です。

しかしLongやSlideでは、

どこから始まったか
どこまで続いているか

の両方が必要になります。

そのため、解析結果を単なる「点」ではなく、「区間」として扱う必要がありました。

startSec だけでは足りない

最初は、イベントを単純に、

startSec = 12.34

のような形で持たせるだけでも、かなり使えると考えていました。

実際、Singleノーツの候補としては十分です。

しかし、ボーカルのロングトーンやギターの持続音を見ると、

12.34秒で始まった

だけでは、

12.60秒までなのか
13.20秒までなのか
14.00秒までなのか

が分かりません。

人間が毎回波形を見たり、音を聞き直したりして終了位置を探すのであれば、せっかく解析しても制作支援としては中途半端です。

そこでeventに、

startSec
endSec

の両方を持たせる設計にしました。

「発音点」から「発音区間」へ

考え方としては、

onset

ではなく、

active interval

として音を見るイメージです。

例えば、

startSec = 24.120
endSec   = 24.860

というイベントがあれば、

24.120秒から24.860秒まで
何らかの音が持続している

と判断できます。

Editorでは、この区間を横方向のバーとして表示できます。

すると、単なる縦線よりも、

ここから音が始まる
ここまで音が続く

という情報が直感的に分かります。

終了時刻は「無音になった瞬間」ではない

ここで難しかったのが、endSecを何として定義するかです。

単純に考えると、

音量が閾値以下になった時刻

を終了位置にすればよさそうです。

しかし実際の音源では、それほど単純ではありません。

例えばボーカルでは、

母音が伸びる
残響が残る
リバーブだけが続く

といったことがあります。

ギターでも、

ピッキング後にサステインする
ディレイが残る
歪みによって減衰が長くなる

ことがあります。

そのため、

完全に無音になるまで

をendSecにすると、必要以上に長い区間になることがあります。

逆に閾値を高くすると、まだ音が続いているのに早く終了したことになります。

局所的なエネルギー変化を見る

そこで、開始後の特徴量を追いかけて、

一定以上のエネルギーが維持されている区間

をイベントの継続候補として扱う方法を試しました。

概念的には、

onset
  ↓
energy high
energy high
energy medium
energy medium
energy low
  ↓
end

のようにします。

単純な絶対閾値だけではなく、

周辺の背景レベル
イベント開始時の強さ
時間方向の減衰

も考慮します。

これにより、静かな曲と大きな曲で同じ固定閾値を使うよりは安定しました。

endSec は厳密な音楽理論上の終端ではない

ここはかなり重要です。

今回のendSecは、

楽譜上の正確な音価を推定したものではありません。

例えば、

四分音符
八分音符
付点二分音符

のような楽譜的な長さを推論しているわけではありません。

あくまで、

音響的に、この区間ではこのstemのイベントが継続している可能性が高い

というガイドです。

この違いは大きいです。

もし自動採譜を目指すなら、

この音符は正確に八分音符である

まで求めたくなります。

しかし制作支援であれば、

だいたいこの辺まで伸びている

ことが分かるだけでも、LongやSlideの終端を探す作業はかなり楽になります。

Longノーツ制作との相性が良かった

Editorでは、Longノーツを配置するときに、

開始位置
終了位置

の両方を指定します。

もし解析イベントが、

startSec = 35.42
endSec   = 36.78

を持っていれば、Longノーツをその区間に近づけるだけで、

開始をstartSecにSnap
終了をendSecにSnap

という操作ができます。

これはかなり実用的でした。

人間が耳だけで、

歌い始めはここ
伸ばし終わりはここ

を探すよりも、最初から候補区間が表示されている方が圧倒的に早いです。

Slideにも使える

Slideでも同じ考え方が使えます。

例えば、

ボーカルが1秒程度伸びる

区間があったとします。

その区間全体をLongにしてもいいですし、

開始
↓
途中でレーン移動
↓
終端

というSlideにしても構いません。

解析側は、

この音をどの種類のノーツにするか

までは決めません。

Analyzerが提供するのは、

この音はこの区間に存在する

という情報だけです。

最終的に、

Singleにする
Longにする
Slideにする
あえてノーツを置かない

という判断は人間が行います。

vocalsでは特に有効だった

startSec / endSecというイベントモデルは、特にvocalsとの相性が良いと感じました。

ボーカルでは、

子音
母音
ロングトーン
フレーズの切れ目

が比較的はっきりしていることがあります。

例えば、

「きーらーめーくー」

のような歌唱では、単純なonsetだけを見るより、

この母音がどこまで続いているか

が分かる方が、譜面表現を考えやすくなります。

音ゲーでは、ボーカルの伸びに合わせてLongやSlideを置くことが多いため、この情報は非常に有用でした。

drumsでは終了時刻の意味が薄い

一方で、すべてのstemにendSecが同じ重要度で必要なわけではありません。

drumsのような打楽器では、

開始時刻

の方が圧倒的に重要です。

スネアやキックは、音響的には余韻があっても、譜面上では瞬間的なSingleとして扱うことが多いからです。

つまり、

vocals
guitar
piano

では区間情報が重要になりやすく、

drums

ではonsetが重要になりやすい、という違いがあります。

この点からも、

stemごとに同じ情報を同じ重みで扱うべきではない

と分かりました。

endSecにもconfidenceが必要

開始時刻に比べると、終了時刻はさらに曖昧です。

例えば、

音は弱くなったがまだ残っている

という状態を、

継続

と判断するか、

終了

と判断するかは難しいです。

そのため将来的には、

startConfidence
endConfidence

のように、開始と終了で別の信頼度を持たせてもよいと考えています。

PoCではそこまで細分化していませんが、

endSecはstartSecより不確実

という前提で扱うのが現実的です。

重なった音では境界が曖昧になる

終了時刻の推定で難しかったのは、同じstem内で音が連続するケースです。

例えばギターで、

音A
↓
音B
↓
音C

がほぼ途切れず続く場合、

Aの終了
Bの開始

をどこで分けるべきか曖昧になります。

ボーカルでも、

1つの音節が終わる
次の音節が始まる

境界が滑らかにつながることがあります。

この場合、単純なエネルギーだけではイベントを分離しにくくなります。

ここは今後、

pitch変化
spectral change
onset strength

などを組み合わせることで改善できる可能性があります。

区間情報はEditorで価値が増える

重要だったのは、endSec単体の精度よりも、

Editor上で区間として見えること

でした。

タイムライン上に、

|------ vocals ------|
       |-- guitar --|
            |drum|

のように表示されると、曲の構造がかなり見やすくなります。

波形だけでは分かりにくかった、

このボーカルの途中でギターが入る
この区間ではドラムが細かい
ここで各パートが同時に終わる

といった情報が視覚的に確認できます。

結果として、イベント解析は、

ノーツ候補を生成するため

だけではなく、

楽曲構造を視覚化するため

にも役立ちました。

「点」より「区間」の方が制作支援には強い

今回のPoCでかなり大きかった発見の一つが、

譜面制作支援では、発音点だけより発音区間の方が情報量が圧倒的に多い

ということです。

onsetだけなら、

ここで何か鳴った

までしか分かりません。

startSec / endSecがあれば、

ここから鳴り始めた
ここまで続いた

が分かります。

さらにstem情報と組み合わせると、

この区間はvocals
この区間はguitar
この瞬間はdrums

という形で楽曲をかなり具体的に読み解けます。

これが後のSnap機能やLong / Slide制作に直結しました。

ただし「音の高さ」はまだ分からない

ここまでで、

いつ始まったか
どこまで続いたか
どのstemか

はある程度分かるようになりました。

しかし、旋律系のパートについてはもう一つ重要な情報があります。

それが、

音の高さ、つまりpitch

です。

同じボーカルの区間でも、

同じ音程を伸ばしている
途中で音程が上がった
途中で音程が下がった

では、譜面表現を変えられる可能性があります。

そこで次に、torchcrepeを使ってpitch解析も試しました。

次章では、pitch情報をどのように取得し、譜面制作へ利用できるかを検証した内容について紹介します。

7. ピッチ解析 ― torchcrepe

ここまでの解析で、

いつ音が始まったか
どこまで続いたか
どのstemに属するか

はある程度取得できるようになりました。

ただし、ボーカルやギター、ピアノのような旋律系のパートでは、もう一つ重要な情報があります。

それが pitch(音高) です。

同じ1秒間のボーカルでも、

同じ音程を伸ばしている
途中で上がっている
途中で下がっている
細かく上下している

では、譜面として表現したい動きが変わります。

そこでPoCでは、pitch推定も試しました。

使用したのが torchcrepe です。

pitchを取ろうと思った理由

今回のPoCの目的は、自動で楽譜を起こすことではありません。

それでもpitchを取ろうとした理由は、音の上下方向の動きを譜面制作のガイドに利用できる可能性があると考えたからです。

例えばボーカルが、

低い音
↓
高い音
↓
さらに高い音

と動いている場合、Slideノーツも、

左
↓
中央
↓
右

あるいは逆方向へ動かすことで、メロディの動きを視覚的に表現できます。

もちろん、

pitchが高い = 必ず右レーン
pitchが低い = 必ず左レーン

のような固定ルールにしたいわけではありません。

あくまで、

譜面制作者がフレーズの形を考える材料

として使えないかを試しました。

torchcrepe

pitch推定にはtorchcrepeを使用しました。

CREPEはニューラルネットワークを用いたpitch estimationの手法で、torchcrepeはPyTorchベースで利用できます。

今回の環境ではGPUを使って処理できるため、Demucsなど他の解析処理と同じPython / PyTorch系のパイプラインへ比較的自然に組み込めました。

概念的には、

Audio
↓
torchcrepe
↓
time + frequency + confidence

という出力を得ます。

例えば、

12.000 sec  440 Hz
12.010 sec  442 Hz
12.020 sec  445 Hz
12.030 sec  447 Hz
...

のように、時間に対する周波数の変化を追うことができます。

mix全体ではなくstemに対して解析する

pitch推定でも、Demucsによるstem分離が役立ちます。

完成音源には、

ボーカル
ベース
ギター
ピアノ

など、複数のpitchを持つ音が同時に存在します。

その状態で単一のpitchを推定しても、

何の音程を追跡しているのか

が分かりにくくなります。

そこで例えば、

vocals stem
↓
torchcrepe

とすることで、主にボーカルのpitch contourを取得します。

同様に、条件が合えばguitarなどのstemにも適用できます。

つまりDemucsは、event detectionだけでなくpitch解析の前処理としても有効でした。

pitch contourとして見る

今回重要だったのは、単一の音名を当てることより、

時間方向のpitch contourを見ること

でした。

例えば、

440
445
453
470
500
530

と上がっていれば、

pitchが上昇している

ことが分かります。

逆に、

520
500
480
460
440

なら下降しています。

譜面制作支援という観点では、

これはA4です
これはB4です

と厳密に音名へ変換するより、

このフレーズは上がっている
この区間はほぼ一定
ここで大きく下がった

ことが分かる方が、Slideの形を考える際には有用です。

Slideとの相性が良い

pitch情報を使える可能性が最も高いと感じたのはSlideです。

例えばボーカルが、

──────╮
      ╰──────

のように音程を下げるフレーズなら、Slideも同じ方向へ流すことで、音と操作を対応させられます。

逆にpitchが上昇するなら、

╭──────
╯

のような形にできます。

ここで重要なのは、pitchからSlideを自動生成することではありません。

例えば同じフレーズでも、

音程の動きをそのままSlideにする
リズムだけをSingleで取る
歌詞のアクセントをFlickにする

など、譜面デザインには複数の正解があります。

pitchはその判断材料の一つとして使います。

pitchにもconfidenceがある

pitch estimationは常に正確な値が出るわけではありません。

特に、

無声音
息
子音
残響
伴奏の漏れ
Demucsのartifact

などがある区間では、推定値が不安定になります。

そのためtorchcrepeのconfidenceも重要です。

概念的には、

time
frequency
confidence

の3つを扱います。

confidenceが低い場所については、

pitchとして信用しない

あるいは、

Editor上で弱く表示する

といった使い方ができます。

ボーカルは常にきれいな一本線にはならない

実際の歌声にpitch estimationをかけると、教科書的な一本線にはなりません。

例えばビブラートがあれば、

~~~~~~

のように細かく上下します。

しゃくりやフォールがあれば、大きなpitch変化になります。

さらに子音区間ではpitchが取れなかったり、突然別の周波数へ飛ぶこともあります。

そのため、生のpitch seriesをそのままEditorへ表示すると情報量が多すぎます。

smoothingが必要になる

譜面制作で欲しいのは、10ms単位の微細な揺れではなく、

このフレーズは大きく上昇している

といった形です。

そのため実用化するには、

median filter
moving average
confidence threshold
短い異常値の除去

などによるsmoothingが必要になります。

例えば生データが、

440
443
438
446
441
520
444
447

となっていたとして、520Hzだけが誤検出なら、そのまま使うと不自然なpitch jumpになります。

局所的な中央値などを使えば、このような外れ値を抑制できます。

image.png

キラメキ☆Cメロ、ボーカルをスライドノーツで表現した個所の例

eventとの組み合わせが重要

pitchを単独で扱うより、前章までで作ったevent情報と組み合わせた方が有用です。

例えば、

vocals event
startSec = 20.2
endSec   = 21.4

があった場合、この1.2秒間だけpitch seriesを見ることができます。

すると、

event 1
20.2 ───────── 21.4
pitch     ↗↗↗↗↗

という情報になります。

つまり、

event
=
どの区間を見るか

pitch
=
その区間の中でどう音が動いているか

という役割分担です。

この方が、楽曲全体のpitchを延々と追うより譜面制作には使いやすくなります。

pitchからレーン位置を直接決めることはしなかった

PoCでは、pitchをそのままレーン位置へ変換する処理までは採用しませんでした。

例えば、

低い音 → 左
高い音 → 右

というマッピングは簡単に実装できます。

しかし実際の譜面では、レーン位置は音程だけでは決まりません。

前後のノーツ
手の動かしやすさ
難易度
視覚的な美しさ
歌詞
リズム
Flick方向

など、多くの要素があります。

pitchだけで位置を決めると、

音楽的には正しいがゲームとして叩きにくい

譜面になりやすいと考えました。

そのため今回も、

解析は候補を提示するだけ

という方針を維持しました。

楽器によってpitch解析の難しさが違う

torchcrepeは、基本的には単音性の音源と相性が良いです。

ボーカルのように、

基本的に1人が1つの主旋律を歌う

場合は比較的扱いやすいです。

一方でguitarやpianoは、

コード
複数音
倍音

を含むことがあります。

例えばギターのコードを鳴らした場合、

どのpitchを代表値として取るのか

という問題が出ます。

そのため、

vocals → torchcrepe

は比較的自然でも、

piano → torchcrepe

が常に同じように有効とは限りません。

polyphonicなパートについては、別の解析手法を検討する余地があります。

pitch解析はPoCの中でも発展途上

今回試した技術の中で、pitchは特に、

将来的な可能性は大きいが、現時点ではまだ使い方を詰める余地が大きい

部分でした。

beatはそのままgridとして使えます。

onsetはSingleノーツ候補としてかなり分かりやすく使えます。

startSec / endSecもLongの候補として直感的です。

一方pitchは、

値が取れた
↓
それをどのような制作支援UXに変えるか

の間に、まだ設計の余地があります。

それでも「音の形」が見える

一方で、pitchを取ることで得られる情報量は非常に大きいです。

ここまでの解析では、

いつ鳴ったか
どこまで続いたか

まででした。

pitchを加えると、

その間に音がどう動いたか

まで分かります。

つまり、

time
+
duration
+
source
+
pitch

が揃うことで、楽曲内のイベントをかなり立体的に表現できます。

これは将来的に、

Slide形状の候補提示
メロディに沿ったノーツ配置
フレーズ単位の可視化
同音反復の検出

などへ応用できると考えています。

複数の解析を組み合わせる

ここまでで試した解析を並べると、

Demucs
    → 何のパートか

Beat This!
    → 拍はどこか

onset / event detection
    → 音がいつ始まったか

duration estimation
    → どこまで続くか

torchcrepe
    → 音程がどう動いたか

となります。

どれか一つだけでは、譜面制作を大きく支援するには足りませんでした。

しかし、それぞれが異なる情報を持っています。

今回のPoCで重要だったのは、これらを個別の解析結果のまま放置せず、一つの共通データへ統合したことです。

次章では、beat、stem event、pitch、confidenceなど、異なる解析結果をAnalysis JSONへまとめ、AnalyzerとEditorの契約として扱った方法について紹介します。

8. 解析結果をどう統合したか ― Analysis JSONとSchema

image.png

今回のPoCで作成した譜面データ

ここまでで、

Demucs
Beat This!
onset / event detection
duration estimation
torchcrepe

と、複数の解析を試しました。

ただし、これらの結果をそれぞれ別々の形式で保持していると、Editor側の実装が非常に複雑になります。

例えば、

Beat This!の出力形式
Demucsのstem構成
torchcrepeのpitch series
独自event detectorの結果

をEditorが個別に理解し始めると、AnalyzerとEditorが強く結合してしまいます。

そこで今回のPoCでは、解析結果を一度 Analysis JSON に正規化し、AnalyzerとEditorの間の共通契約として扱うようにしました。

Editorに解析ライブラリを意識させない

設計上かなり重視したのが、

Editorは「どのライブラリで解析したか」を知らなくてよい

という点です。

例えばEditorから見れば、

12.34秒にguitarのeventがある
confidenceは0.82
13.02秒まで継続している

という情報が分かれば十分です。

そのイベントを生成した内部処理が、

librosa
rolling median
local peak detection
将来的な別のMLモデル

のどれであっても、Editorには関係ありません。

そのため、

解析アルゴリズム
↓
共通形式へ変換
↓
Analysis JSON
↓
Editor

という構造にしました。

解析結果を共通の時間軸へ集約する

Analysis JSONでは、すべての解析結果を基本的に「秒」を基準とした時間軸へ載せます。

例えば、

beat       10.500 sec
downbeat   11.760 sec

vocals
  start    12.100 sec
  end      12.840 sec

guitar
  start    12.460 sec
  end      12.610 sec

という形です。

これによりEditorでは、

秒
↓
画面上のX座標

という共通変換だけで、異なる解析結果を同じタイムラインへ表示できます。

beatだけ別の座標系、pitchだけ別の座標系、といった設計にはしていません。

これは後のSnap処理でも非常に重要になりました。

beatとdownbeat

beat系の情報は、概念的には時刻の配列です。

{
  "beats": [
    0.84,
    1.26,
    1.68
  ]
}

downbeatも同様です。

{
  "downbeats": [
    0.84,
    2.52,
    4.20
  ]
}

Editorはこの値を使って、

beat grid
小節線
Snap候補

を生成できます。

重要なのは、Editorが再度BPM解析をしなくてよいことです。

stem event

各stemの解析結果は、単なる時刻一覧より少し情報量を増やしました。

概念的には、

{
  "startSec": 12.34,
  "endSec": 12.81,
  "confidence": 0.86
}

のようなeventです。

さらに、

vocals
drums
bass
guitar
piano
other

のどのレイヤーに属するかが分かるようにします。

これによりEditorでは、

vocalsだけ表示
drumsだけ表示
guitarだけ表示

といった切り替えができます。

point eventとinterval eventを同じ世界で扱う

実際の解析結果には、

瞬間的なイベント

と、

ある程度の長さを持つイベント

の両方があります。

例えばdrumsは、

12.34秒でスネア

というpointに近い情報です。

一方vocalsでは、

12.34秒から13.12秒まで発音

というintervalが意味を持ちます。

そこで、データモデルとしては、

startSec
endSec

を基本にしておくと扱いやすくなります。

非常に短いイベントであれば、

endSec ≒ startSec

として扱えます。

この方がEditor側で、

これはpoint用の別型
これはinterval用の別型

と大量に分岐する必要がありません。

pitchは時間系列として保持する

pitchについてはeventとは性質が違います。

pitchは、

ある瞬間の値

が時間方向に連続するseriesだからです。

概念的には、

[
  {
    "timeSec": 12.00,
    "frequencyHz": 440.0,
    "confidence": 0.95
  },
  {
    "timeSec": 12.01,
    "frequencyHz": 443.2,
    "confidence": 0.94
  }
]

のようなデータになります。

このseriesをeventのstartSec / endSecと組み合わせれば、

このvocal eventの中で
pitchがどう変化したか

を見ることができます。

つまりAnalysis JSONでは、

離散イベント
+
連続的な時系列

の両方を持てるようにする必要がありました。

confidenceも共通の概念にする

解析結果を統合する上で便利だったのがconfidenceです。

例えば、

guitar event  confidence 0.92
vocal event   confidence 0.61
pitch point   confidence 0.37

というように、

その解析結果をどの程度信用できるか

をデータとして残せます。

もちろん、異なるアルゴリズムのconfidenceを単純比較できるとは限りません。

例えば、

Beat This!のconfidence 0.8
torchcrepeのconfidence 0.8
独自event detectorのconfidence 0.8

が、まったく同じ意味を持つとは限りません。

そのため現時点では、

0.8なら必ず同じ品質

という絶対尺度としてではなく、

その解析器の中での相対的な信頼度

として扱う方が安全です。

Schemaを「契約」として使う

Analysis JSONにはJSON Schemaを用意しました。

目的は単なるvalidationだけではありません。

Schemaを、

AnalyzerとEditorの間の契約

として使うためです。

例えばAnalyzer側を変更して、

start → startSec

のように勝手にフィールド名を変えれば、Editorとの契約違反になります。

逆にSchemaに正式に変更を加えれば、

Analyzer
Editor
fixture
example
test

のどこを追従させる必要があるのか明確になります。

PoCでは仕様変更がかなり多かったため、この仕組みは特に有効でした。

最初から完璧なSchemaを設計しなかった

今回のAnalysis Schemaは、最初に机上で完成形を決めたわけではありません。

実際の曲を解析し、

この情報も必要だった
これは実データでは使いにくかった
この表現ではEditor側が困る

という問題を確認しながら進化させました。

実際、PoC中にはAnalysis contractを実データから見直し、0.2系へ更新しています。

これは今回のような探索的PoCでは正しい進め方だったと思っています。

音響解析の出力が実際にどうなるかを確認せずに、

理想的なデータモデル

を先に固定すると、現実のデータに合わないSchemaになりやすいからです。

exampleとfixtureもSchemaと一緒に管理する

Schemaだけを書いても、

実際にどんなJSONになるのか

は分かりにくいです。

そこで、

analysis.schema.json
analysis.example.json
test fixtures

のように、実例とテストデータも一緒に管理しました。

これにより、

Schema上ではvalidだが、
実際のEditorでは想定していない

というズレを減らせます。

特にSchema変更時に、

既存fixtureが通るか
exampleがvalidか

を自動検証できることは、PoC後半でかなり重要になりました。

project fileとanalysisを分離する

Editorで実際の譜面制作を始めると、

解析結果
譜面
Editorの表示設定
プロジェクト情報

など、保存したい情報が増えていきます。

ここでも、

Analysis JSON = 楽曲解析結果
Chart JSON    = 最終的な譜面
Project       = Editorで作業を再開するための状態

という役割を分けました。

この分離によって、例えばAnalyzerを再実行しても、

人間が作ったChartそのもの

を不用意に上書きせずに済みます。

解析結果を再利用できる

Analysis JSONを独立した成果物にしたことで、一度解析した楽曲を何度も使い回せます。

例えば、

1回目
MASTER相当の譜面を制作

2回目
低難易度譜面を制作

3回目
別のゲーム形式へ変換

といった場合でも、音響解析を毎回やり直す必要はありません。

同じAnalysis JSONを使いながら、複数のChartを作ることができます。

これは処理時間の節約だけでなく、

同じ解析結果を基準に複数譜面を比較できる

という意味でも便利です。

Analyzerを交換できる

この構成のもう一つのメリットは、Analyzerの内部技術を将来交換できることです。

例えば現在は、

Demucs
Beat This!
torchcrepe
librosa
独自event detection

を使っています。

将来より良いモデルが出てきた場合、

新しいbeat tracker
新しいsource separation model
新しいpitch estimator

へ差し替えても、

最終的に同じAnalysis Schemaを出せばEditorはそのまま使えます。

PoC段階で特定ライブラリの出力形式へEditorを直接依存させなかったことは、かなり大きな利点でした。

Editorから見れば「解析レイヤー」にすぎない

最終的にEditorから見ると、各解析技術は、

beat layer
vocals layer
drums layer
bass layer
guitar layer
piano layer
pitch information

という制作ガイドです。

Demucsを使っていることも、Beat This!を使っていることも、EditorのUIから見れば本質ではありません。

重要なのは、

この時刻に
この種類の
この程度信頼できる情報がある

ということです。

この抽象化ができたことで、後からguitarやpianoの解析レイヤーを追加しても、Editor全体を作り直す必要はありませんでした。

JSONを境界にしたことはかなり成功だった

今回のPoCでは解析アルゴリズムそのものに目が行きがちですが、システム全体として見ると、

JSON Schemaを境界にした設計はかなり重要だった

と感じています。

Analyzer
↓
Analysis JSON
↓
Editor
↓
Chart JSON
↓
Player

と分けたことで、

音響解析を改善する
EditorのUXを改善する
Playerを作り直す

という作業を、それぞれ比較的独立して進められました。

実際、PoCの途中ではAnalyzerもEditorもPlayerも大きく変化しましたが、この境界自体は維持できています。

解析技術より「統合」が重要だった

個々の技術だけを見ると、

Demucsでstemを分離できた
Beat This!でbeatが取れた
torchcrepeでpitchが取れた

という話になります。

しかし、譜面制作支援として本当に価値が出たのは、それらを、

同じ時間軸
同じプロジェクト
同じEditor

の上へ載せたときでした。

複数の解析結果を統合すると、

ここはbeat
同時にdrumsが鳴っている
少し遅れてvocalsが始まる
そのvocalsはここまで伸びる
pitchは途中から上昇する

という形で、一つの楽曲イベントを複数の視点から見ることができます。

この状態になって初めて、

音響解析が実際の譜面制作の道具になった

と感じました。

次章では、このAnalyzerを実際の「キラメキ☆」へ適用したときに、どの程度の解析量・処理時間になったのか、PoCの実測結果をまとめます。

9. 解析性能と実測結果

ここまで紹介した解析を、実際の楽曲に対して一通り実行しました。

PoCで主に使用したのは、U149の「キラメキ☆」です。

入力した音源の情報は次の通りでした。

Duration     201.040 sec
Sample Rate  44.1 kHz
Channels     Stereo
Format       FLAC / PCM_16
Frames       8,865,864

約3分21秒の楽曲です。

単純な数秒のテスト音源ではなく、実際に譜面を制作する1曲を最初から最後まで解析することで、

解析精度
処理時間
イベント数
Editorで扱える情報量

をまとめて確認しました。

Analyzer全体では約53秒

この実音源に対して、当時のAnalyzerの一連の処理を実行した結果、

Audio duration  201.04 sec
Analysis time    52.92 sec
RTF               0.263

となりました。

RTFはReal Time Factorで、

処理時間 / 音源時間

です。

今回の場合、

52.92 / 201.04 ≒ 0.263

なので、音源の実時間に対して約3.8倍速で解析できたことになります。

つまり約3分21秒の曲を、1分未満で一通り解析できました。

譜面制作支援として考えた場合、この程度であれば十分現実的だと感じました。

例えば解析に30分かかるのであれば、

パラメータを変える
↓
再解析する
↓
Editorで結果を見る

という試行錯誤がかなり重くなります。

一方、1曲を1分弱で処理できるのであれば、制作工程の中に組み込んでもそれほど大きな待ち時間にはなりません。

各処理の時間

当時の計測では、処理時間はおおむね次のようになりました。

Demucs separation       8.17 sec
Beat This!              1.59 sec
torchcrepe / bass      21.88 sec
torchcrepe / vocals    18.90 sec
librosa / drums         1.45 sec
librosa / other         0.93 sec

この結果を見ると、ボトルネックはかなり明確です。

pitch解析に使ったtorchcrepeが処理時間の大部分を占めていました。

一方で、

Beat This!
librosaによるevent解析

はかなり軽量でした。

Demucsによるsource separationも、GPUを利用することで201秒の楽曲を約8秒で処理できています。

処理時間の大半はpitch解析

各処理のRTFを見ると、さらに分かりやすくなります。

Demucs                0.0407
Beat This!            0.0079
torchcrepe / bass     0.1088
torchcrepe / vocals   0.0940
librosa / drums       0.0072
librosa / other       0.0046

torchcrepeをbassとvocalsへ実行した部分だけで、

約40.8秒

を占めています。

つまりPoC時点では、

pitch情報をどこまで必要とするか

によってAnalyzer全体の速度をかなり変えられます。

例えばbeatとonset中心の制作支援であれば、pitch解析を省略することで大幅に高速化できる可能性があります。

逆に、

ボーカルの音程変化をSlide制作へ利用したい

のであれば、この処理時間を払ってpitchを取得する価値があります。

これは、Analyzerを一枚岩にせず各解析を独立させておいた利点でもあります。

Demucsは約8秒

このベンチマーク時点で使用したDemucsは、4stemのhtdemucsでした。

drums
bass
other
vocals

を生成しています。

201.04秒の入力に対して、

8.17 sec
RTF 0.04065

でした。

GPUメモリについても、計測時のpeakは、

allocated  約549 MiB
reserved   約770 MiB

程度でした。

そのためRTX 3060 12GBでは、VRAM容量そのものが問題になるような処理ではありませんでした。

なお、その後Editorの制作ガイドを増やす過程で、guitarとpianoを独立させるためにhtdemucs_6sも導入しています。

つまりPoCは、

初期
4stemで基本パイプラインを検証

↓

後続
6stemへ拡張し
guitar / piano guideを追加

という順序で進化しました。

Beat This!では461 beats / 117 downbeats

Beat This!による解析では、

beats      461
downbeats  117

が検出されました。

beat間隔の中央値は、

0.42 sec

で、ここから求めた代表的なテンポは、

142.857 BPM

でした。

downbeat間隔の中央値は約、

1.66 sec

です。

4拍分として考えると、

0.42 × 4 ≒ 1.68 sec

なので、基本的な拍構造としてもほぼ整合しています。

このbeat列をEditorへ持っていくことで、手動でBPMとoffsetを設定しなくても、最初からかなり実用的なBeat Gridを表示できました。

完璧ではなかった部分もある

一方で、beat解析が完全だったわけではありません。

検出された最後のbeatは、

189.84 sec

でした。

音源そのものは、

201.04 sec

あるため、最後に約11.2秒のbeatが存在しない区間があります。

曲の末尾や余韻などの構造によるものもありますが、

解析結果が曲の全区間を完全に説明しているわけではない

ことは、この結果からも分かります。

また曲冒頭などにも、人間が見れば不自然に感じるbeat候補が含まれるケースがありました。

そのためEditorでは、解析結果を絶対的な正解とはせず、あくまでガイドとして扱っています。

drum onsetは1,093件

drumsに対するイベント解析では、

1,093 onsets

が得られました。

201秒で1,093件なので、単純平均すると、

約5.4 events / sec

です。

当然ながら、この1,093件をすべてゲームのノーツにするわけではありません。

実際のドラムには、

kick
snare
hi-hat
細かな装飾音
残響由来の変化

などが大量に含まれています。

この数字は、

Analyzerがどの程度細かい候補をEditorへ渡しているか

を示しています。

制作支援としては、候補がゼロに近いより多少多めの方が使いやすく、人間が必要なものだけを選択できます。

pitch runは940件

pitch解析側では、最終的に、

940 pitch runs

が生成されました。

生のpitch推定値をそのまま大量の時系列点として扱うのではなく、ある程度まとまりのあるrunへ整理した結果です。

これにより、

この区間では音程が安定している
ここで上昇する
ここで下降する

といった情報を扱いやすくしました。

beatやdrum onsetとは性質が異なる情報ですが、すべてを共通の時間軸へ載せることでEditorから同時に利用できます。

解析量が多くてもEditorではレイヤーとして扱える

ここまでの結果をそのまま並べると、

461 beats
117 downbeats
1,093 drum onsets
940 pitch runs
その他stemのevent

と、かなり大量の情報になります。

最初は、

こんなに表示したら逆に譜面を作りにくいのではないか

という懸念もありました。

実際、全部を同じ表示方法で重ねると見づらくなります。

そこでEditorでは、

Beat
Vocals
Drums
Bass
Guitar
Piano
...

のように解析結果をレイヤーとして扱うようにしました。

必要なレイヤーだけを有効にすれば、

今はボーカルに合わせて作る
今はギターを確認する
今はbeatだけを見る

という使い分けができます。

解析量を減らすのではなく、

多く解析しておいて、Editor側で必要な情報を選ぶ

という方向です。

「解析速度」より「人間の探索時間を減らせるか」

今回の52.92秒という数字そのものより重要だったのは、

その約53秒で、人間が何度も曲を聞いて探していた情報を一括して得られること

でした。

従来は例えば、

再生する
↓
少し戻す
↓
もう一度聞く
↓
波形を見る
↓
ノーツを置く
↓
タイミングを微調整する

という操作を、1曲の中で何度も繰り返します。

Analyzerを通した後は、

beatが表示されている
stemごとのeventが表示されている
発音区間が表示されている
pitch情報も存在する

状態から始められます。

そのため、解析に1分弱かかっても、譜面制作全体で見れば十分に元が取れると感じました。

PoCとして性能面でも成立した

今回確認したかったのは、研究用途で高精度な解析ができるかだけではありません。

実際の制作ツールとして、

1曲を入れる
↓
現実的な時間で解析が終わる
↓
Editorを開く
↓
すぐ譜面制作に使える

ところまで成立するかでした。

約3分21秒の実楽曲を約53秒で解析できたことから、少なくとも今回のPC環境では、

音源分離・beat解析・event解析・pitch解析を組み合わせても、インタラクティブな制作工程へ組み込める範囲

だと判断しました。

さらに解析の中身を見ると、大半の時間をpitch解析が占めています。

そのため用途に応じて、

高速モード
    beat + onset中心

詳細モード
    pitchまで含める

のように分ければ、さらに実用性を高められそうです。

解析そのものより、次の問題が大きかった

ここまでで、

音源を分離できた
beatを取れた
発音イベントを取れた
発音区間を取れた
pitchも取れた
処理速度も現実的だった

というところまでは確認できました。

ここで次の問題が出てきます。

この大量の解析結果を、人間が実際に譜面を作るときにどう使うのか。

解析結果をJSONファイルとして出力しただけでは、制作作業は楽になりません。

大量のtimestampをテキストエディタで眺めながらノーツを置くのであれば、本末転倒です。

そこでPoCの次の大きなテーマになったのが、

解析結果を直接「譜面制作の操作」に結びつけるEditor

でした。

次章では、Analysis JSONをタイムライン上のガイドとして表示し、実際の譜面制作へ利用するために作ったEditorについて紹介します。

10. 解析結果を譜面Editorのガイドにする

Analyzerで大量の情報を取れるようになっても、それだけでは譜面制作は楽になりません。

例えば、

12.340 sec vocals start
12.580 sec guitar event
12.600 sec beat
12.910 sec drums event

という結果がJSONに並んでいても、人間がそれを見ながらEditorへ手入力するのであれば、実用性はかなり低いです。

そこで次に取り組んだのが、

解析結果をそのまま譜面Editor上の「ガイド」として表示すること

でした。

AnalyzerとEditorを直接つながない

前章まででも触れた通り、AnalyzerとEditorは直接結合していません。

流れは、

Audio
↓
Analyzer
↓
Analysis JSON
↓
Editor

です。

EditorはAnalysis JSONを読み込み、その内容をタイムラインへ描画します。

この構成にしたことで、Editor側はDemucsやBeat This!、torchcrepeを直接実行する必要がありません。

Analyzerの中身が変わっても、Analysis JSONの契約さえ維持すればEditor側はそのまま使えます。

タイムライン上に解析レイヤーを表示する

image.png

キラメキ☆の冒頭箇所、ドラムが入る部分が一目でわかる

Editorでは、Analysis JSONに含まれる情報を複数のレイヤーとして表示します。

主なものは、

Beat
Vocals
Drums
Bass
Guitar
Piano

です。

例えば、画面上では概念的に、

Beat    |    |    |    |    |    |

Vocals  ========      ========

Drums      |  | |   |    | |

Guitar   ---|----|------|---

Piano            |-----|

のように、同じ時間軸に複数の情報を重ねます。

この形にすると、単に波形を見るだけでは分かりづらかった、

このbeatでドラムが鳴っている
少し後からボーカルが始まる
ボーカルの途中でギターが入る
ここで複数パートが同時に鳴る

といった関係が一目で分かります。

「何の音に合わせるか」を選べる

音ゲーの譜面には、唯一の正解があるわけではありません。

同じ曲でも、

ボーカル主体
ドラム主体
ベース主体
メロディ主体

でまったく違う譜面を作れます。

そのため、解析結果を全部一つにまとめて、

ここにノーツを置くべき

と決めるのではなく、レイヤーごとに分けて表示しました。

例えば、

今はVocalsだけ見る
次はDrumsを確認する
サビではGuitarも表示する

という使い方ができます。

この構成は、今回の「自動採譜ではなく制作支援」という目的と相性が良かったです。

Beat Grid

Beat This!で得たbeatとdownbeatは、そのままEditor上のグリッドとして使いました。

beatは通常の縦線、downbeatはより強い小節線として表示できます。

これにより、

小節頭
四分音符
拍間

の位置関係を視覚的に確認できます。

さらに、beat間を分割することで、

1/2
1/4
1/8
1/16

などの細かいグリッドも作れます。

これだけでも、完全に手作業でoffsetとBPMを合わせるよりかなり楽になりました。

eventは「点」ではなく「バー」として見せる

前章までで、解析eventには、

startSec
endSec

を持たせました。

Editor上では、この情報を時間幅のあるバーとして表示できます。

例えば、

Vocals   |==========|
Guitar       |===|

のような形です。

これにより、

ここで音が始まる
ここまで続く

が直感的に分かります。

LongやSlideを作る場合には、単なるonsetの縦線より、この区間情報の方がはるかに便利でした。

stemごとの個別再生

解析レイヤーを見ていると、

このguitar eventは本当にギターなのか
このbass eventは何の音なのか

を耳で確認したくなります。

そこでEditorには、Demucsで分離したstemを個別に再生する機能も追加しました。

例えば、

Vocals only
Drums only
Bass only
Guitar only
Piano only

のように聞き分けられます。

これは解析アルゴリズム以上に、実際の譜面制作では便利でした。

完成音源では埋もれていた音も、stem単体にするとかなり聞きやすくなります。

音量調整

stemを個別再生できるようにすると、次に必要になるのが音量調整です。

例えば、

原曲を小さくする
Guitarだけ少し大きくする
Vocalsを一時的に消す

といった使い方ができると、特定のパートを追いやすくなります。

DAWのような本格的なMixerを目指したわけではありませんが、

解析結果を目で見ながら、対象stemを耳でも確認できる

ところまで持っていくことで、制作支援としての価値が大きく上がりました。

ズーム

image.png

創作譜面制作においてズーム機能は必須である。とあるパートに集中しすぎると譜面全体としてのバランスを欠いてしまう。「木も見て、森も見る」姿勢は大事であるし、そのための仕組みは制作支援としては入れてあげる必要がある。

実際に譜面を作り始めると、ズーム機能も必須でした。

曲全体を見るときと、

数百ms単位でノーツ位置を合わせる

ときでは、必要な表示縮尺がまったく違います。

そこでタイムラインを拡大・縮小できるようにしました。

ズームに応じて、

beat grid
ノーツ
event bar
再生位置

の座標をすべて同じ時間軸から再計算します。

これにより、曲全体の構造を確認する場合と、細かいタイミング調整を行う場合を行き来できます。

横スクロール

1曲を横方向のタイムラインとして扱うため、横スクロールも必要です。

Editorでは、

現在見ている時間範囲

をviewportとして管理し、スクロール位置と再生位置が同じ時間軸を共有するようにしました。

このあたりは、PoCを実際に1曲通して使ってみないと必要性が見えにくい部分でした。

数秒のテストデータでは問題なくても、200秒以上の曲を扱うと、

どこを見ているのか
再生位置がどこへ行ったのか

がすぐ分からなくなります。

再生位置への追従

そこで、再生中は現在位置をEditorが追従できるようにしました。

ただし、常に画面中央へ固定すると編集しにくくなるため、

一定位置まで再生ヘッドが進む
↓
viewportをスクロールする

という方式にしました。

この機能も、解析技術とは直接関係ありません。

しかし実際の制作では、こうしたEditor側の操作性が、解析精度と同じくらい重要でした。

再生速度の変更

細かいノーツ配置を確認するため、

1.0x
0.5x
0.25x
0.1x

などの低速再生も追加しました。

特に、

ボーカルの細かい子音
連続するギター
細かいFlick

を確認するときには、等速再生だけではタイミングを取りづらいことがあります。

低速再生と解析ガイドを組み合わせることで、

目で候補位置を見る
耳で低速確認する

という作業ができます。

配置した位置で音を鳴らす

ノーツを置いたときに、その位置の音を短く再生する機能も追加しました。

例えばノーツを12.34秒へ移動した場合、

12.34秒付近の音声を短く再生

します。

これにより、

位置を動かす
↓
音を聞く
↓
もう少し左へ動かす

という調整がしやすくなりました。

一般的な動画編集ソフトのスクラブやDAWに近い感覚です。

Editorは「解析結果ビューア」ではなくなった

最初は、

解析した情報をタイムラインに表示する

程度のEditorを想定していました。

しかし実際に譜面制作を始めると、

ズーム
スクロール
再生追従
低速再生
stem個別再生
選択
移動
Snap
Undo / Redo

などの機能が必要になりました。

結果的に、単なるAnalysis Viewerではなく、

実際に1曲を完成させられる譜面制作環境

へ発展しました。

解析結果を「見る」だけでは十分ではない

ここで特に重要だったのが、

解析イベントを表示する

だけでは、まだ制作支援としては弱かったことです。

例えばguitar eventが12.340秒に見えていたとしても、人間がノーツを12.340秒へ正確に合わせる必要があるなら、

ガイドを見る
↓
ノーツをドラッグする
↓
微調整する

という作業が残ります。

この「最後の位置合わせ」を減らしたかったため、次に実装したのが、

磁石のようなSnap機能

です。

ノーツを解析イベントの近くまで移動すれば、自動的に正しい時間へ吸着するようにしました。

このSnap機能によって初めて、

音響解析
↓
Editor表示
↓
実際の編集操作

が直接つながりました。

次章では、今回のPoCの中でも特に制作効率への効果が大きかった、Snap / Magnet UXについて紹介します。

11. 磁石のようなSnap UX

image.png

今回のPoCで、実際の譜面制作効率にかなり効いたのがSnap機能です。

解析結果をタイムライン上に表示するだけでも便利でしたが、それだけでは、

ガイドを見る
↓
ノーツを近くまで動かす
↓
拡大する
↓
微調整する

という作業が残ります。

そこで、動画編集ソフトやDAWのように、

近くの有効なタイミングへ磁石のように吸着する

操作をEditorへ導入しました。

最初はBeat GridへのSnapから始めた

最初に実装したのは、比較的単純なBeat GridへのSnapです。

例えばノーツを、

12.482 sec

へドラッグしたとき、その近くに、

beat = 12.500 sec

があれば、自動的に、

12.500 sec

へ合わせます。

音ゲーでは拍に沿った配置が多いため、これだけでもかなり便利でした。

特に、

四分音符
八分音符
十六分音符

のような位置へノーツを揃える作業では、毎回細かく時間を合わせる必要がなくなります。

Beatだけでは足りなかった

ただし、実際に譜面を作り始めると、Beat Gridだけでは不十分でした。

例えばボーカルは、必ずしもbeat上で発音するとは限りません。

ギターやドラムにも、

拍より少し前
拍より少し後
シンコペーション
細かい装飾音

があります。

そのため、

BeatにしかSnapしない

状態では、解析イベントを表示している意味が半減します。

そこでSnap対象を、beatだけでなく、

Vocals
Drums
Bass
Guitar
Piano
その他の解析event

にも広げました。

Analysis EventへのSnap

例えば、ボーカル解析で、

startSec = 18.742

というイベントが存在したとします。

ノーツをその近くへ移動すると、

18.742 sec

へ自動的に吸着します。

これにより、

解析結果を見る
↓
時間を読み取る
↓
同じ時間へノーツを合わせる

という作業そのものをなくせます。

解析結果がそのままEditorの操作対象になった瞬間でした。

Longノーツは開始だけでなく終了もSnapする

Singleノーツの場合は、開始時刻だけを合わせれば済みます。

しかしLongでは、

開始
終了

の2点があります。

そのためLongについては、

startSec
endSec

の両方をSnap対象にしました。

例えばボーカルイベントが、

startSec = 24.180
endSec   = 25.420

だった場合、

Long開始 → 24.180へSnap
Long終了 → 25.420へSnap

できます。

これは、前章までで発音区間を解析するようにしたことが、EditorのUXへ直接つながった例です。

解析イベントの「開始」と「終了」を別のSnap Pointとして扱う

eventを単なる1つのオブジェクトとして扱うだけではなく、

start
end

を個別のSnap Pointとして扱いました。

例えば、

Vocals
|====================|
^                    ^
start                end

というeventがあれば、Snap候補は2つあります。

この構造にすることで、

Singleを発音開始へ合わせる
Long終端を発音終了へ合わせる

といった操作ができます。

他のノーツにもSnapする

解析結果だけでなく、既に配置済みのノーツもSnap対象にしました。

例えば、

既存Note A = 30.500 sec

があり、新しいノーツを30.49秒付近へ持っていけば、

30.500 sec

へ揃います。

音ゲーでは、複数レーンに同時押しを作ることがあります。

この場合、

目視で同じ時間へ合わせる

より、

既存ノーツへSnap

の方が圧倒的に楽です。

「時間」を共通のSnap対象にした

ここで役に立ったのが、Analysis JSONもChart JSONも基本的に秒ベースで扱っていたことです。

Snap候補には、

Beat Grid
Analysis Event
Existing Note
Long Start / End
Slide Point

などがあります。

種類は違いますが、すべて最終的には、

timeSec

へ変換できます。

そのためSnap処理は、

候補の種類ごとに完全に別ロジック

ではなく、

候補をtimeSecへ正規化
↓
現在位置との距離を計算
↓
最も近い候補を選択

という共通構造にできます。

Snap Radius

当然ですが、すべてのノーツを強制的に最寄りのイベントへ合わせてしまうと、自由な配置ができなくなります。

そこで、

十分近い場合だけSnapする

という閾値を設けます。

概念的には、

distance = |noteTime - candidateTime|

if distance < snapThreshold:
    snap

です。

例えば、

Note      12.488
Candidate 12.500

差        12ms

ならSnapする。

一方、

Note      12.350
Candidate 12.500

差        150ms

なら、そのまま自由配置とします。

この閾値は、ズーム倍率やEditorの操作感とも関係します。

ズームとSnapの関係

タイムラインを大きく拡大しているときと、曲全体を縮小表示しているときでは、同じ20msでも画面上の距離が違います。

そのためSnapを、

常に±50ms

のような時間だけで考えると、ズームによって操作感が変わります。

逆に、

画面上で数px以内

という考え方を使えば、ズームしても磁石の感覚をある程度一定にできます。

PoCではこうした、

time distance
screen distance
zoom

の関係も調整する必要がありました。

これはアルゴリズムの問題というより、かなりUX寄りの問題でした。

点線などで「何にSnapしているか」を見せる

磁石のような挙動だけでは、

今どのイベントへ吸着したのか

が分かりにくいことがあります。

そこで、Premiere Proなどの編集ソフトを参考に、

候補位置へガイド線を出す

というUXも重要になりました。

例えば、

Vocals Event
       │
       │  ← Snap guide
       │
     [Note]

のように点線やガイドを表示します。

これにより、

BeatへSnapしたのか
VocalsへSnapしたのか
別ノーツへSnapしたのか

を視覚的に確認できます。

Select ModeでもSnapする

最初の実装では、ノーツを新規配置するときにはSnapしても、

Select Modeで既存ノーツを移動する

場合には同じ挙動にならない部分がありました。

しかし実際の制作では、

配置する
↓
後からタイミングを調整する

操作の方が多いくらいです。

そのため、SnapはPlace Mode専用機能ではなく、

既存ノーツを編集するときにも同じように使える

ようにしました。

ここは実際に使って初めて問題が見えた部分です。

Beatだけ動いて、解析レイヤーにSnapしない問題

開発途中では、

Beat Gridには磁石のように吸着する

一方で、

Vocalsなどの解析イベントには吸着しない

状態もありました。

表示とSnap処理が別々に実装されていたことが原因です。

この状態では、

解析結果は見えているのに、
結局人間が手で合わせる

ことになります。

そこで、表示中の有効レイヤーをSnap Sourceとして統合する方向へ修正しました。

この経験から、

解析結果を表示できることと、編集支援に利用できることは別

だと強く感じました。

LongがSnapしない問題

同様に、SingleではSnapしても、

Longの開始
Longの終了

ではうまく動作しないケースもありました。

Longは時間範囲を持つため、

ノーツ全体を移動する
開始だけを動かす
終了だけをリサイズする

という複数の操作があります。

それぞれに対して、

何をSnapさせるのか

を定義しなければなりません。

例えばLong終端をドラッグしている場合、

開始時刻をSnapする

のではなく、

endSecを解析eventのendSecへSnapする

必要があります。

このあたりはSingleより明らかに複雑でした。

Slideでも同じ問題がある

Slideはさらに複雑です。

Slideには、

開始
複数waypoint
終了

があります。

例えば、

A ---- B ---- C ---- D

というSlideなら、時間方向に複数の編集点があります。

将来的には、

AをVocal Event AへSnap
BをBeatへSnap
CをPitch変化点へSnap
DをEvent EndへSnap

のように、それぞれ異なる解析情報へ合わせることもできます。

PoCではすべてを完全自動化したわけではありませんが、データモデルとして複数waypointを持たせたことで、この方向へ拡張できるようにしました。

Snap Sourceの優先順位

候補が複数ある場合もあります。

例えば、

12.500 sec Beat
12.506 sec Vocal Event
12.510 sec Existing Note

の近くへドラッグした場合です。

このとき単純に最短距離だけで決めることもできますが、

Beatを優先したい
現在選択しているAnalysis Layerを優先したい
Existing Noteを優先したい

というケースも考えられます。

PoCを進める中で、Snapは単なる、

nearest timestamp

ではなく、

candidate type
visibility
active layer
distance

などを考慮するUXへ発展する余地があると分かりました。

有効なレイヤーだけをSnap対象にする

例えばGuitar Layerを非表示にしているのに、

見えないGuitar Eventへ突然ノーツが吸着する

と、ユーザーから見ると不自然です。

そのため、

表示しているレイヤー
有効化しているレイヤー

とSnap Sourceを連動させる設計が自然です。

つまり、

見えているものへSnapする

という原則です。

この考え方にすると、Editorの挙動がかなり予測しやすくなります。

Snapは解析誤差も吸収できる

Snap機能には、もう一つ面白い効果がありました。

解析精度が完全でなくても、人間が最終調整できます。

例えば解析イベントが、

本来 10.500 sec
解析 10.518 sec

だったとします。

完全自動採譜なら、この18msの誤差がそのまま譜面へ入ります。

しかし制作支援なら、

解析候補へ一度Snap
↓
耳で確認
↓
必要なら少しずらす

ことができます。

つまり、

解析は初期位置を提供し、人間が最後の精度を担保する

という役割分担ができます。

解析結果が多少多くてもSnapなら使える

大量の解析イベントがある場合、画面にすべて表示すると情報量が多くなります。

しかしSnap Sourceとして考えると、

近くにある候補だけが意味を持つ

ため、意外と扱いやすくなります。

例えばguitar eventが1500件あっても、現在ドラッグしているノーツの周辺数十msだけ見れば、候補はごく少数です。

つまり、

大量の解析結果

を、

局所的な操作支援

へ変換できるわけです。

「解析精度100%」より「良いSnap候補」の方が価値がある

今回のPoCでかなり重要だと感じたのがこの点です。

音響解析を研究として見ると、

Precision
Recall
F1

などの精度指標を高めたくなります。

もちろん精度は重要です。

しかし制作支援ツールとして考えると、

ユーザーがノーツを置こうとしたとき、
ちょうど良い候補が近くにある

ことの方が、体感的な価値につながる場合があります。

例えば100イベント中10個が誤検出でも、

必要なタイミングがほぼ全部Snap候補として存在する

のであれば、制作はかなり楽になります。

逆にPrecisionが高くても、

欲しいイベントが頻繁に抜ける

と、結局人間が耳で探す必要があります。

このためPoCでは、

多少候補が多くてもRecall寄りに出し、Editor側で人間が選ぶ

という方向が制作支援には合っていると感じました。

SnapによってAnalyzerとEditorが本当につながった

このSnap機能を実装するまでは、

Analyzer
↓
便利な情報を出す

Editor
↓
それを表示する

という、やや受動的な関係でした。

Snapを入れると、

Analyzerが出したevent
↓
Editorの操作候補になる
↓
ノーツ位置そのものへ反映される

ようになります。

つまり、音響解析結果が初めて、

Editorの入力支援機能

になりました。

今回のPoCの目的である、

音を何度も聞いて
タイミングを探して
手で合わせる作業を減らす

という点に、最も直接効いた機能の一つです。

音響解析とUXはセットで考える必要がある

PoCを始めた当初は、

どれだけ正確な解析ができるか

が主な技術課題だと思っていました。

しかし実際に譜面を作り始めると、

解析結果をどう見せるか
どう選ぶか
どうノーツへ反映するか

の方が重要になる場面が多くありました。

特にSnapは、

音響解析
+
Editor UX

が組み合わさって初めて価値が生まれる典型例でした。

解析技術単体ではなく、

解析結果を人間の編集操作へどう接続するか

が、制作支援ツールでは非常に重要だと分かりました。

次章では、こうして作成する譜面そのものをどのようなデータとして表現したのか、Single / Long / Slide / FlickなどのChartデータモデルについて紹介します。

12. 譜面データモデル

Analyzerで得た情報をEditor上で使えるようになると、次に重要になるのが、

最終的な譜面そのものを、どんなデータとして表現するか

です。

今回のPoCでは、解析結果と譜面データを明確に分けました。

Analysis JSONは、

曲の中で何が起きているか

を表します。

一方、Chart JSONは、

プレイヤーに何を操作させるか

を表します。

この2つは似ているようで、役割がまったく違います。

例えばAnalyzerが1000個のdrum eventを検出しても、Chartへ1000ノーツを置く必要はありません。

その中から、

ゲームとして気持ちよい
難易度として適切
フレーズとして分かりやすい

ものを人間が選び、Chartへ落とし込みます。

基本ノーツは4種類

PoCを進める中で、Editorで扱う基本ノーツは最終的に、

Single
Long
Slide
Flick

の4種類に整理しました。

それぞれ役割が異なります。

Single

Singleは、最も単純な瞬間ノーツです。

概念的には、

{
  "type": "single",
  "timeSec": 12.34,
  "lane": 3
}

のような情報を持ちます。

主に、

ドラムのアタック
短いボーカル
短いギター音
Beatに合わせたリズム

などに使います。

Analyzer側でonsetが取れていれば、Singleとの相性は非常に良いです。

Long

Longは、一定時間押し続けるノーツです。

必要なのは、

開始時刻
終了時刻
レーン

です。

概念的には、

{
  "type": "long",
  "startSec": 20.12,
  "endSec": 21.48,
  "lane": 4
}

のようになります。

Longは、前章までで扱った、

startSec
endSec

を持つ解析eventと非常に相性が良いです。

ボーカルの伸ばしや、持続するギター音などを見ながら、その区間へLongを配置できます。

Slide

Slideは、時間とともにレーンを移動するノーツです。

単純な開始点と終了点だけではなく、途中の経路も必要になります。

そこでSlideには、

waypoints

を持たせました。

例えば、

{
  "type": "slide",
  "waypoints": [
    {
      "timeSec": 30.00,
      "lane": 2
    },
    {
      "timeSec": 30.45,
      "lane": 4
    },
    {
      "timeSec": 30.90,
      "lane": 3
    },
    {
      "timeSec": 31.40,
      "lane": 5
    }
  ]
}

のような形です。

これにより、

開始
↓
右へ移動
↓
少し戻る
↓
再び右へ移動

といった複雑な軌道を表現できます。

このwaypoint構造は、pitch解析との相性も良いです。

将来的には、

pitch上昇
↓
Slideを右へ

pitch下降
↓
Slideを左へ

といった候補提示にも利用できます。

最初はSlideが2点しか扱えなかった

PoCの途中では、Slideが実質的に、

開始点
終了点

の2点だけしか作れない状態もありました。

しかし実際の音ゲー譜面では、

A → B → C → D

のような複数点Slideが普通に必要です。

そこでデータモデルを見直し、

waypoints[]

として任意個数の中間点を持てるようにしました。

これは、実際に譜面を作ったからこそ見えた要件の一つです。

Flick

Flickは、瞬間的なノーツですが、

方向

を持ちます。

PoCでは、

left
right

などの方向を持たせています。

Editorでは、選択後に方向を反転できるようにしました。

Flickは単なる見た目ではなく、Player側の入力判定にも影響するため、Chart JSON上で明示的に保持します。

Flickの見た目と方向

EditorではFlickを、通常ノーツと同じ長方形で表示するのではなく、

矢印

を中心に表現しました。

これは、SingleやLongとの違いを一目で分かるようにするためです。

PoC中には、

left → 上向き矢印
right → 下向き矢印

という表示ルールも試しました。

このあたりは、将来的にPlayerのUIへ寄せて変更できる部分です。

End Flick

LongやSlideの終端にFlickを要求したいケースもあります。

例えば、

Longを押し続ける
↓
最後にFlickする

という譜面です。

これを、

Long
+
別のFlick

という2ノーツとして表現すると、意味的には少し違います。

そこでLongやSlideには、

endAction

を持たせました。

概念的には、

{
  "type": "long",
  "startSec": 40.0,
  "endSec": 41.5,
  "lane": 3,
  "endAction": {
    "type": "flick",
    "direction": "right"
  }
}

のような構造です。

これにより、

このノーツの終端でFlickする

という意味をデータ上で直接表現できます。

Flick Chain

Flickは単独で存在するだけでなく、連続することもあります。

例えば、

Flick
  ↓
Flick
  ↓
Flick

という連続操作です。

PoCでは、この関係をノーツ単体の属性だけでなく、

connections[]

として表現する仕組みも追加しました。

概念的には、

{
  "type": "flick",
  "fromNoteId": "note-a",
  "toNoteId": "note-b"
}

のような接続情報です。

これにより、

どのノーツとどのノーツが連結しているか

を明示できます。

ノーツ本体とConnectionを分離する

Connectionを独立させたのは重要でした。

例えば、

Note A
Note B

が存在していても、

AとBは独立

なのか、

AからBへ連続Flick

なのかは別の情報です。

そこで、

notes[]
connections[]

を分離しました。

これはグラフ構造に近い考え方です。

将来的に、

Slide同士の接続
特殊な連結
Decorationとの関連

などへ拡張する場合にも扱いやすくなります。

Decoration

PoCの途中では、ノーツ以外の視覚要素も表現できるようにしました。

例えばプロセカ系の創作譜面にあるような、

文字
演出
装飾

です。

これらはPlayerの判定対象ではありません。

そこでノーツとは別に、

Decoration

として扱います。

この分離により、

ゲームプレイに影響するもの

と、

見た目や演出だけに使うもの

を分けられます。

type名だけで形状を決めない

ここで一つ設計上重要だったのが、

type名だけを見て描画や判定を決めない

ことです。

例えば、

時間幅を持つ
waypointを持つ
endActionを持つ

といった構造そのものも判断材料にします。

これにより、未知のtypeが入ってきても、

完全に読み込めなくなる

より、

既知のフィールドだけ使って最低限表示する

方向へ倒せます。

PoC中に仕様変更が頻繁だったので、この寛容さは役立ちました。

ノーツにはIDを持たせる

Chartを編集するには、それぞれのノーツを一意に識別する必要があります。

そのため各ノーツにはIDを持たせます。

これは、

選択
Undo / Redo
Connection
削除
Inspector編集

などで必要です。

特にFlick ChainのようなConnectionでは、

fromNoteId
toNoteId

でノーツ同士を参照するため、安定したIDが重要になります。

Analysis EventとChart Noteは別物

ここでも改めて重要なのが、

Analysis EventをそのままChart Noteへしない

ことです。

例えばAnalyzerが、

12.340 sec guitar event

を検出したとしても、Chartでは、

何も置かない
Singleを置く
Flickを置く
Longの開始に使う
Slideのwaypointに使う

のどれでも構いません。

つまり、

Analysis Event
=
音楽的事実の候補

Chart Note
=
ゲームデザイン上の判断

です。

この境界を維持したことで、解析に譜面デザインを支配されずに済みました。

Chart JSONにもSchemaを持たせる

Chart JSONについてもJSON Schemaを定義しました。

Playerは、このSchemaに従ったChartを読み込みます。

そのため、

Editor
↓
Chart JSON
↓
Player

の境界が明確です。

Editor内部のReact stateをPlayerへ渡す必要はありません。

Playerから見れば、

有効なChart JSONか

だけが重要です。

Playerを独立実装したことでSchemaを検証できた

このChart Schemaが本当に十分かどうかは、Editorだけ作っていると分かりにくいです。

Editor内部には、

暗黙の状態
一時的なUI state
補助変数

が存在するからです。

そこでPlayerを完全に別実装にしました。

PlayerがChart JSONだけを読んで、

描画
判定
Long
Slide
Flick
End Flick
Flick Chain

を再現できれば、

Chart JSONだけで譜面を表現できている

ことを確認できます。

この意味でもPlayer実装は、Schemaの実証テストになりました。

データモデルは実際に譜面を作りながら進化した

最初から、

Single
Long
Slide
Flick
End Flick
Flick Chain
Decoration

のすべてを考えていたわけではありません。

実際に譜面を作る中で、

Slideを2点以上つなぎたい
Longの最後をFlickにしたい
Flickを連続させたい
文字を表示したい

という要望が出てきました。

そのたびに、

無理に既存フィールドへ詰め込む

のではなく、必要に応じてデータモデルを拡張しました。

これはAnalysis Schemaと同じく、

実データと実際の操作からSchemaを育てる

という方針です。

SchemaはPoCでも重要だった

PoCというと、

とりあえず動けばよい

と考えがちです。

しかし今回のように、

Analyzer
Editor
Player

が別コンポーネントになっている場合、データ形式が曖昧だと開発がすぐに破綻します。

特に途中で仕様を変更したとき、

Editorでは動く
Playerでは壊れる
昔のprojectが読めない

といった問題が起きやすくなります。

そのため今回のPoCでは、JSON Schemaを比較的早い段階から重要な設計要素として扱いました。

「ゲームの仕様」をChart JSONへ閉じ込める

最終的に目指したのは、

Chart JSONを見るだけで
その譜面が何を要求しているか分かる

状態です。

つまり、

何秒にノーツがあるか
どのレーンか
どれくらい長いか
どう移動するか
終端で何をするか
どのノーツと接続しているか

をChartだけで表現します。

こうしておけば、将来的に、

Web Player
Android Player
別のEditor
譜面変換ツール
自動生成ツール

を作っても、同じChart Schemaを利用できます。

譜面データモデルが固まるとEditorの機能も増えた

Chartの表現力が上がると、当然Editor側にも、

複数waypoint編集
Long終端編集
Flick方向変更
Connection作成
範囲選択
Undo / Redo

などが必要になります。

つまり、

データモデル
↓
Editor UX
↓
Player実装

は互いに影響しながら進化しました。

次章では、こうした譜面データを実際に編集するために追加していった、Select / Place、Undo / Redo、Slide編集、再生速度変更などのEditor機能について紹介します。

13. 譜面制作を楽にするために追加したEditor機能

解析結果をガイドとして表示し、Snapも使えるようになると、次に重要になったのが純粋なEditorとしての使いやすさです。

実際に1曲分の譜面を作ろうとすると、

ノーツを置く
↓
選択する
↓
移動する
↓
長さを変える
↓
複数ノーツをまとめて修正する
↓
音を聞いて確認する
↓
間違えたら戻す

という操作を何度も繰り返します。

数個のノーツを置くだけなら最低限のUIでも問題ありません。

しかし数百ノーツを扱い始めると、Editorの操作性そのものが制作速度を大きく左右するようになりました。

そこでPoCの途中から、解析機能だけではなく、一般的な編集ソフトとして必要な機能もかなり追加しました。

PlaceとSelectを分離する

初期のEditorでは、

クリックしたらノーツを置く

という非常に単純な操作でした。

しかし、配置済みのノーツを編集しようとすると、

選択したかったのに新しいノーツを置いてしまう

といった問題が起きます。

そこで操作モードを、

Place
Select

に分けました。

Place Modeでは新しいノーツを配置します。

Select Modeでは、既存のノーツを選択・移動・編集します。

これは小さな変更に見えますが、Editorを本格的に使う上ではかなり重要でした。

範囲選択

ノーツ数が増えてくると、1個ずつクリックして選択するのは現実的ではありません。

そこでSelect Modeでは、ドラッグした矩形の中にあるノーツをまとめて選択できるようにしました。

いわゆるマーキー選択です。

概念的には、

┌─────────────────┐
│   Note   Note   │
│       Note      │
│   Note          │
└─────────────────┘

という矩形を作り、その範囲に交差するノーツを選択します。

内部的には、

rectIntersectsNote
notesInRect

のような判定を用意しました。

これにより、

このフレーズ全体を少し右へ移動する
この小節のノーツをまとめて削除する

といった操作がしやすくなりました。

Undo / Redo

実際の譜面制作ではUndo / Redoも必須でした。

特に、

大量のノーツを選択
↓
間違えて移動

のような操作をしたとき、Undoがなければかなり厳しいです。

そこでEditorの状態変更を履歴として管理する基盤を追加しました。

対象になるのは、

ノーツ追加
削除
移動
リサイズ
Slide編集
Flick方向変更
Connection編集

などです。

PoCの途中でUndo / Redo基盤を入れたことで、その後のEditor機能をかなり安心して試せるようになりました。

SingleとLongを見た目で区別する

初期の表示では、SingleとLongの見た目が似ており、

これはSingleなのか
短いLongなのか

が分かりづらい問題がありました。

そこでSingleは、時間幅のない棒線として表示する方向へ変更しました。

一方Longは、

開始 ───────── 終了

という時間幅を持つバーとして表示します。

データ上で違うだけではなく、

見た瞬間に種類が分かる

ことを重視しました。

ズームしてもノーツの縮尺が合うようにする

初期実装では、タイムラインをズームしてもノーツのサイズがほとんど変わらず、

時間軸は拡大したのに
ノーツだけ同じ大きさ

という不自然な状態がありました。

そこで、ノーツの時間方向のサイズを、

秒
↓
現在のzoom
↓
pixel

へ変換するようにしました。

これにより、例えば1秒のLongは、

拡大時  → 長く見える
縮小時  → 短く見える

ようになります。

タイムラインEditorとしては当然の挙動ですが、実際に作ってみると細かい調整が必要でした。

Longの終端を直接リサイズする

Longを作った後、

開始位置は合っている
終了位置だけ少し違う

ということが頻繁にあります。

そこでLongの右端をドラッグして、

endSec

だけを変更できるようにしました。

この操作にもSnapを適用できるようにし、

Vocal Eventの終了位置
Beat Grid
他ノーツ

へLongの終端を合わせられます。

これは発音区間解析との相性が非常に良い機能でした。

Slideを複数点で編集する

Slideは、単純な直線だけでは足りません。

実際の譜面では、

左
↓
右
↓
中央
↓
右

のように途中で何度も方向を変えることがあります。

そこでSlideを、

waypoints[]

として複数点で表現し、Editor上でもそれぞれのpointを編集できるようにしました。

途中では、

Slideが2点しか作れない

という問題もありました。

これは実際の譜面制作ではかなり大きな制約だったため、複数waypointを連続して作れるように修正しました。

2点を選んでConnectする

Slideを作る操作については、

既存の2点を選択
↓
Connect

という方式も検討しました。

例えば、

●               ●

という2つのポイントを選択すると、

● - - - - - - - ●

のようなガイドを表示し、接続できるようにする考え方です。

このように、

最初から完成したSlideを描く

だけでなく、

点を置く
↓
後から接続する

という操作も、複雑な譜面では有効です。

Flick方向を後から変更できるようにする

Flickは方向を持ちます。

しかし新規配置時に毎回方向を完全に決める必要があると、制作テンポが落ちます。

そこで、

Flickを配置
↓
Select Modeで選択
↓
方向を反転

できるようにしました。

まずリズムだけ置いて、後からFlick方向を調整できます。

譜面制作では、

一度大まかに作る
↓
後から演出や手の動きを調整する

ことが多いため、後編集できることが重要です。

End Flick

LongやSlideの最後にFlickを入れる場合も、Editorから指定できるようにしました。

例えば、

Long
───────────────▶

の終端を選び、

End Action = Flick
Direction = Right

のように設定します。

これによって、

Long
+
別Flick

として2ノーツを無理に重ねる必要がありません。

連続Flick

Flickを連続させるケースにも対応しました。

Chart側ではConnectionとして保持し、Editorではノーツ同士の関係を編集します。

これによって、

Flick → Flick → Flick

という連続した操作を一つの流れとして表現できます。

この機能も、実際に創作譜面を作っている途中で必要になったものです。

キーボードによる時間移動

マウスだけでタイムラインを移動するのは、細かな確認では少し面倒です。

そこで左右の矢印キーを使って、

← 少し前へ
→ 少し後へ

移動できるようにしました。

初期値では移動量が大きすぎたため、後におよそ半分程度へ調整しました。

さらに、zoomに応じて適切な移動量にする方向も検討しています。

拡大して細かく編集している場合は、

数十ms

単位で動き、

縮小表示ではもう少し大きく移動する方が自然です。

上下キーで別レイヤーのイベントへ移動

時間方向だけでなく、

Vocals
Drums
Bass
Guitar

など別レイヤーのイベントへ移動する操作も検討しました。

例えば、

現在の時刻付近
↓
次のVocals Event
↓
次のGuitar Event

のように解析結果を順番に確認できれば、マウス操作を減らせます。

解析結果が大量にあるため、

画面上で探す

だけでなく、

イベント単位で移動する

ナビゲーションも有効です。

移動時に音を鳴らす

キーボードで時間位置を動かしたときにも、その位置の音を短く鳴らす方式を検討しました。

これによって、

→
音を聞く

→
音を聞く

→
音を聞く

という形で、視覚だけでなく聴覚でもタイミングを探せます。

譜面Editorなので、一般的なテキストEditor以上に、

移動操作と音声確認を密接にする

ことが重要でした。

0.25倍速、0.1倍速

細かなイベントを確認するため、再生速度には、

1.0
0.5
0.25
0.1

といった低速も追加しました。

0.1倍速まで落とすと音質そのものは大きく変わりますが、

この音の立ち上がりがどこか

を確認するには役立つ場合があります。

特に、

細かい連打
Flick
ボーカルの子音
ギターの細かなアタック

などでは便利です。

配置時のAudition

ノーツを配置した位置で音を短く鳴らすAuditionも追加しました。

これはかなり単純な機能ですが、制作体験への効果が大きいです。

例えば、

ノーツを置く
↓
「タッ」とその位置の音が鳴る

ことで、

少し遅い
少し早い
ちょうど良い

を即座に判断できます。

毎回Playボタンを押して数秒前から聞き直す必要がなくなります。

横スクロールと再生位置追従

曲全体を扱うには、横スクロールも必要でした。

さらに再生を開始すると、

再生ヘッドだけ画面外へ出てしまう

問題も起こります。

そこでviewportを再生位置へ追従させるようにしました。

途中では、

viewport幅が固定値1000pxとして扱われている

ことが原因で、実際の画面サイズと追従位置がずれる問題もありました。

これを実際のviewport幅から計算するように修正しました。

こうした問題は音響解析とは無関係ですが、

200秒の実楽曲を実際に編集する

ことで初めて見えてきたものです。

スクロールバーも時間軸と同期する

タイムライン下部のスクロールバーについても、独自に位置を持たせるのではなく、

viewport
duration
zoom

からgeometryを計算する方式にしました。

つまり、

タイムライン本体
スクロールバー
再生ヘッド
ノーツ
解析イベント

が、すべて同じ時間モデルから位置を計算します。

この考え方にすると、表示の同期ズレを減らせます。

stem再生とEditor操作を組み合わせる

前章でも触れましたが、

Vocals
Drums
Bass
Guitar
Piano

を個別に再生できるようにしたことで、Editorの使い方もかなり変わりました。

例えば、

Guitar Layerを表示
↓
Guitar stemだけ再生
↓
eventへSnapしながらノーツを置く

という作業ができます。

これは、

解析結果
視覚
音声
編集操作

を同じ環境へ統合した状態です。

Editorを作って初めてAnalyzerの問題も見える

面白かったのは、Editorを実際に使うことでAnalyzer側の問題も見つかったことです。

例えば、

このイベントは多すぎる
このstemではイベントが不足している
endSecが長すぎる

といった問題は、JSONを眺めているだけでは分かりにくいです。

しかしタイムライン上で表示し、実際にノーツを合わせてみると、

これはガイドとして使いやすい
これはノイズが多すぎる

がすぐ分かります。

つまり開発サイクルが、

Analyzerを改善
↓
Editorで実データを見る
↓
譜面を作る
↓
問題を発見
↓
AnalyzerまたはEditorを改善

という形になりました。

解析技術だけでは制作ツールにならない

この段階まで進めて強く感じたのは、

音響解析技術だけでは譜面制作支援ツールにはならない

ということです。

いくら高精度なtimestampを出せても、

見づらい
選びづらい
位置を合わせづらい
修正しづらい

Editorでは、結局作業は大変です。

逆に解析精度が完全ではなくても、

レイヤー表示
stem個別再生
Snap
低速再生
Audition
Undo / Redo

が揃っていると、人間がかなり効率よく補正できます。

今回のPoCでは、音響解析そのものと同じくらい、

Editor UXがHuman-in-the-loopを成立させる鍵

になりました。

最終的には「実際に遊べるか」を確認する必要がある

ここまでで、

解析する
↓
ガイドを見る
↓
譜面を編集する
↓
Chart JSONへ保存する

ところまでは通りました。

しかし、この時点ではまだ、

そのChartが本当にゲームとして成立しているか

は確認できません。

Editor上で綺麗に見えていても、

Playerでノーツが正しく流れるか
LongやSlideが成立するか
Flickが判定できるか
音声とズレないか

は別の問題です。

そこでPoCの最後の大きなコンポーネントとして、Editorから独立したPlayerを作りました。

次章では、Chart JSONだけを読み込んで実際に遊べるPlayerをどのように構築したかを紹介します。

14. Playerを独立実装する

image.png

EditorでChart JSONを書き出せるようになったので、次に必要だったのが、

その譜面が本当にゲームとして成立するのかを確認するPlayer

です。

Editor上でノーツが正しく見えていても、それだけでは十分ではありません。

実際のゲームでは、

音楽に合わせてノーツが流れる
↓
判定ラインへ到達する
↓
ユーザーが入力する
↓
タイミングを判定する
↓
スコアやコンボへ反映する

ところまで動く必要があります。

そこでPoCでは、Editorのプレビュー機能としてPlayerを作るのではなく、Editorとは独立したPlayerを別コンポーネントとして実装しました。

PlayerをEditorから独立させた理由

最も大きな理由は、

Chart JSONだけで本当に譜面を再現できるのか確認したかったから

です。

Editor内部には、

Reactのstate
選択状態
一時的な編集情報
表示用の補助データ

などがあります。

もしEditor内部の情報をそのまま使ってPlayerを作ってしまうと、

Chart JSONには情報が足りないが、
Editor内部のstateが補っている

という状態でも気付けません。

そこでPlayerには、Editorのコードを一切importさせない構成にしました。

共有するのは基本的に、

schemas/chart.schema.json
Chart JSON

です。

これによって、

Editor
    ↓
Chart JSON
    ↓
Player

という境界そのものを検証できます。

Player専用の小さな実装にした

Playerについては、TauriやReactをそのまま採用しませんでした。

PoC時点の構成は非常に小さく、

player/
├── server.mjs
└── web/
    ├── HTML
    ├── CSS
    └── JavaScript ES Modules

という形です。

server.mjsはNode.jsの標準ライブラリだけを使った小さなローカルサーバーです。

ブラウザ側もplain JavaScriptのES Modulesで実装しました。

そのためPlayer専用には、

package.json
lockfile
build step
npm install

すら必要ありません。

Playerの目的はフロントエンド技術の検証ではなく、

Chart JSONを実際の音ゲーとして解釈できることの確認

だったため、できるだけ依存を増やさない構成にしました。

Falling Note方式

Playerでは、一般的な音ゲーと同様に、ノーツが奥から手前へ流れてくるplayfieldを実装しました。

見た目としては、

        遠い
       Notes
      Notes
     Notes

----------------
  Judgement Line
----------------
        手前

という構造です。

単純な2Dの縦スクロールではなく、奥行きを感じるようにperspectiveを付けています。

これによって、Editor上の時間軸だけだったChartが、

実際に遊ぶ音ゲーの譜面

として見えるようになりました。

時刻の基準はAudio Clock

Playerを作る上でかなり重要だったのが、時間同期です。

単純に、

setInterval(...)

や、

requestAnimationFrame(...)

の経過時間だけを基準にノーツを動かすと、音声再生とのズレが少しずつ蓄積する可能性があります。

そこでPlayerでは、

音声の現在再生時刻を基準にノーツ位置を計算する

方式にしました。

概念的には、

audio.currentTime
        ↓
現在の楽曲時刻
        ↓
各NoteのtimeSecとの差
        ↓
画面上の位置

です。

例えば、

現在時刻      10.000 sec
Note時刻      11.500 sec

なら、

あと1.5秒で判定ラインへ到達

する位置へ描画します。

この設計により、描画フレームが一時的に遅れても、次のフレームでは音声時刻を基準に正しい位置へ戻せます。

描画と判定を分離する

Playerでは、

ノーツをどう見せるか

と、

入力をどう判定するか

も分離しました。

これはかなり重要です。

例えば見た目を後から、

レーンの幅を変える
perspectiveを強くする
ノーツデザインを変える

としても、判定ロジックまで変わる必要はありません。

逆に、

Perfect範囲を調整する
Flick判定を改善する
Long判定を変更する

場合でも、描画ロジックとは独立して変更できます。

つまりPlayer内部でも、

Chart interpretation
Rendering
Judgement
Audio clock
Input

をできるだけ別の責務として扱いました。

Single

Singleは最も単純です。

Chart上の、

timeSec
lane

を読み取り、指定した時刻に判定ラインへ到達するように表示します。

ユーザー入力との差を、

inputTime - noteTime

で計算し、その絶対値によって判定します。

概念的には、

0ms                Perfect
少しずれる         Good
大きくずれる       Miss

という一般的な音ゲーの判定です。

具体的な判定幅は、ゲームバランスとして今後調整できる部分です。

Long

Longでは、

startSec
endSec

の両方を解釈します。

画面上では、

Head
│
│
│
Tail

という時間的な長さを持つノーツになります。

Singleと違い、

開始時に入力する
↓
一定時間保持する
↓
終端まで到達する

という状態管理が必要です。

このPlayerを作ったことで、

Editorで作ったLongの開始・終了時刻がゲームとして本当に意味を持つか

も確認できるようになりました。

Slide

Slideではさらに、

waypoints[]

を読み取ります。

例えばChartが、

time 10.0 lane 1
time 10.5 lane 3
time 11.0 lane 2
time 11.5 lane 5

というwaypointを持っていれば、Player側ではその点を結んでSlideの軌道を作ります。

つまり、

Editorで編集したwaypoint

が、

Player上の実際の軌道

になります。

このPlayerを独立実装したことで、複数waypointを持つSlideのChart表現が、本当にEditor固有のものではなくゲーム側でも解釈可能であることを確認できました。

Flick

FlickについてもChartに記録された方向を読み取ります。

Playerでは通常のTapとは別の操作として扱います。

PoCでは、

Keyboard
Mouse
Touch

を入力手段として扱えるようにしました。

最終的にスマートフォンで遊ぶことも想定しているため、Touchを最初から入力経路として考えたのは重要でした。

End Flick

LongやSlideには、

endAction:
    flick

を持たせられます。

Player側ではこれも解釈し、

Longを保持
↓
終端へ到達
↓
Flick入力

という一連の操作として扱います。

これはChart SchemaをPlayerで実際に使用したことで、

endActionという表現で十分か

を確認できた部分でもあります。

Flick Chain

連続Flickについては、

connections[]

の情報を読み取ります。

これによって、

Note A
↓
Note B
↓
Note C

というノーツ間の関係をPlayer上でも再現できます。

単に同じ時間帯にFlickが複数あるだけなのか、

一つの連続した操作としてつながっている

のかを、Chart JSONだけから判断できます。

判定・コンボ・スコア

PoCとはいえ、単にノーツを流すだけではなく、

Judgement
Combo
Score

も実装しました。

入力が成功すればComboが増え、Missすれば途切れます。

曲が終了すると結果を確認できます。

これによって、

見た目として最後まで再生できた

だけではなく、

譜面中のすべての判定対象を
Playerが最後まで処理できたか

も確認できます。

Autoplay

検証用に特に重要だったのがAutoplayです。

人間がプレイして確認すると、

Playerのバグ

なのか、

単に人間がミスした

のか判別しにくくなります。

そこでAutoplayでは、Chartに従って正しいタイミングで入力されたものとして判定を進めます。

これによって、

全ノーツを最後まで処理できるか
Longが途中で壊れないか
Slideのwaypointを処理できるか
FlickやEnd Flickが欠落しないか

を再現性のある形で検証できます。

PoCのEnd-to-End確認では、このAutoplayがかなり役立ちました。

PlayerはSchemaのIntegration Testでもある

このPlayerには、もう一つ重要な役割がありました。

それは、

Chart SchemaそのもののIntegration Test

です。

例えばEditorだけで完結していると、

この値はEditor内部では分かるから
JSONへ保存しなくてもいい

という暗黙の依存が入り込む可能性があります。

しかしPlayerはEditorを一切知りません。

そのためChart JSONに必要な情報が欠けていれば、Playerは正しく動きません。

逆に、

Editorが書き出したChart
↓
独立Playerが読み込む
↓
最後まで遊べる

のであれば、Schemaが実際のゲームを表現する契約として成立していることをかなり強く確認できます。

EditorとPlayerで技術を揃える必要はなかった

今回のPoCでは、

Editor
Tauri + React + TypeScript + Rust

Player
Node.js + plain Browser JavaScript

と、かなり違う構成になりました。

それでも問題なく連携できました。

両者の共通部分が、

Chart JSON

だからです。

これは最初に立てた、

Analyzer、Editor、Playerは同じ技術体系である必要はない

という仮説の実証にもなりました。

将来Playerだけを交換できる

この構成なら、現在のWeb Playerを将来的に、

Android
iOS
Unity
Godot
ネイティブアプリ

などへ置き換えても、Editorを作り直す必要はありません。

必要なのは、

Chart Schemaを理解する新しいPlayer

だけです。

今回のPoCでは最終的にAndroidで遊べる形も視野に入れているため、この独立性は特に重要です。

PoCとして重要なのは「最後まで通ること」

Playerを作るまでは、

Analyzerは動く
Editorも動く
Chartも保存できる

という状態でした。

しかし、それぞれが個別に動いているだけでは、

システム全体として成功した

とはまだ言えません。

Playerが完成したことで初めて、

Audio
↓
Analyzer
↓
Analysis JSON
↓
Editor
↓
Chart JSON
↓
Player
↓
実際にプレイ

という全経路を通せるようになりました。

そして実際に「キラメキ☆」の創作譜面を読み込ませ、曲の最初から最後まで動かすことで、今回のPoCの最終確認を行いました。

次章では、このEnd-to-End検証で実際にどこまで動いたのか、Pilot版として完成した「キラメキ☆」の譜面とPlayerの結果について紹介します。

15. 実際に1曲を最後まで遊んでみる

ここまでで、

楽曲解析
↓
Analysis JSON
↓
Editor
↓
Chart JSON
↓
Player

という各コンポーネントは個別に動くようになりました。

しかしPoCとして最後に確認したかったのは、

実際の1曲を最初から最後まで通したときに、この一連の仕組みが本当に成立するか

という点です。

そこで「キラメキ☆」を使い、実際に創作譜面を制作し、Playerで最後まで再生・判定するEnd-to-End検証を行いました。

実際の楽曲をAnalyzerへ通す

まず、約3分21秒の楽曲をAnalyzerへ入力します。

ここでは、

音源分離
beat / downbeat解析
stemごとのevent解析
pitch解析

などを実行し、Analysis JSONを生成します。

この時点でEditorには、

Beat
Vocals
Drums
Bass
Guitar
Piano

などのガイドを表示できる状態になります。

つまり譜面制作を、

真っ白なタイムライン

から始めるのではなく、

楽曲構造がある程度可視化されたタイムライン

から開始できます。

Editorで実際に譜面を作る

次にAnalysis JSONをEditorへ読み込み、実際の譜面を制作しました。

ここでは単純に解析eventをすべてノーツへ変換したわけではありません。

解析結果を見ながら、

このドラムは取る
このボーカルはLongにする
この部分はSlideにする
ここはあえてノーツを置かない
ここはFlickにする

といった判断を人間が行います。

つまり、最後まで一貫して、

解析は譜面候補を提示し、ゲームデザインは人間が行う

という方針です。

ガイドを見ながらタイミングを合わせる

実際の制作では、解析ガイドとSnapをかなり利用しました。

例えば、

ボーカルの発音開始
ドラムのアタック
ギターのイベント
beat

の近くへノーツを移動すると、該当時刻へ吸着します。

Longでは、

開始をevent startへ合わせる
終了をevent endへ合わせる

といった使い方もできます。

これにより、

曲を再生
↓
戻す
↓
もう一度聞く
↓
少しずらす
↓
再確認

という作業をかなり減らせました。

stemを聞きながら確認する

判断しづらい部分では、Demucsで分離したstemを個別に再生しました。

例えば、

完成音源ではギターが聞き取りにくい
↓
Guitar stemだけ再生
↓
Analysis Eventを確認
↓
ノーツを配置

といった作業ができます。

これは今回のPoCで想定以上に便利だった部分です。

音響解析の結果を使うだけでなく、

分離された音声そのものも人間の制作支援になる

ことが分かりました。

譜面をChart JSONへ保存

完成した譜面はChart JSONとして保存します。

ここには、

Single
Long
Slide
Flick
Slide Waypoints
End Flick
Flick Chain

など、Playerが必要とする情報が入っています。

重要なのは、Analysis JSONはPlayerへ渡さないことです。

Playerが必要なのは、

解析時に何が検出されたか

ではなく、

最終的にプレイヤーに何を操作させるか

だからです。

つまり、

Analysis JSON
     ↓
   人間
     ↓
Chart JSON

の境界を最後まで維持しています。

Playerへ読み込ませる

完成したChart JSONを、Editorとは独立したPlayerへ読み込ませます。

Player側はEditorのコードを使いません。

Chart Schemaだけを契約として、

ノーツ描画
音声同期
入力判定
Long
Slide
Flick
End Flick
Flick Chain
Score
Combo

を処理します。

ここで正しく再生できれば、

Editorがたまたま内部状態で補完していた

のではなく、

Chart JSONだけでゲーム譜面として成立している

ことを確認できます。

曲の最初から最後まで再生する

実際に「キラメキ☆」をPlayerへ読み込み、曲の最初から最後まで通しました。

数秒のテスト区間ではなく、約3分21秒の実楽曲全体です。

これは、

短い区間なら動く

だけでは見つからない問題を確認するためでもあります。

例えば、

長時間再生で音声と描画がずれないか
後半のノーツも正常に生成されるか
Longの状態が壊れないか
Slideが途中で欠落しないか
曲終了処理が正常に動くか

などを確認できます。

Autoplayで全ノーツを検証

人間がプレイして確認するだけでは、入力ミスとPlayerの不具合を区別しにくいため、検証にはAutoplayも使用しました。

AutoplayではChartに従って正しい入力を自動生成します。

その結果、Pilot版では、

693 / 693 Perfect
Full Combo

まで通すことができました。

つまりPlayerが認識した判定対象について、

曲の最初から最後まで全件処理できた

ことになります。

693ノーツすべてを最後まで処理できた

この結果は、単にFull Comboが出たというだけではありません。

PoCとしては、

Chart JSONを読めた
↓
全ノーツを時系列に展開できた
↓
音声と同期して描画できた
↓
判定エンジンへ渡せた
↓
曲終了まで状態が破綻しなかった

ことを意味します。

特に、

Long
Slide
Flick
End Flick
Connection

のような単純なSingle以外の要素も含めて処理できたことが重要でした。

AnalyzerからPlayerまで一本につながった

ここまで通したことで、今回最初に考えていた構成、

Audio
  ↓
Analyzer
  ↓
Analysis JSON
  ↓
Editor
  ↓
Chart JSON
  ↓
Player

が、概念図ではなく実際に動くパイプラインになりました。

Analyzerが出した情報を人間がEditorで利用し、そこで作ったChartを別実装のPlayerが読み込み、実際にゲームとして最後まで実行できます。

この時点で、

音響解析を創作譜面制作へ利用するというアイデアそのものはPoCとして成立した

と判断しました。

完全自動生成ではない

ここは改めて強調しておきたい部分です。

今回、

音声ファイルを入れた
↓
自動的に完成譜面が出た

わけではありません。

あくまで、

音声ファイル
↓
機械が大量のガイドを作る
↓
人間がガイドを利用して譜面をデザインする
↓
Playerで遊ぶ

というHuman-in-the-loopの構成です。

譜面の面白さや難易度、手の動き、演出は依然として人間が決めています。

それでも制作作業の質は大きく変わった

完全自動ではなくても、制作開始時点の状況は大きく変わります。

従来は、

何もない
↓
曲を聞く
↓
音を探す
↓
時間を探す
↓
ノーツを置く

でした。

今回のPoCでは、

beatが見えている
各stemのイベントが見えている
発音区間が見えている
stemを単独で聞ける
近くへ動かせばSnapする

ところから始められます。

人間が担当する仕事を、

音の存在を探す

から、

どの音をどうゲームとして表現するか考える

へ寄せることができます。

これは今回のPoCで最も大きかった成果です。

「解析精度が完璧でなくても成立する」を確認できた

Analyzerには当然誤検出があります。

Demucsにもartifactがあります。

beatも常に正解ではなく、pitchも不安定になる場所があります。

それでも実際に1曲の譜面を完成させてPlayerまで通せました。

これは、

制作支援であれば、個々の解析器が100%正確である必要はない

ことの実証にもなりました。

人間が最終判断をするため、

十分使える候補を大量に提示する

だけでも大きな価値があります。

PoCとして証明できたこと

今回のPilot版で確認できたことをまとめると、

1. 一般的なPC環境で実楽曲を現実的な時間で解析できる

2. 音源分離・beat・event・pitchなど複数の解析結果を
   同じ時間軸へ統合できる

3. 解析結果をEditor上の制作ガイドとして利用できる

4. 解析eventをSnap Sourceとして編集操作へ直接利用できる

5. 人間が最終的なゲームデザインを行う構成で
   1曲分の譜面を完成させられる

6. Chart JSONをEditorとは独立したPlayerで解釈できる

7. 実楽曲を最初から最後までプレイ可能な状態へ持っていける

ということになります。

PoCは大成功だった

もちろん、製品として見れば未完成です。

UIはまだ改善できますし、解析精度にも課題があります。

Playerの見た目も今後大きく変更できます。

しかし今回検証したかったのは、

音源分離、ビート解析、ピッチ解析などを組み合わせれば、創作音ゲーの譜面制作を実際に楽にできるのか。

という問いでした。

少なくとも今回の「キラメキ☆」の制作を通じて、

この方向性は十分成立する

という感触を得ることができました。

解析だけで終わらず、実際に譜面を作り、その譜面を独立Playerで最後まで遊べたという意味で、今回のPoCは大きな成功だったと考えています。

ただし、実際に作ったからこそ見えてきた問題もかなりあります。

次章では、Demucsの分離artifact、イベント重複、beat誤検出、pitchの不安定さなど、今回うまくいかなかった部分と今後改善したい点をまとめます。

16. うまくいかなかったこと・残っている問題

今回のPoCでは、AnalyzerからEditor、Playerまで一連の流れを通すことができました。

一方で、実際に1曲分を作ってみると、

解析できた
=
そのまま実用レベル

ではないこともよく分かりました。

特に残っている課題は、

音源分離
イベント検出
beat解析
pitch解析
confidence
Editor UX
自動化範囲

の各段階にあります。

Demucsの分離は完全ではない

まず前提として、Demucsによるstem分離には限界があります。

例えば、

guitarの一部がotherに残る
vocals成分が別stemへ漏れる
drumsのアタックがotherにも混ざる

といったことが起こります。

その結果、後段の解析では、

同じ音を複数stemで検出する

ことがあります。

例えば実際には1回しか鳴っていない音が、

drums event
other event
guitar event

として近接時刻に複数出てくるケースです。

つまり、stem名をそのまま、

この音は100%ギターである

と解釈するのは危険です。

分離artifactがイベントとして拾われる

Source Separationではartifactも発生します。

人間が聞けば、

これは分離時にできたノイズ

と判断できるものでも、機械的には局所的なピークとして見えることがあります。

その結果、

実際には存在しない発音イベント

として検出されます。

この問題に対して、

dynamic range gate
presence-vs-mix gate
minimum spacing
confidence

などを導入しましたが、完全には消せません。

イベントが多すぎる

解析器をRecall寄りにすると、イベント数はかなり多くなります。

これは制作支援という目的にはある程度合っています。

必要なイベントが欠落するより、

少し多めに候補がある

方が人間は選べるからです。

しかし、あまりにも多いと、

タイムラインがガイドだらけになる

という問題もあります。

特に、

guitar
other
drums

など複数レイヤーを同時に表示すると、情報量がかなり増えます。

今後は、

confidence threshold
近接イベントの統合
強度によるフィルタ
レイヤーごとの表示密度

などが必要だと考えています。

stem間で重複するイベントを統合したい

例えば、

10.500 sec drums
10.507 sec other

のようなイベントがあった場合、

別々の音

なのか、

同じ音がstem leakageで二重検出された

のか判断する必要があります。

単純に、

20ms以内なら統合

としてしまうと、本当に異なる音が同時に鳴ったケースまで消してしまいます。

そのため将来的には、

時間距離
stem種別
音量
spectral feature
元mixとの相関

などを使ったイベント統合が必要になると思います。

onset検出は楽器によってかなり性格が違う

今回のPoCで明確になったのが、

すべての楽器を同じonset detectorで扱うのは難しい

ことです。

drumsは比較的分かりやすいです。

急激なアタック

があるためです。

一方でbassは、

立ち上がりが丸い

ことがあります。

vocalsも、

子音から母音へつながる

ため、どこを「開始」とするか曖昧です。

guitarも、

単音
コード
ストローク
アルペジオ
サステイン

によって特性が違います。

そのため今後は、

stemごとに異なる解析器

を使う方が現実的だと考えています。

endSecはstartSecより難しい

発音開始は比較的検出しやすいですが、

どこで終わったか

はかなり曖昧です。

例えばボーカルでは、

声そのもの
リバーブ
ブレス
次の音節

が連続します。

ギターでも、

サステイン
ディレイ
残響

があります。

そのためendSecは、

音楽理論上の正確な終端

ではなく、

制作上使える区間終端の候補

として扱う方が安全です。

beat解析にも誤検出がある

Beat This!はかなり有用でしたが、常に正しいわけではありません。

特に、

曲冒頭
リズムが弱い場所
SEがある場所
余韻

などでは、人間の感覚と一致しないことがあります。

完全自動化する場合は大きな問題ですが、今回のような制作支援では、

不自然なら使わない

という判断ができます。

それでも将来的には、

手動でbeatを修正
一部区間だけ再解析
offset補正

などができると、さらに使いやすくなるはずです。

pitchは特に不安定な区間がある

torchcrepeによるpitch推定も、すべての区間で安定するわけではありません。

特に、

子音
ブレス
無声音
Demucs artifact
複数音が混ざる区間

では値が飛びやすくなります。

またビブラートのように、本当にpitchが細かく揺れているケースもあります。

そのため、

生のpitch

と、

ノイズ

を区別する必要があります。

実用には、

smoothing
confidence threshold
外れ値除去
フレーズ単位の抽象化

が必要です。

polyphonicな楽器には単一pitchが合わない

guitarやpianoでは、コードを鳴らすことがあります。

その場合、

1つのfrequency

で表現すること自体が適切ではありません。

今回のpitch解析は、どちらかというと、

単旋律に近いvocals

との相性が良いです。

guitarやpianoについては、将来的に、

multi-pitch estimation
chroma
note transcription

など別の手法を使う余地があります。

confidenceは解析器ごとに意味が違う

Analysis JSONではconfidenceを持たせました。

ただし、

0.8

という値が、すべての解析器で同じ意味ではありません。

例えば、

pitch confidence 0.8
event confidence 0.8

を単純比較できるとは限りません。

今後は、

sourceごとのconfidence calibration

あるいは、

high
medium
low

のような制作向けの共通尺度へ変換する必要があるかもしれません。

Analysis Eventをそのままノーツにはできない

今回のPoCでかなりはっきりしたのが、

解析イベントとゲームノーツは別物

ということです。

例えばドラムイベントが正確に取れても、

全部叩かせる

と譜面として面白くなるとは限りません。

むしろ、

情報量が多すぎる
単調になる
難しすぎる

ことがあります。

同じボーカルイベントでも、

Single
Long
Slide
Flick
何も置かない

という複数の選択肢があります。

ここは音響解析だけでは決められません。

難易度設計は別問題

同じ解析結果から、

EASY
NORMAL
HARD
MASTER

のような複数難易度を作れるはずです。

つまり、

音楽に何があるか

と、

どの程度プレイヤーへ操作させるか

は別問題です。

今回のPoCでは、難易度自動設計までは踏み込んでいません。

将来的には、

イベント間引き
密度制御
レーン移動量制限
同時押し頻度
Flick率

などを考慮する必要があります。

人間が作った譜面が「面白い」かは別の評価

技術的に、

693ノーツを最後まで処理できる

ことと、

その譜面が面白い

ことは別です。

譜面制作には、

演出
意外性
手の動き
曲との一体感
視覚的な美しさ
難易度曲線

といった要素があります。

これらは単純な音響解析だけでは測れません。

今回のPoCでは、この部分を人間に残しています。

Snapも万能ではない

Snapは非常に便利でしたが、強すぎると逆に邪魔になります。

例えば、

あえてbeatから少しずらしたい

場合もあります。

そのため、

Snap ON / OFF
対象レイヤー
Snap threshold
優先順位

などを調整できる方がよいです。

また複数候補が近接すると、

どこに吸着したのか分かりにくい

ことがあります。

ここはガイド表示や候補選択UXをさらに改善できます。

レイヤー数が増えるほどUI設計が難しくなる

現在でも、

Beat
Vocals
Drums
Bass
Guitar
Piano

などがあります。

さらに今後、

Pitch
Harmony
Section
Lyrics
Chord

などを追加すると、タイムラインはさらに複雑になります。

解析できる情報を増やすことと、

人間が理解しやすい画面

を維持することは別問題です。

今後は、

レイヤー折りたたみ
プリセット
自動表示切替
重要イベントだけ強調

なども必要になると思います。

PlayerのUIはまだPilot版

Playerについても、現時点ではPoCとして、

実際に遊べる
判定できる
最後まで通る

ことを優先しています。

そのため、

見た目
演出
アニメーション
エフェクト
ゲームとしての完成度

にはまだ改善余地があります。

現在のUIは最終形ではありません。

むしろPlayerは、

Chart Schemaと判定系の実証

を目的にしたPilot版です。

タッチ操作の実機検証も今後必要

PlayerにはTouch入力も考慮していますが、

PCブラウザで動く

ことと、

スマートフォン実機で快適に遊べる

ことは別です。

特に、

入力遅延
タッチ追従
複数指
画面サイズ
リフレッシュレート
オーディオレイテンシ

などは実機で確認する必要があります。

最終的にAndroidでのプレイも視野に入れているため、ここは次の大きな検証ポイントです。

自動採譜を目標にすると急に難しくなる

今回の構成は、制作支援としてはかなりうまくいきました。

しかし、

人間を完全に外す

ことを目標にすると、一気に難易度が上がります。

自動採譜では、

どのイベントを採用するか
どのノーツ種にするか
どのレーンへ置くか
どの難易度にするか
どう演出するか

まで機械が決める必要があります。

これは単なる音響解析ではなく、

譜面デザイン
ゲームバランス
生成
評価

の問題になります。

今回のPoCでは、そこまで無理に自動化しない判断が良かったと思っています。

「100%正確でなくても役に立つ」は強い

一方で、これらの問題があってもPoCは成立しました。

これは今回得られた重要な結果です。

例えば、

Demucsにartifactがある
Beatに誤検出がある
Pitchが飛ぶ
Eventが重複する

としても、

人間が最終判断する

のであれば、十分制作支援として使えます。

つまり必要なのは、

完璧な解析器

ではなく、

人間が判断するために十分な候補を
分かりやすく提示するシステム

です。

今回の課題はPoC失敗ではなく次の改善ポイント

今回残った問題の多くは、

この方式では無理だった

というより、

方向性が成立したので、
次にどこを改善すればよいか分かった

という性質のものです。

実際の1曲を作りきったことで、

解析精度を上げるべき場所
Editor UXを改善すべき場所
自動化しすぎない方がよい場所

がかなり明確になりました。

PoCとしては、この状態まで来たこと自体が大きな成果でした。

次章では、これらの成功と失敗を通じて分かったことを整理し、今回のPoCから得られた設計上の知見をまとめます。

17. PoCで分かったこと

今回のPoCを通して、一番大きかったのは、

完全自動採譜を目指さなくても、音響解析だけで譜面制作は十分に楽にできる

と確認できたことです。

最初は、

楽曲を解析する
↓
ノーツを自動生成する

ところまで踏み込まないと、大きな価値は出ないのではないかとも考えていました。

しかし実際には、

音源分離
beat解析
event解析
duration推定
pitch解析

によって得られた情報をEditorへ持ち込み、

表示する
聞き分ける
Snapする
人間が選ぶ

だけでも、制作体験はかなり変わりました。

完全自動採譜でなくても十分価値がある

音ゲー譜面制作の大変さは、

ノーツを考えること

だけではありません。

その前段に、

この音はどこで鳴っているのか
何秒から始まるのか
どこまで続くのか
この楽器は何をしているのか

を探す作業があります。

ここはクリエイティブというより、かなり探索的な作業です。

今回のPoCでは、この部分を機械側へ寄せることができました。

人間は、

この音をノーツにするか
どういうノーツにするか
どのレーンへ置くか
どういう演出にするか

という、よりゲームデザインに近い判断へ集中できます。

この役割分担はかなり相性が良いと感じました。

音源分離だけでは足りない

Demucsは非常に重要でした。

しかし実際に作ってみると、

stemを分けた

だけでは譜面制作支援にはなりません。

必要だったのは、

何の音か
いつ鳴るか
どこまで続くか
拍との関係はどうか
音程はどう動くか

といった複数の情報です。

そのため、

Demucsだけ
Beat This!だけ
onset detectionだけ
pitch estimationだけ

のどれか1つで解決するのではなく、

複数の弱い解析結果を組み合わせる

方が有効でした。

各解析結果は役割が違う

今回使った解析は、それぞれ役割がかなり異なります。

Demucs
→ 何のパートか

Beat This!
→ 拍はどこか

Event Detection
→ いつ音が立ち上がるか

Duration Estimation
→ どこまで続くか

torchcrepe
→ 音程がどう動くか

これらを重ねることで、

12.5秒はbeat
その少し後にボーカル開始
同時にギターも鳴る
ボーカルは1秒ほど伸びる
その間にpitchが上昇する

という、より意味のある情報になります。

単一の解析器にすべてを解かせようとしなかったことは、今回かなり良かった点です。

複数の「そこそこ正しい情報」を重ねると強い

個々の解析には誤差があります。

例えば、

Beatは少しずれる
Eventは多めに出る
Pitchは一部飛ぶ
Stemには漏れがある

という状態です。

それでもEditor上で複数レイヤーを重ねると、

Beatもここ
Drumsもここ
Guitarもここ

という位置はかなり有力だと分かります。

逆に、

Guitarだけに弱いイベントがある

なら、少し疑って確認できます。

つまり、

複数の解析結果が互いの弱点を補う

ことができます。

精度だけでなくUXが重要

PoCを始める前は、主な技術課題は解析精度だと考えていました。

しかし実際に作ってみると、

解析精度を数%上げる

ことより、

解析結果へSnapできる
stemだけ聞ける
Long終端をevent endへ合わせられる

ことの方が、制作速度へ直接効く場面が多くありました。

つまり制作支援ツールでは、

解析結果を人間がどう操作できるか

が非常に重要です。

Analysis Viewerだけでは足りなかった

もし今回、

解析結果をグラフにしました

だけで終わっていたら、PoCとしての評価はかなり違っていたと思います。

実際にEditorへ組み込み、

ガイドを見る
↓
ノーツを置く
↓
Snapする
↓
stemを聞く
↓
修正する

ところまで通したことで、

この解析は役立つ
これは情報量が多すぎる
このendSecは使いにくい

といった実用上の評価ができました。

解析結果は、実際の作業に組み込んで初めて価値を評価できます。

Human-in-the-loopとの相性が良い

今回のシステムは、典型的なHuman-in-the-loopになりました。

Machine
→ 候補を大量に出す

Human
→ 意味を判断して選択する

という構造です。

これは音ゲー譜面制作との相性が良いです。

譜面には、

唯一の正解

が存在しないからです。

同じ音に、

Singleを置く
Longを置く
Flickにする
あえて何も置かない

のすべてが成立し得ます。

この部分まで機械に決めさせるより、人間へ選択肢を提示する方が自然でした。

Recall寄りの解析でも使える

完全自動生成では、誤検出はそのまま誤ノーツになります。

しかし制作支援では、多少余計な候補があっても、

使わなければよい

で済みます。

そのため、

Precisionを極限まで上げる

より、

必要なイベントをなるべく取りこぼさない

というRecall寄りの設計も選べます。

これは解析器の設計自由度をかなり上げます。

「正しい解析」より「使える解析」

研究的には、

Ground Truthに対してどれだけ正しいか

が重要です。

しかし制作支援では、

人間が判断するときに役に立つか

という別の評価軸があります。

例えば、音楽理論上は厳密でないendSecでも、

Longの長さを決める初期候補

として使えるなら十分価値があります。

今回のPoCでは、

正確な採譜データを作ることより、制作に使える情報を作ること

を優先しました。

JSONを境界にした設計は有効だった

システム構成としては、

Analyzer
↓
Analysis JSON
↓
Editor
↓
Chart JSON
↓
Player

と分けたことも成功でした。

AnalyzerにはPythonを使い、EditorにはTauri + Reactを使い、Playerはまた別の構成にしました。

それでも問題なく接続できました。

これは、

コンポーネント間の契約をJSON Schemaへ閉じ込めた

からです。

技術スタックを統一しない方が楽だった

もし、

すべてPython

や、

すべてTypeScript

に統一することを優先していたら、それぞれの領域で不利な選択をしていた可能性があります。

今回は、

音響解析
→ Python / PyTorch

Editor
→ Tauri / React

Player
→ 小さなWeb実装

としました。

各コンポーネントに適した技術を自由に選びつつ、JSONだけ合わせる構成は、今回のようなPoCと非常に相性が良かったです。

Schemaは早めに作ってよかった

PoCでもSchemaを持つ価値は大きかったです。

途中では、

Analysisの情報を増やす
Slideを複数点対応する
End Flickを追加する
Flick Chainを追加する

など、仕様が何度も変わりました。

そのたびにSchemaが、

何が正式なデータなのか

を示す基準になりました。

特にPlayerを独立実装すると、Schema不足はすぐに露呈します。

PoCだからこそ変更が多く、むしろ契約を明示しておくメリットが大きかったと感じています。

実データからSchemaを育てる方が良かった

一方で、Schemaを最初から完璧に設計しようとしなかったことも良かったです。

例えば、

startSecだけで十分だと思った
↓
Longを作るとendSecも必要だった

というように、実際に使うことで必要な情報が見えてきます。

Slideも、

開始と終了だけでよい
↓
実際には複数waypointが必要

と変化しました。

今回のような探索的PoCでは、

仮のSchema
↓
実データ
↓
Editorで利用
↓
問題発見
↓
Schemaを進化

という流れが自然でした。

Playerを作ったことで「本当に成立したか」を確認できた

Editorまでで止めずにPlayerを作ったことも重要でした。

Playerは、

見た目を確認するため

だけではなく、

Chart JSON単体でゲームとして成立するか

を検証する役割を持ちました。

結果として、

Editorが保存したChart
↓
独立Playerが読む
↓
693 / 693 Perfect
↓
Full Combo

まで通せました。

これによって、コンポーネント間の接続が概念上ではなく実際に成立していることを確認できました。

End-to-Endで試すことが重要

それぞれのライブラリ単体では、

Demucsが動いた
Beat This!が動いた
torchcrepeが動いた

ということは比較的簡単に確認できます。

しかし本当に知りたかったのは、

それで譜面制作が楽になるのか

です。

そのため、

実楽曲を解析
↓
実際にEditorで譜面制作
↓
実際にPlayerで最後まで遊ぶ

ところまで通したことが非常に重要でした。

End-to-Endで試したからこそ、解析精度だけでは見えない問題を大量に発見できました。

AnalyzerとEditorは相互に改善される

今回の開発では、

Analyzerが完成
↓
Editorを作る

という一方向ではありませんでした。

実際には、

Analyzerで出す
↓
Editorで見る
↓
使いにくい
↓
Analyzerを直す

Editorで使う
↓
操作しにくい
↓
Editorを直す

という反復でした。

これは今後も続くと思います。

制作ツールでは、

解析器の品質とUIの品質を別々に最適化できない

ことが分かりました。

「自動化しない」という設計判断も重要

技術的には、

解析eventをそのままSingleへ変換

することは簡単です。

pitchをレーン位置へ機械的にマッピングすることもできます。

しかし今回は、あえてそこを主要機能にはしませんでした。

理由は、

できること

と、

やるべきこと

は別だからです。

自動化すると、人間の作業は減ります。

しかし同時に、

ゲームとしての意図

まで機械が決めることになります。

今回のPoCでは、

面倒な探索作業だけを機械へ渡し、創作部分は人間に残す

境界がかなりうまく機能しました。

人間がやるべき仕事を変えられた

今回一番重要だったのは、作業時間を何%削減したかという具体的な数字よりも、

人間が何をしている時間が増えたか

だと思っています。

従来は、

曲を聞く
↓
位置を探す
↓
何度も戻す
↓
時刻を合わせる

時間がかなりあります。

PoCでは、

解析候補を見る
↓
どの音を採用するか考える
↓
どんな操作にするか考える

へ寄せることができました。

つまり、

探索作業

から、

譜面デザイン

へ時間を移せました。

「自動採譜」より「制作支援」の方が現実的だった

PoCを終えた時点での結論としては、

AIや音響解析で譜面を全部自動生成する

より、

音響解析で制作環境そのものを賢くする

方向の方が、少なくとも現時点では実用性が高いと感じています。

完全自動生成は非常に魅力的ですが、

音楽解析
ゲームデザイン
難易度設計
操作性

をすべて同時に解く必要があります。

一方、制作支援なら既存の解析技術を組み合わせるだけでも、すでに大きな効果があります。

PoCとして得られた最終的な結論

今回のPoCから得た結論を一文でまとめるなら、

音ゲー譜面制作では、解析によって「正解の譜面」を生成する必要はなく、人間が判断できるだけの音楽的なガイドを高速に生成し、それをEditorの操作へ直接接続するだけでも十分大きな価値がある。

ということになります。

音源分離、beat解析、pitch解析などの個々の技術そのものよりも、

解析
+
可視化
+
再生
+
Snap
+
人間の判断

を一つの制作フローへ組み込んだことが、今回のPoCで最も重要だった部分でした。

この方向性が成立することは確認できたので、次は「PoCとして動く」状態から、実際に継続して使える制作環境へ進めていきたいと考えています。

次章では、今回見つかった課題を踏まえて、Analyzerの精度向上、ノーツ候補提示、Android Playerなど、今後試したいことをまとめます。

18. 今後やりたいこと

今回のPoCで、音響解析を使った譜面制作支援という方向性そのものは十分成立することが確認できました。

一方で、現状はまだ、

PoCとして動く

段階です。

今後は、

継続して実際の譜面制作に使える

制作環境へ近づけていきたいと考えています。

解析イベントの重複除去

まず改善したいのが、stem間で重複するイベントです。

Demucsの分離は完全ではないため、

drums
other
guitar

などに同じ音の成分が残ることがあります。

すると、

10.500 sec drums
10.506 sec other

のように、実質同じイベントが複数レイヤーへ出てきます。

今後は、

時間差
音量
周波数特徴
元mixとの相関
stem種別

などを使いながら、

同じ音由来の可能性が高いイベント

をまとめたいです。

ただし、本当に複数の楽器が同時に鳴っているケースまで消してはいけません。

単純な時間距離だけではなく、音響的な類似性も含めて判定する必要があります。

stemごとに解析方法を最適化する

今回のPoCでは、各stemへ共通化できる部分も多くありました。

一方で、

drums
vocals
bass
guitar
piano

は音響的な性質がかなり違います。

そのため今後は、

drums
→ onset中心

vocals
→ onset + duration + pitch

bass
→ onset + low-frequency energy

guitar
→ local peak + duration

piano
→ onset + polyphonic analysis

のように、stemごとに適した解析へ分化させたいです。

最終的なAnalysis JSONは共通形式のままにして、内部の解析器だけを変える構成が理想です。

Pitch情報の使い方を詰める

pitchについては、まだ可能性を十分使えていません。

現状では、

音程の上下が分かる

ところまでです。

今後は、例えば、

上昇フレーズ
下降フレーズ
同音反復
大きな跳躍
ロングトーン

などを抽象化して、

Slide候補
Flick候補
レーン移動候補

として提示できるか試したいです。

生のHz値をそのまま見せるより、

このフレーズは上昇傾向

のような制作向けの情報へ変換した方が実用的だと考えています。

ノーツ候補の提示

今回、Analysis Eventをそのままノーツにすることはしませんでした。

これは今後も基本方針として維持したいです。

ただし、

完全自動生成

ではなく、

候補提示

ならかなり有用だと思っています。

例えば、

このDrum EventはSingle候補
このVocal IntervalはLong候補
このPitch CurveはSlide候補

のようにEditor側で提案できます。

人間は、

採用
却下
種類変更

を選ぶだけです。

この程度の半自動化であれば、創作性を残したままさらに作業量を減らせそうです。

複数イベントからフレーズを認識したい

現在は、基本的にevent単位で解析しています。

しかし実際の譜面制作では、

音1
音2
音3
音4

を個別に見るより、

1つのフレーズ

として見た方が分かりやすい場合があります。

例えばボーカルなら、

歌詞の1フレーズ

ギターなら、

リフ
アルペジオ

などです。

今後は近接eventやpitch contourをまとめて、

Phrase

という上位概念を作れると、譜面制作の支援レベルが一段上がると思います。

Snapの優先順位を改善する

Snapもまだ改善できます。

現在は、

Beat
Analysis Event
Existing Note

など複数の候補があります。

しかし候補が近接すると、

どこへ吸着したのか分かりにくい

場合があります。

今後は、

現在表示しているレイヤー
選択中のガイド種別
距離
confidence

などを使い、Snap候補へ優先順位を付けたいです。

例えば、

Vocal編集モード

のときにはVocal Eventを優先するといったUXも考えられます。

Snap候補を視覚的に選べるようにする

近接候補が複数ある場合、

Beat
Vocals
Guitar

がほぼ同じ位置に存在することがあります。

この場合、自動的に1つへ決めるだけでなく、

候補を表示
↓
ユーザーが選択

できるようにするのもよさそうです。

例えばドラッグ中に、

Beat 12.500
Vocal 12.506
Guitar 12.512

のように候補を出す方式です。

Analysis Layerの表示を整理する

レイヤー数が増えると、Editorがかなり混雑します。

今後は、

Rhythm
Melody
Stem
Pitch

などのグループへまとめたり、

Vocal Editing
Drum Editing
Melody Editing

のようなプリセットを用意したいです。

目的に応じて必要なガイドだけ見える方が、制作には向いています。

Analyzerを部分的に再実行できるようにする

現状では、

1曲全体を解析

という使い方が中心です。

しかし実際には、

この20秒だけGuitar解析をやり直したい

というケースも考えられます。

今後は、

時間範囲
stem
解析種類

を指定して部分的に再解析できると便利です。

例えば、

120〜140秒
Guitar Eventだけ再解析

といった処理です。

これならパラメータ調整の試行錯誤も高速になります。

解析プリセット

楽曲によっても最適な設定は変わります。

例えば、

バンド曲
EDM
ピアノ主体
ボーカル主体

では必要な解析が異なります。

そこで、

Band
Vocal
Rhythm
Detailed
Fast

などの解析プリセットを用意するのもよさそうです。

例えばFastなら、

Beat
Drum onset
簡易event

だけを短時間で実行し、

Detailedなら、

Stem separation
Pitch
Duration
全stem event

まで実行する形です。

EditorからAnalyzerを実行するか

現在はAnalyzerとEditorを意図的に疎結合にしています。

この構造自体は維持したいです。

ただしUXとしては、

Editorから解析ジョブを起動

できてもよいかもしれません。

内部的には、

Editor
↓
Analyzer CLIを実行
↓
Analysis JSONを読み込む

だけにして、データ契約は維持します。

技術的に密結合させずに、操作だけ一体化する方向です。

Editor UIをさらに磨く

Editorについては、かなり多くの機能が入りましたが、まだ改善余地があります。

例えば、

Inspector
ショートカット
レイヤー管理
複数選択
Connection編集
Slide編集

などは、より洗練できます。

特に数百ノーツを長時間編集するツールなので、

1操作を何クリック減らせるか

がかなり重要になります。

譜面制作向けショートカットを増やす

今後は、

Single配置
Long配置
Flick切替
選択
削除
複製
Snap ON/OFF

などをキーボードだけで操作できるようにしたいです。

マウス移動量を減らすだけでも、長時間の制作ではかなり差が出ます。

難易度違いの譜面制作支援

同じAnalysis JSONから、

EASY
NORMAL
HARD
MASTER

など複数譜面を作れる構成にもできます。

例えば既存Chartをベースに、

ノーツ密度を下げる
細かいeventを削る
FlickをSingleへ置換
複雑なSlideを簡略化

といった候補を提示する機能です。

ここでも完全自動ではなく、

簡略化案を提示

する程度から始めるのがよさそうです。

Player UIを作り込む

現在のPlayerはPilot版です。

PoCとして、

ノーツが流れる
入力できる
判定できる
最後まで遊べる

ことを優先しました。

今後は、

ノーツデザイン
レーンデザイン
判定エフェクト
コンボ表示
スコアUI
背景演出

なども作り込みたいです。

特にEditorとPlayerでノーツの見え方が大きく異なると、譜面制作時のイメージと実プレイに差が出るため、両者の見た目をある程度揃える必要があります。

DecorationをPlayerで活用する

Chart側にはDecorationを持たせられるようになりました。

今後はこれを使って、

文字
歌詞
図形
演出

などをPlayer上へ表示できます。

音ゲーの創作譜面では、

譜面そのものを使った演出

も面白さの一部です。

判定ノーツとは別レイヤーで扱えるようにしておけば、ゲームロジックを壊さずに表現を増やせます。

Androidで実際に遊べるPlayer

今回のPoCの先にある大きな目標の一つが、

Android実機で遊べること

です。

現在のPlayerはブラウザベースのPilotですが、Chart JSONがPlayerから独立しているため、

Android専用Player

へ置き換えることもできます。

ここでは、

タッチ入力
複数指
入力遅延
音声遅延
画面サイズ
リフレッシュレート

を実機で検証する必要があります。

PlayerをWebのままPWA化する選択肢もある

必ずしも最初から完全ネイティブへ行く必要もありません。

現在のWeb Playerを発展させて、

PWA
Fullscreen
Touch optimized

とする方法も考えられます。

まずWebベースで実機検証し、必要になればネイティブ化する方が開発コストは低いかもしれません。

他の楽曲でも検証する

今回の主な実証曲は「キラメキ☆」でした。

しかし1曲だけでは、

この曲にたまたま合っていた

可能性を排除できません。

今後は、

テンポの遅い曲
速い曲
EDM
ロック
ピアノ主体
ボーカルが少ない曲
変拍子
テンポ変化がある曲

などで試したいです。

特にジャンルによって、

どの解析が役立つか

がどう変わるかは興味があります。

「制作時間がどれだけ減ったか」を測りたい

今回のPoCでは、

制作が明らかに楽になった

という体感はありました。

しかし、定量的な測定まではしていません。

今後は例えば、

解析ガイドなし
解析ガイドあり

で同程度の譜面を作り、

完成までの時間
再生回数
タイミング修正回数

などを比較してみたいです。

これによって、

PoCとして面白い

だけでなく、

実際にどのくらい省力化できるか

を示せます。

半自動化の境界を探したい

今回のPoCでは、

完全自動化しない

という方針がうまく機能しました。

ただし、どこまで機械に任せると最も効率がよいかは、まだ探れると思っています。

例えば、

完全手動
↓
Snapだけ
↓
候補提示
↓
仮配置
↓
ほぼ自動生成

という段階があります。

おそらく最も使いやすいのは、このどこか中間にあります。

人間の創作性を残しながら、

面倒な操作だけ機械に任せる

境界をさらに探りたいです。

自動生成そのものを否定しているわけではない

今回「自動採譜ではなく制作支援」という結論になりましたが、自動生成そのものに価値がないという意味ではありません。

むしろ、

Analysis
↓
Candidate Generation
↓
人間による修正

のように、初稿を自動生成する方式は十分あり得ます。

ただし今回のPoCから見る限り、

最初から人間を完全に排除する

より、

人間が編集する前提で良い初稿を作る

方が現実的だと思っています。

最終的には「創作に集中できるEditor」にしたい

今後の方向性を一言でまとめるなら、

譜面制作者が、曲のタイミングを探すことではなく、譜面をどう面白くするかに集中できる環境

を作りたいです。

理想的には、

曲を読み込む

↓

数十秒で解析

↓

Beat / Stem / Pitch / Phraseが表示される

↓

必要な場所にはノーツ候補が出る

↓

人間は採用・変更しながら譜面をデザインする

↓

すぐPlayerで確認する

という流れです。

今回のPoCによって、その形が単なるアイデアではなく、実際に実装可能であることは確認できました。

あとは、各解析の精度とEditorのUXを少しずつ改善していけば、かなり実用的な制作環境へ育てられると考えています。

19. まとめ

今回のPoCでは、

音源分離・ビート解析・ピッチ解析などを組み合わせることで、音ゲーの創作譜面制作を実際に楽にできるか

を検証しました。

最初の問題意識はかなり単純でした。

音ゲーの譜面制作では、

曲を何度も聞く
↓
音が鳴った場所を探す
↓
タイミングを合わせる
↓
ノーツを置く

という作業を大量に繰り返します。

このうち、

何の音が
いつ鳴って
どこまで続いているかを探す

部分は、機械にかなり任せられるのではないかと考えました。

最終的に作ったもの

今回構築したパイプラインは、

Audio
  ↓
Analyzer
  ↓
Analysis JSON
  ↓
Editor
  ↓
Chart JSON
  ↓
Player

という構成です。

Analyzerでは、

Demucs
Beat This!
librosa
torchcrepe
独自のevent detection

などを組み合わせました。

その結果から、

beat
downbeat
stemごとのevent
start / end
pitch
confidence

などを抽出し、Analysis JSONへ統合しました。

解析結果をそのまま譜面にはしなかった

今回、特に重要だった設計判断が、

Analysis EventとChart Noteを分離したこと

です。

解析結果が、

ここでドラムが鳴った
ここでボーカルが始まった
この区間にギターがある

と教えてくれても、

それをSingleにするか
Longにするか
Slideにするか
Flickにするか
そもそもノーツを置かないか

は人間が判断します。

つまり、音響解析には、

音楽を説明する

ところまでを任せ、

人間には、

ゲームとしてどう表現するかを決める

部分を残しました。

複数の解析を組み合わせることが重要だった

今回使った技術のどれか1つだけで、譜面制作を大きく改善できたわけではありません。

それぞれ、

Demucs
→ パートを分ける

Beat This!
→ 拍を見つける

Event Detection
→ 発音タイミングを見つける

Duration Estimation
→ 発音区間を見る

torchcrepe
→ 音程の動きを見る

という異なる役割があります。

これらを同じ時間軸へ重ねることで、初めて制作ガイドとして強くなりました。

Editorへ組み込んで初めて価値が出た

解析結果をJSONとして出力しただけでは、制作作業はそれほど楽になりません。

実際に効果が大きかったのは、

解析レイヤー表示
stem個別再生
低速再生
Audition
Snap

などをEditorへ組み込んでからでした。

特にSnapは、

解析結果を見る

だけだったものを、

解析結果を直接編集操作に利用する

ものへ変えました。

ノーツを近くまで移動するだけで、

Beat
Vocal Event
Guitar Event
既存ノーツ

などへ吸着できます。

この部分は、今回のPoCの中でも特に制作支援として効果が大きかったと感じています。

「100%正しい解析」は必要なかった

実際には、解析結果は完全ではありません。

Demucsのartifact
stem leakage
eventの重複
beatの誤検出
pitchの飛び
endSecの曖昧さ

などがあります。

それでも、実際に1曲の譜面を完成させることができました。

これは、

Human-in-the-loopであれば解析精度が100%でなくても十分価値がある

ことを示しています。

例えば候補が10個あり、そのうち2個が間違っていても、人間は不要な2個を無視できます。

それよりも、

必要な候補が最初から表示されている

ことの方が制作上は大きなメリットでした。

約3分21秒の実楽曲を約53秒で解析できた

性能面でも、今回のPC環境では現実的な速度で動きました。

約201秒の「キラメキ☆」に対して、一連の解析は約53秒でした。

つまり、

3分21秒の曲
↓
1分未満で解析

できます。

解析時間の多くはpitch推定だったため、用途によってpitchを省略すればさらに高速化できます。

少なくとも、

実際の制作工程へ解析を組み込む

という用途には十分現実的な範囲でした。

1曲を最初から最後まで通せた

最終的には「キラメキ☆」の創作譜面を制作し、独立したPlayerへ読み込ませました。

Playerでは、

Single
Long
Slide
Flick
End Flick
Flick Chain

などを解釈し、音声再生と同期して実行します。

検証用Autoplayでは、

693 / 693 Perfect
Full Combo

まで通すことができました。

つまり、

Audio
↓
Analysis
↓
Editing
↓
Chart
↓
Playing

という一連の経路を、実際の1曲で最後まで通せました。

これを今回のPoCの大きな成功条件としていました。

Analyzer / Editor / Playerを分離したのも正解だった

システム構成としても、

Analyzer
Editor
Player

をそれぞれ独立させたことは有効でした。

音響解析にはPython / PyTorchを使い、

EditorにはTauri / Reactを使い、

Playerは別の小さなWeb実装にしました。

それでも、

Analysis JSON
Chart JSON
JSON Schema

を契約として使うことで、問題なく接続できました。

各コンポーネントに最適な技術を自由に選べるため、この構成は今後の拡張にも向いています。

PoCを通して一番大きく変わった考え

PoCを始めた当初は、

どうすれば音ゲー譜面を自動生成できるか

という方向にも興味がありました。

しかし実際に作ってみて、

完全自動化しなくても十分大きな価値がある

と分かりました。

むしろ、

機械
→ 面倒な探索を担当

人間
→ 創作的な判断を担当

という分担の方が、現時点では自然に感じています。

譜面には唯一の正解がありません。

だからこそ、

正解の譜面を生成する

より、

人間が良い譜面を作るための情報を揃える

方が実用的でした。

今回のPoCで得た結論

今回の結果を一言でまとめるなら、

音ゲー譜面制作では、音響解析によって完成譜面を自動生成する必要はない。音源分離、ビート解析、発音イベント、発音区間、ピッチなどを制作ガイドとしてEditorへ統合し、人間の編集操作に直接利用できるようにするだけでも、譜面制作はかなり楽にできる。

ということになります。

今回のPlayerやEditorはまだPilot版です。

解析精度も改善できます。

Android実機への展開や、ノーツ候補提示、Phrase解析など、やりたいことも多く残っています。

それでも、

この方向で本当に作れるのか

という最初の問いには、かなり明確に答えを出せました。

少なくとも今回のPoCでは、

音響解析を創作音ゲーの譜面制作支援へ使うというアイデアは、十分実用につながる

という感触を得ています。

image.png

構想自体はあったもののものにするのに時間をかけすぎてしまった。というよりP業の副業(一般人向けに説明すると仕事のことである。本業はアイドルプロデューサーというアイマス界隈のネタ)が忙しく腰を上げたのが...

image.png

2026年の9月4日なので、PoC+譜面制作はここまで実質1週間ぐらいかかった計算になる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?