Source: model/pitch.js

const PitchClass = require('./pitch-class');

/**
 * A Pitch is an immutable representation of the frequency of a musical note.
 * It consists of a {@link PitchClass} and an octave number.
 * It has an underlying value for MIDI pitch, which supports the range 0 to 127 (inclusive).
 *
 * @example
 * // The following are equivalent:
 * new Pitch(60);
 * new Pitch(new PitchClass('C'), 4);
 * new Pitch(new PitchClass('C')); // default octave is 4
 * new Pitch('C4');
 * new Pitch('C', 4);
 * new Pitch('C'); // default octave is 4
 */
class Pitch {

  /**
   * The maximum pitch value supported by MIDI: 127. Note the minimum value is 0.
   */
  static get MAX_VALUE() {
    return 127;
  }

  /**
   * @param {number|PitchClass|string} value - The numeric pitch value, or a PitchClass, or a string of the pitch class name and optional octave. See examples below.
   * @param {number} [octave=4] - The octave to use when the first argument is a PitchClass or a string without the octave. The octave should be in the range -1 to 9 (inclusive) to avoid invalid pitch values.
   */
  constructor(value, octave=4, { pitchesPerOctave=12, pitchValueOffset=0 }={}) {
    let pitchClass;
    if (typeof value === 'number') {
      pitchClass = new PitchClass(value, { pitchesPerOctave });
      octave = Math.floor(value / pitchesPerOctave) - 1;
      value += pitchValueOffset;
    }
    else {
      if (value instanceof PitchClass) {
        pitchClass = value;
      }
      else {
        const string = value.toString();
        const matches = /([A-Ga-g][b#]*)([-_]1|[0-9])$/.exec(string);
        if (matches) {
          pitchClass = new PitchClass(matches[1]);
          octave = Number(matches[2].replace('_','-'));
        }
        else {
          pitchClass = new PitchClass(string);
        }
      }
      value = pitchClass.value + pitchClass.pitchesPerOctave * (octave + 1) + pitchValueOffset;
    }
    /**
     * @member {PitchClass}
     * @readonly */
    this.pitchClass = pitchClass;
    /**
     * @member {number}
     * @readonly */
    this.octave = octave;
    /**
     * The MIDI pitch value. Should be in the range 0 to 127 (inclusive).
     * @member {number}
     * @readonly */
    this.value = value;
    /**
     * The [canonical name]{@link module:Names.PITCHES} for this Pitch.
     * @member {string}
     * @readonly */
    this.name = pitchClass.pitchesPerOctave === 12 ?
      `${pitchClass.name}${octave.toString().replace('-', '_')}` : String(value);
    Object.freeze(this);
  }

  valueOf() {
    return this.value;
  }

  inspect() {
    return this.name;
  }

  /**
   * Return a new Pitch whose value is the sum of this Pitch's value and the given value
   * @param value {number} the value to add to this Pitch's value
   * @returns {Pitch}
   */
  add(value) {
    if (!value) return this;
    return new Pitch(this.value + value, null, {
      pitchesPerOctave: this.pitchClass.pitchesPerOctave,
      pitchValueOffset: this.pitchValueOffset,
    });
  }
}

module.exports = Pitch;