PageRenderTime 87ms CodeModel.GetById 16ms RepoModel.GetById 3ms app.codeStats 1ms

/src/note.js

http://github.com/0xfe/vexflow
JavaScript | 429 lines | 270 code | 72 blank | 87 comment | 39 complexity | 8b811f0a2ab91e099c050267ee2cc49a MD5 | raw file
  1. // [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
  2. //
  3. // ## Description
  4. //
  5. // This file implements an abstract interface for notes and chords that
  6. // are rendered on a stave. Notes have some common properties: All of them
  7. // have a value (e.g., pitch, fret, etc.) and a duration (quarter, half, etc.)
  8. //
  9. // Some notes have stems, heads, dots, etc. Most notational elements that
  10. // surround a note are called *modifiers*, and every note has an associated
  11. // array of them. All notes also have a rendering context and belong to a stave.
  12. import { Vex } from './vex';
  13. import { Flow } from './tables';
  14. import { Tickable } from './tickable';
  15. export class Note extends Tickable {
  16. static get CATEGORY() { return 'note'; }
  17. // Debug helper. Displays various note metrics for the given
  18. // note.
  19. static plotMetrics(ctx, note, yPos) {
  20. const metrics = note.getMetrics();
  21. const xStart = note.getAbsoluteX() - metrics.modLeftPx - metrics.leftDisplacedHeadPx;
  22. const xPre1 = note.getAbsoluteX() - metrics.leftDisplacedHeadPx;
  23. const xAbs = note.getAbsoluteX();
  24. const xPost1 = note.getAbsoluteX() + metrics.notePx;
  25. const xPost2 = note.getAbsoluteX() + metrics.notePx + metrics.rightDisplacedHeadPx;
  26. const xEnd = note.getAbsoluteX()
  27. + metrics.notePx
  28. + metrics.rightDisplacedHeadPx
  29. + metrics.modRightPx;
  30. const xFreedomRight = xEnd + (note.getFormatterMetrics().freedom.right || 0);
  31. const xWidth = xEnd - xStart;
  32. ctx.save();
  33. ctx.setFont('Arial', 8, '');
  34. ctx.fillText(Math.round(xWidth) + 'px', xStart + note.getXShift(), yPos);
  35. const y = (yPos + 7);
  36. function stroke(x1, x2, color, yy = y) {
  37. ctx.beginPath();
  38. ctx.setStrokeStyle(color);
  39. ctx.setFillStyle(color);
  40. ctx.setLineWidth(3);
  41. ctx.moveTo(x1 + note.getXShift(), yy);
  42. ctx.lineTo(x2 + note.getXShift(), yy);
  43. ctx.stroke();
  44. }
  45. stroke(xStart, xPre1, 'red');
  46. stroke(xPre1, xAbs, '#999');
  47. stroke(xAbs, xPost1, 'green');
  48. stroke(xPost1, xPost2, '#999');
  49. stroke(xPost2, xEnd, 'red');
  50. stroke(xEnd, xFreedomRight, '#DD0');
  51. stroke(xStart - note.getXShift(), xStart, '#BBB'); // Shift
  52. Vex.drawDot(ctx, xAbs + note.getXShift(), y, 'blue');
  53. const formatterMetrics = note.getFormatterMetrics();
  54. if (formatterMetrics.iterations > 0) {
  55. const spaceDeviation = formatterMetrics.space.deviation;
  56. const prefix = spaceDeviation >= 0 ? '+' : '';
  57. ctx.setFillStyle('red');
  58. ctx.fillText(prefix + Math.round(spaceDeviation),
  59. xAbs + note.getXShift(), yPos - 10);
  60. }
  61. ctx.restore();
  62. }
  63. static parseDuration(durationString) {
  64. if (typeof (durationString) !== 'string') { return null; }
  65. const regexp = /(\d*\/?\d+|[a-z])(d*)([nrhms]|$)/;
  66. const result = regexp.exec(durationString);
  67. if (!result) { return null; }
  68. const duration = result[1];
  69. const dots = result[2].length;
  70. const type = result[3] || 'n';
  71. return { duration, dots, type };
  72. }
  73. static parseNoteStruct(noteStruct) {
  74. const durationString = noteStruct.duration;
  75. const customTypes = [];
  76. // Preserve backwards-compatibility
  77. const durationProps = Note.parseDuration(durationString);
  78. if (!durationProps) { return null; }
  79. // If specified type is invalid, return null
  80. let type = noteStruct.type;
  81. if (type && !Flow.getGlyphProps.validTypes[type]) { return null; }
  82. // If no type specified, check duration or custom types
  83. if (!type) {
  84. type = durationProps.type || 'n';
  85. // If we have keys, try and check if we've got a custom glyph
  86. if (noteStruct.keys !== undefined) {
  87. noteStruct.keys.forEach((k, i) => {
  88. const result = k.split('/');
  89. // We have a custom glyph specified after the note eg. /X2
  90. customTypes[i] = (result && result.length === 3) ? result[2] : type;
  91. });
  92. }
  93. }
  94. // Calculate the tick duration of the note
  95. let ticks = Flow.durationToTicks(durationProps.duration);
  96. if (ticks == null) { return null; }
  97. // Are there any dots?
  98. const dots = noteStruct.dots ? noteStruct.dots : durationProps.dots;
  99. if (typeof (dots) !== 'number') { return null; }
  100. // Add ticks as necessary depending on the numbr of dots
  101. let currentTicks = ticks;
  102. for (let i = 0; i < dots; i++) {
  103. if (currentTicks <= 1) return null;
  104. currentTicks = currentTicks / 2;
  105. ticks += currentTicks;
  106. }
  107. return {
  108. duration: durationProps.duration,
  109. type,
  110. customTypes,
  111. dots,
  112. ticks,
  113. };
  114. }
  115. // Every note is a tickable, i.e., it can be mutated by the `Formatter` class for
  116. // positioning and layout.
  117. // To create a new note you need to provide a `noteStruct`, which consists
  118. // of the following fields:
  119. //
  120. // `type`: The note type (e.g., `r` for rest, `s` for slash notes, etc.)
  121. // `dots`: The number of dots, which affects the duration.
  122. // `duration`: The time length (e.g., `q` for quarter, `h` for half, `8` for eighth etc.)
  123. //
  124. // The range of values for these parameters are available in `src/tables.js`.
  125. constructor(noteStruct) {
  126. super();
  127. this.setAttribute('type', 'Note');
  128. if (!noteStruct) {
  129. throw new Vex.RuntimeError(
  130. 'BadArguments', 'Note must have valid initialization data to identify duration and type.'
  131. );
  132. }
  133. // Parse `noteStruct` and get note properties.
  134. const initStruct = Note.parseNoteStruct(noteStruct);
  135. if (!initStruct) {
  136. throw new Vex.RuntimeError(
  137. 'BadArguments', `Invalid note initialization object: ${JSON.stringify(noteStruct)}`
  138. );
  139. }
  140. // Set note properties from parameters.
  141. this.duration = initStruct.duration;
  142. this.dots = initStruct.dots;
  143. this.noteType = initStruct.type;
  144. this.customTypes = initStruct.customTypes;
  145. if (noteStruct.duration_override) {
  146. // Custom duration
  147. this.setDuration(noteStruct.duration_override);
  148. } else {
  149. // Default duration
  150. this.setIntrinsicTicks(initStruct.ticks);
  151. }
  152. this.modifiers = [];
  153. // Get the glyph code for this note from the font.
  154. this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
  155. this.customGlyphs = this.customTypes.map(t => Flow.getGlyphProps(this.duration, t));
  156. if (this.positions && (typeof (this.positions) !== 'object' || !this.positions.length)) {
  157. throw new Vex.RuntimeError('BadArguments', 'Note keys must be array type.');
  158. }
  159. // Note to play for audio players.
  160. this.playNote = null;
  161. // Positioning contexts used by the Formatter.
  162. this.tickContext = null; // The current tick context.
  163. this.modifierContext = null;
  164. this.ignore_ticks = false;
  165. // Positioning variables
  166. this.width = 0; // Width in pixels calculated after preFormat
  167. this.leftDisplacedHeadPx = 0; // Extra room on left for displaced note head
  168. this.rightDisplacedHeadPx = 0; // Extra room on right for displaced note head
  169. this.x_shift = 0; // X shift from tick context X
  170. this.voice = null; // The voice that this note is in
  171. this.preFormatted = false; // Is this note preFormatted?
  172. this.ys = []; // list of y coordinates for each note
  173. // we need to hold on to these for ties and beams.
  174. if (noteStruct.align_center) {
  175. this.setCenterAlignment(noteStruct.align_center);
  176. }
  177. // The render surface.
  178. this.stave = null;
  179. this.render_options = {
  180. annotation_spacing: 5,
  181. };
  182. }
  183. // Get and set the play note, which is arbitrary data that can be used by an
  184. // audio player.
  185. getPlayNote() { return this.playNote; }
  186. setPlayNote(note) { this.playNote = note; return this; }
  187. // Don't play notes by default, call them rests. This is also used by things like
  188. // beams and dots for positioning.
  189. isRest() { return false; }
  190. // TODO(0xfe): Why is this method here?
  191. addStroke(index, stroke) {
  192. stroke.setNote(this);
  193. stroke.setIndex(index);
  194. this.modifiers.push(stroke);
  195. this.setPreFormatted(false);
  196. return this;
  197. }
  198. // Get and set the target stave.
  199. getStave() { return this.stave; }
  200. setStave(stave) {
  201. this.stave = stave;
  202. this.setYs([stave.getYForLine(0)]); // Update Y values if the stave is changed.
  203. this.context = this.stave.context;
  204. return this;
  205. }
  206. // `Note` is not really a modifier, but is used in
  207. // a `ModifierContext`.
  208. getCategory() { return Note.CATEGORY; }
  209. // Set the rendering context for the note.
  210. setContext(context) { this.context = context; return this; }
  211. // Get and set spacing to the left and right of the notes.
  212. getLeftDisplacedHeadPx() { return this.leftDisplacedHeadPx; }
  213. getRightDisplacedHeadPx() { return this.rightDisplacedHeadPx; }
  214. setLeftDisplacedHeadPx(x) { this.leftDisplacedHeadPx = x; return this; }
  215. setRightDisplacedHeadPx(x) { this.rightDisplacedHeadPx = x; return this; }
  216. // Returns true if this note has no duration (e.g., bar notes, spacers, etc.)
  217. shouldIgnoreTicks() { return this.ignore_ticks; }
  218. // Get the stave line number for the note.
  219. getLineNumber() { return 0; }
  220. // Get the stave line number for rest.
  221. getLineForRest() { return 0; }
  222. // Get the glyph associated with this note.
  223. getGlyph() { return this.glyph; }
  224. getGlyphWidth() {
  225. // TODO: FIXME (multiple potential values for this.glyph)
  226. if (this.glyph) {
  227. if (this.glyph.getMetrics) {
  228. return this.glyph.getMetrics().width;
  229. } else if (this.glyph.getWidth) {
  230. return this.glyph.getWidth(this.render_options.glyph_font_scale);
  231. }
  232. }
  233. return 0;
  234. }
  235. // Set and get Y positions for this note. Each Y value is associated with
  236. // an individual pitch/key within the note/chord.
  237. setYs(ys) { this.ys = ys; return this; }
  238. getYs() {
  239. if (this.ys.length === 0) {
  240. throw new Vex.RERR('NoYValues', 'No Y-values calculated for this note.');
  241. }
  242. return this.ys;
  243. }
  244. // Get the Y position of the space above the stave onto which text can
  245. // be rendered.
  246. getYForTopText(text_line) {
  247. if (!this.stave) {
  248. throw new Vex.RERR('NoStave', 'No stave attached to this note.');
  249. }
  250. return this.stave.getYForTopText(text_line);
  251. }
  252. // Get a `BoundingBox` for this note.
  253. getBoundingBox() { return null; }
  254. // Returns the voice that this note belongs in.
  255. getVoice() {
  256. if (!this.voice) throw new Vex.RERR('NoVoice', 'Note has no voice.');
  257. return this.voice;
  258. }
  259. // Attach this note to `voice`.
  260. setVoice(voice) {
  261. this.voice = voice;
  262. this.preFormatted = false;
  263. return this;
  264. }
  265. // Get and set the `TickContext` for this note.
  266. getTickContext() { return this.tickContext; }
  267. setTickContext(tc) {
  268. this.tickContext = tc;
  269. this.preFormatted = false;
  270. return this;
  271. }
  272. // Accessors for the note type.
  273. getDuration() { return this.duration; }
  274. isDotted() { return (this.dots > 0); }
  275. hasStem() { return false; }
  276. getDots() { return this.dots; }
  277. getNoteType() { return this.noteType; }
  278. setBeam() { return this; } // ignore parameters
  279. // Attach this note to a modifier context.
  280. setModifierContext(mc) { this.modifierContext = mc; return this; }
  281. // Attach a modifier to this note.
  282. addModifier(modifier, index = 0) {
  283. modifier.setNote(this);
  284. modifier.setIndex(index);
  285. this.modifiers.push(modifier);
  286. this.setPreFormatted(false);
  287. return this;
  288. }
  289. // Get the coordinates for where modifiers begin.
  290. getModifierStartXY() {
  291. if (!this.preFormatted) {
  292. throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
  293. }
  294. return {
  295. x: this.getAbsoluteX(),
  296. y: this.ys[0],
  297. };
  298. }
  299. // Get bounds and metrics for this note.
  300. //
  301. // Returns a struct with fields:
  302. // `width`: The total width of the note (including modifiers.)
  303. // `notePx`: The width of the note head only.
  304. // `left_shift`: The horizontal displacement of the note.
  305. // `modLeftPx`: Start `X` for left modifiers.
  306. // `modRightPx`: Start `X` for right modifiers.
  307. // `leftDisplacedHeadPx`: Extra space on left of note.
  308. // `rightDisplacedHeadPx`: Extra space on right of note.
  309. getMetrics() {
  310. if (!this.preFormatted) {
  311. throw new Vex.RERR('UnformattedNote', "Can't call getMetrics on an unformatted note.");
  312. }
  313. const modLeftPx = this.modifierContext ? this.modifierContext.state.left_shift : 0;
  314. const modRightPx = this.modifierContext ? this.modifierContext.state.right_shift : 0;
  315. const width = this.getWidth();
  316. const glyphWidth = this.getGlyphWidth();
  317. const notePx = width
  318. - modLeftPx // subtract left modifiers
  319. - modRightPx // subtract right modifiers
  320. - this.leftDisplacedHeadPx // subtract left displaced head
  321. - this.rightDisplacedHeadPx; // subtract right displaced head
  322. return {
  323. // ----------
  324. // NOTE: If you change this, remember to update MockTickable in the tests/ directory.
  325. // --------------
  326. width,
  327. glyphWidth,
  328. notePx,
  329. // Modifier spacing.
  330. modLeftPx,
  331. modRightPx,
  332. // Displaced note head on left or right.
  333. leftDisplacedHeadPx: this.leftDisplacedHeadPx,
  334. rightDisplacedHeadPx: this.rightDisplacedHeadPx,
  335. };
  336. }
  337. // Get the absolute `X` position of this note's tick context. This
  338. // excludes x_shift, so you'll need to factor it in if you're
  339. // looking for the post-formatted x-position.
  340. getAbsoluteX() {
  341. if (!this.tickContext) {
  342. throw new Vex.RERR('NoTickContext', 'Note needs a TickContext assigned for an X-Value');
  343. }
  344. // Position note to left edge of tick context.
  345. let x = this.tickContext.getX();
  346. if (this.stave) {
  347. x += this.stave.getNoteStartX() + this.musicFont.lookupMetric('stave.padding');
  348. }
  349. if (this.isCenterAligned()) {
  350. x += this.getCenterXShift();
  351. }
  352. return x;
  353. }
  354. setPreFormatted(value) {
  355. this.preFormatted = value;
  356. }
  357. }