サウンドナンモワカランな人がBgmコネコネした話

Posted: 2018年12月2日 カテゴリー: Unity
タグ:, , ,

こちらはUnity Advent Calendar 2018 2日目の投稿になります。

 

今回はタイトル通り音楽経験は1瞬ドラムを触って遊んでた事があるレベルのサウンドがナンモワカランマンが今製作中の某VRADVゲームのBGM周りの実装をしている話(現在進行系)を紹介して行きます。

 

どんな事をするかというと、Bgmのパーツ化とレイヤー分けをザックリと実装した事の紹介です。

 

こういう音楽で色々やる系に関してはじーくどらむすさんが色々と面白い記事を書いているので是非そちらも読んでみてください!(というかそこだけで良くない?

https://note.mu/geekdrums/m/m4d16cc96ee7b

 

結論:じーくどらむす氏の記事を読もう!

 

以上!

 

・・・はい、すみません、続けます。

 

Bgmのパーツ化

上の記事読んで無い人はパーツ化ってなんやねん?ってなるかも知れませんが、1個の曲をBgmとしてループしていくのでなく、1個の曲を何個かのパーツに区切り、それらをランダムな順番で再生して繋いで行くことで長時間同じ曲を再生していても飽きが来ないようにしようとするアプローチです(他にも意図はありますが、今回は極シンプルな物のみの話です)。

 

今回やったパーツの区切り方はイントロ、アウトロ、メインの3種類になります。

 

イントロは曲の最初に再生されるパーツになります。イントロが終わったらメインのパーツが次に再生されます。

 

メインは曲がループで再生されている最中に流れるパーツです。ここには複数のパーツがあり、その中の一つがランダムで選択され再生されます。そしてここのパーツの再生が終わる時にまたメインの何れかのパーツを再生します。

 

再生していくと、この様な感じになります:

表示している文字はMusic Engineのデバッグ文字に再生中のクリップ名を最後に付け足した物です。Justと書かれている横の数値は左から順番に現在の小節、泊、拍子、の値になります。

 

曲を終了させる指示を発行したら、今再生中のパーツが終わった後にアウトロに繋がるようになります。

 

アウトロは曲の終わりのパーツです。これの再生が終われば曲の再生が完全に終わります。

 

Music Engineがあるよ!

さてこのような実装、真面目にやると結構面倒ですよね。AudioSourceのIsPlayingだけで切り替えるとどこまで正確に行けるかと言うと・・・てへ!

 

そんなワタクシみたいな人の為に又々じーくどらむす氏がMusic Engineという物を公開しています!

https://github.com/geekdrums/MusicEngine

 

これが何かというと、音楽のタイミングが獲得出来るようになるスクリプトで、これを使うことで音楽の特定のタイミングに合わせた処理を実装しやすくなります。

 

更にMusic Engine自体にも1個のAudioClipを分割してパーツ化し、管理するSectionという機能が既に組まれていたりします!至れりつくせり!

 

やったぜ!楽勝だぜ!・・・と思っていた時期が0.1秒ぐらいありました。

 

実装・・・と思いきや余韻ががが!

さて今回のシステムは特定のゲーム向けなのですが、その中でこんな要望がありました:

 

神曲マン「曲の最後の1小節を余韻として次のパーツへの繋ぎにして、それを再生しながら別のパーツを再生して欲しい」

 

よ・・・いん?

 

さてここでMusic Engineの紹介スライドを見てみましょう:

https://www.slideshare.net/geekdrums/about-musicengine-46632468

 

・・・「音楽はいつも1つ」・・・「クロスフェードは甘え!」

 

はい終了・・・ご臨終です。

 

という訳でMusic Engineが有るにしても実装をもう少し考える必要がありました。

 

余韻を考慮した実装

ここまでの前提紹介で凄い尺を食った感がありますが、ここから実装の話に近づいて行きます。Music Engine自体の基本的な使い方は結構色々出回ってるので、今回はスルーします。

 

[*] 前提として今回は各パーツに区切られた音楽ファイルが存在し、それらのBpmや小節数等も全て分かった状態での実装になります。

 

実装でまず行った事はMusic.csをアタッチしたオブジェクトを複数用意し、それらを交互に再生出来るようにしました。今回サンプルで使用した曲の1パーツは9小節で構成されてます(最後の1小節は余韻なので、実質中身は8小節)。そのため必要なのは下記になります:

  • 8小節目に次のパーツを再生する(小節数 – 余韻の長さのタイミング)
  • 9小節全て再生するまでは2つ同時に鳴らす
  • 切り替え後のタイミングで先に次何を鳴らすかランダムに獲得しておく

 

次のパーツ再生処理ですが、Music Engineには今のフレームが小節切り替えのタイミングかどうかを判断するIsJustChangedBar()というメソッドが用意されていますので、それを使い切り替えの時に今が何小節目か見て処理を行っています。これもMusic Engineの中にある今の小節数、泊数、拍子数を保持しているデータを参照しています。

 

小節終わりを見て、次のパーツを再生するかの確認処理の一部です:

[SerializeField]
Music[] m_musicEngines = null;        
public Music Current { get { return m_musicEngines[m_indexCurrent]; } }
public Music Next { get { return m_musicEngines[m_indexNext]; } }

int m_indexCurrent;
int m_indexNext;

int m_revurbLenght =1; // 余韻の小節数
int m_barLength; // 小節数
m_changeClipBar; // AudioClip設定する小節
m_scheduleBar; // 再生予約する小節

public void SetBarLength(int bars)
{
	m_barLength = bars;
	m_changeClipBar = m_barLength - m_revurbLenght;
	m_scheduleBar = m_changeClipBar - 1;
}

…
// 小節数を設定する
SetBarLength(9);
…

private void Update ()
{
//小節変更待ち
	if (!Current.IsJustChangedBar())
	{
		return;
	}

//今の小節数を見る
	int bar = Current.Just.Bar;
	if (bar == m_scheduleBar)
	{
// 次のパーツを再生
		PlayNext();
	}
...

切り替え検知は用意出来ましたので、次に再生処理の方に入ります。

再生処理ですが、AudioSource.Play()をこのタイミングで使うだけだとハードによっては結構再生が遅れる可能性があります。ある程度余裕を見て再生を行うように今回はAudioSouce. PlayScheduled()を使用しています。

 

AudioSouce. PlayScheduled()は再生する時間を指定し、その時間になったら再生される様に予約しておく処理になります。これを使用して今回は余裕を見てパーツ切り替えの1小節前に予約を入れています。

 

さてこれをやる為には1小節の秒数を獲得する必要があります。なので1小節の秒数計算処理を実装します。計算方法は

 

60 * 1拍内の拍子数 * 獲得する小節数 / テンポ

 

になります。小節数は何小節分の時間を計算したいか、という事になるので今回は1で大丈夫です。ちなみに60なのはテンポ(BPM)がBeat Per Minuteの名前の通り、1分間に何ビート(泊)あるかという基準なので、1分を表す60が使用されていると思われます。

 

public double TimePerBar { get { return 60.0 * Current.CurrentSection.UnitPerBeat / Current.CurrentSection.Tempo; } }

 

これで1小節分の時間計算が出来ました。これは結構色々使えるので、用意しておくと便利でした。

 

時間計算が出来たので、後はAudioSouce. PlayScheduled()に時間を突っ込むだけです。時間を指定するにはまず現在の時間をAudioSettings.dspTime で獲得し、それに先程の1小節の時間を足して渡しています

// 次の小節から開始したいから1小節分プラスしたい
double time = AudioSettings.dspTime + TimePerBar;
Next. CurrentSource.PlayScheduled(time);

これで再生の準備が出来て来ました。

 

これまで説明を省いていたのですが、Musicが2つありどちらが現在のかを判断する方法は色々あると思いますが、今回はMusicの配列作り毎回参照用のインデックスを保持しているintの変数の中身を入れ替えています。その為、次のパーツが再生されるタイミングでこの入れ替えを行っています。

 

それに加えてパーツを繋げる為には次のパーツを選ぶ必要があり、更にそのAudioClipをAudioSourceにつける必要があります。今回はこれを余韻も含めた全部が再生し終わったタイミングで行っています。

 

今回の処理のタイミングを9小節の曲前提で纏めると:

  • 7小節目に次のパーツの再生予約
  • 8小節目に予約したパーツが再生される
  • 8小節目に現在と次のMusicのインデックス入れ替え
  • 9小節目に次のパーツ判定と、次ぎのAudioClipをAudioSourceにつける

 

という流れになります。これでパーツのランダム再生を延々とループさせる処理は基本的には出来ました(細かい所は省略

 

やったね!

 

レイヤー化

パーツ化がある程度形になりましたので、次はレイヤーについて話していこうと思います。

レイヤーは言葉通り、サウンドの上乗せになります。これまでのパーツ化が横に呼びていく遷移のイメージでしたが、今度は縦に伸ばしていくイメージになります。

 

単純に言うと、同時に沢山のBgm鳴らすよ、わーい!って事ですねやったね処理負荷!

 

再生していくと、この様な感じになります:

 

途中からピアノが追加され、その後にギターが追加されています。

 

今回はレイヤーとして、アンビエント、ベース、リードの3種類用意することにしました。上記の動画だと最初に鳴るのがアンビエント、ピアノがベース、ギターがリードになります。

 

基本的にはこの順番に上乗せして行きます。なので、ベースを飛ばしてアンビエントとリードのみという事は仕様上無しになります。

 

このレイヤー部分もパーツ化されておりますので、各パーツには3レイヤー分3つのサウンドが用意されています。そのためパーツ再生時も現在再生しているレイヤーの指定したパーツを再生する、という流れになっています。

 

さて実装はAudioSourceを沢山使って同時に再生すれば良い、というのでほぼ正解なのですが、この様な事がまた起こりました:

 

神曲マン「余韻は勿論全てのレイヤーにあるよ」

 

・・・・余韻再び orz

 

さてこれにより全てのレイヤーはパーツ化のみならず、余韻再生の為に切り替え機能も必要になりました。

 

これを実現する為に各レイヤー用のクラスを用意して、その中に切り替え用のAudioSourceを2つ保持して切り替え処理等を行うようにしました。

public class AudioLayer
	: IAudioLayer
{
	[SerializeField]
	AudioSource[] m_audioSources = null;

	public AudioSource Current { get { return m_audioSources[m_indexCurrent]; } }
	public AudioSource Next { get { return m_audioSources[m_indexNext]; }  }
…

ただし、これだとMusic Engineによる計測が出来ないので、一つだけMusic Engineを含んだレイヤーを作り、それを必ず再生されるアンビエントのレイヤーとして割当ました。

public class CoreAudioLayer
	: IAudioLayer
{
	[SerializeField]
	Music[] m_musicEngines = null;

	public Music Current { get { return m_musicEngines[m_indexCurrent]; } }
	public Music Next { get { return m_musicEngines[m_indexNext]; } }
	
...

これで後は使うレイヤー分のインスタンスを配列とかで作って、現在使用するレイヤーの音だけを再生してパーツ化の時と同じ流れにすれば行け・・・無くはないけどまだ微妙な所があります。

 

まずは音量です。レイヤー増やした時には音源が増えるので、単品で再生している時とはバランスを変えないとアンビエントだけが強調されて聞こえている、等違和感がでる形になってしまう場合があります。

 

それを回避する為に各レイヤー設定時のAudioSourceの音量指定をScriptableObjectに保持し、それをレイヤー設定時に各AudioSourceに適用しています

この各Volume配列内のfloat値が各レイヤーのAudioSourceのVolumeに設定されます。

 

さてこれでバランスは取れたぞ完成いえーい!・・・とは行かないですよね。

 

現状の仕組みだと最初からレイヤー設定して再生する場合は問題は無いのですが、途中でレイヤーを切り替えた場合、ドカッと音が入って来たりするので滑らかにレイヤーを変える為のフェード処理が必要だろうとなりました。

 

フェードあれこれ

フェードは単に音量を徐々に変えていけば良いのですが、せっかく音楽のタイミングを図れるので特定秒数でなく、小節数で長さを指定した方がキレイになるのでは?と思い基本的にBGMのフェードは全て小節数指定から秒数を計算するようになっています。

 

[*]フェード処理自体は探せば沢山あるので、取り敢えずDoTweenのAudioSource.DoFade()みたいな物を使う想定で勧めます。

 

レイヤー切り替えのフェードの長さとタイミングですが、最初は再生する小節の半分の長さをフェードする様にしていたのですが、曲によっては(特にサンプル曲)まだ唐突感がありましたので、1パーツの余韻を抜かした再生分丸々フェードしてレイヤー切り替えを行うようにしました。

 

これを行う為に、レイヤー切り替えのフェード開始のタイミングは切り替え指示を受けたら一旦予約状態に入り、パーツ切り替えのタイミングで開始するようにしました。そうすることでフェードが終わったタイミングでちょうど次のパーツの再生を指定音量で入るから滑らか度は少しはマシになったかな?と信じてやりました。

 

また、レイヤー切り替えの時は新しく再生するレイヤーのみならず、再生中のレイヤーも指定の音量にフェードをするようになっています。

 

これでレイヤー切り替え処理は大体実装出来ました!今度こそいえーい!

 

Bgm設定エクセル

機能実装の話は大体終わりましたが、もう一つBgmの曲の小節数やBpm等の設定を纏めておけると曲指定時の設定が楽だなーと思ってエクセルに纏めて、それをScriptableObjectに変換して利用しています。

(画像の値はかなり適当です・・・)

 

これを曲設定時の小節数反映等に使用しています。

 

エクセルからの変換処理は安定のテラシュールさんのやつを使わせて頂いています。

 

問題点

さて最後に、作ってみてあった問題です。

 

ゲームがOculus Go向けに制作している為、処理負荷は結構気にする必要があります。

 

結構細かい事ではあるのですが、Oculus Go上で処理不可が思ってたより高い上に変更が楽な対策を紹介します(率直に言うとアホなミスは気をつけましょう案件)。

 

まずPlaySchduled()を呼ぶ時に使用しているAudioSettings.dspTimeは回数が増えると意外と処理負荷になります(Time.deltatimeよりは重いっぽい)。なので、1フレーム中に1回先に保持しておいてそれを再利用する様にしています。

 

次にMusic Engineで時間計測する為に曲の周波数設定しているのですが、周波数を獲得出来るAudioClip.frequencyがかなりの処理不可になっていた為、曲をロードしたタイミングで周波数を獲得し曲自体を破棄するまでは保持する様にしてました(今となってはエクセル書いた方が色んな意味で早いんじゃね?と思わなくは無いです・・・)。

 

最後にMusic Engine自体はとても便利なのですが、管理が文字列ベースだとか、今回想定外な使い方をしているからいらない機能や処理が有るとかがあるので、AudioClip分割やSection機能は徐々に消していっています。極力プロファイラー上で処理不可が高かったり、ガベージを生み出している所を消していますが、外部スクリプトを使う時はこの辺も気をつけましょうというだけの案件です。

 

以上になります。長い割には色々と端折って説明不足かも知れませんので、何か分からなかったら質問投げてください m_ _m

 

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中