Source: structure/section.js

const Part = require('./part');
const Pitch = require('../model/pitch');
const PitchClass = require('../model/pitch-class');
const Harmony = require('./harmony');
const { ARPEGGIO, BASS, CHORD, CHROMATIC, LEAD, SCALE } = Part.MODES;

function evaluatePartMode(partMode, relativePitch, scale, chord, octave) {
  if (relativePitch instanceof Array) {
    return [].concat(...relativePitch.map(rp => evaluatePartMode(partMode, rp, scale, chord, octave)));
  }
  if (relativePitch instanceof Pitch) {
    return [relativePitch];
  }
  if (relativePitch instanceof PitchClass) {
    return [new Pitch(relativePitch, octave)];
  }
  if (!partMode) {
    return [Number(relativePitch)];
  }
  switch (partMode) {
    case ARPEGGIO:
      return [chord.pitch(relativePitch, { scale, octave })];

    case BASS:
      return [chord.pitch(0, { scale, octave, inversion: 0, offset: relativePitch })];

    case LEAD:
      return [chord.pitch(0, { scale, octave, offset: relativePitch })];

    case CHORD:
      return chord.pitches({ scale, octave, inversion: chord.inversion + relativePitch });

    case SCALE:
      return [scale.pitch(relativePitch, {octave})];

    case CHROMATIC:
      return [scale.pitch(0, { octave }).add(relativePitch)];

    default:
      console.error(`Unsupported part mode "${partMode}"`); // eslint-disable-line no-console
      return [];
  }
}

/**
 * A section of a {@link Song} with a particular scale and harmony.
 *
 * See the overview on the [documentation homepage](./index.html).
 * @implements [iterable protocol]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol}
 */
class Section {

  /**
   * @arg {Object} options
   * @arg {Harmony} options.harmony The Section's harmony (its chord progression)
   * @arg {Part[]|Object[]} options.parts The Section's parts as either an Array of {@link Part} objects,
   *              or an Array of options objects for the [Part constructor]{@link Part}
   * @arg {Scale} options.scale The Section's {@link Scale}.
   *                              Must be provided unless this instance is constructed by the containing {@link Song}
   *                              *and* it's [sectionLength]{@link Song} is set.
   * @arg {Number} [options.length=max {@link Part|Part.length}] The length of the Section in beats.
   */
  constructor({harmony, scale, parts=[], length} = {}) {
    this.scale = scale;
    this.harmony = harmony instanceof Harmony ? harmony : new Harmony(harmony);
    this.parts = parts.map((p,idx) => p instanceof Part ? p : new Part(Object.assign({channel:idx+1},p)));
    this.length = length || Math.max(...this.parts.map(p => p.length + p.delay));
  }

  /**
   * @function @@iterator
   * @memberOf Section
   * @instance
   * @description The `[Symbol.iterator]()` generator function* that implements the
   *              [iterable protocol]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol}
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols|MDN: Iteration Protocols}
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator|MDN: Symbol.iterator}
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*|MDN: function*}
   */
  *[Symbol.iterator]() {
    const { scale, harmony, parts } = this;
    for (const part of parts) {
      const {mode, channel, octave} = part;
      let harmonyIter = harmony[Symbol.iterator]();
      let harmonyCurr = harmonyIter.next();
      let harmonyNext = harmonyIter.next();
      for (const event of part) {
        let { time, pitch, duration, intensity } = event;
        if (time >= this.length) {
          // exceeded section length (we're assuming monotonically increasing times)
          break;
        }
        while (harmonyNext && harmonyNext.value && harmonyNext.value.time <= time) {
          harmonyCurr = harmonyNext;
          harmonyNext = harmonyIter.next();
        }
        const { value:{chord}={} } = harmonyCurr;
        const pitches = evaluatePartMode(mode, pitch, scale, chord, octave).filter(p => p != null);
        const uniquePitchValues = new Set(pitches.map(p => Number(p)));
        for (const p of uniquePitchValues) {
          yield {time, note: {pitch: p, duration, intensity, channel} };
        }
      }
    }
  }
}

module.exports = Section;