Source: structure/song.js

const Section = require('./section');
const { noteJSON } = require('../utils');

/**
 * An object that generates entire songs.
 *
 * 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}
 * @implements [toJSON()]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior}
 */
class Song {

  /**
   * @arg {Object} options
   * @arg {Section[]|Object[]} options.sections the Song's sections as either an Array of {@link Section} objects,
   *              or an Array of options objects for the [Section constructor]{@link Section}
   * @arg {Number} [options.bpm=120] the tempo of the song in beats per minute (a beat is the unit of time)
   * @arg {Scale} [options.scale] default section scale, optional if every {@link Section} defines its scale
   * @arg {Number} [options.sectionLength] default section length, optional if every {@link Section} defines its own length
   */
  constructor({sections=[], bpm=120, scale, sectionLength}={}) {
    this.bpm = bpm;
    this.sections = sections.map(s =>
      s instanceof Section ? s : new Section(Object.assign({ scale, length: sectionLength }, s))
    );
    this.length = this.sections.map(s => s.length).reduce((l1,l2) => l1+l2, 0);
  }

  /**
   * @function @@iterator
   * @memberOf Song
   * @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 {bpm, sections} = this;
    yield {type: 'bpm', value: bpm};
    let timeOffset = 0;
    for (const section of sections) {
      for (const event of section) {
        yield noteJSON(event, timeOffset);
      }
      timeOffset += section.length;
    }
  }

  /**
   * Serialize the Song into a JSON object
   * @todo document the format, example output
   * @returns {Object} JSON object representation (a "plain" JavaScript object containing only Numbers, Strings, Arrays, and nested Objects)
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior|MDN: JSON.stringify()'s toJSON() behavior}
   */
  toJSON() {
    let bpm;
    const tracks = [];
    for (const event of this) {
      if (event.type === 'bpm') {
        bpm = event.value;
      } else {
        const trackIdx = (event.channel || 1) - 1;
        let track = tracks[trackIdx];
        if (!track) track = tracks[trackIdx] = [];
        track.push(event);
      }
    }
    for (const track of tracks) {
      track.push({ type: 'track-end', time: this.length });
    }
    return { bpm, tracks };
  }
}

module.exports = Song;