Change note's duration type from int to Fraction and enable + operator support for note and int
This commit is contained in:
@@ -1,26 +1,38 @@
|
|||||||
package io.smnp.data.entity
|
package io.smnp.data.entity
|
||||||
|
|
||||||
import io.smnp.data.enumeration.Pitch
|
import io.smnp.data.enumeration.Pitch
|
||||||
|
import io.smnp.math.Fraction
|
||||||
|
|
||||||
data class Note(val pitch: Pitch, val octave: Int, val duration: Int, val dot: Boolean) {
|
class Note(val pitch: Pitch, val octave: Int, duration: Fraction, dot: Boolean) {
|
||||||
data class Builder(
|
val duration = if(dot) duration * Fraction(3, 2) else duration
|
||||||
var pitch: Pitch = Pitch.A,
|
|
||||||
var octave: Int = 4,
|
operator fun plus(duration2: Fraction) = Note(pitch, octave, duration + duration2, false)
|
||||||
var duration: Int = 4,
|
|
||||||
var dot: Boolean = false
|
|
||||||
) {
|
|
||||||
fun pitch(pitch: Pitch) = apply { this.pitch = pitch }
|
|
||||||
fun octave(octave: Int) = apply { this.octave = octave }
|
|
||||||
fun duration(duration: Int) = apply { this.duration = duration }
|
|
||||||
fun dot(dot: Boolean) = apply { this.dot = dot }
|
|
||||||
fun build() = Note(pitch, octave, duration, dot)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "${pitch}${octave}:${duration}${if (dot) "d" else ""}"
|
return "${pitch}${octave}:(${duration})"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun intPitch(): Int {
|
fun intPitch(): Int {
|
||||||
return octave * 12 + pitch.ordinal
|
return octave * 12 + pitch.ordinal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Note
|
||||||
|
|
||||||
|
if (pitch != other.pitch) return false
|
||||||
|
if (octave != other.octave) return false
|
||||||
|
if (duration != other.duration) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = pitch.hashCode()
|
||||||
|
result = 31 * result + octave
|
||||||
|
result = 31 * result + duration.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
65
api/src/main/kotlin/io/smnp/math/Fraction.kt
Normal file
65
api/src/main/kotlin/io/smnp/math/Fraction.kt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package io.smnp.math
|
||||||
|
|
||||||
|
class Fraction(val numerator: Int, val denominator: Int) : Comparable<Fraction> {
|
||||||
|
init {
|
||||||
|
if (denominator == 0) {
|
||||||
|
throw RuntimeException("Invalid fraction: denominator can not be of 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun component1() = numerator
|
||||||
|
operator fun component2() = denominator
|
||||||
|
|
||||||
|
val decimal by lazy { numerator.toDouble() / denominator }
|
||||||
|
val inversed by lazy { Fraction(denominator, numerator) }
|
||||||
|
|
||||||
|
val simplified by lazy {
|
||||||
|
val gcd = greatestCommonDenominator(numerator, denominator)
|
||||||
|
Fraction(numerator / gcd, denominator / gcd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun greatestCommonDenominator(a: Int, b: Int): Int = if (b == 0) a else greatestCommonDenominator(b, a % b)
|
||||||
|
|
||||||
|
operator fun unaryMinus() = Fraction(-this.numerator, this.denominator)
|
||||||
|
|
||||||
|
operator fun plus(second: Fraction) =
|
||||||
|
if (this.denominator == second.denominator) Fraction(this.numerator + second.numerator, denominator)
|
||||||
|
else {
|
||||||
|
val numerator = numerator * second.denominator + second.numerator * denominator
|
||||||
|
val denominator = denominator * second.denominator
|
||||||
|
Fraction(numerator, denominator)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minus(second: Fraction) = (this + (-second))
|
||||||
|
|
||||||
|
operator fun times(num: Int) = Fraction(numerator * num, denominator)
|
||||||
|
|
||||||
|
operator fun times(second: Fraction) =
|
||||||
|
Fraction(numerator * second.numerator, denominator * second.denominator)
|
||||||
|
|
||||||
|
operator fun div(num: Int) = this.inversed * num
|
||||||
|
|
||||||
|
operator fun div(second: Fraction) = this * second.inversed
|
||||||
|
|
||||||
|
override fun compareTo(other: Fraction) = decimal.compareTo(other.decimal)
|
||||||
|
|
||||||
|
override fun toString() =
|
||||||
|
this.simplified.let { if (it.denominator == 1) it.numerator.toString() else "${it.numerator}/${it.denominator}" }
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is Fraction) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val (aNum, aDen) = this.simplified
|
||||||
|
val (bNum, bDen) = other.simplified
|
||||||
|
|
||||||
|
return aNum == bNum && aDen == bDen
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun of(int: Int): Fraction {
|
||||||
|
return Fraction(int, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,8 +101,7 @@ data class Value(val type: DataType, val value: Any, val properties: Map<String,
|
|||||||
DataType.NOTE, value, hashMapOf(
|
DataType.NOTE, value, hashMapOf(
|
||||||
Pair("pitch", string(value.pitch.toString())),
|
Pair("pitch", string(value.pitch.toString())),
|
||||||
Pair("octave", int(value.octave)),
|
Pair("octave", int(value.octave)),
|
||||||
Pair("duration", int(value.duration)),
|
Pair("duration", float((value.duration.numerator / value.duration.denominator).toFloat()))
|
||||||
Pair("dot", bool(value.dot))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.smnp.data.entity.Note
|
|||||||
import io.smnp.data.enumeration.Pitch
|
import io.smnp.data.enumeration.Pitch
|
||||||
import io.smnp.dsl.token.model.entity.TokenizerOutput
|
import io.smnp.dsl.token.model.entity.TokenizerOutput
|
||||||
import io.smnp.dsl.token.model.enumeration.TokenType
|
import io.smnp.dsl.token.model.enumeration.TokenType
|
||||||
|
import io.smnp.math.Fraction
|
||||||
|
|
||||||
class NoteTokenizer : Tokenizer {
|
class NoteTokenizer : Tokenizer {
|
||||||
override fun tokenize(input: String, current: Int, line: Int): TokenizerOutput {
|
override fun tokenize(input: String, current: Int, line: Int): TokenizerOutput {
|
||||||
@@ -15,59 +16,55 @@ class NoteTokenizer : Tokenizer {
|
|||||||
var rawValue = ""
|
var rawValue = ""
|
||||||
|
|
||||||
// Note literal start symbol
|
// Note literal start symbol
|
||||||
if(input[current] == '@') {
|
if (input[current] == '@') {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
|
|
||||||
// Note basic pitch
|
// Note basic pitch
|
||||||
if(listOf('c', 'd', 'e', 'f', 'g', 'a', 'h', 'b').contains(input[current+consumedChars].toLowerCase())) {
|
if (listOf('c', 'd', 'e', 'f', 'g', 'a', 'h', 'b').contains(input[current + consumedChars].toLowerCase())) {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
notePitch = input[current+consumedChars].toString()
|
notePitch = input[current + consumedChars].toString()
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
|
|
||||||
// Flat or sharp
|
// Flat or sharp
|
||||||
if(current+consumedChars < input.length && listOf('b', '#').contains(input[current+consumedChars])) {
|
if (current + consumedChars < input.length && listOf('b', '#').contains(input[current + consumedChars])) {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
notePitch += input[current+consumedChars]
|
notePitch += input[current + consumedChars]
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octave
|
// Octave
|
||||||
if(current+consumedChars < input.length && "\\d".toRegex().matches(input[current+consumedChars].toString())) {
|
if (current + consumedChars < input.length && "\\d".toRegex().matches(input[current + consumedChars].toString())) {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
octave = input[current+consumedChars].toString().toInt()
|
octave = input[current + consumedChars].toString().toInt()
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration start symbol
|
// Duration start symbol
|
||||||
if(current+consumedChars < input.length && input[current+consumedChars] == ':') {
|
if (current + consumedChars < input.length && input[current + consumedChars] == ':') {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
duration = ""
|
duration = ""
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
while(current+consumedChars < input.length && "\\d".toRegex().matches(input[current+consumedChars].toString())) {
|
while (current + consumedChars < input.length && "\\d".toRegex().matches(input[current + consumedChars].toString())) {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
duration += input[current+consumedChars]
|
duration += input[current + consumedChars]
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if(duration.isEmpty()) {
|
if (duration.isEmpty()) {
|
||||||
return TokenizerOutput.NONE
|
return TokenizerOutput.NONE
|
||||||
}
|
}
|
||||||
dot = (current+consumedChars < input.length && input[current+consumedChars] == 'd')
|
dot = (current + consumedChars < input.length && input[current + consumedChars] == 'd')
|
||||||
|
|
||||||
if(dot) {
|
if (dot) {
|
||||||
rawValue += input[current+consumedChars]
|
rawValue += input[current + consumedChars]
|
||||||
consumedChars += 1
|
consumedChars += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val note = Note.Builder()
|
val note = Note(Pitch.parse(notePitch), octave ?: 4, Fraction(1, duration?.toInt() ?: 4), dot)
|
||||||
.pitch(Pitch.parse(notePitch))
|
|
||||||
.dot(dot)
|
|
||||||
octave?.let { note.octave(it) }
|
|
||||||
duration?.let { note.duration(it.toInt()) }
|
|
||||||
|
|
||||||
return TokenizerOutput.produce(consumedChars, note.build(), rawValue, TokenType.NOTE, line, current)
|
return TokenizerOutput.produce(consumedChars, note, rawValue, TokenType.NOTE, line, current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.smnp.evaluation.evaluator
|
package io.smnp.evaluation.evaluator
|
||||||
|
|
||||||
|
import io.smnp.data.entity.Note
|
||||||
import io.smnp.dsl.ast.model.node.Node
|
import io.smnp.dsl.ast.model.node.Node
|
||||||
import io.smnp.dsl.ast.model.node.SumOperatorNode
|
import io.smnp.dsl.ast.model.node.SumOperatorNode
|
||||||
import io.smnp.dsl.ast.model.node.TokenNode
|
import io.smnp.dsl.ast.model.node.TokenNode
|
||||||
@@ -11,8 +12,12 @@ import io.smnp.error.PositionException
|
|||||||
import io.smnp.error.ShouldNeverReachThisLineException
|
import io.smnp.error.ShouldNeverReachThisLineException
|
||||||
import io.smnp.evaluation.model.entity.EvaluatorOutput
|
import io.smnp.evaluation.model.entity.EvaluatorOutput
|
||||||
import io.smnp.evaluation.util.NumberUnification.unify
|
import io.smnp.evaluation.util.NumberUnification.unify
|
||||||
import io.smnp.type.enumeration.DataType
|
import io.smnp.math.Fraction
|
||||||
|
import io.smnp.type.enumeration.DataType.*
|
||||||
import io.smnp.type.model.Value
|
import io.smnp.type.model.Value
|
||||||
|
import io.smnp.type.model.Value.Companion.list
|
||||||
|
import io.smnp.type.model.Value.Companion.note
|
||||||
|
import io.smnp.type.model.Value.Companion.string
|
||||||
|
|
||||||
class SumOperatorEvaluator : Evaluator() {
|
class SumOperatorEvaluator : Evaluator() {
|
||||||
override fun supportedNodes() = listOf(SumOperatorNode::class)
|
override fun supportedNodes() = listOf(SumOperatorNode::class)
|
||||||
@@ -34,13 +39,12 @@ class SumOperatorEvaluator : Evaluator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun plus(lhs: Value, plusNode: Node, rhs: Value, environment: Environment): Value {
|
private fun plus(lhs: Value, plusNode: Node, rhs: Value, environment: Environment): Value {
|
||||||
return if (areNumeric(lhs, rhs))
|
return when {
|
||||||
unify(lhs, rhs, int = { (l, r) -> Value.int(l + r) }, float = { (l, r) -> Value.float(l + r) })
|
areNumeric(lhs, rhs) -> unify(lhs, rhs, int = { (l, r) -> Value.int(l + r) }, float = { (l, r) -> Value.float(l + r) })
|
||||||
else if (lhs.type == DataType.STRING)
|
lhs.type == STRING -> string(lhs.value as String + rhs.value.toString())
|
||||||
Value.string(lhs.value as String + rhs.value.toString())
|
areLists(lhs, rhs) -> list(lhs.value as List<Value> + rhs.value as List<Value>)
|
||||||
else if (areLists(lhs, rhs))
|
lhs.type == NOTE && rhs.type == INT -> note(lhs.value as Note + Fraction(1, rhs.value as Int))
|
||||||
Value.list(lhs.value as List<Value> + rhs.value as List<Value>)
|
else -> throw PositionException(
|
||||||
else throw PositionException(
|
|
||||||
EnvironmentException(
|
EnvironmentException(
|
||||||
EvaluationException(
|
EvaluationException(
|
||||||
"The ${lhs.typeName} and ${rhs.typeName} are not supported by + operator"
|
"The ${lhs.typeName} and ${rhs.typeName} are not supported by + operator"
|
||||||
@@ -49,6 +53,7 @@ class SumOperatorEvaluator : Evaluator() {
|
|||||||
), plusNode.position
|
), plusNode.position
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun minus(lhs: Value, minusNode: Node, rhs: Value, environment: Environment): Value {
|
private fun minus(lhs: Value, minusNode: Node, rhs: Value, environment: Environment): Value {
|
||||||
return if (areNumeric(lhs, rhs))
|
return if (areNumeric(lhs, rhs))
|
||||||
@@ -68,5 +73,5 @@ class SumOperatorEvaluator : Evaluator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun areNumeric(lhs: Value, rhs: Value) = lhs.type.isNumeric() && rhs.type.isNumeric()
|
private fun areNumeric(lhs: Value, rhs: Value) = lhs.type.isNumeric() && rhs.type.isNumeric()
|
||||||
private fun areLists(lhs: Value, rhs: Value) = lhs.type == DataType.LIST && rhs.type == DataType.LIST
|
private fun areLists(lhs: Value, rhs: Value) = lhs.type == LIST && rhs.type == LIST
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import io.smnp.callable.function.FunctionDefinitionTool
|
|||||||
import io.smnp.callable.signature.Signature.Companion.simple
|
import io.smnp.callable.signature.Signature.Companion.simple
|
||||||
import io.smnp.data.entity.Note
|
import io.smnp.data.entity.Note
|
||||||
import io.smnp.data.enumeration.Pitch
|
import io.smnp.data.enumeration.Pitch
|
||||||
|
import io.smnp.math.Fraction
|
||||||
import io.smnp.type.enumeration.DataType.*
|
import io.smnp.type.enumeration.DataType.*
|
||||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||||
import io.smnp.type.model.Value
|
import io.smnp.type.model.Value
|
||||||
@@ -18,7 +19,7 @@ class NoteConstructor : Function("Note") {
|
|||||||
ofType(BOOL)
|
ofType(BOOL)
|
||||||
) body { _, (pitchString, octave, duration, dot) ->
|
) body { _, (pitchString, octave, duration, dot) ->
|
||||||
val pitch = Pitch.parse((pitchString.value as String).toLowerCase())
|
val pitch = Pitch.parse((pitchString.value as String).toLowerCase())
|
||||||
val note = Note(pitch, octave.value as Int, duration.value as Int, dot.value as Boolean)
|
val note = Note(pitch, octave.value as Int, Fraction(1, duration.value as Int), dot.value as Boolean)
|
||||||
Value.note(note)
|
Value.note(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import io.smnp.data.entity.Note
|
|||||||
import io.smnp.data.enumeration.Pitch
|
import io.smnp.data.enumeration.Pitch
|
||||||
import io.smnp.error.CustomException
|
import io.smnp.error.CustomException
|
||||||
import io.smnp.ext.midi.Midi
|
import io.smnp.ext.midi.Midi
|
||||||
|
import io.smnp.math.Fraction
|
||||||
import io.smnp.type.enumeration.DataType.*
|
import io.smnp.type.enumeration.DataType.*
|
||||||
import io.smnp.type.matcher.Matcher.Companion.ofType
|
import io.smnp.type.matcher.Matcher.Companion.ofType
|
||||||
import io.smnp.type.matcher.Matcher.Companion.optional
|
import io.smnp.type.matcher.Matcher.Companion.optional
|
||||||
@@ -33,8 +34,8 @@ class MidiHelpFunction : Function("midiHelp") {
|
|||||||
) body { environment, args ->
|
) body { environment, args ->
|
||||||
val instrument = args[0].value as Int
|
val instrument = args[0].value as Int
|
||||||
val bpm = args.getOrNull(1)?.value as Int? ?: 120
|
val bpm = args.getOrNull(1)?.value as Int? ?: 120
|
||||||
val begin = args.getOrNull(2) ?: Value.note(Note(Pitch.C, 1, 4, false))
|
val begin = args.getOrNull(2) ?: Value.note(Note(Pitch.C, 1, Fraction(1, 4), false))
|
||||||
val end = args.getOrNull(3) ?: Value.note(Note(Pitch.H, 9, 4, false))
|
val end = args.getOrNull(3) ?: Value.note(Note(Pitch.H, 9, Fraction(1, 4), false))
|
||||||
val channel = args.getOrNull(4)?.value as Int? ?: 1
|
val channel = args.getOrNull(4)?.value as Int? ?: 1
|
||||||
|
|
||||||
if(channel > 16) {
|
if(channel > 16) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class DefaultSequenceCompiler : SequenceCompiler() {
|
|||||||
track: Track,
|
track: Track,
|
||||||
ppq: Int
|
ppq: Int
|
||||||
): Long {
|
): Long {
|
||||||
val noteDuration = ((if (item.dot) 1.5 else 1.0) * 4L * ppq / item.duration).toLong()
|
val noteDuration = (4L * ppq * item.duration.decimal).toLong()
|
||||||
val noteOffTick = noteOnTick + noteDuration
|
val noteOffTick = noteOnTick + noteDuration
|
||||||
track.add(noteOn(item, channel, noteOnTick))
|
track.add(noteOn(item, channel, noteOnTick))
|
||||||
track.add(noteOff(item, channel, noteOffTick))
|
track.add(noteOff(item, channel, noteOffTick))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PpqSequenceCompiler : SequenceCompiler() {
|
|||||||
track: Track,
|
track: Track,
|
||||||
ppq: Int
|
ppq: Int
|
||||||
): Long {
|
): Long {
|
||||||
val noteOffTick = noteOnTick + item.duration
|
val noteOffTick = noteOnTick + item.duration.denominator
|
||||||
track.add(noteOn(item, channel, noteOnTick))
|
track.add(noteOn(item, channel, noteOnTick))
|
||||||
track.add(noteOff(item, channel, noteOffTick))
|
track.add(noteOff(item, channel, noteOffTick))
|
||||||
return noteOffTick
|
return noteOffTick
|
||||||
|
|||||||
Reference in New Issue
Block a user