トップ » 技術記事 » Javaでピコピコシンセを作ってみよう!(1) - ピコピコ音の正体を暴く

Javaでピコピコシンセを作ってみよう!(1) - ピコピコ音の正体を暴く

タグ: Audio Java

ファミコン世代の私たちは、ピコピコ音に目がありません。本連載ではピコピコ音がどんな風に作られているのかを考察します。都合良く、Javaには、ローレベルのオーディオを操作するAPIが用意されていますので、これを活用してみたいと思います。波形合成の仕組みを学び、ピコピコ音を自由自在に操ってみましょう。

ピコピコ音の正体とは?!

ファミコンや初期の携帯電話、電子オモチャに特有のピコピコ音は、どのようなものなのでしょうか。電子音特有の特徴があるのでしょうか。それとも、チープな音色なので、きっと単純なものに違いないと思うでしょうか。

では、ピコピコした音の正体をグラフにして、視覚的に確認してみます。以下の画像は、プーっという単純な電子音を波形編集ソフトで表示したものとなっています。

http://aoikujira.com/demo/hakkaku/rc/20090410G045qL-square-wave.png

これは、矩形波と呼ばれる単純な波形です。見ただけでは分からないでしょうから、音で聞いてみましょう。(音量に注意してクリックしてみてください。)

→プーっという音

今回は、視覚的に見ることができたこの矩形波を作って楽しんでみます。

Javaなら難しくない波形合成

逆に、プーッという音を作るには、先ほど見た矩形波のような形の波形を作れば良いことになります。Java ならば、ローレベルのオーディオを扱う API が用意されています。ですから、矩形波の形をしたバイト列を作ってあげれば、それを音として鳴らすことができます。とてもシンプルです。

以下が、Java で書いたプーッという音を鳴らす一番シンプルなソースコードです。Eclipse や NetBeans で Java プロジェクトを作った後、PlaySquareWave というクラスを作成し、以下のコードをぺたっと貼り付けて実行すれば、演奏が始まります。

// file: PlaySquareWave.java
import javax.sound.sampled.*;
public class PlaySquareWave {
    public static void main(String[] args)
            throws LineUnavailableException {
        // オーディオ形式を指定
        int SAMPLE_RATE = 44100; // 44.1KHz
        AudioFormat audio_format = new AudioFormat(
                SAMPLE_RATE, 8, 1, true, true);
        DataLine.Info info = new DataLine.Info(
                SourceDataLine.class, audio_format);
        SourceDataLine line = (SourceDataLine)AudioSystem.getLine(info);
        line.open();
        line.start();
        // バイト列に適当な矩形波を作成
        int frequency = 440; // ---------------------- (*1)
        byte[] b = new byte[SAMPLE_RATE];
        for (int i = 0; i < b.length; i++) {
            int r = i / (SAMPLE_RATE / frequency);
            b[i] = (byte)((r % 2 == 0) ? 100 : -100);
        }
        // 再生(バイト列を line に書き込む)
        line.write(b, 0, b.length);
        line.drain(); // 終了まで待機
    }
}

このプログラムは、大きく分けて前半と後半に分けられます。前半では、オーディオの形式(サンプリングレートやビット数、チャンネル数)を指定したり、演奏用の SourceDataLine オブジェクトを開いたりと準備を行っています。そして、後半では、バイト列に矩形波を作り、SourceDataLine オブジェクトに書き込むことで音を鳴らします。

矩形波の性質を確認

ここ(PlaySquareWaveクラス)で作った矩形波ですが、プログラム中の(*1)に注目して見てみると、100 と -100 という値が一定間隔で交互に羅列されているだけのものです。

サンプリングレート(プログラム中ではSAMPLE_RATE)を440で割った値(ここでは110)の周期で繰り返されます。

http://aoikujira.com/demo/hakkaku/rc/2009041036c_y4-square-a.png

バイト列で見てみると、以下のように並んでいます。

100,100,100,...-100,-100,-100,...100,100,100,...

実は、この値が今回の大きなポイントとなってきます。

というのは、この繰り返しの周期が短ければ短いほど、高い音になり、周期が長ければ長いほど、低い音になるのです。実験してみましょう。

プログラム中にある(*1)の変数 frequency の値を大きくすると、100 と -100 が繰り返される周期は短くなります。frequency を先ほどの値の2倍の880にして実行してみてください。

int frequency = 880; // ---------------------- (*1)
byte[] b = new byte[SAMPLE_RATE];
for (int i = 0; i < b.length; i++) {
    int r = i / (SAMPLE_RATE / frequency);
    b[i] = (byte)((r % 2 == 0) ? 100 : -100);
}

どうでしょうか。ここでは、サンプリングレートを880で割った値(ここでは50個)の周期で100と-100が繰り返されることになります。先ほどの110回の周期よりも短いので、高い音が演奏されたのではないでしょうか。逆に、frequency の値を220と小さくして、実行してみてください。すると、200個の周期で繰り返され、低い音が鳴ります。

音階について考える

勘の良い方や、絶対音感をお持ちの方は、気づいたと思いますが、ここで鳴らしてみたのは、オクターブ違いのラ(A)の音です。周期を2倍にすると、オクターブが1つ上のラがなり、2分の1にすると、オクターブ下のラになったのです。

そうなれば、ドレミの12音階を作るのが簡単と思われたのではないでしょうか。1オクターブは、ド・ド#・レ・レ#..シまで、12の音階に分割されているのです。

以下のように、1オクターブを12個の平均で区切って演奏させてみます。

// バイト列に適当な矩形波を作成
byte[] b = new byte[SAMPLE_RATE];
int frequency = 440;
int freq12 = (880 - 440) / 12;
for (int n = 0; n < 12; n++) {
    frequency = 440 + freq12 * n;
    for (int i = 0; i < b.length; i++) {
        int r = i / (SAMPLE_RATE / frequency);
        b[i] = (byte)((r % 2 == 0) ? 100 : -100);
    }
    // 再生(バイト列を line に書き込む)
    line.write(b, 0, b.length);
}
line.drain(); // 終了まで待機

再生してみた方は、「おやっ?!」と思われたのではないでしょうか。ものすごく気持ち悪い音階が再生されました。音階はそれほど単純なものではないのです。そもそも、2倍して、1オクターブ上の音階になるのですから、1オクターブを12個に平均した間隔がうまく得られるはずはありません。

興味がある方は、ぜひ、「純正律」(ピタゴラス音律)や「平均律」について調べてみてください。音階のチューニングについて歴史的な経緯を調べることができます。

気持ちの悪い音を聞きましたので、最後に、ドミソの音を鳴らして終わりたいと思います。ここでは、直接、周波数を指定して音を鳴らします。

// file:PlaySquareWave3.java
import javax.sound.sampled.*;

public class PlaySquareWave3 {
    static int SAMPLE_RATE = 44100; // 44.1KHz
    static AudioFormat audio_format;
    static DataLine.Info info;
    static SourceDataLine line;
    public static void main(String[] args)
            throws LineUnavailableException {
        // オーディオ形式を指定
        audio_format = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
        info = new DataLine.Info(
                SourceDataLine.class, audio_format);
        line = (SourceDataLine)AudioSystem.getLine(info);
        line.open();
        line.start();
        // 演奏
        writeNote(523.25); // C
        writeNote(587.33); // D
        writeNote(659.26); // E
    }
    public static void writeNote(double frequency) {
        byte[] b = new byte[SAMPLE_RATE];
        for (int i = 0; i < b.length; i++) {
            double r = i / (SAMPLE_RATE / frequency);
            b[i] = (byte)((Math.round(r) % 2 == 0) ? 100 : -100);
        }
        line.write(b, 0, b.length);
        line.drain(); // 終了まで待機
    }
}

このプログラムで注目したいのが、writeNote() メソッドで指定している値です。それぞれ、ド(C)レ(D)ミ(E)の音を発音しているのですが、隣り合う値は、等間隔に並んでいる訳ではないという部分が分かることでしょう。

writeNote(523.25); // C + 62.08
writeNote(587.33); // D + 71.93
writeNote(659.26); // E

以下のページに、音階とヘルツの関係が表にまとめられており、今回はこの表を参考にしてみました。

まとめ

今回は、Java のローレベルなオーディオ API を利用して、ピコピコ音を鳴らす方法を紹介しました。はじめは、ブーッと鳴るだけのものを、そして最後には、ドレミを演奏してみました。意外とすんなり演奏できたのではないかと思います。

Series Navigation音程と音長の計算»

執筆者紹介

クジラ飛行机

クジラ飛行机

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

TrackBack URL :