core.js

/**
 * This module provides functions to map scale degrees to chromatic numbers
 * suitable for MIDI use.  A few basic scales are provided as well (modes of
 * the major scale, as well as the harmonic minor scale).  You may add other
 * scales you need to the *scaleChromatics* map at runtime.
 * @module core
 * @examples test/data/examples/core-examples.js
 */

import * as utils from './utils.js'

// all indices are 0-based unless otherwise noted
// scale degrees are 1-based
// scales are zero-based, 0-11

/**
 * Determines location of middle C (default is middle C = 60)
 * @static
 */
const origin = {
  octave: 4,
  shift: 12
}

/**
 * Map of scale name -> chromatic degrees of scale
 * @static
 */
const scaleChromatics = { Chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }

/**
 * Number of degrees in western chromatic scale
 * @static
 */
const numChromatics = scaleChromatics.Chromatic.length

/**
 * Add a named scale to the set of available scales
 * @param {string} name name of scale
 * @param {Array.<(string|number)>} degrees list of ascending scale degrees
 *                   (see {@link module:core.chromatic})
 * @param {string} relativeScale scale to which *degrees* are relative
 * @static
 */
function addScale (name, degrees, relativeScale = 'Ionian') {
  const chromaticDegrees = chromatics(degrees, {
    scale: relativeScale,
    octave: 0,
    shift: 0
  })
  scaleChromatics[name] = chromaticDegrees
}

/**
 * Sets octave which will yield middle C = 60
 * @param {int} octave the octave for middle C
 * @static
 */
function setOrigin (octave) {
  origin.octave = octave
  origin.shift = (5 - octave) * 12
}

/**
 * What is the chromatic number for this scale degree?
 * @param {string|number} degree format is '[#|b]\<int\>', where \<int\> is
 *                          a 1-based scale degree
 * @param {Object=} options defaults are {scale='Ionian', octave=origin.octave,
 *                          shift=origin.shift}
 * @param {string=} options.scale name of scale to use
 * @param {number=}    options.octave default octave for notes with no designation
 * @param {number=}    options.shift an integer used to shift the value up or down
 *                          on the number line
 * @returns {number} the chromatic number
 * @static
 */
function chromatic (degree,
  { scale = 'Ionian', octave = origin.octave, shift = origin.shift } = {}) {
  //
  const chromas = scaleChromatics[scale]
  // let d = parseInt(degree)
  const { base, offset } = utils.resolveQualifiers(degree)
  let d = base - 1 // make it zero-based
  const negativeDegree = (d < 0)

  // figure out octave d is in
  const dPrev = d
  const scaleLength = chromas.length
  d = d % scaleLength
  if (negativeDegree && (d !== 0)) {
    d = scaleLength + d
  }
  let dOct = (dPrev - d) / scaleLength
  if (negativeDegree) {
    dOct = (d - dPrev) / scaleLength
    dOct = -dOct
  }
  let result = chromas[d]
  result += offset
  result += ((octave + dOct) * numChromatics)
  result += shift
  return result
}

/**
 * What are the chromatic numbers for *degrees* in a given scale?
 * @param {Array.<(string|int)>}  degrees see {@link module:core.chromatic}
 * @param {Object=} options see {@link module:core.chromatic}
 * @returns {number[]} the chromatic numbers
 * @static
 */
function chromatics (degrees, options) {
  const result = []
  for (const degree of degrees) {
    result.push(chromatic(degree, options))
  }
  return result
}

/* ----------------------------  module-private ---------------------------- */

/*
 * Creates an initial set of scales, including the modes of the major scale
 */
function _initialize () {
  const modeNames = ['Ionian', 'Dorian', 'Phrygian', 'Lydian',
    'Mixolydian', 'Aeolian', 'Locrian']

  // set up Ionian scale
  addScale(modeNames[0], [1, 3, 5, 6, 8, 10, 12], 'Chromatic')

  // create modes of the major scale
  for (let i = 1; i < modeNames.length; i++) {
    const prev = scaleChromatics[modeNames[i - 1]]
    const delta = prev[1] - prev[0]
    const n = prev.length
    const arr = []
    for (let j = 1; j < n; j++) {
      arr.push(prev[j] - delta + 1)
    }
    arr.push(prev[0] - delta + 12 + 1)
    addScale(modeNames[i], arr, 'Chromatic')
  }

  addScale('Major', [1, 2, 3, 4, 5, 6, 7])
  addScale('Minor', [1, 2, 'b3', 4, 5, 6, 'b7'])
  addScale('Harmonic Minor', [1, 2, 'b3', 4, 5, 'b6', 7])
}

_initialize()

export {
  origin,
  scaleChromatics,
  addScale,
  setOrigin,
  chromatic,
  chromatics
}