Create MapConfigSchema utility function

This commit is contained in:
2020-03-20 17:01:30 +01:00
parent c28ae23774
commit 900d3849f1
11 changed files with 203 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package io.smnp.ext.synth
import io.smnp.type.model.Value
object ConstantEnvelopeFactory : EnvelopeFactory {
override fun createEnvelope(config: Value) = ConstantEnvelope()
}

View File

@@ -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

View File

@@ -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}'")
}
}
}

View File

@@ -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