PSGぽい音を出すプログラムを作ってみた。ボタンを押すと440Hz(いわゆるラ)の音がなって、もう一度おすとまるようなプログラムだ。
package jp.hemohemo.testaudiotrack;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private AudioTrack audioTrack;
private final int SAMPLING_RATE = 44100; // サンプリング周波数
private double angle = 0; // 音の角度?
private boolean noteon = false; // trueの間音がなる。
private final double freq = 440; // ならす音の周波数(440Hz,いわゆるラの音)
private final short vol = 2000; // ボリューム
private short []buf; // 波形データを入れるバッファ
private final int frames = SAMPLING_RATE / 10; // 100msのフレーム数(100ms単位でデータを更新することにする)
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)findViewById(R.id.textView);
// バッファの最低サイスを求める。
int bufSize = AudioTrack.getMinBufferSize(SAMPLING_RATE,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
// 最低200ms分のバッファになるようにbufSizeを調整 ... (1)
if(bufSize < frames * 2 * 2 * 2) {
// バッファサイズを調整
bufSize = frames * 2 * 2 * 2;
}
System.out.println("bufSize:" + bufSize);
// AudioTrackを用意 ... (2)
// ↓このコンストラクタはAndroid 8.0(API Level 26)で非推奨になっている。
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, // 音楽ストリーム
SAMPLING_RATE, // サンプリングレート
AudioFormat.CHANNEL_OUT_STEREO, // ステレオ
AudioFormat.ENCODING_PCM_16BIT, // 16bit PCM
bufSize,
AudioTrack.MODE_STREAM);
// 100ms単位でイベント(onPeriodicNotification)を発生するように設定する。
audioTrack.setPositionNotificationPeriod(frames);
// 波形を生成するメモリを用意(100ms分) ... (3)
buf = new short[frames * 2];
// イベントでの処理を設定
audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
@Override
public void onMarkerReached(AudioTrack track) {
System.out.println("onMarkerReached");
}
@Override
public void onPeriodicNotification(AudioTrack track) {
System.out.println("onPeriodicNotification");
// 100ms分の波形データを用意する
getData(buf);
// AudioTrackに書き込む
track.write(buf, 0, buf.length);
}
});
// 波形をとりあえず生成する。
getData(buf);
// AudioTrackのバッファを埋める。 ... (4)
// よくわからないが、これをしておかないと100msごとのイベントが発生しない。
int ret;
do {
ret = audioTrack.write(buf, 0, buf.length);
} while(ret == buf.length);
textView.setText("OFF");
// ボタンをクリックした時の処理を設定
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
noteon = !noteon;
angle = 0;
if(noteon) {
textView.setText("ON");
} else {
textView.setText("OFF");
}
}
});
// AudioTrackを再生開始
audioTrack.play();
}
// 波形データを用意する。 ... (5)
private void getData(short []buf) {
if(noteon) {
for(int i = 0; i < buf.length / 2; i++) {
if(angle < 180) {
buf[i * 2 + 0] = vol; // 左チャンネル
buf[i * 2 + 1] = vol; // 右チャンネル
} else {
buf[i * 2 + 0] = (short)-vol;
buf[i * 2 + 1] = (short)-vol;
}
// angleに1フレームで動く角度を加算する
angle += (double)360 * freq / SAMPLING_RATE;
if(angle > 360) {
angle = angle - (int)(angle / 360) * 360;
}
}
} else {
for(int i = 0; i < buf.length; i++) {
buf[i] = 0;
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.stop();
audioTrack.release();
}
}
}
(1) 最低200ms分のサイズを計算している。framesは100msのフレーム数なので2倍、さらにステレオなので2倍、さらに16ビットPCMなんで2倍してバッファサイズを計算している。
(2) コメントに書いているとおりコンストラクタはAndroid 8.0(API Level 26)で非推奨になっているが、最低レベルをAPI Level 14にしているので、まっ、いいか(^^;)
(3) ここもステレオなんで2倍。あと、shortの配列を用意しているので16ビットPCMなんで2倍はいらない。
(4) コメントにも書いたがAudioTrackのバッファを埋めとかないといけないみたい。
(5) ステレオなら配列の偶数バイト目が左チャンネルで奇数バイト目が右チャンネルになる。
コメント