From c7f251cbce6f1d309b325876be325e6f08c5fb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Przemys=C5=82aw=20Pluta?= Date: Sat, 14 Mar 2020 12:22:21 +0100 Subject: [PATCH] Create basic scaffolding for MIDI sequencing module --- .../signature/ActualSignatureFormatter.kt | 2 +- .../main/kotlin/io/smnp/data/entity/Note.kt | 31 ++++-- .../main/kotlin/io/smnp/ext/ModuleProvider.kt | 2 + .../kotlin/io/smnp/type/matcher/Matcher.kt | 4 + .../io/smnp/environment/DefaultEnvironment.kt | 1 + modules/midi/build.gradle | 0 modules/midi/gradle.properties | 7 ++ .../src/main/kotlin/io/smnp/ext/MidiModule.kt | 15 +++ .../io/smnp/ext/function/MidiFunction.kt | 57 ++++++++++ .../kotlin/io/smnp/ext/midi/MidiSequencer.kt | 101 ++++++++++++++++++ settings.gradle | 3 +- 11 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 modules/midi/build.gradle create mode 100644 modules/midi/gradle.properties create mode 100644 modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt create mode 100644 modules/midi/src/main/kotlin/io/smnp/ext/function/MidiFunction.kt create mode 100644 modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt diff --git a/api/src/main/kotlin/io/smnp/callable/signature/ActualSignatureFormatter.kt b/api/src/main/kotlin/io/smnp/callable/signature/ActualSignatureFormatter.kt index 37daad6..67c1366 100644 --- a/api/src/main/kotlin/io/smnp/callable/signature/ActualSignatureFormatter.kt +++ b/api/src/main/kotlin/io/smnp/callable/signature/ActualSignatureFormatter.kt @@ -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()}>" } } \ No newline at end of file diff --git a/api/src/main/kotlin/io/smnp/data/entity/Note.kt b/api/src/main/kotlin/io/smnp/data/entity/Note.kt index 21d5d6d..0b94e52 100644 --- a/api/src/main/kotlin/io/smnp/data/entity/Note.kt +++ b/api/src/main/kotlin/io/smnp/data/entity/Note.kt @@ -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 + } } \ No newline at end of file diff --git a/api/src/main/kotlin/io/smnp/ext/ModuleProvider.kt b/api/src/main/kotlin/io/smnp/ext/ModuleProvider.kt index 5d043db..24b76bf 100644 --- a/api/src/main/kotlin/io/smnp/ext/ModuleProvider.kt +++ b/api/src/main/kotlin/io/smnp/ext/ModuleProvider.kt @@ -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 = emptyList() abstract fun provideModule(interpreter: Interpreter): Module } \ No newline at end of file diff --git a/api/src/main/kotlin/io/smnp/type/matcher/Matcher.kt b/api/src/main/kotlin/io/smnp/type/matcher/Matcher.kt index b312928..d8647e5 100644 --- a/api/src/main/kotlin/io/smnp/type/matcher/Matcher.kt +++ b/api/src/main/kotlin/io/smnp/type/matcher/Matcher.kt @@ -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, valueMatchers: List): Matcher { return Matcher( DataType.MAP, diff --git a/app/src/main/kotlin/io/smnp/environment/DefaultEnvironment.kt b/app/src/main/kotlin/io/smnp/environment/DefaultEnvironment.kt index b09011c..d7f0804 100644 --- a/app/src/main/kotlin/io/smnp/environment/DefaultEnvironment.kt +++ b/app/src/main/kotlin/io/smnp/environment/DefaultEnvironment.kt @@ -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) } diff --git a/modules/midi/build.gradle b/modules/midi/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/modules/midi/gradle.properties b/modules/midi/gradle.properties new file mode 100644 index 0000000..cd466aa --- /dev/null +++ b/modules/midi/gradle.properties @@ -0,0 +1,7 @@ +version=0.0.1 + +pluginVersion=0.1 +pluginId=smnp.audio.midi +pluginClass= +pluginProvider=Bartłomiej Pluta +pluginDependencies= \ No newline at end of file diff --git a/modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt b/modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt new file mode 100644 index 0000000..346bb5c --- /dev/null +++ b/modules/midi/src/main/kotlin/io/smnp/ext/MidiModule.kt @@ -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() + } +} \ No newline at end of file diff --git a/modules/midi/src/main/kotlin/io/smnp/ext/function/MidiFunction.kt b/modules/midi/src/main/kotlin/io/smnp/ext/function/MidiFunction.kt new file mode 100644 index 0000000..fa7e2a9 --- /dev/null +++ b/modules/midi/src/main/kotlin/io/smnp/ext/function/MidiFunction.kt @@ -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).map { it.value!! as List } + 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).map { (key, value) -> + key.value!! as Int to ((value.value!! as List).map { it.value!! as List }) + }.toMap() + + val parameters = configParametersMap(config.value!!) + MidiSequencer.playChannels(channels, parameters) + + Value.void() + } + } + + private fun configParametersMap(config: Any): Map { + return (config as Map) + .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() + } +} \ No newline at end of file diff --git a/modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt b/modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt new file mode 100644 index 0000000..44b3224 --- /dev/null +++ b/modules/midi/src/main/kotlin/io/smnp/ext/midi/MidiSequencer.kt @@ -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>>, config: Map) { + 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>, config: Map) { + 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, 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() + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index bb2a0f9..777b7e8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,5 @@ include 'modules:debug' include 'modules:system' include 'modules:io' include 'modules:collection' -include 'modules:text' \ No newline at end of file +include 'modules:text' +include 'modules:midi' \ No newline at end of file