トップ » 技術記事 » Javaでピコピコシンセを作ってみよう!(2) - 音程と音長の計算

Javaでピコピコシンセを作ってみよう!(2) - 音程と音長の計算

タグ: Java MIDI 波形合成 音楽

ファミコン世代の私たちは、ピコピコ音に目がありません。本連載では、Java でピコピコシンセを作ることを目標にします。ピコピコ音の仕組みを考察しながら、Java プログラミングを楽しみます。Javaのオーディオ API を活用してみます。

指定の音階を鳴らすメソッドを作ろう

前回、ピコピコ音の正体に迫り、なんとかドとかラとか音階を鳴らすところまで作ることができました。今回は、指定の音階の音を指定の長さだけ鳴らすことができるメソッドを作ってみようと思います。

任意の音階を発音するために

それで、前回の復習も兼ねて任意の音階を鳴らすメソッドを作ってみます。前回は、話を単純にするために、音階と周波数の計算を省略してみましたが、今回は少しまじめに計算してみたいと思います。

まず、前提となるのが、ラ(A)の音が、440Hzであること、そして、1オクターブ上の音を出すには、周波数を2倍すれば良いと言うこと、また、1オクターブはドレミファソラシの7音ではなく、ド・ド#・レ・レ#・ミ・ファ・ファ#・ソ・ソ#・ラ・ラ#・シの12半音で成り立っていること、つまり、1半音上がることに、周波数が「2の12乗根」倍(約1.06倍)になるということによって求められます。

箇条書きにして確かめてみます。

  • 基準音のラ(A)の音は、440Hz
  • 周波数を2倍にすると、1オクターブ上がる
  • 1オクターブは12半音から成っている
  • 1半音上げるには、2の12乗根だけ倍にする

この規則に則って、音程を周波数として計算してみます。

また、MIDIでは音階を番号で表すことになっています。例えば、440Hz のラ(A)の音は69番と決まっており、ラ#(A#)は70番、シ(B)は71番、ド(C)は72、ド#(C#)は73…という具合です。そこで、このMIDIの番号に応じた周波数テーブルを作ってみます。

基本となるラ(69番)を440Hzとし、70番以降は、これを、2の12乗根分だけかけていき、68番以前は、これを2の12乗根分だけ割っていくことで値を求めています。

// 音程の周波数を計算
static double note_freq[] = new double[128];
static void calcFrequency() {
  // 半音(2の12乗根)を計算
  double r = Math.pow(2.0, 1.0 / 12.0);
  // A(NoteNo=69) より上の音を計算
  note_freq[69] = 440.0; // A のノート
  for (int i = 70; i < 128; i++) {
    note_freq[i] = note_freq[i-1] * r;
  }
  // A(NoteNo=69) より下の音を計算
  for (int i = 68; i >= 0; i--) {
    note_freq[i] = note_freq[i+1] / r;
  }
  // 一覧を表示
  for (int i = 0; i < 128; i++) {
    System.out.println(i + "," + note_freq[i]);
  }
}

これを実行すると、以下のような周波数の値が得られます。Flashで波形合成をするサンプルを公開されておられるテキスケさんのところで定義されている音階と周波数のテーブルを見比べても問題ないので、この考え方で良いことが分かります。

Note No Frequency
C 60 261.6255653005985
C# 61 277.18263097687196
D 62 293.66476791740746
D# 63 311.1269837220808
E 64 329.62755691286986
F 65 349.2282314330038
F# 66 369.99442271163434
G 67 391.99543598174927
G# 68 415.3046975799451
A 69 440.0
A# 70 466.1637615180899
B 71 493.8833012561241
C 72 523.2511306011974
C# 73 554.3652619537443
D 74 587.3295358348153
D# 75 622.253967444162
E 76 659.2551138257401
F 77 698.456462866008
F# 78 739.988845423269

では、このテーブルを利用して、特定の音程を発音するプログラムを作ってみます。

//-----------------------------------------------------
// ドレミファソラシドを演奏するプログラム
//-----------------------------------------------------
import javax.sound.sampled.*;
public class PlayWave1 {
    // 音名を定数で表したもの
    static int C=0,CS=1,D=2,DS=3,E=4,F=5,FS=6,G=7,GS=8,A=9,AS=10,B=11;
    // 音名から NoteNo を計算する
    static int getNoteNo(int octave, int noteName) {
        return octave * 12 + noteName;
    }
    // 周波数を計算してテーブルにセットする
    static double note_freq[] = new double[128];
    static void calcFrequency() {
        // 半音(2の12乗根)を計算
        double r = Math.pow(2.0, 1.0 / 12.0);
        // A(NoteNo=69) より上の音を計算
        note_freq[69] = 440.0; // A のノート
        for (int i = 70; i < 128; i++) {
            note_freq[i] = note_freq[i-1] * r;
        }
        // A(NoteNo=69) より下の音を計算
        for (int i = 68; i >= 0; i--) {
            note_freq[i] = note_freq[i+1] / r;
        }
        // 一覧を表示
        for (int i = 0; i < 128; i++) {
            System.out.println(i + "," + note_freq[i]);
        }
        return;
    }
    static double getNotenoToFreq(int octave, int noteName) {
        return note_freq[getNoteNo(octave, noteName)];
    }
    // 波形のパラメータなどを指定
    static int SAMPLE_RATE = 44100; // 44.1KHz
    static SourceDataLine line;
    public static void main(String[] args)
        throws LineUnavailableException {
        calcFrequency();//周波数を計算してテーブルにセットする
        // オーディオの初期化
        AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
        line = (SourceDataLine)AudioSystem.getLine(info);
        // デバイスを開く
        line.open();
        line.start();
        // 演奏
        writeNote(getNotenoToFreq(6,C));
        writeNote(getNotenoToFreq(6,D));
        writeNote(getNotenoToFreq(6,E));
        writeNote(getNotenoToFreq(6,F));
        writeNote(getNotenoToFreq(6,G));
        writeNote(getNotenoToFreq(6,A));
        writeNote(getNotenoToFreq(6,B));
        writeNote(getNotenoToFreq(7,C));
        line.drain(); // 終了まで待機
    }
    public static void writeNote(double frequency) {
        // バイト列に適当な矩形波を作成
        byte[] b = new byte[SAMPLE_RATE];
        double amplitude = SAMPLE_RATE / frequency; // 波長
        for (int i = 0; i < b.length; i++) {
            double r = i / amplitude;
            b[i] = (byte)((Math.round(r) % 2 == 0) ? 100 : -100);
        }
        // 再生(バイト列を line に書き込む)
        line.write(b, 0, b.length);
    }
}

これを実行すると、ドレミファソラシド!と聞き慣れた音階が再生されます。

音の長さを指定する

音程ができれば、次は音の長さを指定したくなります。そこで、もう少し基本的なところに戻ってみます。これまでオーディオAPIの初期化の部分で、必ず SAMPLE_RATE として 44100(44.1KHz)を指定していました。これは何を表しているのでしょうか。

サンプルレートというのは、1 秒あたりのオーディオ信号のサンプル数を示しています。 つまり、SAMPLE_RATEに、44100を指定したなら、1秒あたり 44,100 個の値を書き込むことになります。

つまり、サンプルレートの値が大きければ大きいほど、滑らかな波形を表現することができます。よって、このサンプルレートが高いほど(アナログの波形に近い)高音質のサウンドが再現できるようになり、逆にサンプルレートが低い場合にはオリジナルの音と比べて音質が悪くなるということです。

そして、AudioFormat クラスを生成するときには、サンプルレートのほか、1サンプルあたりのバイト数やチャンネル数を指定しますが、例えば、下記のように指定すると(44.1KHzで8ビット1チャンネル)、1秒間の音を再生するのに、44,100バイトを必要とします。

new AudioFormat(44100/*sampleRate*/, 8/*SampleSize*/, 1/*Channel*/, true, true)

ところで、44.1KHz というのは、CD 音質のサウンドです。ただし、CDの規格では、「16ビット44.1kHzステレオ」となっており、これを、AudioFormat で表すと次のようになります。

new AudioFormat(44100/*sampleRate*/, 16/*SampleSize*/, 2/*Channel*/, true, true)

この音質で再生するためには、1秒間に、44100 * 2 * 2 = 176400バイトが必要になります。

四分音符を演奏する

さて、少し話がずれましたが、以上の説明から、サンプルレートと音の長さの関係が分かったでしょうか。ポイントは次の点です。

サンプルレートというのは、1 秒あたりのオーディオ信号のサンプル数のこと

そして、音楽には、曲の速さを表すテンポという単位が存在します。テンポの定義は「一分間に四分音符を何回刻むか」というものです。一分間に刻む四分音符の数という意味で、BPM(Beat Per Minute)という言い方もします。

話が見えてきましたか。BPM=120の時には、一分間に四分音符を120回刻むのです。四分音符が何秒かを考えると、60/120秒、そして、それを要するサンプル数は、60/120*sampleRate の式で求められると言えます。

また、音楽で音の長さを表すには、全音符、二分音符、四分音符、八分音符のような単位で表しますが、これらは、全音符を基準として、それを1分割、2分割、4分割、8分割した長さなのです。

もう少し分かりやすく表にしてみます。八分音符を基本にして考えてみると、次のような長さになります。

八分音符 ○○○○○○○○ 玉1つは全音符を8分割した長さ
四分音符 ○ー○ー○ー○ー 玉1つは全音符を4分割した長さ
二分音符 ○ーーー○ーーー 玉1つは全音符を2分割した長さ
全音符   ○ーーーーーーー 玉1つは全音符を1分割した長さ

ここから考えて、BPM=120 のとき、各音符のサンプル数を求める場合、次のように計算できます。

四分音符のサンプル数=(60/120)*sampleRate
全音符のサンプル数=4*(四分音符のサンプル数)
二分音符のサンプル数=(全音符のサンプル数) / 2
八分音符のサンプル数=(全音符のサンプル数) / 8
16分音符のサンプル数=(全音符のサンプル数) / 16

これを踏まえた上で、先ほどのプログラムを書き換えてみます。音程に加えて音の長さを指定できるようにします。

import javax.sound.sampled.*;
public class PlayWave2 {
    static int SAMPLE_RATE = 44100; // 44.1KHz
    static int BPM = 120;
    static SourceDataLine line;
    // 実際に波形を書き込む
    static void writeNote(double frequency, int sampleCount) {
        byte[] b = new byte[sampleCount];
        double amplitude = SAMPLE_RATE / frequency; // 波長
        for (int i = 0; i < b.length; i++) {
            double r = i / amplitude;
            b[i] = (byte)((Math.round(r) % 2 == 0) ? 100 : -100);
        }
        // 再生(バイト列を line に書き込む)
        line.write(b, 0, b.length);
    }
    // 音の長さを計算する
    static int getSampleCount(int nLength/*n分音符*/) {
        double n4 = (60.0 / BPM) * SAMPLE_RATE; // 四分音符のサンプル数
        double n1 = n4 * 4; // 全音符のサンプル数
        return (int)Math.round(n1 / nLength);
    }
    // メインメソッド
    public static void main(String[] args)
        throws LineUnavailableException {
        calcFrequency();
        // Initialize Audio
        AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
        line = (SourceDataLine)AudioSystem.getLine(info);
        // Play
        line.open();
        line.start();
        // 演奏
        int n4 = getSampleCount(4/*分音符*/);
        int n8 = getSampleCount(8/*分音符*/);
        writeNote(getNotenoToFreq(6,G), n8);
        writeNote(getNotenoToFreq(6,E), n8);
        writeNote(getNotenoToFreq(6,E), n4);
        writeNote(getNotenoToFreq(6,F), n8);
        writeNote(getNotenoToFreq(6,D), n8);
        writeNote(getNotenoToFreq(6,D), n4);
        writeNote(getNotenoToFreq(6,C), n8);
        writeNote(getNotenoToFreq(6,D), n8);
        writeNote(getNotenoToFreq(6,E), n8);
        writeNote(getNotenoToFreq(6,F), n8);
        writeNote(getNotenoToFreq(6,G), n8);
        writeNote(getNotenoToFreq(6,G), n8);
        writeNote(getNotenoToFreq(6,G), n4);
        line.drain(); // 終了まで待機
    }
    // 音名を定数で表したもの
    static int C=0,CS=1,D=2,DS=3,E=4,F=5,FS=6,G=7,GS=8,A=9,AS=10,B=11;
    // 音名から NoteNo を計算する
    static int getNoteNo(int octave, int noteName) {
        return octave * 12 + noteName;
    }
    // 周波数を計算してテーブルにセットする
    static double note_freq[] = new double[128];
    static void calcFrequency() {
        // 半音(2の12乗根)を計算
        double r = Math.pow(2.0, 1.0 / 12.0);
        // A(NoteNo=69) より上の音を計算
        note_freq[69] = 440.0; // A のノート
        for (int i = 70; i < 128; i++) {
            note_freq[i] = note_freq[i-1] * r;
        }
        // A(NoteNo=69) より下の音を計算
        for (int i = 68; i >= 0; i--) {
            note_freq[i] = note_freq[i+1] / r;
        }
        return;
    }
    static double getNotenoToFreq(int octave, int noteName) {
        return note_freq[getNoteNo(octave, noteName)];
    }
}

何の曲の一節でしょうか?・・・ちょうちょです。

まとめ

以上、今回は、音程と音長について詳しく見てきました。音程と周波数の計算、そして、サンプルレートと音の長さの関係について考えることができました。今日までの部分では、ピコピコシンセというよりも、BEEP音という感じでした。基本が分かりつつあるので、次回はもう少しシンセ(波形合成)っぽいところにも踏み込んでみたいと思います。

Series Navigation«ピコピコ音の正体を暴く和音を鳴らす»

執筆者紹介

クジラ飛行机

クジラ飛行机

くじらはんど(http://kujirahand/)にて、日本語プログラミング言語「なでしこ」(IPA未踏ユース採択)、テキスト音楽「サクラ」(OSPオンラインソフト大賞入賞)など多くのオンラインソフトを開発。著書に「Flexプロフェッショナルガイド」「なでしこ公式バイブル」、「一週間でマスターするActionScript3.0」など。

TrackBack URL :