Create basic scaffolding for MIDI sequencing module
This commit is contained in:
@@ -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()}>"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user