4
\$\begingroup\$

I've recently started building an API that allows a consumer to create and manipulate musical entities such as notes, intervals, scales and chords.

The first step is to create a a foundation of base structures that are later used by the musical entities. One such structure is a Ring.

A ring in modular arithmic

A Ring is an integer's representation clamped between 0 and the ring size. Its Value is the integer it represents. Its Size is the group size. Its Class is the congruent value clamped in the specified range. Its Group determines the number of ranges it's away from the reference group 0.

A ring from the perspective of Music Theory

The way I'm using a ring is to define a musical note. A note consists of a pitch and a degree. The pitch is a number that represents the frequency of a note. A ring with size 12 is used to represent all semi-tones within a single octave. Each octave is a ring group. Another ring with size 7 is used to define the degree of the note. Common representations of degrees are {C, D, E, F, G, A, B} or do-re-mi-fa-sol-la-ti. The note C on the 5th octave is considered the reference note with pitch 0 and degree 0.

I want to be able to change a ring's properties. The example below shows how the pitch of a note could be represented by a ring.

val pitch = Ring(0, 12)   // the pitch of note 'C5' 
pitch.Group = 1           // the pitch one octave higher 'C6'
pitch.Class = 2           // the pitch class changed to that of note 'D6'
println(pitch)            // Ring(Value=14, Size=12)

The example below shows how the degree of a note could be represented by a ring.

val degree = Ring(0, 7)   // the degree of note 'C5' 
degree.Group = 1          // the degree one octave higher 'C6'
degree.Class = 1          // the degree class changed to that of note 'D6'
println(degree)           // Ring(Value=8, Size=7)

This means the note D6 can be represented by pitch Ring(Value=14, Size=12) and degree Ring(Value=8, Size=7).

I have opted to use a mutable class and data class. I want a mutable class because I require a lot of manipulations to be chained. And I desire a data class because of its nature of hiding boiler-plate code and providing interesting methods such as copy.

val ring = someOtherRing.copy().setClass(2).setGroup(1)

I did read that data classes should be immutable.

So my questions are:

  • Is it allowed to use the data class as mutable class?

  • Does it make sense that I combine mutable and data class for the Ring class?

  • Are there better alternatives to create an API class?

Any additional feedback on Kotlin guidelines, general guidelines, fluent-ish methods and varia is welcome as well.

Ring code:

import kotlin.math.*

data class Ring(var Value: Int, val Size: Int) {

    var Class: Int
        get() = modulo(Value)
        set(value) {
            Value = Group * Size + modulo(value)
        }

    var Group: Int
        get() = Value / Size
        set(value) {
            Value = value * Size + Class
        }

    fun setValue(value: Int): Ring {
        Value = value
        return this
    }

    fun setClass(clazz: Int): Ring {
        Class = clazz
        return this
    }

    fun setGroup(group: Int): Ring {
        Group = group
        return this
    }

    private fun modulo(x: Int): Int {
        return (x % Size + Size) % Size
    }
}
\$\endgroup\$
  • 2
    \$\begingroup\$ When I first read the yellow paragraph, I did not understand any of the words. Is a "ring" a common concept in music? If so, you should link to the Wikipedia article. If it is your own invention, describe it in simple words, not mathematically abstract terms. If you had said "a ring is the pitch of a single tone, split up into the octave and the half-tone in it, such as c5 or a'3", it would have been much clearer. \$\endgroup\$ – Roland Illig Jul 8 at 5:56
  • \$\begingroup\$ @RolandIllig I get what you mean. A ring is a mathematical concept, used in modular arithmic. I require this class as a base structure to create notes and other musical structures. Perhaps I should describe it from the perspective of music rather than mathematics. I will edit the post and make that point more clear. \$\endgroup\$ – dfhwze Jul 8 at 5:59
3
\$\begingroup\$

Your concepts look completely misguided.

A ring is a mathematical concept, an algebraic structure. It does have a size, but it doesn't have a value.

The value would be part of a ring element. Such a ring element is a tuple (ring, value).

The pitch of a note is indeed a ring element. A ring element can only be used to store the pitch of a note, but not its octave. If it did, it would not match the mathematical concept of a ring anymore.

To represent a musical note in Western notation, my first idea is:

data class Note(
    val octave: Int,
    val name: NoteName,
    val mod: NoteModifier,
    val duration: NoteDuration
)

enum class NoteName {
    C, D, E, F, G, A, B
}

enum class NoteModifier {
    Natural, Sharp, Flat, TwoSharp, TwoFlat
}

enum class NoteDuration {
    Full, Half, Quarter, Eighth, Sixteenth
}

The above definitions are very rough and limited. To get a grasp of the actual complexity of typesetting music, have a look at LilyPond, it should have a definition of a note somewhere in the code.

If you just want to replay the music, there's no need to distinguish between c# and d\$\flat\$, which would make the above definitions much simpler.

In Kotlin you don't need setters since the copy function is more powerful than in Java. You can just say:

val note = other.copy(octave = 3, pitch = 5)

This is easier to grasp and less code to write. If you write a setter method in Kotlin, lean back and think twice. Probably you are doing something unusual.

By the way, property names in Kotlin are written in lowercase. Kotlin is based on Java, not C#.

\$\endgroup\$
  • \$\begingroup\$ We concurrently answered and edited the question. Is my edit an invalidation of your answer? \$\endgroup\$ – dfhwze Jul 8 at 6:27
  • \$\begingroup\$ No, it isn't. Everything's fine. :) \$\endgroup\$ – Roland Illig Jul 8 at 6:28
  • \$\begingroup\$ I do think you are right about the concept of a ring, it does not include the group. What I have created is more of a coil (no reference found in math), where we care about which level we are. I do care about enharmonics though, I will post a next question soon with a full implemenation of Note that will use 2 Coil instances. \$\endgroup\$ – dfhwze Jul 8 at 6:34
2
\$\begingroup\$

I have decided against (ab-)using a mathematical construct Ring for creating musical entities. As Roland pointed out, it just didn't fit the purpose. Instead, I have created my own structure Coil which represents a Value with its Index in a group Group of magnitude Size. I'm still using setters, but only for derived properties.

Coil class:

import kotlin.math.*

data class Coil(var value: Int, val size: Int) {

    var index: Int
        get() = modulo(value)
        set(n) { 
            value = group * size + modulo(n) 
        }

    var group: Int
        get() = value / size
        set(n) { 
            value = n * size + index 
        }

    val delta: Int
        get() {
            val d1 = index
            val d2 = size - d1
            return if (d1 <= d2) d1 else -d2
        }

    private fun modulo(n: Int): Int {
        return (n % size + size) % size
    } 
}

To give you an idea how I am going to use a Coil I have created a simplified version of a Note. The way I want to uses a note is to manipulate them and their role within chords and scales. I do not intend to create sheet music, so I don't require a position or duration. A note uses one coil to store its pitch and one to store its degree. Both these values are absolutes, meaning the octave is included in the value. Their setters re-sync the octave. Setting the pitchClass or degreeClass does not change the octave. A note's name consists of its pitchClass in scientific pitch notation, its accidentals (flat, sharp, natural) and octave.

Note Usage: (to give an idea about the interaction between Coil and Note)

fun main() {

    val note = Note(0, 0, 5)  // C5
    note.degreeClass = 1      // Dbb5
    note.pitchClass = 1       // Db5
    note.octave = 6           // Db6
    note.accidentals = 1      // D#6

    println(note.name)        
}

Note class:

import kotlin.math.*

class Note(var _pitch: Int, var _degree: Int, var _octave: Int) {

    private val PITCH_COUNT = 12
    private val DEGREE_COUNT = 7
    private val FLAT = 'b'
    private val SHARP = '#'

    private val DIATONIC_PITCH_CLASS_SET 
        : IntArray = intArrayOf(0, 2, 4, 5, 7, 9, 11)

    private val SCIENTIFIC_PITCH_CLASS_SET 
        : CharArray = charArrayOf('C', 'D', 'E', 'F', 'G', 'A', 'B')

    private val p = Coil(_pitch, PITCH_COUNT)
    private val d = Coil(_degree, DEGREE_COUNT)

    init {
        p.group = _octave
        d.group = _octave
    }

    var pitch: Int
        get() = p.value
        set(n) { 
            p.value = n 
            octave = p.group
        }

    var degree: Int
        get() = d.value
        set(n) { 
            d.value = n 
            octave = d.group
        }

    var octave: Int
        get() = p.group
        set(n) { 
            p.group = n
            d.group = n
        }

    var pitchClass: Int
        get() = p.index
        set(n) { 
            p.index = n
        }

    var degreeClass: Int
        get() = d.index
        set(n) { 
            d.index = n
        }

    var accidentals: Int
        get() {
            val delta = pitchClass - DIATONIC_PITCH_CLASS_SET[degreeClass]
            return Coil(delta, PITCH_COUNT).delta
        }
        set(n) { 
            pitchClass = DIATONIC_PITCH_CLASS_SET[degreeClass] + n
        }

    val name: String
        get() {
          val sb = StringBuilder()
          val d = accidentals
          sb.append(SCIENTIFIC_PITCH_CLASS_SET[degreeClass])
          if (d != 0) {
            sb.append(Character.toString((if (d > 0) SHARP else FLAT)).repeat(abs(d)))
          }
          sb.append(octave)
          return sb.toString()
        } 
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.