Add support for ConfigMap-based commands to smnp.audio.synth module
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user