Create MapConfigSchema utility function
This commit is contained in:
24
api/src/main/kotlin/io/smnp/util/config/MapConfig.kt
Normal file
24
api/src/main/kotlin/io/smnp/util/config/MapConfig.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package io.smnp.util.config
|
||||
|
||||
import io.smnp.error.ShouldNeverReachThisLineException
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
class MapConfig(private val map: Map<Value, Value>) {
|
||||
private val raw by lazy { map.map { (key, value) -> key.unwrap() to value }.toMap() as Map<String, Value> }
|
||||
|
||||
operator fun get(key: String): Value {
|
||||
return raw[key] ?: throw ShouldNeverReachThisLineException()
|
||||
}
|
||||
|
||||
fun <T> getUnwrappedOrDefault(key: String, default: T): T {
|
||||
return raw[key]?.unwrap() as T ?: default
|
||||
}
|
||||
|
||||
fun containsKey(key: String): Boolean {
|
||||
return raw.containsKey(key)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY = MapConfig(emptyMap())
|
||||
}
|
||||
}
|
||||
39
api/src/main/kotlin/io/smnp/util/config/MapConfigSchema.kt
Normal file
39
api/src/main/kotlin/io/smnp/util/config/MapConfigSchema.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
package io.smnp.util.config
|
||||
|
||||
import io.smnp.error.CustomException
|
||||
import io.smnp.type.enumeration.DataType
|
||||
import io.smnp.type.matcher.Matcher
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
class MapConfigSchema {
|
||||
private data class Parameter(val matcher: Matcher, val required: Boolean, val default: Value)
|
||||
|
||||
private val parameters = mutableMapOf<String, Parameter>()
|
||||
|
||||
fun required(name: String, matcher: Matcher): MapConfigSchema {
|
||||
parameters[name] = Parameter(matcher, true, Value.void())
|
||||
return this
|
||||
}
|
||||
|
||||
fun optional(name: String, matcher: Matcher, default: Value = Value.void()): MapConfigSchema {
|
||||
parameters[name] = Parameter(matcher, false, default)
|
||||
return this
|
||||
}
|
||||
|
||||
fun parse(config: Value): MapConfig {
|
||||
val configMap = config.value as Map<Value, Value>
|
||||
|
||||
return MapConfig(parameters.mapNotNull { (name, parameter) ->
|
||||
val value = configMap[Value.string(name)]
|
||||
?: if (parameter.required) throw CustomException("The '$name' parameter of ${parameter.matcher} is required")
|
||||
else parameter.default
|
||||
|
||||
if (!parameter.matcher.match(value) && value.type != DataType.VOID) {
|
||||
throw CustomException("Invalid parameter type: '$name' is supposed to be of ${parameter.matcher} type")
|
||||
}
|
||||
|
||||
if (value.type == DataType.VOID) null
|
||||
else Value.string(name) to value
|
||||
}.toMap())
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,17 @@ 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
|
||||
import io.smnp.util.config.MapConfig
|
||||
import io.smnp.util.config.MapConfigSchema
|
||||
|
||||
|
||||
class MidiFunction : Function("midi") {
|
||||
private val schema = MapConfigSchema()
|
||||
.optional("bpm", ofType(INT), Value.int(120))
|
||||
.optional("ppq", ofType(INT))
|
||||
.optional("output", ofType(STRING))
|
||||
.optional("play", ofType(BOOL))
|
||||
|
||||
override fun define(new: FunctionDefinitionTool) {
|
||||
new function vararg(
|
||||
listOf(NOTE, INT, STRING),
|
||||
@@ -27,7 +35,7 @@ class MidiFunction : Function("midi") {
|
||||
throw CustomException("MIDI standard supports max to 16 channels and that number has been exceeded")
|
||||
}
|
||||
|
||||
Midi.with(unwrapConfig(config)).play(unwrappedLines)
|
||||
Midi.with(schema.parse(config)).play(unwrappedLines)
|
||||
|
||||
Value.void()
|
||||
}
|
||||
@@ -39,7 +47,7 @@ class MidiFunction : Function("midi") {
|
||||
throw CustomException("MIDI standard supports max to 16 channels and that number has been exceeded")
|
||||
}
|
||||
|
||||
Midi.with(emptyMap()).play(unwrappedLines)
|
||||
Midi.with(MapConfig.EMPTY).play(unwrappedLines)
|
||||
|
||||
Value.void()
|
||||
}
|
||||
@@ -54,7 +62,7 @@ class MidiFunction : Function("midi") {
|
||||
throw CustomException("MIDI standard supports max to 16 channels and that number has been exceeded")
|
||||
}
|
||||
|
||||
Midi.with(unwrapConfig(config)).play(unwrappedChannels)
|
||||
Midi.with(schema.parse(config)).play(unwrappedChannels)
|
||||
|
||||
Value.void()
|
||||
}
|
||||
@@ -66,7 +74,7 @@ class MidiFunction : Function("midi") {
|
||||
throw CustomException("MIDI standard supports max to 16 channels and that number has been exceeded")
|
||||
}
|
||||
|
||||
Midi.with(emptyMap()).play(unwrappedChannels)
|
||||
Midi.with(MapConfig.EMPTY).play(unwrappedChannels)
|
||||
|
||||
Value.void()
|
||||
}
|
||||
@@ -76,21 +84,4 @@ class MidiFunction : Function("midi") {
|
||||
Value.void()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapConfig(config: Value): Map<String, Any> {
|
||||
return (config.unwrap() as Map<String, Any>)
|
||||
.map { (key, value) ->
|
||||
key to when (key) {
|
||||
"bpm" -> value as? Int
|
||||
?: throw CustomException("Invalid parameter type: 'bpm' is supposed to be of int type")
|
||||
"ppq" -> value as? Int
|
||||
?: throw CustomException("Invalid parameter type: 'ppq' is supposed to be of int type")
|
||||
"output" -> value as? String
|
||||
?: throw CustomException("Invalid parameter type: 'output' is supposed to be of string type")
|
||||
"play" -> value as? Boolean
|
||||
?: throw CustomException("Invalid parameter type: 'play' is supposed to be of bool type")
|
||||
else -> value
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import io.smnp.type.enumeration.DataType.*
|
||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||
import io.smnp.type.matcher.Matcher.Companion.optional
|
||||
import io.smnp.type.model.Value
|
||||
import io.smnp.util.config.MapConfig
|
||||
|
||||
class MidiHelpFunction : Function("midiHelp") {
|
||||
override fun define(new: FunctionDefinitionTool) {
|
||||
@@ -58,7 +59,7 @@ class MidiHelpFunction : Function("midiHelp") {
|
||||
if (index > 0) {
|
||||
println(it)
|
||||
Midi
|
||||
.with(mapOf("bpm" to bpm))
|
||||
.with(MapConfig(mapOf(Value.string("bpm") to Value.int(bpm))))
|
||||
.play(mapOf(channel to listOf(listOf("i:$instrument", it))))
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.smnp.ext.midi
|
||||
|
||||
import io.smnp.util.config.MapConfig
|
||||
import java.io.File
|
||||
import javax.sound.midi.MidiSystem
|
||||
import javax.sound.midi.Sequence
|
||||
@@ -7,7 +8,6 @@ import javax.sound.midi.Sequencer
|
||||
|
||||
object Midi {
|
||||
private const val DEFAULT_PPQ = 1000
|
||||
private const val DEFAULT_BPM = 120
|
||||
private const val MIDI_FILE_TYPE = 1
|
||||
private val sequencer = MidiSystem.getSequencer()
|
||||
private val synthesizer = MidiSystem.getSynthesizer()
|
||||
@@ -28,38 +28,38 @@ object Midi {
|
||||
sequencer.stop()
|
||||
}
|
||||
|
||||
fun with(config: Map<String, Any>): SequenceExecutor {
|
||||
fun with(config: MapConfig): SequenceExecutor {
|
||||
return SequenceExecutor(sequencer, config)
|
||||
}
|
||||
|
||||
class SequenceExecutor(private val sequencer: Sequencer, private val config: Map<String, Any>) {
|
||||
class SequenceExecutor(private val sequencer: Sequencer, private val config: MapConfig) {
|
||||
fun play(lines: List<List<Any>>) {
|
||||
val sequence = Sequence(Sequence.PPQ, (config.getOrDefault("ppq", DEFAULT_PPQ) as Int))
|
||||
val sequence = Sequence(Sequence.PPQ, config.getUnwrappedOrDefault("ppq", DEFAULT_PPQ))
|
||||
provideCompiler(config).compileLines(lines, sequence)
|
||||
play(sequence)
|
||||
writeToFile(sequence)
|
||||
}
|
||||
|
||||
private fun provideCompiler(config: Map<String, Any>): SequenceCompiler =
|
||||
private fun provideCompiler(config: MapConfig): SequenceCompiler =
|
||||
if (config.containsKey("ppq")) PpqSequenceCompiler()
|
||||
else DefaultSequenceCompiler()
|
||||
|
||||
private fun play(sequence: Sequence) {
|
||||
(config.getOrDefault("play", true) as Boolean).takeIf { it }?.let {
|
||||
config.getUnwrappedOrDefault("play", true).takeIf { it }?.let {
|
||||
playSequence(sequence) {
|
||||
Midi.sequencer.tempoInBPM = (config.getOrDefault("bpm", DEFAULT_BPM) as Int).toFloat()
|
||||
sequencer.tempoInBPM = (config["bpm"].value as Int).toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeToFile(sequence: Sequence) {
|
||||
config.getOrDefault("output", null)?.let {
|
||||
MidiSystem.write(sequence, MIDI_FILE_TYPE, File(it as String))
|
||||
config.getUnwrappedOrDefault<String?>("output", null)?.let {
|
||||
MidiSystem.write(sequence, MIDI_FILE_TYPE, File(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun play(channels: Map<Int, List<List<Any>>>) {
|
||||
val sequence = Sequence(Sequence.PPQ, (config.getOrDefault("ppq", DEFAULT_PPQ) as Int))
|
||||
val sequence = Sequence(Sequence.PPQ, config.getUnwrappedOrDefault("ppq", DEFAULT_PPQ))
|
||||
provideCompiler(config).compileChannels(channels, sequence)
|
||||
play(sequence)
|
||||
writeToFile(sequence)
|
||||
|
||||
@@ -3,23 +3,24 @@ package io.smnp.ext.function
|
||||
import io.smnp.callable.function.Function
|
||||
import io.smnp.callable.function.FunctionDefinitionTool
|
||||
import io.smnp.callable.signature.Signature
|
||||
import io.smnp.ext.synth.AdsrEnvelope
|
||||
import io.smnp.ext.synth.Synthesizer
|
||||
import io.smnp.ext.synth.WaveCompiler
|
||||
import io.smnp.type.enumeration.DataType
|
||||
import io.smnp.type.matcher.Matcher
|
||||
import io.smnp.type.enumeration.DataType.*
|
||||
import io.smnp.type.matcher.Matcher.Companion.anyType
|
||||
import io.smnp.type.matcher.Matcher.Companion.listOf
|
||||
import io.smnp.type.matcher.Matcher.Companion.mapOfMatchers
|
||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
class WaveFunction : Function("wave") {
|
||||
|
||||
|
||||
override fun define(new: FunctionDefinitionTool) {
|
||||
new function Signature.vararg(Matcher.listOf(DataType.NOTE, DataType.INT, DataType.STRING)) body { _, (vararg) ->
|
||||
val compiler = WaveCompiler(
|
||||
listOf(1.0, 0.5),
|
||||
AdsrEnvelope(0.1, 0.3, 0.7, 0.8),
|
||||
120,
|
||||
440.0,
|
||||
Synthesizer.SAMPLING_RATE
|
||||
)
|
||||
new function Signature.vararg(
|
||||
listOf(NOTE, INT, STRING),
|
||||
mapOfMatchers(ofType(STRING), anyType())
|
||||
) body { _, (config, vararg) ->
|
||||
val compiler = WaveCompiler(config, Synthesizer.SAMPLING_RATE)
|
||||
|
||||
val wave = compiler.compileLines(vararg.unwrapCollections() as List<List<Value>>)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.smnp.ext.synth
|
||||
|
||||
import io.smnp.type.enumeration.DataType.FLOAT
|
||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||
import io.smnp.type.model.Value
|
||||
import io.smnp.util.config.MapConfigSchema
|
||||
|
||||
object AdsrEnvelopeFactory : EnvelopeFactory {
|
||||
private val schema = MapConfigSchema()
|
||||
.required("p1", ofType(FLOAT))
|
||||
.required("p2", ofType(FLOAT))
|
||||
.required("p3", ofType(FLOAT))
|
||||
.required("s", ofType(FLOAT))
|
||||
|
||||
override fun createEnvelope(config: Value) = config.let { schema.parse(it) }.let {
|
||||
AdsrEnvelope(
|
||||
(it["p1"].unwrap() as Float).toDouble(),
|
||||
(it["p2"].unwrap() as Float).toDouble(),
|
||||
(it["p3"].unwrap() as Float).toDouble(),
|
||||
(it["s"].unwrap() as Float).toDouble()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.smnp.ext.synth
|
||||
|
||||
import io.smnp.type.model.Value
|
||||
|
||||
object ConstantEnvelopeFactory : EnvelopeFactory {
|
||||
override fun createEnvelope(config: Value) = ConstantEnvelope()
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package io.smnp.ext.synth
|
||||
import org.knowm.xchart.QuickChart
|
||||
import org.knowm.xchart.SwingWrapper
|
||||
|
||||
abstract class Envelope() {
|
||||
abstract class Envelope {
|
||||
abstract fun eval(x: Double, length: Int): Double
|
||||
protected abstract fun name(): String
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.smnp.ext.synth
|
||||
|
||||
import io.smnp.error.CustomException
|
||||
import io.smnp.type.enumeration.DataType
|
||||
import io.smnp.type.matcher.Matcher
|
||||
import io.smnp.type.model.Value
|
||||
import io.smnp.util.config.MapConfigSchema
|
||||
|
||||
interface EnvelopeFactory {
|
||||
fun createEnvelope(config: Value): Envelope
|
||||
|
||||
companion object {
|
||||
private val schema = MapConfigSchema()
|
||||
.required("name", Matcher.ofType(DataType.STRING))
|
||||
|
||||
private val factories = mapOf(
|
||||
"adsr" to AdsrEnvelopeFactory,
|
||||
"const" to ConstantEnvelopeFactory
|
||||
)
|
||||
|
||||
fun provideEnvelope(envelopeConfig: Value) = schema.parse(envelopeConfig)["name"].let {
|
||||
factories[it.value]?.createEnvelope(envelopeConfig) ?: throw CustomException("Unknown envelope '${it.value}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,61 @@ import io.smnp.data.enumeration.Pitch
|
||||
import io.smnp.error.CustomException
|
||||
import io.smnp.math.Fraction
|
||||
import io.smnp.type.enumeration.DataType
|
||||
import io.smnp.type.matcher.Matcher
|
||||
import io.smnp.type.model.Value
|
||||
import io.smnp.util.config.MapConfigSchema
|
||||
import kotlin.math.pow
|
||||
|
||||
class WaveCompiler(
|
||||
private val overtones: List<Double>,
|
||||
private val envelope: Envelope,
|
||||
private val bpm: Int,
|
||||
tuning: Double,
|
||||
private val samplingRate: Double
|
||||
) {
|
||||
private val semitone = 2.0.pow(1.0/12.0)
|
||||
private val tuningTable = Pitch.values().mapIndexed { index, pitch -> pitch to tuning/semitone.pow(57-index) }.toMap()
|
||||
class WaveCompiler(config: Value, private val samplingRate: Double) {
|
||||
private val semitone = 2.0.pow(1.0 / 12.0)
|
||||
private val schema = MapConfigSchema()
|
||||
.optional("bpm", Matcher.ofType(DataType.INT), Value.int(120))
|
||||
.optional(
|
||||
"overtones", Matcher.listOf(DataType.FLOAT), Value.list(
|
||||
listOf(
|
||||
Value.float(0.1F),
|
||||
Value.float(0.3F),
|
||||
Value.float(0.7F),
|
||||
Value.float(0.8F)
|
||||
)
|
||||
)
|
||||
)
|
||||
.optional("tuning", Matcher.ofType(DataType.FLOAT), Value.float(440.0F))
|
||||
.optional(
|
||||
"envelope", Matcher.mapOfMatchers(Matcher.ofType(DataType.STRING), Matcher.anyType()), Value.wrap(
|
||||
mapOf(
|
||||
"name" to "adsr",
|
||||
"p1" to 0.1F,
|
||||
"p2" to 0.3F,
|
||||
"p3" to 0.8F,
|
||||
"s" to 0.8F
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val bpm: Int
|
||||
private val envelope: Envelope
|
||||
private val tuningTable: Map<Pitch, Double>
|
||||
private val overtones: List<Double>
|
||||
|
||||
init {
|
||||
schema.parse(config).let { configMap ->
|
||||
envelope = EnvelopeFactory.provideEnvelope(configMap["envelope"])
|
||||
overtones = (configMap["overtones"].unwrap() as List<Float>).map { it.toDouble() }
|
||||
bpm = configMap["bpm"].value as Int
|
||||
tuningTable = Pitch.values()
|
||||
.mapIndexed { index, pitch -> pitch to (configMap["tuning"].value as Float).toDouble() / semitone.pow(57 - index) }
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun compileLines(lines: List<List<Value>>): Wave {
|
||||
return Wave.merge(*lines.map { compileLine(it) }.toTypedArray())
|
||||
}
|
||||
|
||||
fun compileLine(line: List<Value>): Wave {
|
||||
private fun compileLine(line: List<Value>): Wave {
|
||||
return line.fold(Wave.EMPTY) { acc, value ->
|
||||
acc + when(value.type) {
|
||||
acc + when (value.type) {
|
||||
DataType.NOTE -> compileNote(value.value as Note)
|
||||
DataType.INT -> compileRest(value.value as Int)
|
||||
DataType.STRING -> Wave.EMPTY
|
||||
@@ -33,7 +68,8 @@ class WaveCompiler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileNote(note: Note) = sound((tuningTable[note.pitch] ?: error("")) * 2.0.pow(note.octave), duration(note.duration))
|
||||
private fun compileNote(note: Note) =
|
||||
sound((tuningTable[note.pitch] ?: error("")) * 2.0.pow(note.octave), duration(note.duration))
|
||||
|
||||
private fun duration(duration: Fraction) = 60.0 * 4.0 * duration.decimal / bpm
|
||||
|
||||
|
||||
Reference in New Issue
Block a user