track.js

import { TrackModel, PhraseModel, ExpressionModel } from '@composer/core';
import { SequencedEventProxy } from './util/sequenced_event_proxy';
import { ComposerError } from './errors';
import { BaseSequencedComposer } from './base/sequenced';
import { generateIdentifier } from '@composer/util';

/**
 * Create new tracks with {@link SessionComposer#track|session.track()}:
 * 
 * ##### Examples
 *
 * Play a single note:
 * 
 * ```
 * session.track('drums', ({ track } => {
 *   track.at(0).play(quarter.note('kick'));
 * });
 * ```
 * 
 * Play a simple beat:
 * 
 * ```
 * session.track('drums', ({ track } => {
 *   track.at(0).play.phrase([
 *     quarter.note('kick'),
 *     quarter.note('snare'),
 *     quarter.note('kick'),
 *     quarter.note('snare'),
 *   ]);
 * });
 * ```
 * 
 * @sort 3
 * @label Tracks
 * @extends BaseSequencedComposer
 * @category Composers
 * @hideconstructor
 */
export class TrackComposer extends BaseSequencedComposer {
  static composerContextName = 'track';
  static model = TrackModel;

  /**
   * Sequence one or more {@link NoteComposer|notes}.
   * 
   * ##### Examples
   *
   * Play a single {@link NoteComposer|note}:
   * 
   * ```
   * session.track('drums', ({ track } => {
   *   track.at(0).play(quarter.note('kick'));
   * });
   * ```
   * 
   * Play multiple {@link NoteComposer|notes} at once:
   * 
   * ```
   * session.track('drums', ({ track } => {
   *   track.at(0).play(quarter.note('kick snare ride'));
   * });
   * ```
   * 
   * Play a {@link PhraseComposer|phrase} by name:
   * 
   * ```
   * session.track('drums', ({ track } => {
   *   track.at(0).play.phrase('beat');
   * });
   * ```
   * 
   * Define and play a {@link PhraseComposer|phrase}:
   *
   * ```
   * session.track('drums', ({ track } => {
   *   track.at(0).play.phrase([
   *     quarter.note('kick'),
   *     quarter.note('snare'),
   *     quarter.note('kick'),
   *     quarter.note('snare'),
   *   ]);
   * });
   * ```
   * 
   * @sort 101
   * @proxiedBy at
   * @param {*} note 
   * @param {*} proxy 
   */
  play(note, proxy) {
    this.sequence({
      type: 'note',
      at: proxy.data.at,
      value: note,
    })
  }

  // By name:
  //   phrase(name)
  //
  // By sequence or expression (anonymous)
  //   phrase([ ... ])
  phrase(nameOrSequence, proxy) {
    const anonymous = typeof nameOrSequence !== 'string';
    const phrase = (() => {
      if (anonymous) {
        return this.model.session.phrases.add(PhraseModel.parse({
          name: `${this.model.name}-${generateIdentifier()}`,
          sequence: ExpressionModel.coerce(nameOrSequence)
        }));
      }
      else {
        return this.model.session.phrases.filterByProperty('name', nameOrSequence)[0];
      }
    })();

    if (!phrase) {
      throw new ComposerError(`Phrase "${nameOrSequence}" is not defined.`)
    }

    this.sequence({
      type: 'phrase',
      at: proxy.data.at,
      value: phrase.name,
    });
  }

  /**
   * Set key/root note for this track only.
   * 
   * @ignore
   * @todo not implemented
   * @proxiedBy at
   * @param {*} key 
   * @param {*} proxy 
   */
  key(key, proxy) {
    this.sequence({
      type: 'key',
      at: proxy.data.at,
      value: key,
    });
  }

  /**
   * Set scale for this track only.
   * 
   * @ignore
   * @todo not implemented
   * @proxiedBy at
   * @param {*} scale 
   * @param {*} proxy 
   */
  scale(scale, proxy) {
    this.sequence({
      type: 'scale',
      at: proxy.data.at,
      value: scale,
    });
  }

  /**
   * Set track volume in dB.
   * 
   * ``` javascript
   * session.track('drums', ({ track }) => {
   *   track.at(0).volume(-10);
   * });
   * 
   * @sort 102
   * @proxiedBy at
   * @param {number} volume - volume in decibels 
   * @param {*} proxy 
   */
  volume(volume, proxy) {
    this.sequence({
      type: 'volume',
      at: proxy.data.at,
      value: volume,
    });
  }

  /**
   * Set track panning (-1 to 1).
   * 
   * ``` javascript
   * session.track('drums', ({ track }) => {
   *   track.at(0).pan(-0.75);
   * });
   * 
   * @sort 103
   * @proxiedBy at
   * @param {number} pan - pan from -1 (L) to 1 (R)
   * @param {*} proxy 
   */
  pan(pan, proxy) {
    this.sequence({
      type: 'pan',
      at: proxy.data.at,
      value: pan,
    });
  }

  /**
   * Mute the track.
   * 
   * ##### Examples
   * 
   * Mute track at measure 5:
   * 
   * ``` javascript
   * session.track('drums', ({ track }) => {
   *   track.at(5).mute();
   * });
   * ```
   * 
   * Mute entire track:
   * 
   * ``` javascript
   * session.track('drums', ({ track }) => {
   *   track.mute();
   * });
   * ```
   * 
   *  Mute for first 10 measures only:
   * 
   * ``` javascript
   * session.track('drums', ({ track }) => {
   *   track.at(0).mute();
   *   track.at(9).mute(false);
   * });
   * ```
   * 
   * @sort 104
   * @proxiedBy at
   * @param {boolean} mute - mute value (default true)
   * @param {*} proxy 
   */
  mute() {
    const args = [ ...arguments ];
    const proxy = args.pop();
    const mute = args.length === 1 ? args[0] : true;

    this.sequence({
      type: 'mute',
      at: proxy.data.at,
      value: mute,
    });
  }

  /**
   * Solo the track (mute everything else).
   * 
   * @ignore
   * @sort 105
   * @todo not implemented
   * @proxiedBy at
   * @param {*} solo 
   * @param {*} proxy 
   */
  solo() {
    const args = [ ...arguments ];
    const proxy = args.pop();
    const solo = args.length === 1 ? args[0] : true;

    this.sequence({
      type: 'solo',
      at: proxy.data.at,
      value: solo,
    });
  }

}

export const TrackComposerProxy = SequencedEventProxy.create(TrackComposer, {
  methods: [ 
    'play',
    'phrase',
    'key',
    'scale',
    'volume',
    'pan',
    'mute',
    'solo',
  ],
  aliases: [
    {
      target: 'phrase',
      attachTo: 'play',
      name: 'phrase',
    }
  ]
});

export const track = TrackComposer.composer();