Javaでピコピコシンセを作ってみよう!(3) - 和音を鳴らす
街中で電子音を聞かないことはありませし、ファミコン世代の私たちは、ピコピコ音に目がありません。本連載では、Java でピコピコシンセを作ることを目標にします。ピコピコ音の仕組みを考察しながら、Java プログラミングを楽しみます。Javaのオーディオ API を活用してみます。
今回は、和音の演奏に挑戦してみます。
ここまでの復習
ここまで、Java でピコピコシンセの基本となる部分を考察してきました。Java のオーディオAPIを利用して矩形波を鳴らす方法、そして、音程や音の長さの計算方法について見てきました。簡潔に復習してみます。
ピコピコ音の正体である矩形波というのは、グラフにしてみると以下のような波形でした。
この値としては、100,100,…,-100,-100,…という値が繰り返されているものでした。Java でピコピコ音を鳴らすには、APIのメソッドにこれらの値を書き込んでやれば良いのでした。(そして、矩形波の上側と下側の幅(値)が大きければ大きな音が鳴り、小さければ小さな音が鳴ります。)
それから、音程を表す時には、繰り返される値の周期を調整するのでした。この周期が長ければ低い音が鳴り、短ければ高い音が鳴ります。
そして、この繰り返し周期を決められた値で繰り返すことにより、ドレミファソラシの音程を鳴らすことができるのでした。前回は、この12音階の周期を計算で求める方法について紹介しました。(また、サンプルレートやテンポを元に音の長さを計算する方法も紹介しました。)
WAV形式のファイルを書き出す方法
簡単なピコピコ音を演奏する方法は何度も紹介していますので、ここでは、これを音声ファイルに残す方法についても紹介しておきます。音声ファイルとして書き出すのも、演奏を行うのとほとんど一緒です。
import java.io.*;
import javax.sound.sampled.*;
public class TestWriteWave {
static int SAMPLE_RATE = 44100; // サンプルレート
static AudioFormat fmt = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
static SourceDataLine line;
static InputStream stream;
// 波形の書き込み
public static void writeData(double freq) {
double amplitude = SAMPLE_RATE / freq;
byte[] data = new byte[SAMPLE_RATE];
for (int i = 0; i < data.length; i++) {
int m = (int)Math.round((i / amplitude)) % 2;
data[i] = (m == 0) ? (byte)100 : (byte)-100;
}
line.write(data, 0, data.length); // 音を鳴らす
writeFile(new File("test"+freq+".wav"), data); // ファイルへ保存
}
public static void writeFile(java.io.File target, byte[] buf) {
InputStream in = new ByteArrayInputStream(buf);
AudioInputStream ais = new AudioInputStream(in, fmt, buf.length);
try {
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, target);
} catch (IOException e) {
System.out.println(e);
}
}
public static void main(String[] args) {
try {
DataLine.Info info = new DataLine.Info(SourceDataLine.class, fmt);
line = (SourceDataLine)AudioSystem.getLine(info);
line.open(); line.start();
writeData(440.00);
writeData(880.00);
line.drain();
} catch (LineUnavailableException e) {
System.out.println(e);
System.exit(1);
}
}
}
面倒なことは、全部、Java の API がラップしてくれているので、APIの使い方さえ間違えなければファイル書き出しもそれほど難しくないことが分かります。
和音を鳴らす方法
さて、ここまで短音でだけ音を鳴らしていましたが、そろそろ和音を鳴らしてみたいものです。和音というのは、複数の音が重なって鳴っているものです。Java で和音を鳴らす場合も、単純に加算するだけで表現できます。ただし、このとき、1バイトの範囲を超えないように注意する必要します。
以下は、ドミソの和音を鳴らすプログラムです。ほぼ半分は、前回作ったものを流用していますので、main メソッドと、波形を書き込んでいる writeWave メソッドの内容に注目してください。
import javax.sound.sampled.*;
public class PlayWave3 {
static int SAMPLE_RATE = 44100; // 44.1KHz
static int BPM = 120;
static AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
static DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
static SourceDataLine line;
// メインメソッド
public static void main(String[] args)
throws LineUnavailableException {
calcFrequency();
SourceDataLine line = (SourceDataLine)AudioSystem.getLine(info);
// Initialize Audio
line.open(); line.start();
// 演奏
int L2 = getSampleCount(2/*分音符*/);
byte[] block = new byte[L2];
writeWave(block, getNoteFreq(6,C), 50);
writeWave(block, getNoteFreq(6,E), 50);
writeWave(block, getNoteFreq(6,G), 50);
line.write(block, 0, block.length);
line.drain();
}
// 実際に波形を書き込む
static void writeWave(byte[] b, double frequency, int velocity) {
double amplitude = SAMPLE_RATE / frequency;
for (int i = 0; i < b.length; i++) {
double r = i / amplitude;
int v = (Math.round(r) % 2 == 0) ? velocity : -velocity;
v += b[i];
v = Math.min(Math.max(v, -128), 127);
b[i] = (byte)v;
}
}
// 音の長さを計算する
static int getSampleCount(int nLength/*n分音符*/) {
double n4 = (60.0 / BPM) * SAMPLE_RATE; // 四分音符のサンプル数
double n1 = n4 * 4; // 全音符のサンプル数
return (int)Math.round(n1 / nLength);
}
// 音名を定数で表したもの
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 getNoteFreq(int octave, int noteName) {
return note_freq[getNoteNo(octave, noteName)];
}
}
writeWave() メソッドを見ると、既存のバイト列に対して、今回書き込むべき値を加算しています。
for (int i = 0; i < b.length; i++) {
double r = i / amplitude;
int v = (Math.round(r) % 2 == 0) ? velocity : -velocity;
v += b[i]; // 以前の値に加算→和音
v = Math.min(Math.max(v, -128), 127); // バイト範囲を超えないように調整
b[i] = (byte)v;
}
ノイズを発生させる
ささっと和音を鳴らす処理を書いた時、バイト範囲(-128 ~ 127)を超えない処理を入れ忘れてしまいました。すると、ガガガーっというノイズがところどころに入ってしまいました。バイトの範囲を超えた値というのは、予期しない値が設定されることになります。つまり、ノイズ音を出そうと思ったら、適当な値を書き込んでやればノイズを発生させることができます。
ピコピコサウンドにノイズは欠かせません。ドラム音に利用したり、効果音として利用したり用途はさまざまです。ですから、ランダムな値を書き込むことでノイズを発生させてみます。
import javax.sound.sampled.*;
public class PlayNoise {
static int SAMPLE_RATE = 44100; // 44.1KHz
static AudioFormat af = new AudioFormat(SAMPLE_RATE, 8, 1, true, true);
static DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
static SourceDataLine line;
// メインメソッド
public static void main(String[] args)
throws LineUnavailableException {
SourceDataLine line = (SourceDataLine)AudioSystem.getLine(info);
line.open(); line.start();
// 演奏
byte[] block = new byte[SAMPLE_RATE];
writeNoise(block, 30);
line.write(block, 0, block.length);
writeNoise(block, 50);
line.write(block, 0, block.length);
line.drain();
}
// 実際に波形を書き込む
static void writeNoise(byte[] b, int velocity) {
for (int i = 0; i < b.length; i++) {
b[i] = (byte)Math.floor(Math.random() * velocity);
}
}
}
まとめ
今回は、和音を鳴らす方法と、ノイズを鳴らす方法を紹介しました。和音は単純に波形の各要素を足すだけなので、分かりやすかったのではないでしょうか。
このサイトについて
TrackBack URL :



