ヒーローズリーグ2021のオンラインリーグに作成中のソフト音源を投稿しました。
内容は以下のリンクから確認できます。
開発中のソフト音源アプリ
ちなみに、ソースもGitHubにアップしています。
なぜ作っているのか?
ヒーローズリーグのほうには簡単にしか書いてないですが、「音源を自分で作ってみたい」というのがまずあります。
中学生くらいの時はMSX BASICで音を鳴らして遊んでみたり、高校になってPCと一緒にYAMAHAのHello!Music!(TG300がセットのやつ)を買ってもらいました。
で、DTMを趣味でやっていたわけです。
で、バンと組んでKORG X5を手に入れたりして、TG300やKORG X5で音をいじったり、曲を作ったりしたわけです。
さらに時は進み、携帯電話でYAMAHAがSMAF発表してDTMマガジンにYAMAHAのチップの音を作るソフトが収録されたりして、多分そのころからソフトで音源作ってみたいなぁと思ったと思います。
今のところの目標としてはゲームボーイ的な音を出せたらいいなぁと思っています。
先は長そうですが・・・^^;
実は数年前にNDK使って簡単な音を出すプログラムを組んでいたんですが、Kotlinのお勉強と、C/C++使わないでどれだけできるかな?
?と思って作り直しています。
音を自分で作ることに関して知識が全然足りていないので、ググって試行錯誤しています。
ネットに情報を公開してくださっている皆さんに感謝!
なにしてるの?
楽譜データを再生する機能と、音を生成する機能を作成しています。
まずは音を生成する部分ですが、AudioTrackクラスを使って音を出しています。
リアルタイムで音を出したいので、バッファにため込んだりせずにストリームに流し込んでいます。
なので、コードを見るとわかると思いますが、1バイトのデータをひたすらAudioTrackに書き込んでいます。
ただ、最近気づいたんですが、シーケンサとして動かす場合、バッファリングして音を出すほうがいいような気がしているのでこの辺は要検討といったところです。
private fun playTone(){
val toneBuff = ByteArrayOutputStream()
val len = 0
toneBuff.use { buff ->
try {
if(AudioTrack.PLAYSTATE_PLAYING == audioTrack?.playState){
for (i in 0..len) {
var data = generate(sampleRate)
buff.write(data)
}
val buffer = buff.toByteArray()
if(buffer.isNotEmpty()){
when(audioTrack?.write(buffer, 0, buffer.size)){
AudioTrack.ERROR_BAD_VALUE -> Log.i(ToneController.APP_NAME, "ERROR_BAD_VALUE")
AudioTrack.ERROR_DEAD_OBJECT -> Log.i(ToneController.APP_NAME, "ERROR_DEAD_OBJECT")
AudioTrack.ERROR_INVALID_OPERATION -> Log.i(ToneController.APP_NAME, "ERROR_INVALID_OPERATION")
}
}
}
true
}catch(e:Exception){
Log.e(ToneController.APP_NAME, "Error!!!", e)
}
}
}
音を鳴らす部分と、音を生成するところは別で、コードだと、generate()メソッドが音のデータを生成するメソッドになります。
でgenerate()メソッドはこんな感じ。
private fun generate(sampleRate : Int ) : Int{
var r = this.cnt / (sampleRate / this.frequency) * (Math.PI * 2)
var toneData = when(func){
0->sin(r) // sin波
1->if( sin(r) > 0.5) 1.0 else -1.0 // 矩形波
2->if( sin(r) > 0.25) 1.0 else -1.0 // 矩形波
3->(0..1000).random() / 10.0 // ノイズ
else -> sin(r)
}
this.cnt++
if(this.cnt >= sampleRate * frequency){
this.cnt = 0
}
return floor(toneData).toInt()
}
理解ができてないところがあって、cntの条件が本当にこれでいいのか?などの疑問がありますが・・・^^;
変数funcの値で、作成する波形を決めています。
その結果を戻り値として返しています。
音を作る部分は将来的にもっと柔軟性を持たせたいと思っていますが、まずは音を出すことが目標なのでこのような形になっています。
次にシーケンサの部分。
シーケンサと音の生成するクラスのこんなクラス構成です。
(ざっくりとしたクラス図ですみません)
ちょっと気に入らない部分があるので、今後改善していく予定です。
Sequencerクラスで全体の管理を行います。
Trackクラスで実際の演奏データを処理して、音を鳴らすときにはToneControllerクラスを使って音を鳴らします。
ただ問題なのは、音の発生タイミングがおかしい。
これを回避するためにちゃんとバッファリングして、例えば1秒単位のデータを生成してAudioTrackクラスに書き込まないといけないのかな?と思っています。
今後
こんなことしたいなぁと思っています。
- 来年のヒーローズリーグにこれを使って変なアプリを投稿できればと思っています。
- 来年のSPAJAMで使えたらたらいいなぁ・・・(今年も使ったけど)
- Webに移植して今作っているダンジョンRPGで使いたいなと思っています。(いつになることやら・・・)
- 作曲で使えるようにしたい。具体的には音作りに幅を持たせたい。