GPUSound入門(6) 作曲サンプル

## はじめに

Shadertoy上でGPUSound(music shader)のコメント含めたサンプル作ってみたけど、ちょっと情報まとめといたほうがいいのかな?とも思ったので、記述を残すもの。

## サンプル公開

一度作曲のサンプルにコメント加えた形で公開します。
(公開してるデモに組み込んだ曲ですが)

https://www.shadertoy.com/view/3tX3D8

## サンプル公開の経緯

公開できてない状態だったのでリハビリ代わりにいろいろ見てたところ、結構ノウハウが溜まってるなと思いまして、プロセスを変更して「生きたサンプルからノウハウを公開してみる」方向にシフトしてみました。

今後どうするかは不明(CPU制御も試したりしてる)ですが、実験環境としてGPUSoundを書いてくのもひとつかなあ、と思ったりしてます。
(GPUが馬力あるおかげでそこまで難しいチューニング考えなくてもいいので)

## ソース内コメントの説明が面倒そうな部分を抽出します

基本的には過去の記事で書いてる内容なのですが、
こういうのがないと自分が困る!と思ったので記載を残しておきます。

### GPUSoundの特徴

1. GPUSoundは、通常CPUを使ってレンダリングする音を
GPUに計算をぶん投げて形作るものと定義する(Shadertoyでのレンダリング方式、自分での実装でも演奏可能)
2. 1秒あたりのデータは44100byteとして、
音楽のデータをレンダリングする
(自分で実装する際はレート変更が可能)
3. GPUSoundでは、曲の全部を一気にレンダリングする
4. GPUSoundでは、レンダリング1byteあたりで
どのような波形を書くか・どんな音量かを記述する。
すなわち、ADSRなら特定位置での音量を式で計算する

1.はどちらかというとシェーダだけで曲書くときこうせざるを得ないっていう書き方ですね。(Shadertoyでこういう形で書く様になってる)
勿論自分で制御するならそこらへん自由にできますが、結局CPU-GPUのやりとりをどうするかという話が出てくるわけで、計算を全部任せるならGPU側で書くのが一番手っ取り早いかと。

2.はShadertoyだとレートが決め打ちになってます。
とはいうものの、他に選択するかという話でしかないので、ここは変わったら対応というくらいかな、と。(根っこの方なのでよほどじゃなければ変更しないと思われます)

3.は多分特徴的な部分かと。
すなわち、曲のはじめから終わりまでを1ソース上で書かなきゃいけないという制限になります。理想なのは「譜面」「音色」「パン制御」等でソースコード分けることなんですが、今の仕様上そうもいかない様子。

4.は3.の補足という形ですね。
あくまで全部の秒数に対応する形でソースコードを書かなければいけないため、手続き型言語とは思考が違う形になるのに注意。
テンプレート化してしまうのも有効かと思います。

### GPUSoundの制限

1. GPUSoundでは、前後のバッファを保存することができない。
よって、BiquadFilter(双2次フィルタ)は実装ができない。
2. GPUSoundでは、一時的な変数を次のフレームに保存ができない
よって、ADSRみたいに音量を変数で確保するのが不可能

1.については、意外と知らない方が多いのでは?と思いました。
実際自分でもTDFで急ぎ曲作る中で知ったのですが、これによって

これはどういうことかというと、GPUSoundの「前に書き込んだデータを知る方法がない」という仕様が原因になります。
この書き方では「レンダリングすることを並列的に処理」します。その結果、前後の結果を知ろうにも並列的に処理してるから知りようがないんですね。
勿論前フレームを取得するということも自前実装で出来るわけですが、それを行う場合前述のCPU-GPU間のデータ転送が関わってくるので逆にパフォーマンスが落ちかねないという結果になるかと。

Biquadフィルタってのは、シンセサイザーを実装する中で一番有名そうなフィルタの種類のことです。
詳しくはここ参照。

https://github.com/libaudioverse/libaudioverse/blob/master/audio%20eq%20cookbook.txt
(元あったところから消滅しちゃってらぁ・・・)

すごい簡単に言うと「前の出力結果を利用してフィルタを作っていく」方式です。
つまり上記の仕様との相性は抜群に悪いです。

一応フィルタは別の方法で実装可能ですが、今の所実験的にいくつか作ってるだけなので、簡単に制御可能になったら公開予定とします。

2.については結局のところ1.と同様の要因ですね。
例えばADSRなら、処理時間を積み重ねてカウンタが~のときにRに移行するよ、みたいな書き方ができないです。
上記ケースなら「*exp(-3.0*time)」で終わりです。
(これは時間経過するごとにだんだんと収束していく、という記述になります)

#### 補足

こういうこともできますが、あくまでパスがつながってる場合だけですね。
(別のタブでアーメンブレイクのデータを書き込んで参照するようにしてる)

https://www.shadertoy.com/view/MddyWH

### シーケンスの数値出力関数

sqcという関数を作ってます。
多分これは結構使うんじゃないかと思うので、記述を残しておきます。

現状のShadertoyの仕様だと時間変数「time」を引数にして、timeにおける音の出力結果をレンダリングすることになります。

で、音楽を作る場合、キックを作るとかメロディを作るか、ということは突き詰めると「どのタイミングで発音する・離散する」というところに収束する形になります。
この組み合わせが実質的に無限だからいろんな音楽が成立するわけで。

ということで、timeの値ごとに拍子をどのくらいカウントしているのか・1回のカウント単位における経過時間はどのくらいかを計算したのがこの関数です。

#### 計算方法

ここでは「ティック=1小節における譜面の1単位」とお考えください。
4分音符なら全体で4ティック、みたいな。

以下、式を抽出。

##### 経過した小節数を出力する
式:floor(time/(60.0*4.0)*BPM);
floor:整数値を出力する
time:経過時間
BPM:BeatPerMinutes(曲の速さ)
出力:0~(整数)
##### ティックごとの経過割合を出力する(1ティック中のどのくらい経過しているか)
これによって音を徐々にフェードアウトしていく等の処理が書ける形になります。
式:fract( time/(60.0*4.0)*BPM*float(tick) );
fract:小数値を出力する。
tick:基準となるティック値
出力:0.0~1.0のはず
##### 現在演奏中のティック値を出力する
式:mod( floor( time/(60.0*4.0)*BPM*float(tick), tick );
mod:剰余
例:4ティック中3ティック目を鳴らした=4つ打ちにおいて3つ目のキックを鳴らした。
出力:0~tick-1(4ティックだったら0,1,2,3になります)

#### 記述内のティック関数の使い方

記述例:
(mod(sq.w,8.) == 7. && mod(sq.z, 16.) > 7.)?1.0:flag;

sq.z:シーケンス関数から、現在演奏しているティック数を呼び出す
sq.w:シーケンス関数から、経過した小節数を呼び出す
flag:ただのフラグ変数です。

上記の例だと「8小節ごとの繰り返しのうち8小節目に(計算結果は0から始まるので7を返す)」+「16ティックを刻む中で8ティック以降に所属したとき(計算結果は0から始まるので7を返す)」1.0を返し、そうじゃないときはフラグ変数の値を返します。
この記述はスネアロール(スネアずっと鳴らし続ける処理)のときに実装した処理ですね。
こんなのを簡単に書けるのがシーケンス関数を作った理由です。

## とりあえず強引にまとめる

まだ出来ることがありそうなので、いくつか書いていきたいと思います。
あとちょっと別件でGPU叩くヤツがありそうなので、そういうのも絡めてシンセと結び付けられたらなあ、とか思っていくつか書きたいかと。