Pitch.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // The value of the enum indicates the number of halftoneSteps from one note to the next
  2. export enum NoteEnum {
  3. C = 0,
  4. D = 2,
  5. E = 4,
  6. F = 5,
  7. G = 7,
  8. A = 9,
  9. B = 11
  10. }
  11. /** Describes Accidental types.
  12. * Do not use the number values of these enum members directly for calculation anymore.
  13. * To use these for pitch calculation, use pitch.AccidentalHalfTones()
  14. * or Pitch.HalfTonesFromAccidental(accidentalEnum).
  15. */
  16. export enum AccidentalEnum {
  17. SHARP,
  18. FLAT,
  19. NONE,
  20. NATURAL,
  21. DOUBLESHARP,
  22. DOUBLEFLAT,
  23. TRIPLESHARP,
  24. TRIPLEFLAT,
  25. QUARTERTONESHARP,
  26. QUARTERTONEFLAT,
  27. SLASHFLAT,
  28. THREEQUARTERSSHARP,
  29. THREEQUARTERSFLAT,
  30. SLASHQUARTERSHARP,
  31. SLASHSHARP,
  32. DOUBLESLASHFLAT,
  33. SORI,
  34. KORON
  35. }
  36. // This class represents a musical note. The middle A (440 Hz) lies in the octave with the value 1.
  37. export class Pitch {
  38. public static pitchEnumValues: NoteEnum[] = [
  39. NoteEnum.C, NoteEnum.D, NoteEnum.E, NoteEnum.F, NoteEnum.G, NoteEnum.A, NoteEnum.B,
  40. ];
  41. private static halftoneFactor: number = 12 / (Math.LN2 / Math.LN10);
  42. private static octXmlDiff: number = 3;
  43. // private _sourceOctave: number;
  44. // private _sourceFundamentalNote: NoteEnum;
  45. // private _sourceAccidental: AccidentalEnum = AccidentalEnum.NONE;
  46. private octave: number;
  47. private fundamentalNote: NoteEnum;
  48. private accidental: AccidentalEnum = AccidentalEnum.NONE;
  49. private accidentalXml: string;
  50. private frequency: number;
  51. private halfTone: number;
  52. public static getNoteEnumString(note: NoteEnum): string {
  53. switch (note) {
  54. case NoteEnum.C:
  55. return "C";
  56. case NoteEnum.D:
  57. return "D";
  58. case NoteEnum.E:
  59. return "E";
  60. case NoteEnum.F:
  61. return "F";
  62. case NoteEnum.G:
  63. return "G";
  64. case NoteEnum.A:
  65. return "A";
  66. case NoteEnum.B:
  67. return "B";
  68. default:
  69. return "";
  70. }
  71. }
  72. /** Changes a note x lines/steps up (+) or down (-) from a NoteEnum on a staffline/keyboard (white keys).
  73. * E.g. Two lines down (-2) from a D is a B.
  74. * Two lines up from an A is a C.
  75. * (e.g. in the treble/violin clef, going one line up: E -> F (semitone), F -> G (2 semitones)).
  76. * Returns new NoteEnum and the octave shift (e.g. -1 = new octave is one octave down). */
  77. public static lineShiftFromNoteEnum(noteEnum: NoteEnum, lines: number): [NoteEnum, number] {
  78. if (lines === 0) {
  79. return [noteEnum, 0];
  80. }
  81. const enums: NoteEnum[] = Pitch.pitchEnumValues;
  82. const originalIndex: number = enums.indexOf(noteEnum);
  83. let octaveShift: number = 0;
  84. let newIndex: number = (originalIndex + lines) % enums.length; // modulo only handles positive overflow
  85. if (originalIndex + lines > enums.length - 1) {
  86. octaveShift = 1;
  87. }
  88. if (newIndex < 0) {
  89. newIndex = enums.length + newIndex; // handle underflow, e.g. - 1: enums.length + (-1) = last element
  90. octaveShift = -1;
  91. }
  92. return [enums[newIndex], octaveShift];
  93. }
  94. /**
  95. * @param the input pitch
  96. * @param the number of halftones to transpose with
  97. * @returns ret[0] = the transposed fundamental.
  98. * ret[1] = the octave shift (not the new octave!)
  99. * @constructor
  100. */
  101. public static CalculateTransposedHalfTone(pitch: Pitch, transpose: number): { halftone: number, overflow: number } {
  102. const newHalfTone: number = <number>pitch.fundamentalNote + pitch.AccidentalHalfTones + transpose;
  103. return Pitch.WrapAroundCheck(newHalfTone, 12);
  104. }
  105. public static WrapAroundCheck(value: number, limit: number): { halftone: number, overflow: number } {
  106. let overflow: number = 0;
  107. while (value < 0) {
  108. value += limit;
  109. overflow--; // the octave change
  110. }
  111. while (value >= limit) {
  112. value -= limit;
  113. overflow++; // the octave change
  114. }
  115. return {overflow: overflow, halftone: value};
  116. }
  117. //public static calcFrequency(pitch: Pitch): number;
  118. //public static calcFrequency(fractionalKey: number): number;
  119. public static calcFrequency(obj: Pitch|number): number {
  120. let octaveSteps: number = 0;
  121. let halfToneSteps: number;
  122. if (obj instanceof Pitch) {
  123. // obj is a pitch
  124. const pitch: Pitch = obj;
  125. octaveSteps = pitch.octave - 1;
  126. halfToneSteps = <number>pitch.fundamentalNote - <number>NoteEnum.A + pitch.AccidentalHalfTones;
  127. } else if (typeof obj === "number") {
  128. // obj is a fractional key
  129. const fractionalKey: number = obj;
  130. halfToneSteps = fractionalKey - 57.0;
  131. }
  132. // Return frequency:
  133. return 440.0 * Math.pow(2, octaveSteps) * Math.pow(2, halfToneSteps / 12.0);
  134. }
  135. public static calcFractionalKey(frequency: number): number {
  136. // Return half-tone frequency:
  137. return Math.log(frequency / 440.0) / Math.LN10 * Pitch.halftoneFactor + 57.0;
  138. }
  139. public static fromFrequency(frequency: number): Pitch {
  140. const key: number = Pitch.calcFractionalKey(frequency) + 0.5;
  141. const octave: number = Math.floor(key / 12) - Pitch.octXmlDiff;
  142. const halftone: number = Math.floor(key) % 12;
  143. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  144. let accidental: AccidentalEnum = AccidentalEnum.NONE;
  145. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  146. fundamentalNote = <NoteEnum>(halftone - 1);
  147. accidental = AccidentalEnum.SHARP;
  148. }
  149. return new Pitch(fundamentalNote, octave, accidental);
  150. }
  151. public static fromHalftone(halftone: number): Pitch {
  152. const octave: number = Math.floor(halftone / 12) - Pitch.octXmlDiff;
  153. const halftoneInOctave: number = halftone % 12;
  154. let fundamentalNote: NoteEnum = <NoteEnum>halftoneInOctave;
  155. let accidental: AccidentalEnum = AccidentalEnum.NONE;
  156. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  157. fundamentalNote = <NoteEnum>(halftoneInOctave - 1);
  158. accidental = AccidentalEnum.SHARP;
  159. }
  160. return new Pitch(fundamentalNote, octave, accidental);
  161. }
  162. public static ceiling(halftone: number): NoteEnum {
  163. halftone = (halftone) % 12;
  164. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  165. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  166. fundamentalNote = <NoteEnum>(halftone + 1);
  167. }
  168. return fundamentalNote;
  169. }
  170. public static floor(halftone: number): NoteEnum {
  171. halftone = halftone % 12;
  172. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  173. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  174. fundamentalNote = <NoteEnum>(halftone - 1);
  175. }
  176. return fundamentalNote;
  177. }
  178. constructor(fundamentalNote: NoteEnum, octave: number, accidental: AccidentalEnum, accidentalXml: string = undefined) {
  179. this.fundamentalNote = fundamentalNote;
  180. this.octave = octave;
  181. this.accidental = accidental;
  182. this.accidentalXml = accidentalXml;
  183. this.halfTone = <number>(fundamentalNote) + (octave + Pitch.octXmlDiff) * 12 +
  184. Pitch.HalfTonesFromAccidental(accidental);
  185. this.frequency = Pitch.calcFrequency(this);
  186. }
  187. /** Turns an AccidentalEnum into half tone steps for pitch calculation.
  188. *
  189. */
  190. public static HalfTonesFromAccidental(accidental: AccidentalEnum): number {
  191. // about equal performance to hashmap/dictionary. could be turned into hashmap for convenience
  192. // switch is very slightly faster, but both are negligibly short anyways.
  193. switch (accidental) {
  194. // ordered from most to least common to improve average runtime
  195. case AccidentalEnum.NONE:
  196. return 0;
  197. case AccidentalEnum.SHARP:
  198. return 1;
  199. case AccidentalEnum.FLAT:
  200. return -1;
  201. case AccidentalEnum.NATURAL:
  202. return 0;
  203. case AccidentalEnum.DOUBLESHARP:
  204. return 2;
  205. case AccidentalEnum.DOUBLEFLAT:
  206. return -2;
  207. case AccidentalEnum.TRIPLESHARP: // very rare, in some classical pieces
  208. return 3;
  209. case AccidentalEnum.TRIPLEFLAT:
  210. return -3;
  211. case AccidentalEnum.QUARTERTONESHARP:
  212. return 0.5;
  213. case AccidentalEnum.QUARTERTONEFLAT:
  214. return -0.5;
  215. case AccidentalEnum.SLASHFLAT:
  216. return -0.51; // TODO currently necessary for quarter tone flat rendering after slash flat
  217. case AccidentalEnum.THREEQUARTERSSHARP:
  218. return 1.5;
  219. case AccidentalEnum.THREEQUARTERSFLAT:
  220. return -1.5;
  221. case AccidentalEnum.SLASHQUARTERSHARP:
  222. return 0.0013; // tmp for identification
  223. case AccidentalEnum.SLASHSHARP:
  224. return 0.0014; // tmp for identification
  225. case AccidentalEnum.DOUBLESLASHFLAT:
  226. return -0.0015; // tmp for identification
  227. case AccidentalEnum.SORI:
  228. return 0.0016; // tmp for identification
  229. case AccidentalEnum.KORON:
  230. return 0.0017; // tmp for identification
  231. default:
  232. throw new Error("Unhandled AccidentalEnum value");
  233. // return 0;
  234. }
  235. }
  236. public static AccidentalFromHalfTones(halfTones: number): AccidentalEnum {
  237. switch (halfTones) {
  238. case 0:
  239. // for enharmonic change, we won't get a Natural accidental. Maybe there are edge cases though?
  240. return AccidentalEnum.NONE;
  241. case 1:
  242. return AccidentalEnum.SHARP;
  243. case -1:
  244. return AccidentalEnum.FLAT;
  245. case 2:
  246. return AccidentalEnum.DOUBLESHARP;
  247. case -2:
  248. return AccidentalEnum.DOUBLEFLAT;
  249. case 3:
  250. return AccidentalEnum.TRIPLESHARP;
  251. case -3:
  252. return AccidentalEnum.TRIPLEFLAT;
  253. case 0.5:
  254. return AccidentalEnum.QUARTERTONESHARP;
  255. case -0.5:
  256. return AccidentalEnum.QUARTERTONEFLAT;
  257. case 1.5:
  258. return AccidentalEnum.THREEQUARTERSSHARP;
  259. case -1.5:
  260. return AccidentalEnum.THREEQUARTERSFLAT;
  261. default:
  262. if (halfTones > 0 && halfTones < 1) {
  263. return AccidentalEnum.QUARTERTONESHARP;
  264. } else if (halfTones < 0 && halfTones > -1) {
  265. return AccidentalEnum.QUARTERTONEFLAT;
  266. }
  267. // potentially unhandled or broken accidental halfTone value
  268. return AccidentalEnum.QUARTERTONESHARP; // to signal unhandled value
  269. }
  270. }
  271. /**
  272. * Converts AccidentalEnum to a string which represents an accidental in VexFlow
  273. * Can also be useful in other cases, but has to match Vexflow accidental codes.
  274. * @param accidental
  275. * @returns {string} Vexflow Accidental code
  276. */
  277. public static accidentalVexflow(accidental: AccidentalEnum): string {
  278. let acc: string;
  279. switch (accidental) {
  280. case AccidentalEnum.NATURAL:
  281. acc = "n";
  282. break;
  283. case AccidentalEnum.FLAT:
  284. acc = "b";
  285. break;
  286. case AccidentalEnum.SHARP:
  287. acc = "#";
  288. break;
  289. case AccidentalEnum.DOUBLESHARP:
  290. acc = "##";
  291. break;
  292. case AccidentalEnum.TRIPLESHARP:
  293. acc = "###";
  294. break;
  295. case AccidentalEnum.DOUBLEFLAT:
  296. acc = "bb";
  297. break;
  298. case AccidentalEnum.TRIPLEFLAT:
  299. acc = "bbs"; // there is no "bbb" in VexFlow yet, unfortunately.
  300. break;
  301. case AccidentalEnum.QUARTERTONESHARP:
  302. acc = "+";
  303. break;
  304. case AccidentalEnum.QUARTERTONEFLAT:
  305. acc = "d";
  306. break;
  307. case AccidentalEnum.SLASHFLAT:
  308. acc = "bs";
  309. break;
  310. case AccidentalEnum.THREEQUARTERSSHARP:
  311. acc = "++";
  312. break;
  313. case AccidentalEnum.THREEQUARTERSFLAT:
  314. acc = "db";
  315. break;
  316. case AccidentalEnum.SLASHQUARTERSHARP:
  317. acc = "+-";
  318. break;
  319. case AccidentalEnum.SLASHSHARP:
  320. acc = "++-";
  321. break;
  322. case AccidentalEnum.DOUBLESLASHFLAT:
  323. acc = "bss";
  324. break;
  325. case AccidentalEnum.SORI:
  326. acc = "o";
  327. break;
  328. case AccidentalEnum.KORON:
  329. acc = "k";
  330. break;
  331. default:
  332. }
  333. return acc;
  334. }
  335. public get AccidentalHalfTones(): number {
  336. return Pitch.HalfTonesFromAccidental(this.accidental);
  337. }
  338. public get Octave(): number {
  339. return this.octave;
  340. }
  341. public get FundamentalNote(): NoteEnum {
  342. return this.fundamentalNote;
  343. }
  344. public get Accidental(): AccidentalEnum {
  345. return this.accidental;
  346. }
  347. public get AccidentalXml(): string {
  348. return this.accidentalXml;
  349. }
  350. public get Frequency(): number {
  351. return this.frequency;
  352. }
  353. public static get OctaveXmlDifference(): number {
  354. return Pitch.octXmlDiff;
  355. }
  356. public getHalfTone(): number {
  357. return this.halfTone;
  358. }
  359. // This method returns a new Pitch transposed by the given factor
  360. public getTransposedPitch(factor: number): Pitch {
  361. if (factor > 12) {
  362. throw new Error("rewrite this method to handle bigger octave changes or don't use is with bigger octave changes!");
  363. }
  364. if (factor > 0) {
  365. return this.getHigherPitchByTransposeFactor(factor);
  366. }
  367. if (factor < 0) {
  368. return this.getLowerPitchByTransposeFactor(-factor);
  369. }
  370. return this;
  371. }
  372. public DoEnharmonicChange(): void {
  373. switch (this.accidental) {
  374. case AccidentalEnum.FLAT:
  375. case AccidentalEnum.DOUBLEFLAT:
  376. this.fundamentalNote = this.getPreviousFundamentalNote(this.fundamentalNote);
  377. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  378. (this.octave + Pitch.octXmlDiff) * 12));
  379. break;
  380. case AccidentalEnum.SHARP:
  381. case AccidentalEnum.DOUBLESHARP:
  382. this.fundamentalNote = this.getNextFundamentalNote(this.fundamentalNote);
  383. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  384. (this.octave + Pitch.octXmlDiff) * 12));
  385. break;
  386. default:
  387. return;
  388. }
  389. }
  390. public ToString(): string {
  391. let accidentalString: string = Pitch.accidentalVexflow(this.accidental);
  392. if (!accidentalString) {
  393. accidentalString = "";
  394. }
  395. return "Key: " + Pitch.getNoteEnumString(this.fundamentalNote) + accidentalString +
  396. ", Note: " + this.fundamentalNote + ", octave: " + this.octave.toString();
  397. }
  398. public OperatorEquals(p2: Pitch): boolean {
  399. const p1: Pitch = this;
  400. // if (ReferenceEquals(p1, p2)) {
  401. // return true;
  402. // }
  403. if (!p1 || !p2) {
  404. return false;
  405. }
  406. return (p1.FundamentalNote === p2.FundamentalNote && p1.Octave === p2.Octave && p1.Accidental === p2.Accidental);
  407. }
  408. public OperatorNotEqual(p2: Pitch): boolean {
  409. const p1: Pitch = this;
  410. return !(p1 === p2);
  411. }
  412. //These don't take into account accidentals! which isn't needed for our current purpose
  413. public OperatorFundamentalGreaterThan(p2: Pitch): boolean {
  414. const p1: Pitch = this;
  415. if (p1.Octave === p2.Octave) {
  416. return p1.FundamentalNote > p2.FundamentalNote;
  417. } else {
  418. return p1.Octave > p2.Octave;
  419. }
  420. }
  421. public OperatorFundamentalLessThan(p2: Pitch): boolean {
  422. const p1: Pitch = this;
  423. if (p1.Octave === p2.Octave) {
  424. return p1.FundamentalNote < p2.FundamentalNote;
  425. } else {
  426. return p1.Octave < p2.Octave;
  427. }
  428. }
  429. // This method returns a new Pitch factor-Halftones higher than the current Pitch
  430. private getHigherPitchByTransposeFactor(factor: number): Pitch {
  431. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  432. let newOctave: number = this.octave;
  433. let newNoteEnum: NoteEnum;
  434. if (noteEnumIndex + factor > Pitch.pitchEnumValues.length - 1) {
  435. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor - Pitch.pitchEnumValues.length];
  436. newOctave++;
  437. } else {
  438. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor];
  439. }
  440. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  441. }
  442. private getLowerPitchByTransposeFactor(factor: number): Pitch {
  443. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  444. let newOctave: number = this.octave;
  445. let newNoteEnum: NoteEnum;
  446. if (noteEnumIndex - factor < 0) {
  447. newNoteEnum = Pitch.pitchEnumValues[Pitch.pitchEnumValues.length + noteEnumIndex - factor];
  448. newOctave--;
  449. } else {
  450. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex - factor];
  451. }
  452. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  453. }
  454. private getNextFundamentalNote(fundamental: NoteEnum): NoteEnum {
  455. let i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  456. i = (i + 1) % Pitch.pitchEnumValues.length;
  457. return Pitch.pitchEnumValues[i];
  458. }
  459. private getPreviousFundamentalNote(fundamental: NoteEnum): NoteEnum {
  460. const i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  461. if (i > 0) {
  462. return Pitch.pitchEnumValues[i - 1];
  463. } else {
  464. return Pitch.pitchEnumValues[Pitch.pitchEnumValues.length - 1];
  465. }
  466. }
  467. }