note.js

import {
  ApplicationError,
  ChordModel,
  ExpressionModel,
  NoteModel,
  RestModel,
} from "@composer/core";

import { ComposerError } from "./errors";

function isNote (pitch) {
  return (
    typeof pitch === 'number' ||
    (
      typeof pitch === 'string' &&
      pitch.match(/^[abcdefg][b\#]{0,1}[0-9]{0,1}$/i)
    )
  );
}

/**
 * Sequence single notes, chords and rests with note factories.
 * 
 * The following example uses {@link quarter}, {@link eighth} and {@link dotted.quarter} note factories:
 * 
 * ``` javascript
 * // Single note:
 * track.at(0).play(quarter.note('C5'))
 * 
 * // Named chord:
 * track.at(0).play(quarter.note('Cmaj7'))
 *
 * // Custom chord:
 * track.at(0).play(quarter.note('C4 D4 E4 G5'))
 * 
 * // One bar phrase with chords and a rest:
 * track.at(0).play.phrase([
 *   dotted.quarter.note('Cmaj7'),
 *   eighth.note('Dmin'),
 *   quarter.rest(),
 *   quarter.note('Bmaj')
 * ]);
 * ```
 * 
 * ### Note Factories
 * 
 * Note factories are subclasses of {@link NoteComposer} bound to a specific time unit.
 * 
 * **Standard**<br>
 * {@link large}
 * {@link long}
 * {@link doubleWhole}
 * {@link whole}
 * {@link half}
 * {@link quarter}
 * {@link eighth}
 * {@link sixteenth}
 * {@link thirtySecond}
 * {@link sixtyFourth}
 * 
 * **Dotted**<br>
 * {@link dotted.large}
 * {@link dotted.long}
 * {@link dotted.doubleWhole}
 * {@link dotted.whole}
 * {@link dotted.half}
 * {@link dotted.quarter}
 * {@link dotted.eighth}
 * {@link dotted.sixteenth}
 * {@link dotted.thirtySecond}
 * {@link dotted.sixtyFourth}
 * 
 * **Double Dotted**<br>
 * {@link doubleDotted.large}
 * {@link doubleDotted.long}
 * {@link doubleDotted.doubleWhole}
 * {@link doubleDotted.whole}
 * {@link doubleDotted.half}
 * {@link doubleDotted.quarter}
 * {@link doubleDotted.eighth}
 * {@link doubleDotted.sixteenth}
 * {@link doubleDotted.thirtySecond}
 * {@link doubleDotted.sixtyFourth}
 * 
 * **Triplets**<br>
 * {@link triplet.quarter}
 * {@link triplet.eighth}
 * {@link triplet.sixteenth}
 * 
 * **Quintuplets**<br>
 * {@link quintuplet.quarter}
 * {@link quintuplet.eighth}
 * {@link quintuplet.sixteenth}
 * 
 * **Septuplets**<br>
 * {@link septuplet.quarter}
 * {@link septuplet.eighth}
 * {@link septuplet.sixteenth}
 * 
 * ### Pitches
 * 
 * Pitches are specified in {@link https://en.wikipedia.org/wiki/ABC_notation|ABC Notation} or as integers relative to the key signature.
 *
 * #### ABC Notation
 * 
 * ```
 * // Default octave (4)
 * track.at(0).play(quarter.note('C'));
 * 
 * // With specific octave:
 * track.at(0).play(quarter.note('C2'));
 * ```
 *
 * #### Relative Notation
 * 
 * Relative pitches are signed integers that will be transposed to notes in the appropriate key signature at the time they are played.
 * 
 * Consider the following:
 * 
 * ```
 * session('my-song', ({ session }) => {
 *   session.at(0)
 *     .key('C')
 *     .scale('major');
 * 
 *   session.phrase('melody', [
 *     quarter.note(0),
 *     quarter.note(1),
 *     quarter.note(2),
 *     quarter.note(3),
 *   ])
 * 
 *   session.track('my-track', ({ track }) => {
 *     track.at(0).play.phrase('melody'); // ==> C4, D4, E4, F4
 *     track.at(2).play.phrase('melody'); // ==> C4, D4, E4, F4
 *   });
 * });
 * ```
 * 
 * If we change the key signature at measure 2, the resulting notes are:
 * 
 * ```
 * session('my-song', ({ session }) => {
 *   session.at(0)
 *     .key('C')
 *     .scale('major');
 * 
 *   session.at(2)
 *     .key('E')
 *     .scale('major');
 * 
 *   session.phrase('melody', [
 *     quarter.note(0),
 *     quarter.note(1),
 *     quarter.note(2),
 *     quarter.note(3),
 *    ])
 * 
 *   track('my-track', ({ track }) => {
 *     track.at(0).play.phrase('melody'); // ==> C4, D4, E4, F4
 *     track.at(2).play.phrase('melody'); // ==> E4, F#, G#, A4
 *   });
 * });
 * ```
 * 
 * ### Chords
 * 
 * #### Named Chords
 *
 * Harmonicon supports a large number of chords that can be referenced by name:
 * 
 * ``` javascript
 * session.track('my-track', () => {
 *  track.at(0).play(quarter.note('cmaj7'))
 * })
 * ```
 * 
 * #### Custom Chord Structures
 * 
 * To specify chord notes manually, provide an array of notes:
 * 
 * ``` javascript
 * session.track('my-track', () => {
 *  track.at(0).play(quarter.note([ 'C4', 'E4', 'F4' ]));
 * })
 * ```
 *
 * Alternatively, a space-delimited string also works:
 * 
 * ``` javascript
 * session.track('my-track', () => {
 *  track.at(0).play(quarter.note('C4 E4 F4');
 * })
 * ```
 * 
 * @abstract
 * @hideconstructor
 * @sort 4
 * @label Notes
 * @category Composers
 */
export class NoteComposer {

  static inferParser(value) {

    // Array of notes
    if (Array.isArray(value)) {
      return { value, parser: 'unnamedChord' };
    }

    value = String(value);

    // Multiple space-delimited notes
    if (value.match(/ /)) {
      return {
        value: value.split(/ /),
        parser: 'unnamedChord' 
      };
    }

    // Explicit chord (beginning with "*")
    else if (value.match(/^\*/)) {
      return { 
        value: value.replace(/^\*/, ''),
        parser: 'namedChord'
      };
    }

    // Relative note
    else if (value.match(/^\-{0,1}[0-9]+$/)) {
      return { value, parser: 'note' };
    }

    // Absolute (ABC notation) note
    else if (value.match(/^[abcdefg][b\#]{0,1}[0-9]{0,1}$/i)) {
      return { value, parser: 'note' };
    }

    // Assume a chord of some kind...
    else {
      return { value, parser: 'namedChord' };
    }
  }

  /**
   * Sequence one or more pitches at a position on the timeline.
   *
   * @example
   * quarter.note('c4', { velocity: 0.5 })
   * 
   * @param {(Array|Integer|String)} pitch - pitch, chord, or array of pitches
   * @param {Object} options
   * @param {Integer} options.velocity - velocity (0 to 1)
   * @param {Integer} options.octave - octave (0 to 7)
   * @returns {(NoteModel|NoteModel[])}
   */
  static note(input, options = {}, deferFailure = true) {
    const duration = this.unit;
    const parsers = {};

    parsers.note = (pitch) => {
      return NoteModel.parse(
        Object.assign({ duration, pitch }, options)
      );
    }

    parsers.unnamedChord = (pitches) => {
      return ExpressionModel.parse({
        sequence: false,
        source: pitches.map((pitch) => {
          return NoteModel.parse(
            Object.assign({ duration, pitch }, options)
          )
        })
      });
    }

    parsers.namedChord = (symbol) => {
      return ExpressionModel.parse({
        sequence: false,
        source: ChordModel.parse(
          Object.assign({ duration, symbol }, options)
        ).toNotes()
      });
    }

    try {
      const { parser, value } = this.inferParser(input);

      if (parsers[parser]) {
        return parsers[parser](value);
      }
      else {
        throw new ComposerError(`Unable to parse ${input}`);
      }
    }
    catch (e) {
      if (e instanceof ApplicationError) {
        if (deferFailure) {
          return parsers.note(input);
        }
        else {
          throw new ComposerError(e.message);
        }
      }
      else {
        throw e;
      }
    }
  }


  /**
   * Sequence a rest inside a phrase.
   * 
   * @example
   * // One bar phrase with a 2 beat rest:
   * session.phrase('my-phrase', [
   *   quarter.note('C'),
   *   half.rest(),
   *   quarter.note('D'),
   * ])
   * 
   * @param {*} options 
   * @returns {RestModel}
   */
  static rest (options = {}) {
    return RestModel.parse(Object.assign({ duration: this.unit }, options));
  }

}