diff --git a/api/src/main/kotlin/io/smnp/util/config/ConfigMap.kt b/api/src/main/kotlin/io/smnp/util/config/ConfigMap.kt index 26638b6..8fa6815 100644 --- a/api/src/main/kotlin/io/smnp/util/config/ConfigMap.kt +++ b/api/src/main/kotlin/io/smnp/util/config/ConfigMap.kt @@ -6,10 +6,17 @@ import io.smnp.type.model.Value class ConfigMap(private val map: Map) { private val raw by lazy { map.map { (key, value) -> key.unwrap() to value }.toMap() as Map } + val entries: Set> + get() = raw.entries + operator fun get(key: String): Value { return raw[key] ?: throw ShouldNeverReachThisLineException() } + fun getOrNull(key: String): Value? { + return raw[key] + } + fun getUnwrappedOrDefault(key: String, default: T): T { return raw[key]?.unwrap() as T ?: default } diff --git a/api/src/main/kotlin/io/smnp/util/config/ConfigMapSchema.kt b/api/src/main/kotlin/io/smnp/util/config/ConfigMapSchema.kt index 1d947c0..475a9b8 100644 --- a/api/src/main/kotlin/io/smnp/util/config/ConfigMapSchema.kt +++ b/api/src/main/kotlin/io/smnp/util/config/ConfigMapSchema.kt @@ -20,11 +20,13 @@ class ConfigMapSchema { return this } - fun parse(config: Value): ConfigMap { + fun parse(config: Value, vararg cascade: ConfigMap): ConfigMap { val configMap = config.value as Map - 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") else parameter.default diff --git a/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/model/CompilationParameters.kt b/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/model/CompilationParameters.kt new file mode 100644 index 0000000..c74fc2c --- /dev/null +++ b/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/model/CompilationParameters.kt @@ -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).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() + } +} \ No newline at end of file diff --git a/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/wave/WaveCompiler.kt b/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/wave/WaveCompiler.kt index 407eb2d..d61a168 100644 --- a/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/wave/WaveCompiler.kt +++ b/modules/synth/src/main/kotlin/io/smnp/ext/synth/lib/wave/WaveCompiler.kt @@ -1,10 +1,8 @@ package io.smnp.ext.synth.lib.wave import io.smnp.data.entity.Note -import io.smnp.data.enumeration.Pitch import io.smnp.error.CustomException -import io.smnp.ext.synth.lib.envelope.Envelope -import io.smnp.ext.synth.lib.envelope.EnvelopeFactory +import io.smnp.ext.synth.lib.model.CompilationParameters import io.smnp.math.Fraction import io.smnp.type.enumeration.DataType import io.smnp.type.matcher.Matcher @@ -13,7 +11,6 @@ import io.smnp.util.config.ConfigMapSchema import kotlin.math.pow class WaveCompiler(config: Value, private val samplingRate: Double) { - private val semitone = 2.0.pow(1.0 / 12.0) private val schema = ConfigMapSchema() .optional("bpm", Matcher.ofType(DataType.INT), Value.int(120)) .optional( @@ -39,52 +36,46 @@ class WaveCompiler(config: Value, private val samplingRate: Double) { ) ) - private val bpm: Int - private val envelope: Envelope - private val tuningTable: Map - private val overtones: List - - init { - schema.parse(config).let { configMap -> - envelope = EnvelopeFactory.provideEnvelope(configMap["envelope"]) - overtones = (configMap["overtones"].unwrap() as List).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() - } - } + private val globalConfig = schema.parse(config) fun compileLines(lines: List>): 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): Wave { + var config = globalConfig + var parameters = CompilationParameters(config) + return line.fold(Wave.EMPTY) { acc, value -> acc + when (value.type) { - DataType.NOTE -> compileNote(value.value as Note) - DataType.INT -> compileRest(value.value as Int) - DataType.STRING -> Wave.EMPTY + DataType.NOTE -> compileNote(value.value as Note, parameters) + DataType.INT -> compileRest(value.value as Int, parameters) + DataType.MAP -> { + config = schema.parse(value, config, globalConfig) + parameters = CompilationParameters(config) + Wave.EMPTY + } else -> throw CustomException("Invalid data type: '${value.typeName}") } } } - private fun compileNote(note: Note) = - sound((tuningTable[note.pitch] ?: error("")) * 2.0.pow(note.octave), duration(note.duration)) + private fun compileNote(note: Note, parameters: CompilationParameters) = + 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( frequency: Double, - duration: Double + duration: Double, + parameters: CompilationParameters ): 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 }.toTypedArray()) - return envelope.apply(wave) + return parameters.envelope.apply(wave) } } \ No newline at end of file