【クリスマスだし】Androidで8ビット音を生成してジングルベルを奏でてみる【25日目の1】
今回は、Androidで音生成に挑戦します。
今回はファミコンのようなピコピコ音を生成し、ジングルベルの数小節分を演奏してみましょう。音自体は、割と簡単に作ることができます。
音の基礎知識
まずは基礎知識から。
音は波形であるというのはご存知でしょう。どんな音かは周波数で決定されます。
音階の周波数対応
音階はいろんな資料が転がっているので、調べればすぐにわかります。
C, D, E, F, G, A, B で、ドレミファソラシ を表します(音楽の知識が少しある人なら今更感がありますが)
しかし、この音の周波数といわれると、即答できる人はいないのでは。簡単に表にしておきます。
音階名 | コード | 周波数(小数点6桁以下を切り捨て) |
---|---|---|
ラ | A | 220.0 |
シ | B | 246.941650 |
ド | C | 261.625565 |
レ | D | 293.664767 |
ミ | E | 329.627556 |
ファ | F | 349.228231 |
ソ | G | 391.994227 |
ラ | A | 440.0 |
変化が一定でないので、単純な等分になりません。今回は小数点6ケタ以下で切り捨てます
サンプリング周波数について
音をAD変換して表現するときに、サンプリング周波数という要素が必要になります。簡単に解説します。
現実の音は波形であるため、アナログ波形はなめらかな曲線の変化になります。これをデジタル変換(AD変換)する際には、小さい四角形の集合を近似して波形に見せるため、細かなデータの損失を避けて通れません
この「どれだけ細かい四角形を形成して、よりアナログ波形に近い波形に近似するか」の値を指定するのが、サンプリング周波数になります。この値が大きければ大きいほど、より細かく曲線に近似されるため、元の音に近づきます。ある程度正確に表現するためには、再現したい音の倍以上の周波数でサンプリングしてやれば良いそうです。
よく知られているのは、CD音源のサンプリング周波数でしょうか。44.1kHz(44100Hz)でサンプリングされています。1秒間あたりのデータ数を44100分割してアナログ波形に近似して音を表現します。
ピコピコ音を生成する
では、ファミコンなどのようなピコピコ音を生成するにはどうしたら良いでしょうか。原理は結構簡単です。なめらかな波形を、ある分岐点で最大値と最低値に振り分け、強制的に2値化してあげればOKです。
まずは、通常の波形データの作成方法を。数式だけ
Sin(bufferIndex / (SamplingRate / Frequency) * Math.PI * 2)
bufferIndex=バッファ位置, SamplingRate=サンプリング周波数, Frequency=周波数
これでFrequency(Hz)の音波形データが作成できます。図にすると次のような感じ
実際にはデジタルデータで近似した波形なので、拡大してやるとこんな感じになっているはずです
これを、さらに2値化してギザギザの波形にしてやります。Sin波形なので、0.0を境界線に最大値と最低値の2値に振ってあげます。イメージは次のような感じ
これでピコピコ音を鳴らすことができます。あとは周波数をいじってやれば、どんな音でもOK!(多分)
プログラム解説
ndroidで音声データを生成して再生するには、次のクラスを利用します
AudioTrack - Android Developer
大雑把に言うと、PCMストリーミングデータを書き込んで再生することができるようなクラスのようです。2つのモードが存在します
モード | 概要 |
---|---|
MODE_STATIC | STATICモード。予め演奏する演奏データを書き込み、play()で書き込んだデータを再生するモードです。 |
MODE_STREAM | STREAMINGモード。play()呼び出し後、データが書き込まれたタイミングで音を再生していきます。 |
今回はMODE_STREAMを利用し、1オクターブ分の音階データのみ用意します。
音を生成するクラスは下記の通り
/** * ピコピコ音を作成する * * @author komuro * */ public class DigitalSoundGenerator { // とりあえず1オクターブ分の音階を確保(半音階含む) public static final double FREQ_A = 220.0; public static final double FREQ_As = 233.081880; public static final double FREQ_B = 246.941650; public static final double FREQ_C = 261.625565; public static final double FREQ_Cs = 277.182630; public static final double FREQ_D = 293.664767; public static final double FREQ_Ds = 311.126983; public static final double FREQ_E = 329.627556; public static final double FREQ_F = 349.228231; public static final double FREQ_Fs = 369.994227; public static final double FREQ_G = 391.994535; public static final double FREQ_Gs = 415.304697; // public static final double FREQ_A = 440.0; // public static final double FREQ_As = 466.163761; // public static final double FREQ_B = 493.883301; private AudioTrack audioTrack; // サンプリング周波数 private int sampleRate; // バッファ・サイズ private int bufferSize; /** * コンストラクタ */ public DigitalSoundGenerator(int sampleRate, int bufferSize) { this.sampleRate = sampleRate; this.bufferSize = bufferSize; // AudioTrackを作成 this.audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, // 音楽ストリームを設定 sampleRate, // サンプルレート AudioFormat.CHANNEL_OUT_MONO, // モノラル AudioFormat.ENCODING_DEFAULT, // オーディオデータフォーマットPCM16とかPCM8とか bufferSize, // バッファ・サイズ AudioTrack.MODE_STREAM); // Streamモード。データを書きながら再生する } /** * サウンド生成 * @param frequency 鳴らしたい音の周波数 * @param soundLengh 音の長さ * @return 音声データ */ public byte[] getSound(double frequency, double soundLength) { // byteバッファを作成 byte[] buffer = new byte[(int)Math.ceil(bufferSize * soundLength)]; for(int i=0; i<buffer.length; i++) { double wave = i / (this.sampleRate / frequency) * (Math.PI * 2); wave = Math.sin(wave); buffer[i] = (byte)(wave > 0.0 ? Byte.MAX_VALUE : Byte.MIN_VALUE); } return buffer; } /** * いわゆる休符 * @param frequency * @param soundLength * @return 無音データ */ public byte[] getEmptySound(double soundLength) { byte[] buff = new byte[(int)Math.ceil(bufferSize * soundLength)]; for(int i=0; i<buff.length; i++) { buff[i] = (byte)0; } return buff; } /** * * @return */ public AudioTrack getAudioTrack() { return this.audioTrack; } }
サウンドデータを管理するDTOクラスを作成します
public class SoundDto { // 音声データ private byte[] sound; // 長さ private double length; /** * 引数付きコンストラクタ * @param source * @param length */ public SoundDto(byte[] source, double length) { this.sound = source; this.length = length; } public byte[] getSound() { return sound; } public void setSound(byte[] sound) { this.sound = sound; } public double getLength() { return length; } public void setLength(double length) { this.length = length; } }
スコアをGenerateして、再生するMainActivityクラス
public class MainActivity extends Activity implements Runnable{ public static final double EIGHTH_NOTE = 0.125; public static final double FORTH_NOTE = 0.25; public static final double HALF_NOTE = 0.5; public static final double WHOLE_NOTE = 1.0; // Sound生成クラス private DigitalSoundGenerator soundGenerator; // Sound再生クラス private AudioTrack audioTrack; // 譜面データ private List<SoundDto> soundList = new ArrayList<SoundDto>(); /** * 譜面データを作成 */ private void initScoreData() { // 譜面データ作成 soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_G, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_C, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, FORTH_NOTE), FORTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, FORTH_NOTE), FORTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_G, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_G, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_C, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, 3), 3)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, FORTH_NOTE), FORTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, EIGHTH_NOTE), EIGHTH_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_E, WHOLE_NOTE), WHOLE_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_G, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_G, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_F, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_D, HALF_NOTE), HALF_NOTE)); soundList.add(new SoundDto(generateSound(soundGenerator, DigitalSoundGenerator.FREQ_C, WHOLE_NOTE), WHOLE_NOTE)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // SoundGeneratorクラスをサンプルレート44100で作成 soundGenerator = new DigitalSoundGenerator(44100, 44100); // 再生用AudioTrackは、同じサンプルレートで初期化したものを利用する audioTrack = soundGenerator.getAudioTrack(); findViewById(R.id.startMelody).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // start sound Thread th = new Thread(MainActivity.this); th.start(); } }); // スコアデータを作成 initScoreData(); } @Override protected void onPause() { super.onPause(); } @Override protected void onDestroy() { super.onDestroy(); // 再生中だったら停止してリリース if(audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.stop(); audioTrack.release(); } } @Override protected void onResume() { super.onResume(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } /** * 8ビットのピコピコ音を生成する. * @param gen Generator * @param freq 周波数(音階) * @param length 音の長さ * @return 音データ */ public byte[] generateSound(DigitalSoundGenerator gen, double freq, double length) { return gen.getSound(freq, length); } /** * 無音データを作成する * @param gen Generator * @param length 無音データの長さ * @return 無音データ */ public byte[] generateEmptySound(DigitalSoundGenerator gen, double length) { return gen.getEmptySound(length); } @Override public void run() { // 再生中なら一旦止める if(audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.stop(); audioTrack.reloadStaticData(); } // 再生開始 audioTrack.play(); // スコアデータを書き込む for(SoundDto dto : soundList) { audioTrack.write(dto.getSound(), 0, dto.getSound().length); } // 再生停止 audioTrack.stop(); } }
MainのUIスレッド動作させると、UIスレッドを占拠してしまうので、別スレッドで走らせます。
再生させてみると微妙にリズムがカクカクしたジングルベルが流れるはずです!ピコピコピコー♪ちなみに、譜面はリズムが正確じゃないのであしからず。
今回のプログラムでは、一応8分音符の長さまでは対応しているはずです
ハッピークリスマス for All Androider!
いつかやるかもしれない
AudioTrackを複数走らせるとDQみたいに荘厳な感じの曲もできるかもしれません。Thread複数動作させた時のシンクロのさせ方が、フュージョンのキメ並みに難しい気もしますけど
ソースコード
全ソースコードはこちらに公開しておきます。DLして実行してみて下さい
参考資料
- Javaでピコピコシンセを作ってみよう!(2) - 音程と音長の計算
- 音階と周波数の対応表(JavaScript)
- AudioTrack(1)
- javaで波形データをbyte配列上に作成し、音として再生するサンプル
- サンプリング周波数 - wikipedia
- けいおん風ジェネレータ
スペシャルサンクス
この記事を書きあげるにあたって勝手に協力してもらったものたち
- コメダ珈琲
- 深夜に放送してたけいおん!劇場版
- NHK-FMでやってた歌う声優三昧