@@ -1,55 +0,0 @@ | |||
package me.jfenn.audio | |||
import kotlinx.coroutines.flow.transform | |||
import kotlinx.coroutines.runBlocking | |||
import me.jfenn.audio.constants.* | |||
import me.jfenn.audio.model.Note | |||
import me.jfenn.audio.model.toNote | |||
import me.jfenn.audio.model.withVelocity | |||
import me.jfenn.audio.utils.midi | |||
fun main() = runBlocking<Unit> { | |||
AudioManager(scope = this).start { | |||
val metro = metronome(500) | |||
val pitch = metro.every(8).transform { | |||
emit(arrayOf( | |||
midi("3C#"), | |||
midi("3D#"), | |||
midi("3E") | |||
).random()) | |||
}.shared() | |||
// strings | |||
pitch.transform { number -> | |||
emit(number) | |||
emit(midi("4B")) | |||
emit(midi("4G#")) | |||
emit(number + 12) | |||
}.toNote(STRING_ENSEMBLE_2, 3000).withVelocity(70).done() | |||
// chord | |||
pitch.transform { number -> | |||
emit(Note(HARPSICHORD, number + 12, 250).delay(150)) | |||
emit(Note(HARPSICHORD, number + 24, 250).delay(200)) | |||
emit(Note(HARPSICHORD, number + 36, 250).delay(250)) | |||
}.withVelocity(60).done() | |||
// melody | |||
pitch.transform { number -> | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, number, 250).delay(1000)) | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, "4B", 250).delay(2000)) | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, "3G#", 250).delay(3000)) | |||
}.withVelocity(100).done() | |||
// bass | |||
metro.every(4).transform { | |||
emit(Note(MUSIC_BOX, 28, 1500, 100)) | |||
}.withVelocity(100).done() | |||
// tempo beat / percussion | |||
metro.every(2, offset = 1).transform { | |||
emit(Note(SFX_GUNSHOT, 44, 100)) | |||
}.done() | |||
} | |||
} |
@@ -2,17 +2,17 @@ package me.jfenn.audio.model | |||
import kotlinx.coroutines.flow.Flow | |||
import kotlinx.coroutines.flow.transform | |||
import me.jfenn.audio.utils.midi | |||
import me.jfenn.audio.utils.note | |||
import me.jfenn.audio.utils.octave | |||
import me.jfenn.audio.utils.midiNote | |||
import me.jfenn.audio.utils.midiOctave | |||
import me.jfenn.audio.utils.toMidi | |||
import java.lang.System.currentTimeMillis | |||
data class Note( | |||
val instrument: Int, | |||
val number: Int, | |||
val duration: Long, | |||
val velocity: Int = 100, | |||
val time: Long = currentTimeMillis() | |||
var instrument: Int, | |||
var number: Int, | |||
var duration: Long, | |||
var velocity: Int = 100, | |||
var time: Long = currentTimeMillis() | |||
) { | |||
constructor( | |||
@@ -20,10 +20,10 @@ data class Note( | |||
note: String, | |||
duration: Long, | |||
velocity: Int = 100 | |||
) : this(instrument, midi(note), duration, velocity) | |||
) : this(instrument, note.toMidi(), duration, velocity) | |||
override fun toString(): String { | |||
return "${instrument}: \t${octave(number)}${note(number)}\t ${duration}ms\t ${velocity}vol" | |||
return "${instrument}: \t${midiOctave(number)}${midiNote(number)}\t ${duration}ms\t ${velocity}vol" | |||
} | |||
fun delay(delay: Long) : Note { | |||
@@ -31,26 +31,15 @@ data class Note( | |||
} | |||
} | |||
fun Flow<Int>.toNote(instrument: Int = 0, duration: Long = 250) : Flow<Note> = transform { number -> | |||
emit(Note(instrument, number, duration)) | |||
fun Flow<Int>.toNote(block: Note.() -> Unit) : Flow<Note> = transform { midiNumber -> | |||
emit(Note(0, midiNumber, 250).apply(block)) | |||
} | |||
fun Flow<Note>.withInstrument(value: Int = 0, lambda: (Note) -> Int = { value }) = transform { note -> | |||
emit(note.copy(instrument = lambda(note))) | |||
@JvmName("toNoteString") | |||
fun Flow<String>.toNote(block: Note.() -> Unit) : Flow<Note> = transform { midiString -> | |||
emit(Note(0, midiString, 250).apply(block)) | |||
} | |||
fun Flow<Note>.withNumber(value: Int = 0, lambda: (Note) -> Int = { value }) = transform { note -> | |||
emit(note.copy(number = lambda(note))) | |||
} | |||
fun Flow<Note>.withDuration(value: Long = 0, lambda: (Note) -> Long = { value }) = transform { note -> | |||
emit(note.copy(duration = lambda(note))) | |||
} | |||
fun Flow<Note>.withVelocity(value: Int = 0, lambda: (Note) -> Int = { value }) = transform { note -> | |||
emit(note.copy(velocity = lambda(note))) | |||
} | |||
fun Flow<Note>.withDelay(value: Long = 0, lambda: (Note) -> Long = { value }) = transform { note -> | |||
emit(note.copy(time = note.time + lambda(note))) | |||
fun Flow<Note>.applyNote(block: Note.() -> Unit) = transform { note -> | |||
emit(note.apply(block)) | |||
} |
@@ -2,18 +2,18 @@ package me.jfenn.audio.utils | |||
val MIDI_NOTES = listOf("A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#") | |||
fun note(midi: Int) : String { | |||
fun midiNote(midi: Int) : String { | |||
return MIDI_NOTES[(midi + 3) % 12] | |||
} | |||
fun octave(midi: Int) : Int { | |||
fun midiOctave(midi: Int) : Int { | |||
return (midi - 9) / 12; | |||
} | |||
fun midi(note: String) : Int { | |||
val regex = Regex("[\\d\\-]+").find(note) | |||
fun String.toMidi() : Int { | |||
val regex = Regex("[\\d\\-]+").find(this) | |||
val octave = regex?.value?.toInt() ?: 0 | |||
val index = MIDI_NOTES.indexOf(note.substring(regex?.value?.length ?: 0)) | |||
val index = MIDI_NOTES.indexOf(substring(regex?.value?.length ?: 0)) | |||
assert(index >= 0) | |||
return (index + 9) + (octave * 12) |
@@ -0,0 +1,26 @@ | |||
package me.jfenn.audio.utils | |||
fun <T, A, R> withCounter(lambda: suspend T.(A, Int) -> R) : suspend T.(A) -> R { | |||
var count = 0 | |||
return { arg -> | |||
lambda(arg, count++) | |||
} | |||
} | |||
fun <T, A, R, V> withCycle(vararg choices: V, lambda: suspend T.(A, V) -> R) : suspend T.(A) -> R { | |||
return withCounter { arg, count -> | |||
lambda(arg, choices[count % choices.size]) | |||
} | |||
} | |||
fun <T, A, R, V> withRandom(vararg choices: V, isUnique: Boolean = false, lambda: suspend T.(A, V) -> R) : suspend T.(A) -> R { | |||
var lastChoice: V? = null | |||
return { arg -> | |||
var choice: V? = null | |||
while (!isUnique || choice?.equals(lastChoice) != false) | |||
choice = listOf(*choices).random() | |||
lastChoice = choice | |||
lambda(arg, choice) | |||
} | |||
} |
@@ -1,6 +1,6 @@ | |||
// Top-level build file where you can add configuration options common to all sub-projects/modules. | |||
buildscript { | |||
ext.kotlin_version = '1.4.0' | |||
ext.kotlin_version = '1.4.21' | |||
ext.coroutines_version = '1.4.2' | |||
ext.serialization_version = '0.20.0' | |||
@@ -1,9 +1,11 @@ | |||
apply plugin: 'kotlin' | |||
apply plugin: 'application' | |||
group 'me.jfenn' | |||
group 'me.jfenn.exampleaudio' | |||
version '0.0.1' | |||
mainClassName = "me.jfenn.exampleaudio.MainKt" | |||
sourceSets { | |||
main.kotlin.srcDirs = main.java.srcDirs = ['src'] | |||
test.kotlin.srcDirs = test.java.srcDirs = ['test'] | |||
@@ -13,6 +15,7 @@ sourceSets { | |||
dependencies { | |||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | |||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" | |||
implementation project(':audio-automation') | |||
} |
@@ -1,6 +0,0 @@ | |||
package me.jfenn | |||
fun main(args: Array<String>) : Unit { | |||
println("Hello world!") | |||
} | |||
@@ -0,0 +1,10 @@ | |||
package me.jfenn.exampleaudio | |||
import java.util.* | |||
fun main(args: Array<String>) : Unit { | |||
when (args.getOrNull(0) ?: "RandomPitchStrings") { | |||
"RandomPitchStrings" -> RandomPitchStrings.main(args) | |||
} | |||
} | |||
@@ -0,0 +1,64 @@ | |||
package me.jfenn.exampleaudio | |||
import kotlinx.coroutines.flow.transform | |||
import kotlinx.coroutines.runBlocking | |||
import me.jfenn.audio.AudioManager | |||
import me.jfenn.audio.constants.* | |||
import me.jfenn.audio.model.Note | |||
import me.jfenn.audio.model.applyNote | |||
import me.jfenn.audio.model.toNote | |||
import me.jfenn.audio.utils.toMidi | |||
import me.jfenn.audio.utils.withRandom | |||
object RandomPitchStrings { | |||
@JvmStatic | |||
fun main(args: Array<String>) = runBlocking<Unit> { | |||
AudioManager(scope = this).start { | |||
val metro = metronome(500) | |||
val pitch = metro.every(8).transform<Int, Int>( | |||
withRandom("2G#", "3B", "3C#", "3D#", "3E", isUnique = true) { _, arg -> | |||
emit(arg.toMidi()) | |||
} | |||
).shared() | |||
// strings | |||
pitch.transform { number -> | |||
emit(number) | |||
emit("4B".toMidi()) | |||
emit("4G#".toMidi()) | |||
emit(number + 12) | |||
}.toNote { | |||
instrument = STRING_ENSEMBLE_2 | |||
duration = 3000 | |||
velocity = 70 | |||
}.done() | |||
// chord | |||
pitch.transform { number -> | |||
emit(Note(HARPSICHORD, number + 12, 250).delay(150)) | |||
emit(Note(HARPSICHORD, number + 24, 250).delay(200)) | |||
emit(Note(HARPSICHORD, number + 36, 250).delay(250)) | |||
}.applyNote { | |||
velocity = 60 | |||
}.done() | |||
// melody | |||
pitch.transform { number -> | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, number, 250).delay(1000)) | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, "4B", 250).delay(2000)) | |||
emit(Note(ACOUSTIC_GUITAR_NYLON, "3G#", 250).delay(3000)) | |||
}.done() | |||
// bass | |||
metro.every(4).transform { | |||
emit(Note(MUSIC_BOX, 28, 1500, 100)) | |||
}.done() | |||
// tempo beat / percussion | |||
metro.every(2, offset = 1).transform { | |||
emit(Note(SFX_GUNSHOT, 44, 100)) | |||
}.done() | |||
} | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
package me.jfenn.exampleaudio | |||
import kotlinx.coroutines.flow.transform | |||
import kotlinx.coroutines.runBlocking | |||
import me.jfenn.audio.AudioManager | |||
import me.jfenn.audio.constants.HARPSICHORD | |||
import me.jfenn.audio.model.Note | |||
object SequenceBuilder { | |||
@JvmStatic | |||
fun main(args: Array<String>) = runBlocking<Unit> { | |||
AudioManager(scope = this).start { | |||
val metro = metronome(1500) | |||
val sequence = listOf("2G#", "3B", "3C#", "3D#", "3E", "3G#", "4B", "4C#", "4D#", "4E", "4G#", "5B") | |||
metro.transform { count -> | |||
for (i in (0..(count % sequence.size))) { | |||
emit(Note(HARPSICHORD, sequence[i], 250).delay(i * 250L)) | |||
} | |||
}.done() | |||
} | |||
} | |||
} |