6
\$\begingroup\$

As a follow-up on isosceles' question, I have created a small API to generate the notes of a scale given the pitch class set and key note. I've added the reinventhing-the-wheel tag because I know there exist far superior frameworks that deal with notes, chords, scales, musical notations.

Description

A pitch class set describes a collection of pitch classes in steps relative to the reference note, which has pitch set class 0 and corresponds to the specified key note. For instance, the major scale is identified by pitch class set [0, 2, 4, 5, 7, 9, 11].

Usage

The main purpose of the API is to generate scale notes as follows:

 console.log(getScale('C', [0, 2, 4, 5, 7, 9, 11]).map(i => i.NameClass));

printing the following to the console:

["C" ,"D" ,"E" ,"F" ,"G" ,"A" ,"B"]

Notes can be manipulated:

  let scale = {};
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.enharmonic().NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.ascendingEnharmonic().NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.descendingEnharmonic().NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.transpose(-2).NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.transpose(new Interval(1, 0)).NameClass);
  scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.invert().NameClass);

Questions

  • Have I written idiomatic javascript or what can I improve?
  • Is my API useful and reusable for more complex operations involving notes and scales?
  • Are my methods self-describing or are comments required?

Code

(function() {
  'use strict';

  function getScale(key, pitchClassSet) {
      const keyNote = Note.FromName(key);
      return pitchClassSet.reduce(function (scale, scaleStep, i) {
          const note = keyNote.clone().transpose(new Interval(i, scaleStep));
          scale.push(note);
          return scale;
      }, []);
  }

  class Interval {
      constructor(di, pi) {
          this.di = di;
          this.pi = pi;
      }
      get DegreeInterval() {
          return this.di;
      }
      get PitchInterval() {
          return this.pi;
      }
  }

  class Note {
      static FromName(name) {
          const note = new Note(0, 0);
          note.Name = name;
          return note;
      }
      static FromDiatonicPitchClass(degree) {
          const note = new Note(degree, 0);
          note.PitchClass = Note.DiatonicPitchClassSet[note.DegreeClass];
          return note;
      }
      static get DiatonicPitchClassSet() {
          return (new function() {
              const pcs = [0,2,4,5,7,9,11];
              return function () { 
                    return pcs; 
              }
          })();
      }
      static get ScientificPitchClassSet() {
          return (new function() {
              const pcs = ['C','D','E','F','G','A','B'];
              return function () { 
                  return pcs; 
              }
          })();
      }
      constructor(degree, pitch) {
          this.d = Coil.FromDegree(degree);
          this.p = Coil.FromPitch(pitch);
      }
      get DegreeClass() {
          return this.d.Class;
      }
      set DegreeClass(n) {
          this.d.Class = n;
      }
      get PitchClass() {
          return this.p.Class;
      }
      set PitchClass(n) {
          this.p.Class = n;
      }
      get Pitch() {
          return this.p.Value;
      }
      set Pitch(n) {
          this.p.Value = n;
          this.d.Group = this.p.Group;
      }
      get Octave() {
          return this.p.Group;
      }
      set Octave(n) {
          this.p.Group = n;
          this.d.Group = n;
      }
      get Accidentals() {
          const diatonic = Note.FromDiatonicPitchClass(this.DegreeClass);
          const coil = Coil.FromPitch(this.PitchClass - diatonic.PitchClass);
          return coil.Delta;
      }
      set Accidentals(n) {
          const diatonic = Note.FromDiatonicPitchClass(this.DegreeClass);
          const coil = Coil.FromPitch(diatonic.PitchClass + n);
          this.PitchClass = coil.Class;
      }
      get Name() {
          const octaveName = 5 + this.Octave;
          return this.NameClass + octaveName;
      }
      get NameClass() {
          const degreeName = Note.ScientificPitchClassSet[this.DegreeClass];
          const accidentals = this.Accidentals;
          const accidentalsToken = accidentals === 0 ? ''
                : (accidentals < 0 ? 'b' : '#' ).repeat(Math.abs(this.Accidentals));
          return degreeName + accidentalsToken;
      }
      set Name(name) {
          const regexp = /(?<degree>[a-gA-G])(?<accidentals>[b#]*)(?<octave>[-]?\d*)/
          const result = regexp.exec(name);
          const degreeToken = result.groups.degree.toUpperCase();
          const accidentalsToken = result.groups.accidentals;
          const octaveToken = result.groups.octave;
          const degree = Note.ScientificPitchClassSet.indexOf(degreeToken);
          const octave = octaveToken.length > 0 ? parseInt(octaveToken) : 5;
          const accidentals = accidentalsToken.split('')
            .map(c => c == 'b' ? -1 : c == '#' ? 1 : 0)
            .reduce((a, b) => a + b, 0);
          this.DegreeClass = degree;
          this.Octave = octave - 5;
          this.Accidentals = accidentals;
      }
      copy(other) {
          this.d = other.d.clone();
          this.p = other.p.clone();
          return this;
      }
      clone() {
          return new Note(0, 0).copy(this);
      }
      equals(other) {
          if (typeof other === "undefined") { return false; }
          return other.d.Equals(this.d) 
              && other.p.Equals(this.p);
      }
      isEnharmonicEquivalent(other) {
          return other.Pitch === this.Pitch;
      }
      isInversionalEquivalent(other) {
          return other.p.clone().invert().Class === this.p.Class;
      }
      isOctaveEquivalent(other) {
          return other.DegreeClass === this.DegreeClass
              && other.PitchClass === this.PitchClass;
      }
      normalize() {
          this.Octave = 0;
          return this;
      }
      enharmonic(preference, force) {
          if (typeof force === "undefined") {
              force = false;
          }
          if (typeof preference === "undefined") {
              preference = 0;
          }
          let degree = Note.DiatonicPitchClassSet.indexOf(this.PitchClass);
          if (degree === -1) {
              degree = Note.DiatonicPitchClassSet.indexOf(this.PitchClass - 1);
              const degreeAlt = Coil.FromDegree(degree + 1).Class;
              if (force || (this.DegreeClass !== degree && this.DegreeClass !== degreeAlt)) {
                  this.DegreeClass = (preference < 0) ? degreeAlt : degree;
              }
          }
          else {
              this.DegreeClass = degree;
          }
          return this;
      }
      ascendingEnharmonic() {
          return this.enharmonic(1, true);
      }
      descendingEnharmonic() {
          return this.enharmonic(-1, true);
      }
      transpose(interval) {
          let di = 0;
          let pi = 0;
          if (interval instanceof Note) {
              di = interval.d.Value;
              pi = interval.p.Value;
          }
          else if (interval instanceof Interval) {
              di = interval.DegreeInterval;
              pi = interval.PitchInterval;
          } 
          else {
              pi = interval;
          }
          this.d.translate(di);
          this.p.translate(pi);
          return this;
      }
      invert() {
          this.d.invert();
          this.p.invert();
          return this;
      }
      reflect(pivot, pivot2) {
          if (typeof pivot2 === "undefined") {
              pivot2 = pivot;
          }
          this.d.reflect(pivot.d.Value, pivot2.d.Value);
          this.p.reflect(pivot.p.Value, pivot2.p.Value);
          return this;
      }
      negate() {
          this.d.negate();
          this.p.negate();
          return this;
      }
      setDegreeClass(n) {
          this.DegreeClass = n;
          return this;
      }
      setPitchClass(n) {
          this.PitchClass = n;
          return this;
      }
      setPitch(n) {
          this.Pitch = n;
          return this;
      }
      setOctave(n) {
          this.Octave = n;
          return this;
      }
      setAccidentals(n) {
          this.Accidentals = n;
          return this;
      }
      setName(n) {
          this.Name = n;
          return this;
      }
  }

  class Coil {
      static FromDegree(n) {
          return new Coil(n, 7);
      }
      static FromPitch(n) {
          return new Coil(n, 12);
      }
      constructor(value, size) {
          this.Value = value;
          this.Size = size;
      }
      get Value() {
          return this.value;
      }
      set Value(n) {
          this.value = n;
      }
      get Size() {
          return this.size;
      }
      set Size(n) {
          this.size = Math.max(1, Math.abs(n));
      }
      get Class() {
          return this.modulo(this.Value);
      }
      set Class(n) {
          this.Value = this.Group * this.Size + this.modulo(n);
      }
      get Group() {
          return Math.floor((this.Value) / this.Size);
      }
      set Group(n) {
          this.Value = n * this.Size + this.Class;
      }
      get Delta() {
          const d1 = this.Class;
          const d2 = this.clone().invert().Class;
          if (d1 > d2) {
              return d2 === 0 ? d2 : -d2;
          }
          return d1;
      }
      get Distance() {
          return Math.abs(this.Delta);
      }
      copy(other) {
          this.Value = other.Value;
          this.Size = other.Size;
          return this;
      }
      clone() {
          return new Coil(0, 0).copy(this);
      }
      equals(other) {
          if (typeof other === "undefined") { return false; }
          return other.Value === this.Value 
              && other.Size == this.Size;
      }
      normalize() {
          this.Value = this.Class;
          return this;
      }
      normalizeUnordered() {
          this.Value = this.Distance;
          return this;
      }
      translate(n) {
          let d = 0;
          if (n instanceof Coil) {
              d = n.Value;
          } else {
              d = n;
          }
          this.Value += d;
          return this;
      }
      invert() {
          this.Class = this.Size - this.Class;
          return this;
      }
      reflect(pivot, pivot2) {
          if (typeof pivot2 === "undefined") {
              pivot2 = pivot;
          }
          this.Value = pivot + pivot2 - this.Value;
          return this;
      }
      negate() {
          this.Value *= -1;
          return this;
      }
      add(other) {
          return this.translate(other.Value);
      }
      addScalar(n) {
          return this.translate(n);
      }
      subtract(other) {
          return this.translate(-other.Value);
      }
      subtractScalar(n) {
          return this.translate(-n);
      }
      modulo(n) {
          return ((n % this.Size) + this.Size) % this.Size;
      }
  }

})();
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Those are some very thorough class definitions, with many getters and setters. For context: I have some music experience (e.g. 22 years playing clarinet and saxophone, 18 years playing acoustic guitar) but wouldn't consider myself an expert in theory. I've gotten used to transposing from C to Eflat and Bflat in my head. I haven't really learned about coils in the context of pitches.

Good stuff:

  • Separation of code into classes with many getters and setters
  • using const for anything that doesn't get re-assigned
  • many methods allow chaining by returning this

I read over the answer to the linked question by Flambino and it looks like you have improved indentation, added spacing and made many other improvements. However, I don't see a return at the end of the IIFE, so nothing is really exposed and thus any code to utilize your code would need to be added inside the IIFE. Perhaps that is fine for you.

Have you considered using a linter? It would help clean up a lot of things. I tried running your code through the linter on jslint.com - it advised things like using double quotes on string literals and "use strict". Apparently it had an issue with the regex with named groups...

I would expect a linter (maybe I am thinking of eslint) would mention that some variables are only used once - e.g. in Note::Name() (the setter) many variables are only used once after assignment - e.g. degree, octave, etc. Also in Coil::translate() there is little point in declaring d. The whole method could be simplified to add either n.Value or n to this.Value without assigning either value to d.

To answer your question

Are my methods self-describing or are comments required?

I would say they are mostly self-describing but it would be wise to include documentation - at least for the parameters. For instance, the interval constructor takes two arguments: di, pi. I can tell by the other method names that one is for the Degree interval and the other is for the Pitch interval but if I wasn't looking at that it would be difficult to know.


In getScale() there is a call to pitchClasSet.reduce() that always pushes a Note clone into the array and returns the array. Use Array.map() instead of Array.reduce() when simply pushing elements into an array and returning the array.

 return pitchClassSet.reduce(function (scale, scaleStep, i) {
      const note = keyNote.clone().transpose(new Interval(i, scaleStep));
      scale.push(note);
      return scale;
  }, []);

can be simplified to:

return pitchClassSet.map((scaleStep, i) => keyNote.clone().transpose(new Interval(i, scaleStep)))

The ES-6 standard adds default parameter values - so places like:

enharmonic(preference, force) {
    if (typeof force === "undefined") {
        force = false;
    }
    if (typeof preference === "undefined") {
        preference = 0;
    }

Can be simplified to:

enharmonic(preference = 0, force = false) {
\$\endgroup\$
2
  • \$\begingroup\$ These are some good code cleanup suggestions. My ultimate goal is to make software that could recognize chords. I need a solid base with coils and notes to start making chords, scales and what not :) \$\endgroup\$
    – dfhwze
    Aug 31, 2019 at 5:58
  • \$\begingroup\$ About exposing the API, I'm still contemplating what would be the best pattern... using a master object or just exposing each of the classes ;. \$\endgroup\$
    – dfhwze
    Aug 31, 2019 at 5:59

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.