Pitch.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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): { value: 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): { value: 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, value: 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. public get AccidentalHalfTones(): number {
  220. return Pitch.HalfTonesFromAccidental(this.accidental);
  221. }
  222. public get Octave(): number {
  223. return this.octave;
  224. }
  225. public get FundamentalNote(): NoteEnum {
  226. return this.fundamentalNote;
  227. }
  228. public get Accidental(): AccidentalEnum {
  229. return this.accidental;
  230. }
  231. public get Frequency(): number {
  232. return this.frequency;
  233. }
  234. public static get OctaveXmlDifference(): number {
  235. return Pitch.octXmlDiff;
  236. }
  237. public getHalfTone(): number {
  238. return this.halfTone;
  239. }
  240. // This method returns a new Pitch transposed by the given factor
  241. public getTransposedPitch(factor: number): Pitch {
  242. if (factor > 12) {
  243. throw new Error("rewrite this method to handle bigger octave changes or don't use is with bigger octave changes!");
  244. }
  245. if (factor > 0) {
  246. return this.getHigherPitchByTransposeFactor(factor);
  247. }
  248. if (factor < 0) {
  249. return this.getLowerPitchByTransposeFactor(-factor);
  250. }
  251. return this;
  252. }
  253. public DoEnharmonicChange(): void {
  254. switch (this.accidental) {
  255. case AccidentalEnum.FLAT:
  256. case AccidentalEnum.DOUBLEFLAT:
  257. this.fundamentalNote = this.getPreviousFundamentalNote(this.fundamentalNote);
  258. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  259. (this.octave + Pitch.octXmlDiff) * 12));
  260. break;
  261. case AccidentalEnum.SHARP:
  262. case AccidentalEnum.DOUBLESHARP:
  263. this.fundamentalNote = this.getNextFundamentalNote(this.fundamentalNote);
  264. this.accidental = Pitch.AccidentalFromHalfTones(this.halfTone - (<number>(this.fundamentalNote) +
  265. (this.octave + Pitch.octXmlDiff) * 12));
  266. break;
  267. default:
  268. return;
  269. }
  270. }
  271. public ToString(): string {
  272. return "Note: " + this.fundamentalNote + ", octave: " + this.octave.toString() + ", alter: " +
  273. this.accidental;
  274. }
  275. public OperatorEquals(p2: Pitch): boolean {
  276. const p1: Pitch = this;
  277. // if (ReferenceEquals(p1, p2)) {
  278. // return true;
  279. // }
  280. if ((<Object>p1 === undefined) || (<Object>p2 === undefined)) {
  281. return false;
  282. }
  283. return (p1.FundamentalNote === p2.FundamentalNote && p1.Octave === p2.Octave && p1.Accidental === p2.Accidental);
  284. }
  285. public OperatorNotEqual(p2: Pitch): boolean {
  286. const p1: Pitch = this;
  287. return !(p1 === p2);
  288. }
  289. // This method returns a new Pitch factor-Halftones higher than the current Pitch
  290. private getHigherPitchByTransposeFactor(factor: number): Pitch {
  291. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  292. let newOctave: number = this.octave;
  293. let newNoteEnum: NoteEnum;
  294. if (noteEnumIndex + factor > Pitch.pitchEnumValues.length - 1) {
  295. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor - Pitch.pitchEnumValues.length];
  296. newOctave++;
  297. } else {
  298. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex + factor];
  299. }
  300. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  301. }
  302. private getLowerPitchByTransposeFactor(factor: number): Pitch {
  303. const noteEnumIndex: number = Pitch.pitchEnumValues.indexOf(this.fundamentalNote);
  304. let newOctave: number = this.octave;
  305. let newNoteEnum: NoteEnum;
  306. if (noteEnumIndex - factor < 0) {
  307. newNoteEnum = Pitch.pitchEnumValues[Pitch.pitchEnumValues.length + noteEnumIndex - factor];
  308. newOctave--;
  309. } else {
  310. newNoteEnum = Pitch.pitchEnumValues[noteEnumIndex - factor];
  311. }
  312. return new Pitch(newNoteEnum, newOctave, AccidentalEnum.NONE);
  313. }
  314. private getNextFundamentalNote(fundamental: NoteEnum): NoteEnum {
  315. let i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  316. i = (i + 1) % Pitch.pitchEnumValues.length;
  317. return Pitch.pitchEnumValues[i];
  318. }
  319. private getPreviousFundamentalNote(fundamental: NoteEnum): NoteEnum {
  320. const i: number = Pitch.pitchEnumValues.indexOf(fundamental);
  321. if (i > 0) {
  322. return Pitch.pitchEnumValues[i - 1];
  323. } else {
  324. return Pitch.pitchEnumValues[Pitch.pitchEnumValues.length - 1];
  325. }
  326. }
  327. }