Add support for ConfigMap-based commands to smnp.audio.synth module

This commit is contained in:
2020-04-06 12:48:03 +02:00
parent 4086b04b59
commit e7e76e9165
4 changed files with 52 additions and 33 deletions

View File

@@ -6,10 +6,17 @@ import io.smnp.type.model.Value
class ConfigMap(private val map: Map<Value, Value>) { class ConfigMap(private val map: Map<Value, Value>) {
private val raw by lazy { map.map { (key, value) -> key.unwrap() to value }.toMap() as Map<String, Value> } private val raw by lazy { map.map { (key, value) -> key.unwrap() to value }.toMap() as Map<String, Value> }
val entries: Set<Map.Entry<String, Value>>
get() = raw.entries
operator fun get(key: String): Value { operator fun get(key: String): Value {
return raw[key] ?: throw ShouldNeverReachThisLineException() return raw[key] ?: throw ShouldNeverReachThisLineException()
} }
fun getOrNull(key: String): Value? {
return raw[key]
}
fun <T> getUnwrappedOrDefault(key: String, default: T): T { fun <T> getUnwrappedOrDefault(key: String, default: T): T {
return raw[key]?.unwrap() as T ?: default return raw[key]?.unwrap() as T ?: default
} }

View File

@@ -20,11 +20,13 @@ class ConfigMapSchema {
return this return this
} }
fun parse(config: Value): ConfigMap { fun parse(config: Value, vararg cascade: ConfigMap): ConfigMap {
val configMap = config.value as Map<Value, Value> val configMap = config.value as Map<Value, Value>
return ConfigMap(parameters.mapNotNull { (name, parameter) -> return ConfigMap(parameters.mapNotNull { (name, parameter) ->
val value = configMap[Value.string(name)] val key = Value.string(name)
val value = configMap[key]
?: cascade.flatMap { it.entries } .firstOrNull { it.key == name } ?.value
?: if (parameter.required) throw CustomException("The '$name' parameter of ${parameter.matcher} is required") ?: if (parameter.required) throw CustomException("The '$name' parameter of ${parameter.matcher} is required")
else parameter.default else parameter.default

View File

@@ -0,0 +1,19 @@
package io.smnp.ext.synth.lib.model
import io.smnp.data.enumeration.Pitch
import io.smnp.ext.synth.lib.envelope.EnvelopeFactory
import io.smnp.util.config.ConfigMap
import kotlin.math.pow
private val SEMITONE = 2.0.pow(1.0 / 12.0)
class CompilationParameters(config: ConfigMap) {
val envelope by lazy { EnvelopeFactory.provideEnvelope(config["envelope"]) }
val overtones by lazy { (config["overtones"].unwrap() as List<Float>).map { it.toDouble() } }
val bpm by lazy { config["bpm"].value as Int }
val tuningTable by lazy {
Pitch.values()
.mapIndexed { index, pitch -> pitch to (config["tuning"].value as Float).toDouble() / SEMITONE.pow(57 - index) }
.toMap()
}
}

View File

@@ -1,10 +1,8 @@
package io.smnp.ext.synth.lib.wave package io.smnp.ext.synth.lib.wave
import io.smnp.data.entity.Note import io.smnp.data.entity.Note
import io.smnp.data.enumeration.Pitch
import io.smnp.error.CustomException import io.smnp.error.CustomException
import io.smnp.ext.synth.lib.envelope.Envelope import io.smnp.ext.synth.lib.model.CompilationParameters
import io.smnp.ext.synth.lib.envelope.EnvelopeFactory
import io.smnp.math.Fraction import io.smnp.math.Fraction
import io.smnp.type.enumeration.DataType import io.smnp.type.enumeration.DataType
import io.smnp.type.matcher.Matcher import io.smnp.type.matcher.Matcher
@@ -13,7 +11,6 @@ import io.smnp.util.config.ConfigMapSchema
import kotlin.math.pow import kotlin.math.pow
class WaveCompiler(config: Value, private val samplingRate: Double) { class WaveCompiler(config: Value, private val samplingRate: Double) {
private val semitone = 2.0.pow(1.0 / 12.0)
private val schema = ConfigMapSchema() private val schema = ConfigMapSchema()
.optional("bpm", Matcher.ofType(DataType.INT), Value.int(120)) .optional("bpm", Matcher.ofType(DataType.INT), Value.int(120))
.optional( .optional(
@@ -39,52 +36,46 @@ class WaveCompiler(config: Value, private val samplingRate: Double) {
) )
) )
private val bpm: Int private val globalConfig = schema.parse(config)
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 { fun compileLines(lines: List<List<Value>>): Wave {
return Wave.merge(*lines.map { compileLine(it) }.toTypedArray()) return Wave.merge(*lines.parallelStream().map { compileLine(it) }.toArray { Array(it) { Wave.EMPTY } })
} }
private fun compileLine(line: List<Value>): Wave { private fun compileLine(line: List<Value>): Wave {
var config = globalConfig
var parameters = CompilationParameters(config)
return line.fold(Wave.EMPTY) { acc, value -> return line.fold(Wave.EMPTY) { acc, value ->
acc + when (value.type) { acc + when (value.type) {
DataType.NOTE -> compileNote(value.value as Note) DataType.NOTE -> compileNote(value.value as Note, parameters)
DataType.INT -> compileRest(value.value as Int) DataType.INT -> compileRest(value.value as Int, parameters)
DataType.STRING -> Wave.EMPTY DataType.MAP -> {
config = schema.parse(value, config, globalConfig)
parameters = CompilationParameters(config)
Wave.EMPTY
}
else -> throw CustomException("Invalid data type: '${value.typeName}") else -> throw CustomException("Invalid data type: '${value.typeName}")
} }
} }
} }
private fun compileNote(note: Note) = private fun compileNote(note: Note, parameters: CompilationParameters) =
sound((tuningTable[note.pitch] ?: error("")) * 2.0.pow(note.octave), duration(note.duration)) sound((parameters.tuningTable[note.pitch] ?: error("")) * 2.0.pow(note.octave), duration(note.duration, parameters), parameters)
private fun duration(duration: Fraction) = 60.0 * 4.0 * duration.decimal / bpm private fun duration(duration: Fraction, parameters: CompilationParameters) = 60.0 * 4.0 * duration.decimal / parameters.bpm
private fun compileRest(rest: Int) = Wave.zeros(duration(Fraction(1, rest)), samplingRate) private fun compileRest(rest: Int, parameters: CompilationParameters) = Wave.zeros(duration(Fraction(1, rest), parameters), samplingRate)
private fun sound( private fun sound(
frequency: Double, frequency: Double,
duration: Double duration: Double,
parameters: CompilationParameters
): Wave { ): Wave {
val wave = Wave.merge(*overtones.mapIndexed { overtone, ratio -> val wave = Wave.merge(*parameters.overtones.mapIndexed { overtone, ratio ->
Wave.sine(frequency * (overtone + 1), duration, samplingRate) * ratio Wave.sine(frequency * (overtone + 1), duration, samplingRate) * ratio
}.toTypedArray()) }.toTypedArray())
return envelope.apply(wave) return parameters.envelope.apply(wave)
} }
} }