Pitch.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. }
  28. // This class represents a musical note. The middle A (440 Hz) lies in the octave with the value 1.
  29. export class Pitch {
  30. public static pitchEnumValues: NoteEnum[] = [
  31. NoteEnum.C, NoteEnum.D, NoteEnum.E, NoteEnum.F, NoteEnum.G, NoteEnum.A, NoteEnum.B,
  32. ];
  33. private static halftoneFactor: number = 12 / (Math.LN2 / Math.LN10);
  34. private static octXmlDiff: number = 3;
  35. // private _sourceOctave: number;
  36. // private _sourceFundamentalNote: NoteEnum;
  37. // private _sourceAccidental: AccidentalEnum = AccidentalEnum.NONE;
  38. private octave: number;
  39. private fundamentalNote: NoteEnum;
  40. private accidental: AccidentalEnum = AccidentalEnum.NONE;
  41. private frequency: number;
  42. private halfTone: number;
  43. public static getNoteEnumString(note: NoteEnum): string {
  44. switch (note) {
  45. case NoteEnum.C:
  46. return "C";
  47. case NoteEnum.D:
  48. return "D";
  49. case NoteEnum.E:
  50. return "E";
  51. case NoteEnum.F:
  52. return "F";
  53. case NoteEnum.G:
  54. return "G";
  55. case NoteEnum.A:
  56. return "A";
  57. case NoteEnum.B:
  58. return "B";
  59. default:
  60. return "";
  61. }
  62. }
  63. /**
  64. * @param the input pitch
  65. * @param the number of halftones to transpose with
  66. * @returns ret[0] = the transposed fundamental.
  67. * ret[1] = the octave shift (not the new octave!)
  68. * @constructor
  69. */
  70. public static CalculateTransposedHalfTone(pitch: Pitch, transpose: number): { halftone: number; overflow: number; } {
  71. const newHalfTone: number = <number>pitch.fundamentalNote + pitch.AccidentalHalfTones + transpose;
  72. return Pitch.WrapAroundCheck(newHalfTone, 12);
  73. }
  74. public static WrapAroundCheck(value: number, limit: number): { halftone: number; overflow: number; } {
  75. let overflow: number = 0;
  76. while (value < 0) {
  77. value += limit;
  78. overflow--; // the octave change
  79. }
  80. while (value >= limit) {
  81. value -= limit;
  82. overflow++; // the octave change
  83. }
  84. return {overflow: overflow, halftone: value};
  85. }
  86. //public static calcFrequency(pitch: Pitch): number;
  87. //public static calcFrequency(fractionalKey: number): number;
  88. public static calcFrequency(obj: Pitch|number): number {
  89. let octaveSteps: number = 0;
  90. let halfToneSteps: number;
  91. if (obj instanceof Pitch) {
  92. // obj is a pitch
  93. const pitch: Pitch = obj;
  94. octaveSteps = pitch.octave - 1;
  95. halfToneSteps = <number>pitch.fundamentalNote - <number>NoteEnum.A + pitch.AccidentalHalfTones;
  96. } else if (typeof obj === "number") {
  97. // obj is a fractional key
  98. const fractionalKey: number = obj;
  99. halfToneSteps = fractionalKey - 57.0;
  100. }
  101. // Return frequency:
  102. return 440.0 * Math.pow(2, octaveSteps) * Math.pow(2, halfToneSteps / 12.0);
  103. }
  104. public static calcFractionalKey(frequency: number): number {
  105. // Return half-tone frequency:
  106. return Math.log(frequency / 440.0) / Math.LN10 * Pitch.halftoneFactor + 57.0;
  107. }
  108. public static fromFrequency(frequency: number): Pitch {
  109. const key: number = Pitch.calcFractionalKey(frequency) + 0.5;
  110. const octave: number = Math.floor(key / 12) - Pitch.octXmlDiff;
  111. const halftone: number = Math.floor(key) % 12;
  112. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  113. let accidental: AccidentalEnum = AccidentalEnum.NONE;
  114. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  115. fundamentalNote = <NoteEnum>(halftone - 1);
  116. accidental = AccidentalEnum.SHARP;
  117. }
  118. return new Pitch(fundamentalNote, octave, accidental);
  119. }
  120. public static fromHalftone(halftone: number): Pitch {
  121. const octave: number = Math.floor(halftone / 12) - Pitch.octXmlDiff;
  122. const halftoneInOctave: number = halftone % 12;
  123. let fundamentalNote: NoteEnum = <NoteEnum>halftoneInOctave;
  124. let accidental: AccidentalEnum = AccidentalEnum.NONE;
  125. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  126. fundamentalNote = <NoteEnum>(halftoneInOctave - 1);
  127. accidental = AccidentalEnum.SHARP;
  128. }
  129. return new Pitch(fundamentalNote, octave, accidental);
  130. }
  131. public static ceiling(halftone: number): NoteEnum {
  132. halftone = (halftone) % 12;
  133. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  134. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  135. fundamentalNote = <NoteEnum>(halftone + 1);
  136. }
  137. return fundamentalNote;
  138. }
  139. public static floor(halftone: number): NoteEnum {
  140. halftone = halftone % 12;
  141. let fundamentalNote: NoteEnum = <NoteEnum>halftone;
  142. if (this.pitchEnumValues.indexOf(fundamentalNote) === -1) {
  143. fundamentalNote = <NoteEnum>(halftone - 1);
  144. }
  145. return fundamentalNote;
  146. }
  147. constructor(fundamentalNote: NoteEnum, octave: number, accidental: AccidentalEnum) {
  148. this.fundamentalNote = fundamentalNote;
  149. this.octave = octave;
  150. this.accidental = accidental;
  151. this.halfTone = <number>(fundamentalNote) + (octave + Pitch.octXmlDiff) * 12 +
  152. Pitch.HalfTonesFromAccidental(accidental);
  153. this.frequency = Pitch.calcFrequency(this);
  154. }
  155. /** Turns an AccidentalEnum into half tone steps for pitch calculation.
  156. *
  157. */
  158. public static HalfTonesFromAccidental(accidental: AccidentalEnum): number {
  159. // about equal performance to hashmap/dictionary. could be turned into hashmap for convenience
  160. // switch is very slightly faster, but both are negligibly short anyways.
  161. switch (accidental) {
  162. // ordered from most to least common to improve average runtime
  163. case AccidentalEnum.NONE:
  164. return 0;
  165. case AccidentalEnum.SHARP:
  166. return 1;
  167. case AccidentalEnum.FLAT:
  168. return -1;
  169. case AccidentalEnum.NATURAL:
  170. return 0;
  171. case AccidentalEnum.DOUBLESHARP:
  172. return 2;
  173. case AccidentalEnum.DOUBLEFLAT:
  174. return -2;
  175. case AccidentalEnum.QUARTERTONESHARP:
  176. return 0.5;
  177. case AccidentalEnum.QUARTERTONEFLAT:
  178. return -0.5;
  179. case AccidentalEnum.TRIPLESHARP: // very rare, in some classical pieces
  180. return 3;
  181. case AccidentalEnum.TRIPLEFLAT:
  182. return -3;
  183. default:
  184. throw new Error("Unhandled AccidentalEnum value");
  185. // return 0;
  186. }
  187. }
  188. public static AccidentalFromHalfTones(halfTones: number): AccidentalEnum {
  189. switch (halfTones) {
  190. case 0:
  191. // for enharmonic change, we won't get a Natural accidental. Maybe there are edge cases though?
  192. return AccidentalEnum.NONE;
  193. case 1:
  194. return AccidentalEnum.SHARP;
  195. case -1:
  196. return AccidentalEnum.FLAT;
  197. case 2:
  198. return AccidentalEnum.DOUBLESHARP;
  199. case -2:
  200. return AccidentalEnum.DOUBLEFLAT;
  201. case 0.5:
  202. return AccidentalEnum.QUARTERTONESHARP;
  203. case -0.5:
  204. return AccidentalEnum.QUARTERTONEFLAT;
  205. case 3:
  206. return AccidentalEnum.TRIPLESHARP;
  207. case -3:
  208. return AccidentalEnum.TRIPLEFLAT;
  209. default:
  210. if (halfTones > 0 && halfTones < 1) {
  211. return AccidentalEnum.QUARTERTONESHARP;
  212. } else if (halfTones < 0 && halfTones > -1) {
  213. return AccidentalEnum.QUARTERTONEFLAT;
  214. }
  215. // potentially unhandled or broken accidental halfTone value
  216. return AccidentalEnum.QUARTERTONESHARP; // to signal unhandled value
  217. }
  218. }
  219. /**
  220. * Converts AccidentalEnum to a string which represents an accidental in VexFlow
  221. * Can also be useful in other cases, but has to match Vexflow accidental codes.
  222. * @param accidental
  223. * @returns {string} Vexflow Accidental code
  224. */
  225. public static accidentalVexflow(accidental: AccidentalEnum): string {
  226. let acc: string;
  227. switch (accidental) {
  228. case AccidentalEnum.NATURAL:
  229. acc = "n";
  230. break;
  231. case AccidentalEnum.FLAT:
  232. acc = "b";
  233. break;
  234. case AccidentalEnum.SHARP:
  235. acc = "#";
  236. break;
  237. case AccidentalEnum.DOUBLESHARP:
  238. acc = "##";
  239. break;
  240. case AccidentalEnum.TRIPLESHARP:
  241. acc = "++";
  242. break;
  243. case AccidentalEnum.DOUBLEFLAT:
  244. acc = "bb";
  245. break;
  246. case AccidentalEnum.TRIPLEFLAT:
  247. acc = "bbs"; // there is no "bbb" in VexFlow yet, unfortunately.
  248. break;
  249. case AccidentalEnum.QUARTERTONESHARP:
  250. acc = "+";
  251. break;
  252. case AccidentalEnum.QUARTERTONEFLAT:
  253. acc = "d";
  254. break;
  255. default:
  256. }
  257. return acc;
  258. }
  259. public get AccidentalHalfTones(): number {
  260. return Pitch.HalfTonesFromAccidental(this.accidental);
  261. }
  262. public get Octave(): number {
  263. return this.octave;
  264. }
  265. public get FundamentalNote(): NoteEnum {
  266. return this.fundamentalNote;
  267. }
  268. public get Accidental(): AccidentalEnum {
  269. return this.accidental;
  270. }
  271. public get Frequency(): number {
  272. return this.frequency;
  273. }
  274. public static get OctaveXmlDifference(): number {
  275. return Pitch.octXmlDiff;
  276. }
  277. public getHalfTone(): number {
  278. return this.halfTone;
  279. }
  280. // This method returns a new Pitch transposed by the given factor
  281. public getTransposedPitch(factor: number): Pitch {
  282. if (factor > 12) {
  283. throw new Error("rewrite this method to handle bigger octave changes or don't use is with bigger octave changes!");
  284. }
  285. if (factor > 0) {
  286. return this.getHigherPitchByTransposeFactor(factor);
  287. }
  288. if (factor < 0) {
  289. return this.getLowerPitchByTransposeFactor(-factor);
  290. }
  291. return this;
  292. }
  293. public DoEnharmonicChange(): void {
  294. switch (this.accidental) {
  295. case AccidentalEnum.FLAT:
  296. case AccidentalEnum.DOUBLEFLAT:
  297. this.fundamentalNote = this.getPreviousFundamentalNote(this.fundamentalNote);
  298. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  299. (this.octave + Pitch.octXmlDiff) * 12));
  300. break;
  301. case AccidentalEnum.SHARP:
  302. case AccidentalEnum.DOUBLESHARP:
  303. this.fundamentalNote = this.getNextFundamentalNote(this.fundamentalNote);
  304. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  305. (this.octave + Pitch.octXmlDiff) * 12));
  306. break;
  307. default:
  308. return;
  309. }
  310. }
  311. public ToString(): string {
  312. let accidentalString: string = Pitch.accidentalVexflow(this.accidental);
  313. if (!accidentalString) {
  314. accidentalString = "";
  315. }
  316. return "Key: " + Pitch.getNoteEnumString(this.fundamentalNote) + accidentalString +
  317. ", Note: " + this.fundamentalNote + ", octave: " + this.octave.toString();
  318. }
  319. public OperatorEquals(p2: Pitch): boolean {
  320. const p1: Pitch = this;
  321. // if (ReferenceEquals(p1, p2)) {
  322. // return true;
  323. // }
  324. if (!p1 || !p2) {
  325. return false;
  326. }
  327. return (p1.FundamentalNote === p2.FundamentalNote && p1.Octave === p2.Octave && p1.Accidental === p2.Accidental);
  328. }
  329. public OperatorNotEqual(p2: Pitch): boolean {
  330. const p1: Pitch = this;
  331. return !(p1 === p2);
  332. }
  333. //These don't take into account accidentals! which isn't needed for our current purpose
  334. public OperatorFundamentalGreaterThan(p2: Pitch): boolean {
  335. const p1: Pitch = this;
  336. if (p1.Octave === p2.Octave) {
  337. return p1.FundamentalNote > p2.FundamentalNote;
  338. } else {
  339. return p1.Octave > p2.Octave;
  340. }
  341. }
  342. public OperatorFundamentalLessThan(p2: Pitch): boolean {
  343. const p1: Pitch = this;
  344. if (p1.Octave === p2.Octave) {
  345. return p1.FundamentalNote < p2.FundamentalNote;
  346. } else {
  347. return p1.Octave < p2.Octave;
  348. }
  349. }
  350. // This method returns a new Pitch factor-Halftones higher than the current Pitch
  351. private getHigherPitchByTransposeFactor(factor: number): Pitch {
  352. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  353. let newOctave: number = this.octave;
  354. let newNoteEnum: NoteEnum;
  355. if (noteEnumIndex + factor > Pitch.pitchEnumValues.length - 1) {
  356. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor - Pitch.pitchEnumValues.length];
  357. newOctave++;
  358. } else {
  359. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor];
  360. }
  361. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  362. }
  363. private getLowerPitchByTransposeFactor(factor: number): Pitch {
  364. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  365. let newOctave: number = this.octave;
  366. let newNoteEnum: NoteEnum;
  367. if (noteEnumIndex - factor < 0) {
  368. newNoteEnum = Pitch.pitchEnumValues[Pitch.pitchEnumValues.length + noteEnumIndex - factor];
  369. newOctave--;
  370. } else {
  371. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex - factor];
  372. }
  373. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  374. }
  375. private getNextFundamentalNote(fundamental: NoteEnum): NoteEnum {
  376. let i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  377. i = (i + 1) % Pitch.pitchEnumValues.length;
  378. return Pitch.pitchEnumValues[i];
  379. }
  380. private getPreviousFundamentalNote(fundamental: NoteEnum): NoteEnum {
  381. const i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  382. if (i > 0) {
  383. return Pitch.pitchEnumValues[i - 1];
  384. } else {
  385. return Pitch.pitchEnumValues[Pitch.pitchEnumValues.length - 1];
  386. }
  387. }
  388. }