Javaでピアノを作りたい
- Swingでキーボードイベント処理
- Soundで音を作成
を実現したい。
執筆時の最終的到達点
- キーボードを押すと、音を出すことができるようになった
- キーコード、周波数、音量等の指定が可能
執筆時の未到達点
- わずかに低遅延の反応(現状、キーを押してからラグがあるように感じる、実際バッファサイズ分22.7msの遅延が発生する可能性がある。バッファサイズを減らすにはバッファの更新間隔を減らす必要がある。更新イベントはTimerで起こしてるが、15ms程度遅延が発生することがある。結果として、これ以上バッファサイズを減らすことはできない。キーボードイベントが発生した段階でバッファの内容をflushして、内容を書き換える手法を取ればキーボードイベントに対して遅れが発生しないはずなので、この手法は今後の目標とする。ー>追記:単純にwhile(true)で何度もバッファをチェックするのが一番よかった。)
- 正弦波を出しているが、ノイズが乗っているのでフィルタが必要である(このノイズはKey.releaseイベントが起きた段階で音を0にしているためステップ上の波形が発生していることによると考えられる。)
- ローパスフィルタ等の実装
- MIDIデータの演奏
音を出す
まず、Sound.sampledというライブラリについて調べました。
単純には
AudioFormat frmt= new AudioFormat(44100,8,1,true,false); //フォーマットの設定
DataLine.Info info= new DataLine.Info(SourceDataLine.class,frmt); //Infoデータに変換
this.source = (SourceDataLine) AudioSystem.getLine(info); //AudioSystemクラスを用いてSourceDataLineインスタンスを取得する
を使って、SourceDataLineインスタンスを取得します。
SourceDataLine.write(byte[] wave,0,length);
でバッファーに音データを書き込みます。
SourceDataLine.start();
で音の再生が始まります。
バッファーが空になってもSTOPイベントなどは発生しません。再生中に
SourceDataLine.write(byte[] wave,0,length);
を繰り返して途切れさせないようにします。
第二引数は”配列の先頭からの座標”とありますが、0を指定しておけば自動的に最後尾に追加される形になります。
START、OPEN、STOP、CLOSEイベントを追加することも可能です。
implement LineListener
を継承し
SourceDataLine.addLineListener(this);
でイベントを追加できます。
イベントが発生した場合、
LineListener.update(LineEvent)
が自動で呼び出されます。updateをOverrideして処理を書きましょう。
バッファの更新について
バッファに残っているデータ量は
int byte_size = SourceDataLine.available();
で取得できます。
この数値が特定の値以下になったらバッファの内容を追加するように処理しました。
int now_available = this.source.available();
if( this.buffer_size - now_available < this.frame_size){
//bufferに残った量 < フレームサイズ
this.calculate_buffer(this.wave_frame,this.wave_frame.length);
//this.source.write(this.wave_frame,this.buffer_size - now_available,this.wave_frame.length);
this.source.write(this.wave_frame,0,this.wave_frame.length);
}
こんな感じです。
バッファデータは音符インスタンスをもとにして作っています。
最後に完成物を乗せました。
成果物
Keyboard.java
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import static java.awt.event.KeyEvent.*;
public class Keyboard extends JFrame implements LineListener ,KeyListener {
private final int height = 600;
private final int width = 600;
private final int buffer_size = 4000;
private final int frame_size = 1000;
private SourceDataLine source;
List<Note> list_note = new LinkedList<Note>();
private byte[] wave = new byte[buffer_size];
private byte[] wave_frame = new byte[frame_size];
public static void main(String[] args){
System.out.println("Start main");
Keyboard keyboard = new Keyboard();
java.util.Timer timer = new Timer();
Timer_task timer_task = new Timer_task();
timer_task.setKeyboard(keyboard);
timer_task.setTimer(timer);
timer_task.clearEndTimer();
timer.scheduleAtFixedRate(timer_task ,0,1);
}
public Keyboard(){
//フレーム作成
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(this.width, this.height);
setVisible(true);
addKeyListener(this);
//音符の作成
Note C4 = new Note(VK_A,269.292,20,44100);
Note D4 = new Note(VK_S,302.270,20,44100);
Note E4 = new Note(VK_D,339.286,20,44100);
Note F4 = new Note(VK_F,359.461,20,44100);
Note G4 = new Note(VK_J,403.482,20,44100);
Note A4 = new Note(VK_K,452.893,20,44100);
Note B4 = new Note(VK_L,508.355,20,44100);
Note C5 = new Note(59,538.584,20,44100);
list_note.add(C4);
list_note.add(D4);
list_note.add(E4);
list_note.add(F4);
list_note.add(G4);
list_note.add(A4);
list_note.add(B4);
list_note.add(C5);
C4.set_volume(20);
//C4.set_on();
//D4.set_on();
//E4.set_on();
//音声出力設定
AudioFormat frmt= new AudioFormat(44100,8,1,true,false);
DataLine.Info info= new DataLine.Info(SourceDataLine.class,frmt);
try {
this.source = (SourceDataLine) AudioSystem.getLine(info);
} catch (LineUnavailableException e) {
System.out.println("cant get line///");
throw new RuntimeException(e);
}
this.source.addLineListener(this);
this.source.flush();
try {
this.source.open(frmt,buffer_size);
} catch (LineUnavailableException e) {
System.out.println("cant open line....");
throw new RuntimeException(e);
}
this.source.start();
}
public void send_state(){
System.out.print("active ");
System.out.print(this.source.isActive());
System.out.print(" , running ");
System.out.println(this.source.isRunning());
}
public void send_available(){
System.out.print("available ");
System.out.println(this.source.available());
}
public void send_remain(){
System.out.print("remain size ");
System.out.println(this.buffer_size - this.source.available());
}
public void send_buffer_size(){
System.out.print("buffer size ");
System.out.println(this.source.getBufferSize());
}
public void calculate_buffer(byte[] wave,int length){
//バッファーの計算
for(int i = 0;i < length;i++) {
double value = 0;
for (Note j : this.list_note) {
if (j.get_state()) {
value += j.value();
j.inc_phase();
}
}
long value_long;
value_long = Math.round(value);
if (value_long > 127){
wave[i] = 127;
}else if(value_long <-128 ){
wave[i] = -128;
}else{
wave[i] = (byte)Math.round(value_long);
}
}
}
@Override
public void paint(Graphics g){
Image imgBuf = createImage(this.width,this.height);
Graphics gBuf = imgBuf.getGraphics(); //gBufがバッファの画像
gBuf.setColor(Color.white);
gBuf.fillRect(0,0,this.width,this.height);
//各種 描画開始
//System.out.println(this.source.getBufferSize());
//System.out.println(this.source.available());
//send_state();
//描画完了
Graphics graphics = getContentPane().getGraphics();
graphics.drawImage(imgBuf,0,0,this);
}
public boolean buffer_update(){
//タイマーが呼び出す
//buffer内が特定の数値以下になった場合内容を追加する。
//44100Hzの場合,1msは44.1Frameとなる
//this.send_available();
//this.send_remain();
long before_time = System.nanoTime();
int now_available = this.source.available();
if( this.buffer_size - now_available < this.frame_size){
//bufferに残った量 < フレームサイズ
this.calculate_buffer(this.wave_frame,this.wave_frame.length);
//this.source.write(this.wave_frame,this.buffer_size - now_available,this.wave_frame.length);
this.source.write(this.wave_frame,0,this.wave_frame.length);
long now_time = System.nanoTime();
long elapsed = now_time - before_time;
//System.out.print("elapsed [us] ");
//System.out.println((int)(elapsed/1000));
return true;
}
return false;
//System.out.println("update buffer");
//this.send_state();
}
@Override
public void update(LineEvent event){
//source_data_lineの open,close,start,stopイベント処理
LineEvent.Type type = event.getType();
if(type == LineEvent.Type.STOP){
System.out.println("STOP source");
this.buffer_update();
//this.calculate_buffer(this.wave,this.wave.length);
//this.source.write(this.wave,0,this.wave.length);
System.out.println(this.source.isActive());
}else if(type == LineEvent.Type.OPEN){
System.out.println("OPEN source");
//this.buffer_update();
//this.calculate_buffer(this.wave,this.wave.length);
//this.source.write(this.wave,0,this.wave.length);
}else if(type == LineEvent.Type.START){
System.out.println("START source");
}
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
int key_code = e.getKeyCode();
//System.out.println(key_code);
for(Note i:list_note){
if(i.get_key() == key_code){
i.set_on();
}
}
}
@Override
public void keyReleased(KeyEvent e) {
int key_code = e.getKeyCode();
for(Note i:list_note){
if(i.get_key() == key_code){
i.set_off();
}
}
}
}
Note.java
public class Note {
private int key;
private int volume = 1;
private int times = 0;
private double frq = 1;
private double angle_frq = 1;
private double angle_frq_discrete = 1;
private double sample_frq = 1;
private boolean on_flag = false;
public Note(int key,double frq,int volume,double sample_frq){
this.key = key;
this.frq =frq;
this.volume = volume;
this.sample_frq = sample_frq;
this.angle_frq = this.frq * 2 * Math.PI;
this.angle_frq_discrete = this.angle_frq/this.sample_frq;
}
public double value(){
//現在の位相の値における値の取得
double value;
value = this.volume * Math.sin(this.angle_frq_discrete * this.times);
return value;
}
public void set_volume(int volume){
this.volume = volume;
}
public void clear_phase(){
this.times = 0;
}
public void inc_phase(){
this.times = this.times + 1;
}
public void set_on(){
this.on_flag = true;
}
public void set_off(){
this.on_flag = false;
}
public boolean get_state(){
return this.on_flag;
}
public int get_key(){
return this.key;
}
}
Timer_task.java
import java.util.Timer;
import java.util.TimerTask;
public class Timer_task extends TimerTask {
Keyboard keyboard;
private int num = 0;
private boolean endFlag = true;
Timer timer;
private long start_time;
private long before_time;
public void run() {
num++;
if(num>1000 && endFlag == true){
System.out.println("End");
timer.cancel();
}
System.out.print(keyboard.buffer_update());
if(num % 1000 == 0){
keyboard.repaint();
}
send_elapsed_time_us();
//send_elapsed_time();
}
public void send_elapsed_time(){
long now_time = System.currentTimeMillis();
long elapsed = now_time - this.before_time;
System.out.print("elapsed [ms] ");
System.out.println(elapsed);
/*
System.out.print(" ");
System.out.print(now_time);
System.out.print(" ");
System.out.println(before_time);
*/
this.before_time = now_time;
}
public void send_elapsed_time_us(){
long now_time = System.nanoTime();
long elapsed = now_time - this.before_time;
System.out.print("elapsed [us] ");
System.out.println((int)(elapsed/1000));
/*
System.out.print(" ");
System.out.print(now_time);
System.out.print(" ");
System.out.println(before_time);
*/
this.before_time = now_time;
}
public void setKeyboard(Keyboard keyboard){
this.keyboard = keyboard;
}
public void setTimer(Timer time3){
this.timer = time3;
}
public void setEndTimer(){
this.endFlag = true;
}
public void clearEndTimer(){
this.endFlag = false;
}
}