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

@@ -54,6 +54,6 @@ object ActualSignatureFormatter {
}
}
return "map<${output.keys.toSet().joinToString()}><${output.values.toSet().joinToString()}}>"
return "map<${output.keys.toSet().joinToString()}><${output.values.toSet().joinToString()}>"
}
}

View File

@@ -2,16 +2,25 @@ package io.smnp.data.entity
import io.smnp.data.enumeration.Pitch
data class Note (val pitch: Pitch, val octave: Int, val duration: Int, val dot: Boolean) {
data class Builder(var pitch: Pitch = Pitch.A, var octave: Int = 4, var duration: Int = 4, var dot: Boolean = false) {
fun pitch(pitch: Pitch) = apply { this.pitch = pitch }
fun octave(octave: Int) = apply { this.octave = octave }
fun duration(duration: Int) = apply { this.duration = duration }
fun dot(dot: Boolean) = apply { this.dot = dot }
fun build() = Note(pitch, octave, duration, dot)
}
data class Note(val pitch: Pitch, val octave: Int, val duration: Int, val dot: Boolean) {
data class Builder(
var pitch: Pitch = Pitch.A,
var octave: Int = 4,
var duration: Int = 4,
var dot: Boolean = false
) {
fun pitch(pitch: Pitch) = apply { this.pitch = pitch }
fun octave(octave: Int) = apply { this.octave = octave }
fun duration(duration: Int) = apply { this.duration = duration }
fun dot(dot: Boolean) = apply { this.dot = dot }
fun build() = Note(pitch, octave, duration, dot)
}
override fun toString(): String {
return "${pitch}${octave}:${duration}${if (dot) "d" else ""}"
}
override fun toString(): String {
return "${pitch}${octave}:${duration}${if (dot) "d" else ""}"
}
fun intPitch(): Int {
return octave * 12 + pitch.ordinal
}
}

View File

@@ -1,10 +1,12 @@
package io.smnp.ext
import io.smnp.environment.Environment
import io.smnp.interpreter.Interpreter
import io.smnp.type.module.Module
import org.pf4j.ExtensionPoint
abstract class ModuleProvider(val path: String) : ExtensionPoint {
open fun onModuleLoad(environment: Environment) {}
open fun dependencies(): List<String> = emptyList()
abstract fun provideModule(interpreter: Interpreter): Module
}

View File

@@ -40,6 +40,10 @@ class Matcher(val type: DataType?, private val matcher: (Value) -> Boolean, priv
)
}
fun mapOfMatchers(keyMatcher: Matcher, valueMatcher: Matcher): Matcher {
return mapOfMatchers(listOf(keyMatcher), listOf(valueMatcher))
}
fun mapOfMatchers(keyMatchers: List<Matcher>, valueMatchers: List<Matcher>): Matcher {
return Matcher(
DataType.MAP,

View File

@@ -30,6 +30,7 @@ class DefaultEnvironment : Environment {
private fun loadModule(moduleProvider: ModuleProvider, consumer: (ModuleProvider) -> Unit = {}) {
if (!loadedModules.contains(moduleProvider.path)) {
rootModule.addSubmodule(moduleProvider.provideModule(LanguageModuleInterpreter()))
moduleProvider.onModuleLoad(this)
loadedModules.add(moduleProvider.path)
consumer(moduleProvider)
}

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

View File

@@ -8,4 +8,5 @@ include 'modules:debug'
include 'modules:system'
include 'modules:io'
include 'modules:collection'
include 'modules:text'
include 'modules:text'
include 'modules:midi'