mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

はじめよう!コンピューターミュージック

iPhoneゲームの買い過ぎでついにアプリが7ページ目に突入してしまった bonar こと中野恭兵です。今のお気に入りは手軽に遊べる"frenzic"と本格派ファンタジーパズル"Aurora Feint" 。最高です。

普段はアプリケーション開発グループ ミュージック開発チームに所属していまして、仕事中は常に mixi Radio 付けっぱなしなわけですが(マイブームは"Monica Uranglass(音が出ます)")、やっぱりコンピューターがある以上、聴くだけでなく自分でも作ってみたいと思うものです。

僕自身弾ける楽器が何もなく、音楽的な教養も無いのですが、まずは最初の一歩を踏み出したいと思い少し調べてみました。

音とは何か

音楽はいろんな音の複雑な合成物なので、音とは何かという部分から考える必要があります。 ご存知の通り、音とは空気の振動です。振動とはつまり一定の周期を持った規則的な波で、 それが鼓膜に伝わり音として知覚されます。この時その振動がどういう波形をしているか(滑らかに 円を描くsin波か、角張った矩形波か等)が音の種類を決め、1秒間に何回その波形が反復 されるかで音の高低が決まります。この「1秒間に何回その波動が起こるか」を"周波数"と 呼び、Hz(ヘルツ)という単位で表現します。1秒間に220回同じ波形が繰り返す音は 220Hzの音である、という言い方をします。

音のデジタル保存

自分でサウンドファイルを作成する際には、どうやって音がデジタル化/記録されるかを知る 必要があります。

本来なめらかなアナログ波形である音をデジタルデータとして保存するという行為は、 アナログな波形を特定の経過時間における音圧スナップショットの連続として記録して行く作業です。 ありふれた例えですが、波形の曲線を棒グラフに置き換えて行くと考えるとイメージしやすいかもしれません。

CDをリッピングする際やオーディオフォーマットを変換する際に、「サンプリングレート」や 「ビットレート」といった値を設定したりします。これらは漠然と、上げれば上げる程音がよくなる 数字として認識されがちですが、実は上記のようなアナログ波形をデジタル化する際のパラーメータです。 棒グラフの例で言えば横軸の棒の数がサンプリングレート(1秒間に何回スナップショットを取るか)で、 各棒グラフの縦の目盛りの細かさがビットレート(音圧をどれくらい細かく計測するか)です。これらが 細かくなればなる程 連続した棒グラフは元の曲線に近くなめらかになるので、再現度が高くなり、 高い音質として感じられるのです。

例)サンプリングレート44.1KHz, ビットレート16bit
= 1秒間に4万4100回スナップショットを取り、その際の音圧を65536段階で記録する

sin波を書く

それでは上記のことを確認するために、引数で受け取った周波数のsin波を.wavファイルとして書き出すプログラムを書いてみましょう。以下がコードです。

http://gist.github.com/97992

import java.lang.Math; 
import java.util.Vector; 
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import javax.sound.sampled.*; 

public class SinWave { 
    private static final float SAMPLE_RATE = 44100.0f;
    private static final int   SAMPLE_SIZE = 16;
    private static final int   CHANNEL     = 2;
    private static final int   FRAME_SIZE  = 2;
    private static final float FRAME_RATE  = 44100.0f;

    private static final int VOLUME = 50;

    public static void main (final String[] arg) 
        throws IOException {

        Vector<Float> freqs = new Vector<Float>();
        for (int i = 0; i < arg.length; i++) {
            freqs.add(Float.valueOf(arg[i]));
        }

        int buffsize = ((int)SAMPLE_RATE * CHANNEL);
        byte[] buff = new byte[(buffsize * freqs.size())];
        int cursor = 0;

        for (Float freq : freqs) {
            // SAMPLE_RATE 内で target_hz 個の波形を入れる必要が
            // あるため、一回の波形に割り当てるサンプル数は 
            // (SAMPLE_RATE / target_hz) になる。
            float target_freq = freq.floatValue();
            int samples_per_round = (int)(SAMPLE_RATE / target_freq);

            // target_freq 回波形を書く
            for (int i = 0; i < (int)target_freq; i++) {
                // 1回の波形を samples_per_round の点に分けて書く
                for (int x = 0; x < samples_per_round; x++) {
                    float progress = ((float)x / samples_per_round);
                    double radian  = progress * (2 * Math.PI);
                    byte y = (byte)(Math.sin(radian) * VOLUME); 
                    buff[cursor++] = y; // Left
                    buff[cursor++] = y; // Right
                }
            }
        }

        // WAVE ファイルとして出力
        AudioFormat fmt = new AudioFormat( 
            // AudioFormat.Encoding encoding
            AudioFormat.Encoding.PCM_SIGNED, 
            SAMPLE_RATE, // sampleRate 
            SAMPLE_SIZE, // sampleSizeInBits 
            CHANNEL,     // channels 
            FRAME_SIZE,  // frameSize 
            FRAME_RATE,  // frameRate 
            true         //int bigEndian 
        );
        AudioInputStream audio_stream = new AudioInputStream(
            new ByteArrayInputStream(buff), fmt, cursor);
        File outfile = new File("sinwave.wav");
        AudioSystem.write(audio_stream
            , AudioFileFormat.Type.WAVE, outfile);
    }
}

これを以下の引数で実行すると sinwave.wav という.wavファイルが作成されます。

$ java SinWave 220 440 880

めんどくさい方はこちらからどうぞ。
sinwave_220_440_880
だんだんと音が高くなっていくのがわかりますね。

WAVEファイルを読む

では出来た .wav ファイルの中身を見てみましょう。

$ xxd -c 8 sinwave.wav | head -n 20
0000000: 5249 4646 a40e 0800  RIFF....
0000008: 5741 5645 666d 7420  WAVEfmt 
0000010: 1000 0000 0100 0200  ........
0000018: 44ac 0000 10b1 0200  D.......
0000020: 0400 1000 6461 7461  ....data
0000028: 800e 0800 0000 0101  ........
0000030: 0303 0404 0606 0707  ........
0000038: 0909 0a0a 0c0c 0d0d  ........
0000040: 0f0f 1010 1212 1313  ........
0000048: 1515 1616 1818 1919  ........
0000050: 1a1a 1c1c 1d1d 1e1e  ........
0000058: 1f1f 2121 2222 2323  ..!!""##
0000060: 2424 2525 2626 2727  $$%%&&''
0000068: 2828 2929 2a2a 2b2b  (())**++
0000070: 2b2b 2c2c 2d2d 2d2d  ++,,----
0000078: 2e2e 2f2f 2f2f 3030  ..////00
0000080: 3030 3030 3131 3131  00001111
0000088: 3131 3131 3131 3131  11111111
0000090: 3232 3131 3131 3131  22111111
0000098: 3131 3131 3131 3030  11111100

2b までのヘッダ部分以降は 2 byte 1セットの音圧値がひたすら続いていいて、他には何もありません。また、数値を眺めるだけでもそれが滑らかな曲線を描こうとしていることがわかります。音というのはつかみ所の無いものですが、このように単純な波のスナップショットの連続であることがわかります。感動的ですね。

周波数比の世界

上記の.wavファイル生成では唐突に3つの周波数を出しましたが、220 Hz は「ラ」の音です。調律師の機嫌で音程が変わっては大変なので、この音はこの周波数、というのが決まっています。その後にそれよりも高い 440 Hz, 880 Hz の音が続きます。音を聞いて気づかれた方もいるかもしれませんが、440 Hz は1オクターブ高い「ラ」、880 Hz はさらに1オクターブ高い「ラ」になります。通常楽器の調律等はこの 440Hzのラを基準にして行われているようです。

つまり、周波数が倍になると(波形の周期が半分になると)、1オクターブ高い音になります。人間の耳には「あ、1個あがったな」くらいにしかわかりませんが、実際の周波数は音が高くなればなるほど間隔が開いていることになります。ピアノの鍵盤を周波数の数直線だと考えると、それは等差数列ではなく等比数列だったんですね。

ドレミを作ろう!

音の周波数比が一定だと仮定すると、オクターブの上げ下げだけでなく、同じオクターブ内での各音の周波数比も一定のはずです。ドレミファソラシド を CDEFGAB と置き換えると、1オクターブは C C# D D# E F F# G G# A A# B という12個の音で構成されています(D# と Eフラット等の異名同音を除く)。そのそれぞれの比が一定であるということは、C : C# の周波数比を12乗すると2になる(オクターブがあがると周波数が倍になる)というになります。2^(1/12) を計算すると以下の値になります。

$ perl -le 'print 10 ** ((log(2)/log(10))/12)'
1.0594630943593

この比と先ほどのラの周波数 220 Hz を元に各音の周波数を計算してみます。

A  | 220.000
A# | 233.082
B  | 246.942
C  | 261.626
C# | 277.183
D  | 293.665
D# | 311.127
E  | 329.628
F  | 349.228
F# | 369.994
G  | 391.995
G# | 415.305
A  | 440.000
A# | 466.164
B  | 493.883
C  | 523.251
C# | 554.365
D  | 587.330
D# | 622.254
E  | 659.255
F  | 698.456
F# | 739.989
G  | 783.991
G# | 830.609

ここから#の部分を抜いてドレミファソラシドを作ってみましょう。

$ java SinWave 261.626 293.665 329.628 349.228 391.995 440.000 493.883 523.251

.wavファイルはこちら
sin波でドレミファソラシド

おなじみのドレミになりましたね。これでどんな曲でも作れます。

音の調和と周波数比

ちなみに、隣り合った音の周波数比を 2^(1/12) にするというこういった調律(ドレミ -> 周波数 マッピング)を「平均律」といい、現在もっとも普及しています。
#昔は純正律等の別の音律が使われていました。

ちょっと回り道になりますが、C と G の周波数に注目してみます。ドとソですね。これはピアノ等で弾いてみると非常に美しく調和します。周波数は 261.626 と 391.995 です。よく見ると 周波数比が 3/2 に非常に近い事がわかります。このように周波数が簡単な比で表せる音は同時に鳴らした時によく調和することが知られています。

しかし、3/2 に近いものの、ぴったり 3/2 ではありません。実はこれが平均律の弱点なのです。音の間隔が一定じゃないと転調がすごくやりづらくなるため、多少の誤差には目をつぶることにしたのです。実際にメリットの方が大きく、この調律が音楽を一気に合理化しました。ズレに関してもほぼ気にならないものです。

聞き比べてみましょう。

261.626 - 391.995 (平均律)
261.626 - 392.439 (正確な比)

違いがほとんどわかりませんね。ピアノ等の様に打鍵した瞬間から音量が急速に落ちるような楽器ではなおさらわからないと思います。

C の 3/2 は G ということを見てきましたが、もっと他の比に関してはどうでしょうか。例えば C の 4/3 は何かと見て行くと、

$ perl -le 'print ((261.626/3)*4)'
348.834666666667

となり、F の周波数 349.228 と非常に近い事がわかります。実際にピアノ等で弾いてみるとわかりますがC と F もとてもよく調和します。

周波数間が単純な比で表せる時にその音が調和する、ということであればドレミの点を基準にする必要はないはずです。ためしに 240 Hz というドレミには該当する音がない周波数で試してみましょう。

480Hz 240Hz 120Hz (前後のオクターブ)
240Hz 320Hz (周波数比 4/3)
240Hz 360Hz (周波数比 3/2)

妙な感じですが、まとまった音として聞こえますね。

アナログ音楽の世界へ

上記以外にも音色を決める倍音構成や様々な協和に関するトピック等おもしろい話がたくさんあるのですが、この辺は僕自信よく理解出来ていないところがあるので次回に是非。

音楽といわれると多くの人は五線譜に書かれた音符をイメージします。しかし実際には今まで見てきたように、音というのは空気の振動でしかなく、ドレミはその振動が生み出す無限の周波数の中の特定の点でしかありません。ド と ドのシャープ の間には無限の数の音があります。こういった今まで光のあたらなかった音や、それらが生み出す調和から、新しい音楽の可能性が見えるかもしれません。ドレミを全く無視した自分だけの音の世界をつくることだって出来ます。

僕も偉そうな事をいいながら何もわかってないのですが、音楽の世界は思った以上に数学的でエキサイティングな世界のようです。