Edited at
GetWildDay 10

音源がなくてもGET WILD

More than 1 year has passed since last update.

仕事中どうしてもGET WILDが聴きたくなることというのはよくあると思います。

たとえば以下のような状況。


  • ホワイトボードに「XYZ」と書かれていた

  • 同僚が巨体のサングラス坊主

  • もっこり

しかしGET WILDがいつでもあなたのそばにあるとは限りません。

今回はそんなときのために、AndroidとKotlinをつかって自らGET WILDをする方法を提供します。


AudioTrack

音源がないのであれば、自分で演奏すれば良いわけです。

AndroidにはAudioTrackという音を再生できるAPIが提供されているのでこいつを使ってGET WILDしましょう。


Key

まずはKeyを定義しましょう。

enum class Key(val freq: Float) {

C(16.35f),
C_SHARP(17.32f),
D(18.35f),
D_SHARP(19.45f),
E(20.6f),
F(21.83f),
F_SHARP(23.12f),
G(24.5f),
G_SHARP(25.96f),
A(27.5f),
A_SHARP(29.14f),
B(30.87f);
}

enumとして以下を定義しました。

それぞれに一番低いオクターブの周波数も持たせています。


  • C

  • C#

  • D

  • D#

  • E

  • F

  • F#

  • G

  • G#

  • A

  • A#

  • B


Note Length

つぎに音の長さです。

enum class NoteLength(val length: Float) {

NOTE_1(1f),
NOTE_2_DOTTED(0.75f),
NOTE_2(0.5f),
NOTE_4_DOTTED(0.375f),
NOTE_4(0.25f),
NOTE_8_DOTTED(0.1875f),
NOTE_8(0.125f),
NOTE_16_DOTTED(0.09375f),
NOTE_16(0.0625f);
}

これもenumとして定義しました。

とりあえずは以下のみを定義しました。

それぞれに一小節あたりの長さをFloatとして持たせています。


  • 全音符

  • 付点2分音符

  • 2分音符

  • 付点4分音符

  • 4分音符

  • 付点8分音符

  • 8分音符

  • 付点16分音符

  • 16分音符


Note

音程と音の長さを定義できました。つぎはNoteをつくります。

音符と休符のスーパークラスとなる抽象クラスとしてNoteを定義しました。

abstract class Note {

abstract val frequency: Float
abstract val length: Float
}


MusicalNote

次に音符としてMusicalNoteを定義しました。

MusicalNoteはコンストラクタで先程定義したKeyとNoteLength、オクターブを受け取ります。

プロパティのfrequencyKey.freq * (2^octave)を返します。これがその音符の周波数です。

open class MusicalNote(private val key: Key,

private val octave: Int,
private val noteLength: NoteLength): Note() {

override val frequency: Float
get() = key.freq * Math.pow(2.0, octave.toDouble()).toFloat()

override val length: Float
get() = noteLength.length
}


RestNote

次に休符としてRestNoteを定義しました。

休符は音を持たないのでプロパティfrequency0を返します。

class RestNote(private val noteLength: NoteLength): Note() {

override val frequency: Float
get() = 0f

override val length: Float
get() = noteLength.length
}


楽譜を作ろう

さて、Noteを一通り実装できたのでさっそく楽譜をつくりましょう。

今回はGET WILDの楽譜をつくるわけですが、たかが石ころ一つガンダムで押し返してやるときにはBeyond The Timeを聴きたくなることもあるかもしれません。楽譜は書きやすく読みやすい方がいいですよね?

ということで、Recorderクラスをつくります。

class Recorder {

private val notes = arrayListOf<Note>()

fun c(octave: Int) = MusicalNoteBuilder(Key.C, octave, notes)
fun cS(octave: Int) = MusicalNoteBuilder(Key.C_SHARP, octave, notes)
fun d(octave: Int) = MusicalNoteBuilder(Key.D, octave, notes)
fun dS(octave: Int) = MusicalNoteBuilder(Key.D_SHARP, octave, notes)
fun e(octave: Int) = MusicalNoteBuilder(Key.E, octave, notes)
fun f(octave: Int) = MusicalNoteBuilder(Key.F, octave, notes)
fun fS(octave: Int) = MusicalNoteBuilder(Key.F_SHARP, octave, notes)
fun g(octave: Int) = MusicalNoteBuilder(Key.G_SHARP, octave, notes)
fun gS(octave: Int) = MusicalNoteBuilder(Key.G, octave, notes)
fun a(octave: Int) = MusicalNoteBuilder(Key.A, octave, notes)
fun aS(octave: Int) = MusicalNoteBuilder(Key.A_SHARP, octave, notes)
fun b(octave: Int) = MusicalNoteBuilder(Key.B, octave, notes)
fun rest() = RestNoteBuilder(notes)

fun record(block: Recorder.()->Unit) = apply(block)
fun export() = Music(notes)

abstract class NoteBuilder(protected val notes: ArrayList<Note>) {
fun whole() = newNote(NoteLength.NOTE_1)
fun dottedHalf() = newNote(NoteLength.NOTE_2_DOTTED)
fun half() = newNote(NoteLength.NOTE_2)
fun dottedQuarter() = newNote(NoteLength.NOTE_4_DOTTED)
fun quarter() = newNote(NoteLength.NOTE_4)
fun dottedEight() = newNote(NoteLength.NOTE_8_DOTTED)
fun eight() = newNote(NoteLength.NOTE_8)
fun dottedSixteenth() = newNote(NoteLength.NOTE_16_DOTTED)
fun sixteenth() = newNote(NoteLength.NOTE_16)

abstract protected fun newNote(length: NoteLength)
}

class MusicalNoteBuilder(private val key: Key,
private val octave: Int,
notes: ArrayList<Note>): NoteBuilder(notes) {

override fun newNote(length: NoteLength) {
notes.add(MusicalNote(key, octave, length))
}
}

class RestNoteBuilder(notes: ArrayList<Note>): NoteBuilder(notes) {

override fun newNote(length: NoteLength) {
notes.add(RestNote(length))
}
}
}

Recorderには各Keyを名前とした関数を実装しました。引数はオクターブ数です。この関数はMusicalNoteBuilderを返します。

こいつには音の長さを名前とした関数が実装されています。


record関数

record関数は高階関数になっていて、引数はRecorderの拡張関数です。

つまり、record関数に渡す関数はRecorderの一部となるので、その関数のなかではthisキーワードがRecorderと一致します。

実際に使ってみましょう。

Recorder().record {

dS(4).quarter()
cS(4).quarter()
b(3).quarter()
rest().eight()
dS(4).eight()

cS(4).dottedEight()
b(3).dottedEight()
b(3).dottedQuarter()
rest().eight()
b(3).sixteenth()
cS(4).sixteenth()

dS(4).dottedEight()
dS(4).dottedEight()
e(4).eight()
dS(4).dottedEight()
b(3).dottedEight()
dS(4).eight()

dS(4).dottedEight()
cS(4).dottedEight()
b(3).quarter()
rest().quarter()

dS(4).quarter()
cS(4).quarter()
b(3).quarter()
rest().eight()
dS(4).eight()

cS(4).dottedEight()
b(3).dottedEight()
b(3).dottedQuarter()
rest().eight()
b(3).sixteenth()
cS(4).sixteenth()

dS(4).dottedEight()
dS(4).dottedEight()
e(4).eight()
dS(4).dottedEight()
b(3).dottedEight()
dS(4).eight()
dS(4).dottedEight()
cS(4).dottedEight()
b(3).eight()
b(3).quarter()
}.export()

recordの後にexportすることでMusicクラスを取得できます。

MusicクラスはNoteのリストを持ってるデータクラスです。


Synthesizer

楽譜ができたらそこから音を鳴らしてくれるクラスをつくりましょう。

class Synthesizer(private val audioTrack: AudioTrack,

private val samplingRate: Int,
private val bufferSize: Int) {

fun play(music: Music) {
music.notes.map { toSineWave(it) }

Thread {
audioTrack.play()
music.notes.map { toSineWave(it) }
.forEach { audioTrack.write(it, 0, it.size) }
audioTrack.stop()
}.start()
}

private fun toSineWave(note: Note): ByteArray {
with(note) {
val noteLength = (bufferSize + length).toDouble()
val buffer = ByteArray(Math.ceil(noteLength).toInt()) { i ->
val wave = Math.sin( i / (samplingRate/frequency) * (Math.PI * 2) )
if (wave > 0.0) Byte.MAX_VALUE else Byte.MIN_VALUE
}
return buffer
}
}

}

コンストラクタは冒頭でかいたAudioTrackとサンプリングレートとバッファサイズです。


サイン波

音は波です。

toSineWave関数はNoteクラスからサイン波をByteArrayにしたものを返してくれます。


AudioTrackで再生

まずNoteリスト(List<Note>)をtoSineWaveをつかってすべてByteArrayのリスト(List<ByteArray>)にします。

そうしたら、forEachでまわしてAudioTrack#writeにByteArrayを渡していきましょう。


GetWildはいつもそばに

はい、これであなたはいつでもAndroidでGetWildを聴くことが出来ます。

GitHubにもあげていますので、より詳細にコードをみたいひとはそちらをごらんくだせい。

GetWildroid