Create basic scaffolding for MIDI sequencing module
This commit is contained in:
0
modules/midi/build.gradle
Normal file
0
modules/midi/build.gradle
Normal file
7
modules/midi/gradle.properties
Normal file
7
modules/midi/gradle.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
version=0.0.1
|
||||
|
||||
pluginVersion=0.1
|
||||
pluginId=smnp.audio.midi
|
||||
pluginClass=
|
||||
pluginProvider=Bartłomiej Pluta
|
||||
pluginDependencies=
|
||||
15
modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt
Normal file
15
modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package io.smnp.ext
|
||||
|
||||
import io.smnp.environment.Environment
|
||||
import io.smnp.ext.function.MidiFunction
|
||||
import io.smnp.ext.midi.MidiSequencer
|
||||
import org.pf4j.Extension
|
||||
|
||||
@Extension
|
||||
class MidiModule : NativeModuleProvider("smnp.audio.midi") {
|
||||
override fun functions() = listOf(MidiFunction())
|
||||
|
||||
override fun onModuleLoad(environment: Environment) {
|
||||
MidiSequencer.init()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.smnp.ext.function
|
||||
|
||||
import io.smnp.callable.function.Function
|
||||
import io.smnp.callable.function.FunctionDefinitionTool
|
||||
import io.smnp.callable.signature.Signature.Companion.simple
|
||||
import io.smnp.callable.signature.Signature.Companion.vararg
|
||||
import io.smnp.error.EvaluationException
|
||||
import io.smnp.ext.midi.MidiSequencer
|
||||
import io.smnp.type.enumeration.DataType.*
|
||||
import io.smnp.type.matcher.Matcher.Companion.allTypes
|
||||
import io.smnp.type.matcher.Matcher.Companion.listOf
|
||||
import io.smnp.type.matcher.Matcher.Companion.listOfMatchers
|
||||
import io.smnp.type.matcher.Matcher.Companion.mapOfMatchers
|
||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
|
||||
class MidiFunction : Function("midi") {
|
||||
override fun define(new: FunctionDefinitionTool) {
|
||||
new function vararg(
|
||||
listOf(NOTE, INT, STRING),
|
||||
mapOfMatchers(ofType(INT), allTypes())
|
||||
) body { _, (config, lines) ->
|
||||
|
||||
val lines = (lines.value!! as List<Value>).map { it.value!! as List<Value> }
|
||||
val parameters = configParametersMap(config.value!!)
|
||||
MidiSequencer.playLines(lines, parameters)
|
||||
Value.void()
|
||||
}
|
||||
|
||||
new function simple(
|
||||
mapOfMatchers(allTypes(), allTypes()),
|
||||
mapOfMatchers(ofType(INT), listOfMatchers(listOf(NOTE, INT, STRING)))
|
||||
) body { _, (config, channels) ->
|
||||
val channels = (channels.value!! as Map<Value, Value>).map { (key, value) ->
|
||||
key.value!! as Int to ((value.value!! as List<Value>).map { it.value!! as List<Value> })
|
||||
}.toMap()
|
||||
|
||||
val parameters = configParametersMap(config.value!!)
|
||||
MidiSequencer.playChannels(channels, parameters)
|
||||
|
||||
Value.void()
|
||||
}
|
||||
}
|
||||
|
||||
private fun configParametersMap(config: Any): Map<String, Any> {
|
||||
return (config as Map<Value, Value>)
|
||||
.map { (key, value) -> key.value!! as String to value }
|
||||
.map { (key, value) ->
|
||||
key to when (key) {
|
||||
"bpm" -> if (value.type == INT) value.value!! else throw EvaluationException("Invalid parameter type: 'bpm' is supposed to be of int type")
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
101
modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt
Normal file
101
modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
package io.smnp.ext.midi
|
||||
|
||||
import io.smnp.data.entity.Note
|
||||
import io.smnp.error.EvaluationException
|
||||
import io.smnp.error.ShouldNeverReachThisLineException
|
||||
import io.smnp.type.enumeration.DataType.*
|
||||
import io.smnp.type.model.Value
|
||||
import javax.sound.midi.*
|
||||
|
||||
object MidiSequencer {
|
||||
private const val PPQ = 1000
|
||||
private val sequencer = MidiSystem.getSequencer()
|
||||
|
||||
fun playChannels(channels: Map<Int, List<List<Value>>>, config: Map<String, Any>) {
|
||||
val sequence = Sequence(Sequence.PPQ, PPQ)
|
||||
|
||||
channels.forEach { (channel, lines) ->
|
||||
lines.forEach { line -> playLine(line, channel, sequence) }
|
||||
}
|
||||
|
||||
sequencer.sequence = sequence
|
||||
sequencer.tempoInBPM = (config.getOrDefault("bpm", 120) as Int).toFloat()
|
||||
|
||||
|
||||
sequencer.start()
|
||||
while(sequencer.isRunning) Thread.sleep(20)
|
||||
sequencer.stop()
|
||||
}
|
||||
|
||||
fun playLines(lines: List<List<Value>>, config: Map<String, Any>) {
|
||||
val sequence = Sequence(Sequence.PPQ, PPQ)
|
||||
|
||||
lines.forEachIndexed { channel, line -> playLine(line, channel, sequence) }
|
||||
|
||||
sequencer.sequence = sequence
|
||||
sequencer.tempoInBPM = (config.getOrDefault("bpm", 120) as Int).toFloat()
|
||||
|
||||
sequencer.start()
|
||||
while(sequencer.isRunning) Thread.sleep(20)
|
||||
sequencer.stop()
|
||||
}
|
||||
|
||||
private fun playLine(line: List<Value>, channel: Int, sequence: Sequence) {
|
||||
val track = sequence.createTrack()
|
||||
|
||||
line.fold(0L) { noteOnTick, item ->
|
||||
when (item.type) {
|
||||
NOTE -> {
|
||||
note(item, channel, noteOnTick, track)
|
||||
}
|
||||
INT -> noteOnTick + 4L * PPQ / (item.value!! as Int)
|
||||
STRING -> command(item, channel, noteOnTick, track)
|
||||
else -> throw ShouldNeverReachThisLineException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun command(item: Value, channel: Int, beginTick: Long, track: Track): Long {
|
||||
val instruction = item.value!! as String
|
||||
if(instruction.isBlank()) {
|
||||
throw EvaluationException("Empty strings are not allowed here")
|
||||
}
|
||||
val commandWithArguments = instruction.split(":")
|
||||
val (command, args) = if(commandWithArguments.size == 2) commandWithArguments else listOf(commandWithArguments[0], "0,0")
|
||||
val arguments = args.split(",")
|
||||
val cmdCode = when(command) {
|
||||
"i" -> 192 + channel
|
||||
"pitch" -> 0xE0
|
||||
else -> throw EvaluationException("Unknown command '$command'")
|
||||
}
|
||||
track.add(event(cmdCode, channel, arguments.getOrNull(0)?.toInt() ?: 0, arguments.getOrNull(1)?.toInt() ?: 0, beginTick))
|
||||
return beginTick
|
||||
}
|
||||
|
||||
private fun note(item: Value, channel: Int, noteOnTick: Long, track: Track): Long {
|
||||
val note = item.value!! as Note
|
||||
val noteDuration = ((if (note.dot) 1.5 else 1.0) * 4L * PPQ / note.duration).toLong()
|
||||
val noteOffTick = noteOnTick + noteDuration
|
||||
track.add(noteOn(note, channel, noteOnTick))
|
||||
track.add(noteOff(note, channel, noteOffTick))
|
||||
return noteOffTick
|
||||
}
|
||||
|
||||
private fun noteOn(note: Note, channel: Int, tick: Long): MidiEvent {
|
||||
return event(144, channel, note.intPitch() + 12, 127, tick)
|
||||
}
|
||||
|
||||
private fun noteOff(note: Note, channel: Int, tick: Long): MidiEvent {
|
||||
return event(128, channel, note.intPitch() + 12, 127, tick)
|
||||
}
|
||||
|
||||
private fun event(command: Int, channel: Int, data1: Int, data2: Int, tick: Long): MidiEvent {
|
||||
val message = ShortMessage()
|
||||
message.setMessage(command, channel, data1, data2)
|
||||
return MidiEvent(message, tick)
|
||||
}
|
||||
|
||||
fun init() {
|
||||
sequencer.open()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user