Seanitzel/Note-Art

View on GitHub
src/Theory.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { enharmonicPitchClass, getClassSet, getPitchClassSet, noteToObject, getInterval, isNote, normalizePitchClass } from './utilities/PureMusicUtils.js';
import {
  FLAT_CLASS_NOTES, NUMBER_OF_PITCH_CLASSES, PITCH_CLASSES, SHARP_CLASS_NOTES,
} from './Constants.js';
import { freqToMidi } from './utilities/ScientificFunctions.js';
import { Chord, PitchClassLetter, Note, Scale, PitchClass, RawPitchClass, RawFlatPitchClass } from 'types.js';
import { rearrangeArray } from './utilities/GeneralFunctions.js';

/**
   * Transposes a pitch class by the given interval.
   * @param {PitchClass} pitchClass pitch class to transpose, e.g 5, 7
   * @param {number} interval The interval, e.g 5, 7
   * @returns {PitchClass}
   * @example
   * transposePitchClass('C', 7) // => 'G'
   */
export function transposePitchClass(pitchClass: PitchClass, interval: number): PitchClass {
  const normalizedInterval = interval % NUMBER_OF_PITCH_CLASSES;
  const normalizedPitchClass: PitchClass = normalizePitchClass(pitchClass);
  const classSet = getClassSet(pitchClass);
  const classIndex = getPitchClassSet(classSet).indexOf(normalizedPitchClass);
  if(PITCH_CLASSES.includes(pitchClass as RawPitchClass)) {
    const index = Math.abs((classIndex + NUMBER_OF_PITCH_CLASSES + normalizedInterval) % NUMBER_OF_PITCH_CLASSES);
    return getPitchClassSet(classSet)[index];
  } else {
    const classIndex = getPitchClassSet(classSet).indexOf(pitchClass[0] as PitchClassLetter);
    const index = Math.abs((classIndex + NUMBER_OF_PITCH_CLASSES + normalizedInterval) % NUMBER_OF_PITCH_CLASSES);
    let [letter, acc] = getPitchClassSet(classSet)[index];
    const accidentals = pitchClass.slice(1);
    if(acc === 'b' && !accidentals.includes('b')) {
      const pc2 = FLAT_CLASS_NOTES[(FLAT_CLASS_NOTES.indexOf(`${letter}${acc}` as RawFlatPitchClass) - 1) % NUMBER_OF_PITCH_CLASSES];
      const enharmonic = enharmonicPitchClass(`${letter}${acc}` as PitchClass, pc2);
      letter = enharmonic[0];
      acc = enharmonic[1];
    }
    if(accidentals[accidentals.length - 1] === '#' && acc) {
      return `${letter}${accidentals.slice(0, accidentals.length - 1)}x` as PitchClass;
    }
    return `${letter}${accidentals}${acc ? acc : ''}` as PitchClass;
  }
}

/**
   * Transposes a note by the given interval.
   * @param {Note} note note to transpose, e.g 5, 7
   * @param {number} interval The interval, e.g 5, 7
   * @returns {PitchClass}
   * @example
   * transposeNote('C4', 7) // => 'G4'
   */
export function transposeNote(note: Note, interval: number): Note {
  const { pitchClass, octave } = noteToObject(note);
  const normalizedPitchClass = normalizePitchClass(pitchClass);
  const classSet = getClassSet(pitchClass);
  const classIndex = getPitchClassSet(classSet).indexOf(normalizedPitchClass);
  
  const newPitchClass = transposePitchClass(pitchClass, interval);
  
  let octDiff = Math.floor((classIndex + interval) / 12);
  if(interval < 0) {
    octDiff = classIndex + interval < 0 ? octDiff : 0;
  }
  return `${newPitchClass}${octave + octDiff}` as Note;
}

/**
   * Transposes a pitch class or a note by the given interval.
   * @param {PitchClass | Note} note note to transpose, e.g 5, 7
   * @param {number} interval The interval, e.g 5, 7
   * @returns {PitchClass}
   * @example
   * transpose('C4', 7) // => 'G4'
   */
export function transpose(note: PitchClass | Note, interval: number): string {
  return isNote(note) ?
    transposeNote(note as Note, interval) :
    transposePitchClass(note as PitchClass, interval);
}

/**
   * Generate a note from frequency.
   * @param frequency
   * @returns {Note}
   * @example
   * noteFromFrequency(440) // => 'A4'
   */
export function noteFromFrequency(frequency: number): string {
  const n = freqToMidi(frequency);
  const pitchClass = SHARP_CLASS_NOTES[n % NUMBER_OF_PITCH_CLASSES];
  const octave = Math.floor(n / NUMBER_OF_PITCH_CLASSES - 1);

  return `${pitchClass}${octave}`;
}

/**
 * Creates an array of notes from a note/pitch class & an array of intervals.
 * @param {PitchClass | Note} note 
 * @param {Array<number>} pattern 
 * @returns {Array<PitchClass | Note>}
 * @example
 * notesFromPattern('C4', [0, 2, 4, 5, 7, 9, 11]) // => ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4']
 */
export function intervalsToNotes(note: PitchClass | Note , pattern: Array<number>): Array<PitchClass | Note> {
  return pattern.map(interval => transpose(note, interval) as Note);
}

/**
 * Inverts a chord
 * @param chord
 * @param type
 * @returns {Array<PitchClass | Note>}
 */
export function invertChord(chord: Chord,type: number): Array<PitchClass | Note> {
  if(typeof type === 'number' && type <= chord.length) {
    return rearrangeArray(chord, type) as Array<PitchClass | Note>;
  }
  throw new Error('inversion cant be bigger then the number of pitch classes in the chord');
}

/**
 * Returns the note in degree of a scale(array of notes).
 * for example - if the Scale is a C Major,
 * than interval(1) will return D.
 * @param {Scale} scale An array of notes.
 * @param {Number} degree The degree of the note.
 * @returns {PitchClass | Note}
 * @example
 * const majorScale = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
 * scaleDegree(majorScale, 1) // => 'D'
 */
export function scaleDegree(scale: Scale, degree: number): PitchClass | Note{
  return scale[degree - 1];
}

/**
   * Returns the chord at the degree with specified size.
   * @param {Scale} scale An array of notes.
   * @param {number} degree Degree to get chord at.
   * @param {number} size Number of notes in the chord.
   * @returns {Chord}
   */
export function getChordFromScale(scale: Scale, degree: number, size = 3): Chord {
  const root = scaleDegree(scale, degree);
  const index = degree - 1;
  const pattern = [0];

  for(let i = 1; i < size; ++i) {
    const currIndex = index + (i * 2);
    const note = scale[currIndex % scale.length] as Note;
    const { pitchClass, octave } = noteToObject(note);
    const newOctave = octave + Math.floor(currIndex / scale.length);
    const newNote = `${pitchClass}${newOctave}` as Note;
    pattern.push(getInterval(root, newNote));
  }

  return intervalsToNotes(root, pattern);
}

/**
 * Returns an array of chords from a scale with specified size.
 * @param scale 
 * @param size
 * @returns {Array<Chord>}
 */
export function scaleToChords(scale: Scale, size = 3): Array<Chord> {
  return scale.map((_, degree) => getChordFromScale(scale, degree + 1, size));
}