Create basic scaffolding for MIDI sequencing module

This commit is contained in:
2020-03-14 12:22:21 +01:00
parent a0a09ecb55
commit c7f251cbce
11 changed files with 210 additions and 13 deletions

View File

View File

@@ -0,0 +1,7 @@
version=0.0.1
pluginVersion=0.1
pluginId=smnp.audio.midi
pluginClass=
pluginProvider=Bartłomiej Pluta
pluginDependencies=

View 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()
}
}

View File

@@ -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()
}
}

View 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()
}
}