addon/mixins/musical-identity.js

import Ember from 'ember';
import { frequencyMap } from 'ember-audio/utils';

/**
 * Provides helper classes that represent musical concepts meant to be used by
 * classes from the Audio module.
 *
 * @public
 * @module MusicalConcepts
 */

const {
  computed,
  Mixin
} = Ember;

/**
 * This mixin allows an object to have an awareness of it's "musical identity"
 * or "note value" based on western musical standards (a standard piano).
 * If any of the following are provided, all of the remaining properties will be
 * calculated:
 *
 * 1. frequency
 * 2. identifier (i.e. "Ab1")
 * 3. letter, octave, and (optionally) accidental
 *
 * This mixin only makes sense when the consuming object is part of a collection,
 * as the only functionality it provides serves to facilitate identification.
 *
 * @public
 * @class MusicalIdentity
 */
export default Mixin.create({

  /**
   * For note `Ab5`, this would be `A`.
   *
   * @public
   * @property letter
   * @type {string}
   */
  letter: null,

  /**
   * For note `Ab5`, this would be `b`.
   *
   * @public
   * @property accidental
   * @type {string}
   */
  accidental: null,

  /**
   * For note `Ab5`, this would be `5`.
   *
   * @public
   * @property octave
   * @type {string}
   */
  octave: null,

  /**
   * Computed property. Value is `${letter}` or `${letter}${accidental}` if
   * accidental exists.
   *
   * @public
   * @property name
   * @type {string}
   */
  name: computed('letter', 'accidental', function() {
    const accidental = this.get('accidental');
    const letter = this.get('letter');

    if (accidental) {
      return `${letter}${accidental}`;
    } else {
      return letter;
    }
  }),

  /**
   * Computed property. The frequency of the note in hertz. Calculated by
   * comparing western musical standards (a standard piano) and the note
   * identifier (i.e. `Ab1`). If this property is set directly, all other
   * properties are updated to reflect the provided frequency.
   *
   * @public
   * @property
   * @type {number}
   */
  frequency: computed('identifier', {
    get() {
      const identifier = this.get('identifier');

      if (identifier) {
        return frequencyMap[identifier];
      }
    },

    set(key, value) {
      for (let key in frequencyMap) {
        if (value === frequencyMap[key]) {
          this.set('identifier', key);
          return value;
        }
      }
    }
  }),

  /**
   * Computed property. Value is `${letter}${octave}` or
   * `${letter}${accidental}${octave}` if accidental exists. If this property
   * is set directly, all other properties are updated to reflect the provided
   * identifier.
   *
   * @public
   * @property identifier
   * @type {string}
   */
  identifier: computed('letter', 'octave', 'accidental', {
    get() {
      const accidental = this.get('accidental');
      const letter = this.get('letter');
      const octave = this.get('octave');
      let output;

      if (accidental) {
        output = `${letter}${accidental}${octave}`;
      } else {
        output = `${letter}${octave}`;
      }

      return output;
    },

    set(key, value) {
      const [ letter ] = value;
      const octave = value[2] || value[1];
      let accidental;

      if (value[2]) {
        accidental = value[1];
      }

      this.setProperties({ letter, octave, accidental });

      return value;
    }
  })
});