Source: model/scale.js

const PitchClass = require('./pitch-class');
const Pitch = require('./pitch');
const { mod } = require('../utils');

// The raw value for the pitch class that hasn't had modular math applied to "normalize" it,
// so octave offsets can be calculated properly in Scale.pitch()
function pitchValue(scale, relativePitch) {
  const degree = relativePitch.degree || Number(relativePitch);
  let value = Number(scale.root);
  for (let i =  0; i < degree;  i++) value += scale.intervals[i % scale.length];
  for (let i = -1; i >= degree; i--) value -= scale.intervals[mod(i, scale.length)];
  value += relativePitch.shift || 0;
  return value;
}

/**
 * A list of pitch classes, which can be converted to pitches
 *
 * @see https://en.wikipedia.org/wiki/Scale_(music)
 */
class Scale {

  constructor(intervals, root=new PitchClass(0), { pitchValueOffset=0 }={}) {
    if (!(intervals instanceof Array)) throw new TypeError('Scale intervals must be an Array');
    // list of integers for the interval distance between consecutive notes of the scale:
    // intervals sum is root.pitchesPerOctave (usually 12) for octave-repeating scales
    const pitchesPerOctave = intervals.reduce((a,b) => a+b, 0);
    this.intervals = Object.freeze(intervals.slice());
    this.root = new PitchClass(Number(root), {pitchesPerOctave});
    this.pitchValueOffset = pitchValueOffset;
    Object.freeze(this);
  }

  get length() {
    return this.intervals.length;
  }

  /**
   * The size of the scale in semitones (keys on a piano keyboard).
   * For most scales, this value will be 12, which means the scale repeats every octave.
   * In other words, when this is 12, if the scale starts on a C, it will end on the next higher C.
   */
  get semitones() {
    return this.intervals.reduce((a,b) => a + b, 0);
  }

  /**
   *
   * @param relativePitch {Number|RelativePitch}
   * @returns {PitchClass}
   */
  pitchClass(relativePitch) {
    return new PitchClass(pitchValue(this, relativePitch), { pitchesPerOctave: this.root.pitchesPerOctave });
  }

  /**
   *
   * @param relativePitch {Number|RelativePitch}
   * @param octave {Number}
   * @returns {Pitch}
   */
  pitch(relativePitch, { octave=4 }={}) {
    const value = pitchValue(this, relativePitch);
    const pitchClass = new PitchClass(value, { pitchesPerOctave: this.root.pitchesPerOctave });
    return new Pitch(pitchClass, octave + Math.floor(value / this.root.pitchesPerOctave), {
      pitchValueOffset: this.pitchValueOffset,
    });
  }
}

module.exports = Scale;