概要
この記事は個人的にbytebeatを調査したもののまとめです。
サンプリングレート8000(8k) bytebeat 0~255(floatbeat 1~-1ではない)前提で書かれています。
html5bytebeat前提なので、ワンライナーjavascriptが使える想定です。
bytebeatとは
コードでビートを作る
ビット演算を多用することからCPUのアセンブラコードに近いです。
bytebeatはどれだけコードを短く音楽を奏でるかに特化しています。デモシーンでのコード圧縮を想定した作りです。
html5bytebeatはワンライナーjavascriptが使えMathモジュールが標準で使えるようになっています。
Math.sinやMath.PIなどがそのままsinやPIが使える。ワンライナーjavascriptなので、複数行書きたい場合はセミコロン『;』が使えません。『,』で複数定義でき、変数の定義や、アロー演算子や関数の定義が出来ます。
※ 注意 bit演算を使うと整数になり、浮動小数点はなくなります
glitchとは
bytebeatを元にシンセ的な音楽を作りやすい方向に進化したのがglitchになります。
本記事ではbytebeatでglitchと同じようにシンセ的な音楽を作る方法について書いていきます。
そもそも、基礎が分かってないとbytebeatのコード圧縮が分からないですし。
ビット演算
bytebeatで良く使われるビット演算子
ビット演算をした時点で整数になります。小数部切り捨て
右シフト演算子 >>
tからbpmを生成するのに使われます。『t>>10』、『t>>11』、『t>>12』あたりが使われます。
AND演算子 &
主に2つの使われかたをしていてメロディ生成と音色の矩形波生成に使われています。
2の乗数0,1,2,4,8,16...128かどうかで挙動が変わってきます。
###『&2の倍数』
- メロディ 単一音
- 音色 矩形波
###『&2の倍数以外』
- メロディ フラクタルのメロディ
- 音色 ビット欠け矩形波
OR演算子 |
『x|0』つまりビット演算的には何も変化がありませんが、ビット演算をした時点で整数になる特徴を利用し、小数部を切り捨てします。≒『floor(x)』floorの代わりに使われますが、xが-だと挙動が異なるので注意
NOT演算子 ~
フラクタルのメロディで使うとメロディが逆転します。
基本波形
正確な波形
波形 | 式 |
---|---|
ノコギリ波 | t%256 |
正弦波 | sin(t%256/256PI2)*127+128 |
矩形波 | (t%256<128)*255 |
三角波 | abs(t%256-127)*(255/128) |
ノイズ | random()*255 |
ノコギリ波
bytebeatの基本波形はノコギリ波です。
ノコギリ波以外の正確な波形はコードが長いのでbytebeatでは使われません。
出力先が『byte』なので時間変数t(サンプリングレート8kなので1秒間で8000カウント)の出力は256まで行くと0にリセットされることを利用すると
t
と書くだけでノコギリ波が発生します。が単純に出力する場合には良いのですが、最終アウトプット結果が255以上の数が切り捨てられるということなので、複数ノコギリ波を合成する場合などはt%256としてshaderのfractのような感じで明示的に指定しておいた方が無難です。原因不明のバグに悩まされることになります。最適化の際に取り除けばよいでしょう。
shaderの最終出力結果がsaturateされるのと同じように、途中で加算する場合は、個別にsaturateを掛けておくのと同じです。
矩形波
ANDビット演算子の性質『&2のn乗』とすると矩形波が出来ることを利用してbytebeatではコードが短くお手軽なので
t&128
が良く使われます。ただ、これは正確な矩形波ではありません。片側 矩形波になります。音量的には半分になります。
t&64
t&32
t&16
t&8
t&4
t&2
t&1
でも矩形波になりますが、音が小さいので使われないです。
なんで
t&256
が使われないのかというと
t%256&256
256=0となってしまい音が鳴らなくなってしまうからですね。
t&255
だと2の倍数ではないので音が出ません
256を255にフィットさせて
(t&128)*(255/128)
か
(t*2&256)*(255/256)
で正確な矩形波を出せますがコード量が変わらないので
(t%256<128)*255
こっちの方が良いでしょう。あと、比率を変えてパルス波を作れるので、シンセ的にもこっちの方がいいです。
(t%256<16)*255
(t%256<200)*255
尚、2の乗数でないAND演算をするとビット欠けしたエモイ矩形波が発生します。音色に深みが出るので色々試してみるといいでしょう。
t&0xae
t&0xef
など
尚上位ビットを削りすぎると音が鳴らなくなるので注意が必要です。
t&3
とかほとんど音が鳴らない
三角波
簡易版
abs(t%255-127)*2
absを使う場合、0が中心の場合、範囲が奇数じゃないと行けないので256-1で
1ビット欠けてますね。音的にはあまり変わりません。
abs(t%256-127)*(255/128)
これも正確にやるなら256を255に丸めてあげる必要があります。
正弦波
音叉、基準音、時報の音です。単体ではつまらない音なので単体ではあまり使われません。
基本的に正弦波同士を掛け合わせ、FM変調して使います。
sin(t*(440/8000*256)%256/256*PI*2)*127+128
ノイズ
random()*255
上記の標準的なものから、tを高周波数にして適当にビット演算してノイズっぽくしたもの
shaderのノイズと同じようにsinでカオスを作って疑似ノイズ
ビット演算が使えるからxorshiftでノイズを作るなどがあります。
音楽的にノイズとして聞こえれば何でもよいかと
メロディを奏でる
基準音 440Hz ラを鳴らす
srは8000Hzでbtye(256)
(440/8000*256)をtに掛ければいい
ノコギリ波 t*(440/8000256)%256
正弦波 sin(t(440/8000256)%256/256PI2)127+127
矩形波 (t(440/8000256)%256<128)255
三角波 abs(t(440/8000*256)%255-127)*2
テンポ(bpm)
サンプリングレートが8kなのでtは1秒間に8000カウントアップされます。
なので、tは1分間に60 x 8000 = 480000 bpm
ということになります。
abs((t*440*pow(2,[3,5,7,8,10,12,14,15][t%8]/12)/8000*256)%256-127)*(255/128)
このまま鳴らしたらノイズにしかならないので
ビット演算子を使って
- 480000 >> 11 = 234 bpm
t>>11
- 480000 >> 12 = 117 bpm
t>>12
音楽的には主に上の2つが使われます。8分音符や16分音符を使いたい場合はビットシフト>>を減らせばよし
ビット演算を使った時点で整数に丸められるのでシンプルです。
付点、連符の場合はビットシフトが使えないので注意!!
細かいテンポを指定したい場合は 200 bpmにしたい場合は
t/(480000 / 200)|0
abs((t*440*pow(2,[3,5,7,8,10,12,14,15][(t/(480000 / 200)|0)%8]/12)/8000*256)%256-127)*(255/128)
ドレミファソラシドを鳴らす
abs((t*440*pow(2,[3,5,7,8,10,12,14,15][(t>>12)%8]/12)/8000*256)%256-127)*(255/128)
フラクタルで鳴らす
詳細はこちらを参考に
上記を踏まえ、&演算子を使ってフラクタル(シェルピンスキー)を使って音を鳴らしてみましょう
※ AND演算子の特徴として2の乗数は1音しか鳴りません
((t >> 10) & 42) * t % 128
デフォルトのサンプル曲がまさにそうで、42メロディと言われています。
((t >> 10) & 42) * t % 128
((t >> 10) & 3) * t % 128
3,5,6,7,9,10,11,12,13,14,15,17,18,19,20....42が限界
色々試してみましょう。
フラクタルを使っていると、だいたい同じような感じになってしまいます
ビット演算~notを使うと
(~(t >> 11) & 5) * t % 128
メロディを逆転できます。
このフラクタルを色々加工して音楽を作っていきます。
同時に複数音を出す
単純に足せばよいです。
%256の付け忘れに注意
エフェクト
ディレイ
(s = t => (~(t >> 10) & 5 ) * t & 128),s(t)+s(t-100)
ユニゾン(デチューン)
(s = t => (~(t >> 10) & 5 ) * t & 128),s(t)+s(t*(1+1e-6))
フィードバック
(s = t => (~(t >> 10) & 5 ) * t &0xae ),s(t)+s(s(t))
簡易オーバードライブ
(s = t => (~(t>> 10) & 5 ) * t % 256 ),min(255,max(0,(s(t)-128)*2+128))
FM音源のフィードバック
fm音源feedback
- 1:1 ノコギリ
tt=t%256/256*PI*2,sin(tt+(sin(tt*2+sin(tt*4+sin(tt*8+sin(tt*16))))))*127+128
再帰対応版
tt=t%256/256*PI*2,fb=((f=(n,t,r)=>sin(t+(n>1?f(n-1,t*r,r):0)))=>f)(),fb(127,tt,1)*127+128
- 1:1 矩形波
tt=t%256/256*PI*2,sin(tt+(sin(tt+sin(tt+sin(tt+sin(tt))))))*127+128
再帰対応版
tt=t%256/256*PI*2,fb=((f=(n,t,r)=>sin(t+(n>1?f(n-1,t*r,r):0)))=>f)(),fb(127,tt,2)*127+128
127回feedbackしても綺麗な波形にならない。
アロー演算子で再帰はこちらを参考に、肝はデフォルトパラメーター
- https://qiita.com/yohhoy/items/5f1222fb1ee2457688fc
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
PCMでスネア再生
ドラムキットはPCMで音色が欲しくなるのでPCMでドラムを再生できるようにする。
glitchはtr808が用意されている。さすが。
8bit waveデータを16進数で文字列化する
bytebeatはWAVのフォーマット準拠なので文字列化したものを流すだけです。
8bitだったら0~256、16bitだったら-32767~32768
デコーダーは簡単
'7e69c5882d6ca4ad6035a6877064769b5871718b67739d7c786f97726b66937b707f848d788173737b8169807d81708184867775807d797c7d7a7a7e7c767a7f7f837e7a807d797e817e807c7f817c7d7d7d7f807e7c7d7d7c7d7c7e7d7d7d7c7d7d7d7c7d7d7e7d7e7c7e7d7e7d7d7e7d7e7c7e7e7c7e7f7d7c7d7c7d7e7e7e7e7d7d7d7c7d7e7e7d7d7d7c7c7d7e7d7d7e7d7e7e7c7d7e7c7d7d7e7d7c7c7c7e7c7e7d7d7d7d7c7d7d7d7c7e7c7d7e7d7c7e7d7e7e7c7e7d7c7e7e7d7e'.match(/.{2}/g).map(x=>'0x'+x|0)[(t/12|0)%200]||128
WAVファイルからデータを抜き出して16進数文字列化するのがめんどく
とりあえず、javascriptでwaveファイルをWebAudioに読み込んで、そのメモリ内容を16進数文字列に変換した。
WebAudioで読み込むとメモリに44.1K固定で展開されるらしく適当にデータを間引いている
あと、Array.fromでarraybuffer(float型付配列)を通常のarrayに変換している。こうしないと後続のmapの結果もfloatに強制され最終結果がおかしくなる。特にmapで文字列に変換するとおかしな結果を招く。webAudioあるある。
Array.from(channelLs).filter((x,i)=>i%(441000/8000|0) ===0 ).map(x=>parseInt(((x*128+127)|0)).toString(16)).join('')
webAudio経由ではなく、直接WAVファイルを解析した方がいいかも
全文
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title></title>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();
var xhr = new XMLHttpRequest();
var url = 'http://localhost/pcm/8bitdrum.wav';
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
if (xhr.status === 200) {
var arrayBuffer = xhr.response;
if (arrayBuffer instanceof ArrayBuffer) {
var successCallback = function(audioBuffer) {
var channelLs = new Float32Array(audioBuffer.length);
var channelRs = new Float32Array(audioBuffer.length);
if (audioBuffer.numberOfChannels > 1) {
channelLs.set(audioBuffer.getChannelData(0));
channelRs.set(audioBuffer.getChannelData(1));
} else if (audioBuffer.numberOfChannels > 0) {
channelLs.set(audioBuffer.getChannelData(0));
} else {
window.alert('The number of channels is invalid.');
return;
}
console.log(
Array.from(channelLs).filter((x,i)=>i%(441000/8000|0) ===0 ).map(x=>parseInt(((x*128+127)|0)).toString(16)).join('')
);
};
var errorCallback = function(error) {};
context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
}
}
};
xhr.open('GET', url, true);
xhr.send(null);
</script>
</head>
<body></body>
</html>
欲しいけどまだ作っていないもの
だいたいglitchの方では実装されている
FM音源
基本的に正弦波の出力を正弦波の入力に入れる重ね合わせの連鎖、あとは、どう加算するか
エンベローブ
音量変化
基本三角波とかと同じだが、比率とかあるので、参考演算子で実装かな
ローパスフィルタ
生シンセデータを鳴らしていると高周波成分が気になるのでローパスフィルタを掛けたい
画像処理のブラ―と同じように前後のデータから平均値をフィルターを掛ければいいが、計算式の実装がちょい面倒
リバーブ
原理的はフィードバックとディレイと同じだけどPCMと同じでIRデータを持ってくるのが面倒
オーバードライブ
クリッピングだけではなく歪みも実装
これもローパスフィルターと同様
bytebeat的コード圧縮解説まとめ
ビット演算 ビットシフト>> でbpmを作る
右ビットシフトは2の乗数で割る
具体的に以下3つのコードが良く使われる
bpm | 式 |
---|---|
468 | t>>10 |
234 | t>>11 |
117 | t>>12 |
任意のbpmは割ればいいがコードが長くなるのであまり使われない。
例えばbpm 200であれば『t/(480000/200)|0』=『t/2400|0』
メロディはANDビット演算のフラクタルで作る
2の乗数ではないANDビット演算はフラクタルなメロディを作るのでこれを加工してメロディを作る &3,&5,....,&42など
音色は、デフォルトのノコギリ波を使う
つまり『t』、たまに2のn乗のANDビット演算子を使い矩形波を使う『t&128』
代わりに、『t&0xae』、『t&0xef』とか使っても良いでしょう。ビット欠けの矩形波は他のシンセサイザーにはない深みのある音色になります。
まとめると
(t >> 10) & 5 ) * t
これだけでメロディを作ることができる。最少構成