Processingで作曲をする (part 1)
この記事は NCC Advent Calendar の25日目です。
何を書くか非常に迷いました。 当初は 作曲のノウハウを共有する ことを考えていましたが、結構フィーリングの部分も多くて他人に説明することはとても難しい。 では、何をすれば他人に、特にFMS生に作曲法を伝授できるか?と考えたとき、僕は気が付いたら Processingで作曲を開始していました ……
この記事では、Processingで作曲をします。 ただし、Processing実行時に音が鳴るわけではなく、 wavファイルとして出力 します。 さらに、この記事では 一切のライブラリを使用しません 。 生のProcessingよりフルスクラッチで様々な処理を行っていきます。 難易度はちょっぴり高めですが、電子音楽に関する様々な知識が身についてとても良いと思いますよ!
とにかく、Processingからwavファイルを吐き出せるか試してみようよ! ここでは、 arrayToWav という、 配列を投げるとwavファイルを出力してくれる関数 を作ります。
そもそも音ってなんでしたっけ? 音声信号を格納するためのファイルフォーマットは数多く存在します。 wavはもちろんのこと、mp3やogg、flacやwvなどがありますよね。 これらが格納しているデータはどれも 一次元配列 です。 コンピュータの音はスピーカー(とかヘッドホンとか)を通じて僕たちの耳に届きますが、このとき、スピーカーを操っているのは時間の次元をもつ信号です。
じゃあ今回もmp3やoggでもよいじゃん、と思ってしまうかもしれません。 しかし、wav以外のほとんどの音声信号形式はどれも圧縮されていて、直感的に読めるファイルフォーマットではありません。 対してwavファイルは、ヘッダから先は配列の値をファイルのバイナリに直接書き込むだけでよいので、プログラミングで扱うには非常に簡単なファイルフォーマットです。
また、ステレオ(二つのスピーカーから別々の音が鳴る)信号の場合は、信号が2つ必要なため 一次元配列も2つ必要 です。 wavファイルなどにもしっかり2つ分の音声信号が格納されています。 今回もステレオの音楽をつくりたいので、配列は2つ用意します。
では、どんな長さの配列が必要なの?と思いますが、ななななんと、 44100 * 再生時間 もの長さの配列が必要となります。ひょえ~! 一般的なCDに格納されている音声信号は、1秒間の音のために44100サンプルを記録します(このサンプル数を サンプルレート と呼びます)。 それと同じように音声ファイルを作ろうとすると、こんな莫大な配列が必要になってしまうのですね。
wavを含め、だいたいファイルの先頭には ヘッダ というものが付きます。 これは、ファイルの実データを格納する前に、ファイルの扱い方を説明するためのデータを格納する箇所ですね。
wavファイルは数値を リトルエンディアン で格納します。たとえば、32bitの整数を格納しようとした場合、iをオフセット、vを格納する数値としたとき、以下のようなプログラムで実現できます。
data[ i + 0 ] = byte( v ); data[ i + 1 ] = byte( v >>> 8 ); data[ i + 2 ] = byte( v >>> 16 ); data[ i + 3 ] = byte( v >>> 24 );
以下はwavファイルの構造例です(信号の長さが10000の場合)。僕も暗記しているわけではないので、こんなデータが必要なのか~とか思っていただければ。
オフセット 長さ バイナリ データ 意味 0 4 52 49 46 46 "RIFF" RIFF ヘッダ 4 4 64 9C 00 00 40036 これ以降のファイル長、信号長 * 4 + 36 8 4 57 41 56 45 "WAVE" WAVE ヘッダ 12 4 66 6D 74 20 "fmt " fmt チャンクヘッダ 16 4 10 00 00 00 16 fmt チャンク 長さ 20 2 01 00 1 フォーマットID 22 2 02 00 2 チャンネル数 24 4 44 AC 00 00 44100 サンプリングレート 28 4 10 B1 02 00 176400 バイト/秒(サンプリングレート * 4) 32 2 04 00 4 ブロックサイズ 34 2 10 00 16 ビット/サンプル 36 4 57 41 56 45 "data" data チャンクヘッダ 40 4 40 9C 00 00 40000 data チャンク 長さ(信号長 * 4) 44 40000 --- --- 実データ
のような感じで格納します。 先程も言ったように、wavファイルはリトルエンディアン形式で値を格納していきますよ。
16bitと聞いて不思議に思われるかもしれませんが、一般的なwavファイルは1サンプルを -32768 - 32767 の範囲でしか格納していません。 CDも同様です。 たかが 65536 段階の高さしかもたない信号でここまできれいに聞こえてしまうのですから、人間の耳なんてチョロいですね!
関数の名前、型、引数は void arrayToWav( float[][] _wave, String _name ) とします。
_wave : [ 左チャンネルの信号, 右チャンネルの信号 ]
返り値: なし、ファイルをスケッチフォルダ内に指定ファイル名でセーブする
信号はfloat型で、 -1.0 - 1.0 の範囲で格納したものを投げます。 float型の信号をこの関数内で -32768 - 32767 の範囲のintに変換し、バイナリにぶち込んでいきます。
void arrayToWav( float[][] _wave, String _name ) { int len = _wave[ 0 ].length; byte[] file = new byte[ 44 + len * 4 ]; // "RIFF" file[ 0 ] = 'R'; file[ 1 ] = 'I'; file[ 2 ] = 'F'; file[ 3 ] = 'F'; // filesize - 8 file[ 4 ] = byte( 36 + len * 4 ); file[ 5 ] = byte( ( 36 + len * 4 ) >>> 8 ); file[ 6 ] = byte( ( 36 + len * 4 ) >>> 16 ); file[ 7 ] = byte( ( 36 + len * 4 ) >>> 24 ); // "WAVE" file[ 8 ] = 'W'; file[ 9 ] = 'A'; file[ 10 ] = 'V'; file[ 11 ] = 'E'; // fmt chunk header file[ 12 ] = 'f'; file[ 13 ] = 'm'; file[ 14 ] = 't'; file[ 15 ] = ' '; // fmt chunk bytes file[ 16 ] = 16; file[ 17 ] = 0; file[ 18 ] = 0; file[ 19 ] = 0; // format ID file[ 20 ] = 1; file[ 21 ] = 0; // channel file[ 22 ] = 2; file[ 23 ] = 0; // sampling rate file[ 24 ] = byte( RATE ); file[ 25 ] = byte( RATE >>> 8 ); file[ 26 ] = byte( RATE >>> 16 ); file[ 27 ] = byte( RATE >>> 24 ); // byte / sec file[ 28 ] = byte( ( RATE * 4 ) ); file[ 29 ] = byte( ( RATE * 4 ) >>> 8 ); file[ 30 ] = byte( ( RATE * 4 ) >>> 16 ); file[ 31 ] = byte( ( RATE * 4 ) >>> 24 ); // block size file[ 32 ] = 4; file[ 33 ] = 0; // bit / sample file[ 34 ] = 16; file[ 35 ] = 0; // data chunk header file[ 36 ] = 'd'; file[ 37 ] = 'a'; file[ 38 ] = 't'; file[ 39 ] = 'a'; // data chunk bytes file[ 40 ] = byte( len * 4 ); file[ 41 ] = byte( len * 4 >>> 8 ); file[ 42 ] = byte( len * 4 >>> 16 ); file[ 43 ] = byte( len * 4 >>> 24 ); // data for ( int iSample = 0; iSample < len; iSample ++ ) { int l = min( max( int( _wave[ 0 ][ iSample ] * 32768.0 ), -32768 ), 32767 ); file[ 44 + iSample * 4 + 0 ] = byte( l ); file[ 44 + iSample * 4 + 1 ] = byte( l >>> 8 ); int r = min( max( int( _wave[ 1 ][ iSample ] * 32768.0 ), -32768 ), 32767 ); file[ 44 + iSample * 4 + 2 ] = byte( r ); file[ 44 + iSample * 4 + 3 ] = byte( r >>> 8 ); } // save! saveBytes( _name, file ); }
今回は、音を作る関数の基本フォーマットは以下のようにします。
float[][] synth( int _len, float _freq )
返り値: [ 左チャンネルの信号, 右チャンネルの信号 ]
この関数の返り値を先ほどの arrayToWav の _wave の部分にまるごと投げてしまえば、作った音がそのままwavファイルとして出力されます。最高じゃん!
待てよ、この関数には周波数を直接入れなきゃいけないのか…? 流石にそれは大変なので、MIDIノート番号を与えると周波数を返してくれる関数を作ります。 MIDIノート番号は、ピアノの各鍵盤に順番に数字を割り当てたものと解釈すればよいです。69番を440.0Hz(ラの音)とし、番号を1上げると1半音上がります。
float note( int _note ) { return pow( 2.0, ( _note - 69 ) / 12.0 ) * 440.0; }
とりあえず、もっともプリミティブな音の一つであるサイン波を出す関数を作ってみましょう。 sin関数を使えばサイン波が音として出力されるとても優しい世界です!最高!!
float[][] synthSin( int _len, float _freq ) { float[][] ret = new float[ 2 ][ _len ]; for ( int iSample = 0; iSample < _len; iSample ++ ) { ret[ 0 ][ iSample ] = sin( iSample * _freq / RATE * PI * 2.0 ); ret[ 1 ][ iSample ] = sin( iSample * _freq / RATE * PI * 2.0 ); } return ret; }
実は、先ほどの関数、実際に出力して聴いてみると、時間が経つにつれだんだん音質が悪くなるのがわかると思います。
これ、実はfloatの精度の問題なのです。 _freq が 440.0 の場合、 iSample * _freq / RATE * PI * 2.0 は、 iSample が1増えるごとに 0.037 程度の小さな変化をしますが、これ自体は再生開始1秒程度で既に 1650 程度の大きな数となっています。浮動小数点数は大きな数の小さな変化に弱いですから、長く再生すればするほど音質がどんどん悪くなってしまいます。
そこで、float型の変数が扱う数をなるべく小さく抑えるように工夫をした関数がこちらです。
float[][] synthSin( int _len, float _freq ) { float[][] ret = new float[ 2 ][ _len ]; float phase = 0.0; for ( int iSample = 0; iSample < _len; iSample ++ ) { ret[ 0 ][ iSample ] = sin( phase ); ret[ 1 ][ iSample ] = sin( phase ); phase = ( phase + _freq * PI * 2.0 / RATE ) % ( PI * 2.0 ); } return ret; }
みなさんご存知のように、 sin( x + 2.0 * PI ) と sin( x ) の出力は(数学的には)全く同じです。 そこで、sin関数の中に入れる変数を 2.0 * PI 以下に抑えられるように工夫をしました。 phase という sin 関数の中に入れるための変数を用意し、1サンプル書き込むごとに変化差分を phase に足し算していき、もし 2.0 * PI を超えてしまったら剰余を取る、という実装をしています。
この関数により正確なサイン波の出力が得られ、実際、このやり方で生成したサイン波はとても綺麗な音を奏でます。
音を左右に広げることはとても重要な事です。 左右に広がっていない音は、篭った感じに聞こえ、他の音との聞き分けが非常にしづらいです。 (一方で、ベースなど、真ん中に寄せたほうが効果的に聞こえる音も存在します。ここらへんはミキシングの非常に難しい部分なのでここでは説明しません。) 先ほど作った synthSin 関数は、左右どちらとも同じ信号を出力しているため、当然左右に広がっていません。
では。どうすればサイン波を左右に広げられるか? 答えは単純です。左右でサイン波の位相を変える。これだけです。 以下の関数では、右側のサイン波の位相を 1.0 だけずらしました。
float offset = 1.0; ret[ 0 ][ iSample ] = sin( phase ); ret[ 1 ][ iSample ] = sin( phase + offset );
また、左右で周波数を少しだけ変えると、より効果的に左右に広げることができます。 phase に相当する変数をもうひとつ用意しなければならないので面倒ではありますが…
残念ながら、ここでクリスマスを迎えてしまいました…。 記事の長さ的にも、限られた時間でプログラミングで音作りから作曲をこなすのは少し難しかったようです。 Advent Calendarのトリとして非常に恥ずかしい結果となってしまいました…
しかし、今回の時点で音が作れているので、この調子で音をもっと作り、音を並べていけば音楽が作れるのでは?という感覚は伝わっているのではないでしょうか。 ぜひ、この続きは近いうちにやりたいと考えています。