opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/Common/DataObjects/Pitch.ts

Summary

Maintainability
F
4 days
Test Coverage
// The value of the enum indicates the number of halftoneSteps from one note to the next
export enum NoteEnum {
    C = 0,
    D = 2,
    E = 4,
    F = 5,
    G = 7,
    A = 9,
    B = 11
}

/** Describes Accidental types.
 * Do not use the number values of these enum members directly for calculation anymore.
 * To use these for pitch calculation, use pitch.AccidentalHalfTones()
 * or Pitch.HalfTonesFromAccidental(accidentalEnum).
 */
export enum AccidentalEnum {
    SHARP,
    FLAT,
    NONE,
    NATURAL,
    DOUBLESHARP,
    DOUBLEFLAT,
    TRIPLESHARP,
    TRIPLEFLAT,
    QUARTERTONESHARP,
    QUARTERTONEFLAT,
    SLASHFLAT,
    THREEQUARTERSSHARP,
    THREEQUARTERSFLAT,
    SLASHQUARTERSHARP,
    SLASHSHARP,
    DOUBLESLASHFLAT,
    SORI,
    KORON
}

// This class represents a musical note. The middle A (440 Hz) lies in the octave with the value 1.
export class Pitch {
    public static pitchEnumValues: NoteEnum[] = [
        NoteEnum.C, NoteEnum.D, NoteEnum.E, NoteEnum.F, NoteEnum.G, NoteEnum.A, NoteEnum.B,
    ];

    private static halftoneFactor: number = 12 / (Math.LN2 / Math.LN10);
    private static octXmlDiff: number = 3;

    // private _sourceOctave: number;
    // private _sourceFundamentalNote: NoteEnum;
    // private _sourceAccidental: AccidentalEnum = AccidentalEnum.NONE;
    private octave: number;
    private fundamentalNote: NoteEnum;
    private accidental: AccidentalEnum = AccidentalEnum.NONE;
    private accidentalXml: string;
    private frequency: number;
    private halfTone: number;

    public static getNoteEnumString(note: NoteEnum): string {
        switch (note) {
            case NoteEnum.C:
                return "C";
            case NoteEnum.D:
                return "D";
            case NoteEnum.E:
                return "E";
            case NoteEnum.F:
                return "F";
            case NoteEnum.G:
                return "G";
            case NoteEnum.A:
                return "A";
            case NoteEnum.B:
                return "B";
            default:
                return "";
        }
    }

    /** Changes a note x lines/steps up (+) or down (-) from a NoteEnum on a staffline/keyboard (white keys).
     * E.g. Two lines down (-2) from a D is a B.
     * Two lines up from an A is a C.
     *   (e.g. in the treble/violin clef, going one line up: E -> F (semitone), F -> G (2 semitones)).
     * Returns new NoteEnum and the octave shift (e.g. -1 = new octave is one octave down). */
    public static lineShiftFromNoteEnum(noteEnum: NoteEnum, lines: number): [NoteEnum, number] {
        if (lines === 0) {
            return [noteEnum, 0];
        }
        const enums: NoteEnum[] = Pitch.pitchEnumValues;
        const originalIndex: number = enums.indexOf(noteEnum);
        let octaveShift: number = 0;
        let newIndex: number = (originalIndex + lines) % enums.length; // modulo only handles positive overflow
        if (originalIndex + lines > enums.length - 1) {
            octaveShift = 1;
        }
        if (newIndex < 0) {
            newIndex = enums.length + newIndex; // handle underflow, e.g. - 1: enums.length + (-1) = last element
            octaveShift = -1;
        }
        return [enums[newIndex], octaveShift];
    }

    /**
     * @param the input pitch
     * @param the number of halftones to transpose with
     * @returns ret[0] = the transposed fundamental.
     * ret[1] = the octave shift (not the new octave!)
     * @constructor
     */
    public static CalculateTransposedHalfTone(pitch: Pitch, transpose: number): { halftone: number, overflow: number } {
        const newHalfTone: number = <number>pitch.fundamentalNote + pitch.AccidentalHalfTones + transpose;
        return Pitch.WrapAroundCheck(newHalfTone, 12);
    }

    /** Returns the fundamental note x (0 <= x <= 11, e.g. 0 = C) with octave change/overflow.
     * The halftone will be one of the values in the enum NoteEnum, converted to number here as we need numbers for calculation.
     */
    public static WrapAroundCheck(value: number, limit: number): { halftone: number, overflow: number } {
        // the following one-line solution produces the same result, but isn't faster for -128 <= value <=, and harder to understand.
        //   For very large (unrealistic) numbers it's much faster, see PR #1374.
        // return {overflow: Math.floor(value / limit) || 0 , halftone:(value % limit + limit) % limit || 0 };
        let overflow: number = 0;

        while (value < 0) {
            value += limit;
            overflow--; // octave change downwards
        }
        while (value >= limit) {
            value -= limit;
            overflow++; // octave change upwards
        }
        return {overflow: overflow, halftone: value};
    }

    //public static calcFrequency(pitch: Pitch): number;

    //public static calcFrequency(fractionalKey: number): number;

    public static calcFrequency(obj: Pitch|number): number {
        let octaveSteps: number = 0;
        let halfToneSteps: number;
        if (obj instanceof Pitch) {
            // obj is a pitch
            const pitch: Pitch = obj;
            octaveSteps = pitch.octave - 1;
            halfToneSteps = <number>pitch.fundamentalNote - <number>NoteEnum.A + pitch.AccidentalHalfTones;
        } else if (typeof obj === "number") {
            // obj is a fractional key
            const fractionalKey: number = obj;
            halfToneSteps = fractionalKey - 57.0;
        }
        // Return frequency:
        return 440.0 * Math.pow(2, octaveSteps) * Math.pow(2, halfToneSteps / 12.0);
    }

    public static calcFractionalKey(frequency: number): number {
        // Return half-tone frequency:
        return Math.log(frequency / 440.0) / Math.LN10 * Pitch.halftoneFactor + 57.0;
    }

    public static fromFrequency(frequency: number): Pitch {
        const key: number = Pitch.calcFractionalKey(frequency) + 0.5;
        const octave: number = Math.floor(key / 12) - Pitch.octXmlDiff;
        const halftone: number = Math.floor(key) % 12;
        let fundamentalNote: NoteEnum = <NoteEnum>halftone;
        let accidental: AccidentalEnum = AccidentalEnum.NONE;
        if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
            fundamentalNote = <NoteEnum>(halftone - 1);
            accidental = AccidentalEnum.SHARP;
        }
        return new Pitch(fundamentalNote, octave, accidental);
    }

    public static fromHalftone(halftone: number): Pitch {
        const octave: number = Math.floor(halftone / 12) - Pitch.octXmlDiff;
        const halftoneInOctave: number = halftone % 12;
        let fundamentalNote: NoteEnum = <NoteEnum>halftoneInOctave;
        let accidental: AccidentalEnum = AccidentalEnum.NONE;
        if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
            fundamentalNote = <NoteEnum>(halftoneInOctave - 1);
            accidental = AccidentalEnum.SHARP;
        }
        return new Pitch(fundamentalNote, octave, accidental);
    }

    public static ceiling(halftone: number): NoteEnum {
        halftone = (halftone) % 12;
        let fundamentalNote: NoteEnum = <NoteEnum>halftone;
        if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
            fundamentalNote = <NoteEnum>(halftone + 1);
        }
        return fundamentalNote;
    }

    public static floor(halftone: number): NoteEnum {
        halftone = halftone % 12;
        let fundamentalNote: NoteEnum = <NoteEnum>halftone;
        if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
            fundamentalNote = <NoteEnum>(halftone - 1);
        }
        return fundamentalNote;
    }

    constructor(fundamentalNote: NoteEnum, octave: number, accidental: AccidentalEnum,
        accidentalXml: string = undefined, isRest: boolean = false) {
        this.fundamentalNote = fundamentalNote;
        this.octave = octave;
        this.accidental = accidental;
        this.accidentalXml = accidentalXml;
        this.halfTone = <number>(fundamentalNote) + (octave + Pitch.octXmlDiff) * 12 +
            Pitch.HalfTonesFromAccidental(accidental);
        if (!isRest) {
            this.frequency = Pitch.calcFrequency(this);
        }
    }

    /** Turns an AccidentalEnum into half tone steps for pitch calculation.
     *
     */
    public static HalfTonesFromAccidental(accidental: AccidentalEnum): number {
        // about equal performance to hashmap/dictionary. could be turned into hashmap for convenience
        // switch is very slightly faster, but both are negligibly short anyways.
        switch (accidental) {
            // ordered from most to least common to improve average runtime
            case AccidentalEnum.NONE:
                return 0;
            case AccidentalEnum.SHARP:
                return 1;
            case AccidentalEnum.FLAT:
                return -1;
            case AccidentalEnum.NATURAL:
                return 0;
            case AccidentalEnum.DOUBLESHARP:
                return 2;
            case AccidentalEnum.DOUBLEFLAT:
                return -2;
            case AccidentalEnum.TRIPLESHARP: // very rare, in some classical pieces
                return 3;
            case AccidentalEnum.TRIPLEFLAT:
                return -3;
            case AccidentalEnum.QUARTERTONESHARP:
                return 0.5;
            case AccidentalEnum.QUARTERTONEFLAT:
                return -0.5;
            case AccidentalEnum.SLASHFLAT:
                return -0.51; // TODO currently necessary for quarter tone flat rendering after slash flat
            case AccidentalEnum.THREEQUARTERSSHARP:
                return 1.5;
            case AccidentalEnum.THREEQUARTERSFLAT:
                return -1.5;
            case AccidentalEnum.SLASHQUARTERSHARP:
                return 0.0013; // tmp for identification
            case AccidentalEnum.SLASHSHARP:
                return 0.0014; // tmp for identification
            case AccidentalEnum.DOUBLESLASHFLAT:
                return -0.0015; // tmp for identification
            case AccidentalEnum.SORI:
                return 0.0016; // tmp for identification
            case AccidentalEnum.KORON:
                return 0.0017; // tmp for identification
            default:
                throw new Error("Unhandled AccidentalEnum value");
                // return 0;
        }
    }

    public static AccidentalFromHalfTones(halfTones: number): AccidentalEnum {
        switch (halfTones) {
            case 0:
                // for enharmonic change, we won't get a Natural accidental. Maybe there are edge cases though?
                return AccidentalEnum.NONE;
            case 1:
                return AccidentalEnum.SHARP;
            case -1:
                return AccidentalEnum.FLAT;
            case 2:
                return AccidentalEnum.DOUBLESHARP;
            case -2:
                return AccidentalEnum.DOUBLEFLAT;
            case 3:
                return AccidentalEnum.TRIPLESHARP;
            case -3:
                return AccidentalEnum.TRIPLEFLAT;
            case 0.5:
                return AccidentalEnum.QUARTERTONESHARP;
            case -0.5:
                return AccidentalEnum.QUARTERTONEFLAT;
            case 1.5:
                return AccidentalEnum.THREEQUARTERSSHARP;
            case -1.5:
                return AccidentalEnum.THREEQUARTERSFLAT;
            default:
                if (halfTones > 0 && halfTones < 1) {
                    return AccidentalEnum.QUARTERTONESHARP;
                } else if (halfTones < 0 && halfTones > -1) {
                    return AccidentalEnum.QUARTERTONEFLAT;
                }
                // potentially unhandled or broken accidental halfTone value
                return AccidentalEnum.QUARTERTONESHARP; // to signal unhandled value
        }
    }

    /**
     * Converts AccidentalEnum to a string which represents an accidental in VexFlow
     * Can also be useful in other cases, but has to match Vexflow accidental codes.
     * @param accidental
     * @returns {string} Vexflow Accidental code
     */
    public static accidentalVexflow(accidental: AccidentalEnum): string {
        let acc: string;
        switch (accidental) {
            case AccidentalEnum.NATURAL:
                acc = "n";
                break;
            case AccidentalEnum.FLAT:
                acc = "b";
                break;
            case AccidentalEnum.SHARP:
                acc = "#";
                break;
            case AccidentalEnum.DOUBLESHARP:
                acc = "##";
                break;
            case AccidentalEnum.TRIPLESHARP:
                acc = "###";
                break;
            case AccidentalEnum.DOUBLEFLAT:
                acc = "bb";
                break;
            case AccidentalEnum.TRIPLEFLAT:
                acc = "bbs"; // there is no "bbb" in VexFlow yet, unfortunately.
                break;
            case AccidentalEnum.QUARTERTONESHARP:
                acc = "+";
                break;
            case AccidentalEnum.QUARTERTONEFLAT:
                acc = "d";
                break;
            case AccidentalEnum.SLASHFLAT:
                acc = "bs";
                break;
            case AccidentalEnum.THREEQUARTERSSHARP:
                acc = "++";
                break;
            case AccidentalEnum.THREEQUARTERSFLAT:
                acc = "db";
                break;
            case AccidentalEnum.SLASHQUARTERSHARP:
                acc = "+-";
                break;
            case AccidentalEnum.SLASHSHARP:
                acc = "++-";
                break;
            case AccidentalEnum.DOUBLESLASHFLAT:
                acc = "bss";
                break;
            case AccidentalEnum.SORI:
                acc = "o";
                break;
            case AccidentalEnum.KORON:
                acc = "k";
                break;
            default:
        }
        return acc;
    }

    public get AccidentalHalfTones(): number {
        return Pitch.HalfTonesFromAccidental(this.accidental);
    }

    public get Octave(): number {
        return this.octave;
    }

    public get FundamentalNote(): NoteEnum {
        return this.fundamentalNote;
    }

    public get Accidental(): AccidentalEnum {
        return this.accidental;
    }

    public get AccidentalXml(): string {
        return this.accidentalXml;
    }

    public get Frequency(): number {
        return this.frequency;
    }

    public static get OctaveXmlDifference(): number {
        return Pitch.octXmlDiff;
    }

    public getHalfTone(): number {
        return this.halfTone;
    }

    // This method returns a new Pitch transposed by the given factor
    public getTransposedPitch(factor: number): Pitch {
        if (factor > 12) {
            throw new Error("rewrite this method to handle bigger octave changes or don't use is with bigger octave changes!");
        }
        if (factor > 0) {
            return this.getHigherPitchByTransposeFactor(factor);
        }
        if (factor < 0) {
            return this.getLowerPitchByTransposeFactor(-factor);
        }
        return this;
    }

    public DoEnharmonicChange(): void {
        switch (this.accidental) {
            case AccidentalEnum.FLAT:
            case AccidentalEnum.DOUBLEFLAT:
                this.fundamentalNote = this.getPreviousFundamentalNote(this.fundamentalNote);
                this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
                (this.octave + Pitch.octXmlDiff) * 12));
                break;
            case AccidentalEnum.SHARP:
            case AccidentalEnum.DOUBLESHARP:
                this.fundamentalNote = this.getNextFundamentalNote(this.fundamentalNote);
                this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
                (this.octave + Pitch.octXmlDiff) * 12));
                break;
            default:
                return;
        }
    }

    public ToString(): string {
        let accidentalString: string = Pitch.accidentalVexflow(this.accidental);
        if (!accidentalString) {
            accidentalString = "";
        }
        return "Key: " + Pitch.getNoteEnumString(this.fundamentalNote) + accidentalString +
        ", Note: " + this.fundamentalNote + ", octave: " + this.octave.toString();
    }

    /** A short representation of the note like A4 (A, octave 4), Ab5 or C#4. */
    public ToStringShort(octaveOffset: number = 0): string {
        let accidentalString: string = Pitch.accidentalVexflow(this.accidental);
        if (!accidentalString) {
            accidentalString = "";
        }
        const octave: number = this.octave + octaveOffset;
        return Pitch.getNoteEnumString(this.fundamentalNote) + accidentalString + octave;
    }

    /** A shortcut getter for ToStringShort that can be useful for debugging. */
    public get ToStringShortGet(): string {
        return this.ToStringShort(0); // note that a getter cannot have parameters.
    }

    public OperatorEquals(p2: Pitch): boolean {
        const p1: Pitch = this;
        // if (ReferenceEquals(p1, p2)) {
        //     return true;
        // }
        if (!p1 || !p2) {
            return false;
        }
        return (p1.FundamentalNote === p2.FundamentalNote && p1.Octave === p2.Octave && p1.Accidental === p2.Accidental);
    }

    public OperatorNotEqual(p2: Pitch): boolean {
        const p1: Pitch = this;
        return !(p1 === p2);
    }

    //These don't take into account accidentals! which isn't needed for our current purpose
    public OperatorFundamentalGreaterThan(p2: Pitch): boolean {
        const p1: Pitch = this;
        if (p1.Octave === p2.Octave) {
            return p1.FundamentalNote > p2.FundamentalNote;
        } else {
            return p1.Octave > p2.Octave;
        }
    }

    public OperatorFundamentalLessThan(p2: Pitch): boolean {
        const p1: Pitch = this;
        if (p1.Octave === p2.Octave) {
            return p1.FundamentalNote < p2.FundamentalNote;
        } else {
            return p1.Octave < p2.Octave;
        }
    }

    // This method returns a new Pitch factor-Halftones higher than the current Pitch
    private getHigherPitchByTransposeFactor(factor: number): Pitch {
        const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
        let newOctave: number = this.octave;
        let newNoteEnum: NoteEnum;
        if (noteEnumIndex + factor > Pitch.pitchEnumValues.length - 1) {
            newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor - Pitch.pitchEnumValues.length];
            newOctave++;
        } else {
            newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor];
        }
        return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
    }

    private getLowerPitchByTransposeFactor(factor: number): Pitch {
        const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
        let newOctave: number = this.octave;
        let newNoteEnum: NoteEnum;
        if (noteEnumIndex - factor < 0) {
            newNoteEnum = Pitch.pitchEnumValues[Pitch.pitchEnumValues.length + noteEnumIndex - factor];
            newOctave--;
        } else {
            newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex - factor];
        }
        return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
    }

    private getNextFundamentalNote(fundamental: NoteEnum): NoteEnum {
        let i: number = Pitch.pitchEnumValues.indexOf(fundamental);
        i = (i + 1) % Pitch.pitchEnumValues.length;
        return Pitch.pitchEnumValues[i];
    }

    private getPreviousFundamentalNote(fundamental: NoteEnum): NoteEnum {
        const i: number = Pitch.pitchEnumValues.indexOf(fundamental);
        if (i > 0) {
            return Pitch.pitchEnumValues[i - 1];
        } else {
            return Pitch.pitchEnumValues[Pitch.pitchEnumValues.length - 1];
        }
    }
}