OpenSheetMusicDisplay.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import {IXmlElement} from "./../Common/FileIO/Xml";
  2. import {VexFlowMusicSheetCalculator} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator";
  3. import {VexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/VexFlowBackend";
  4. import {MusicSheetReader} from "./../MusicalScore/ScoreIO/MusicSheetReader";
  5. import {GraphicalMusicSheet} from "./../MusicalScore/Graphical/GraphicalMusicSheet";
  6. import {MusicSheetCalculator} from "./../MusicalScore/Graphical/MusicSheetCalculator";
  7. import {VexFlowMusicSheetDrawer} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer";
  8. import {SvgVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/SvgVexFlowBackend";
  9. import {CanvasVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend";
  10. import {MusicSheet} from "./../MusicalScore/MusicSheet";
  11. import {Cursor} from "./Cursor";
  12. import {MXLHelper} from "../Common/FileIO/Mxl";
  13. import {Promise} from "es6-promise";
  14. import {AJAX} from "./AJAX";
  15. import * as log from "loglevel";
  16. import {DrawingParametersEnum, DrawingParameters} from "../MusicalScore/Graphical/DrawingParameters";
  17. import {IOSMDOptions, OSMDOptions, AutoBeamOptions} from "./OSMDOptions";
  18. import {EngravingRules} from "../MusicalScore/Graphical/EngravingRules";
  19. import {AbstractExpression} from "../MusicalScore/VoiceData/Expressions/AbstractExpression";
  20. /**
  21. * The main class and control point of OpenSheetMusicDisplay.<br>
  22. * It can display MusicXML sheet music files in an HTML element container.<br>
  23. * After the constructor, use load() and render() to load and render a MusicXML file.
  24. */
  25. export class OpenSheetMusicDisplay {
  26. /**
  27. * Creates and attaches an OpenSheetMusicDisplay object to an HTML element container.<br>
  28. * After the constructor, use load() and render() to load and render a MusicXML file.
  29. * @param container The container element OSMD will be rendered into.<br>
  30. * Either a string specifying the ID of an HTML container element,<br>
  31. * or a reference to the HTML element itself (e.g. div)
  32. * @param options An object for rendering options like the backend (svg/canvas) or autoResize.<br>
  33. * For defaults see the OSMDOptionsStandard method in the [[OSMDOptions]] class.
  34. */
  35. constructor(container: string|HTMLElement,
  36. options: IOSMDOptions = OSMDOptions.OSMDOptionsStandard()) {
  37. // Store container element
  38. if (typeof container === "string") {
  39. // ID passed
  40. this.container = document.getElementById(<string>container);
  41. } else if (container && "appendChild" in <any>container) {
  42. // Element passed
  43. this.container = <HTMLElement>container;
  44. }
  45. if (!this.container) {
  46. throw new Error("Please pass a valid div container to OpenSheetMusicDisplay");
  47. }
  48. if (options.autoResize === undefined) {
  49. options.autoResize = true;
  50. }
  51. this.setOptions(options);
  52. }
  53. public cursor: Cursor;
  54. public zoom: number = 1.0;
  55. private container: HTMLElement;
  56. private canvas: HTMLElement;
  57. private backend: VexFlowBackend;
  58. private innerElement: HTMLElement;
  59. private sheet: MusicSheet;
  60. private drawer: VexFlowMusicSheetDrawer;
  61. private graphic: GraphicalMusicSheet;
  62. private drawingParameters: DrawingParameters;
  63. private autoResizeEnabled: boolean;
  64. private resizeHandlerAttached: boolean;
  65. /**
  66. * Load a MusicXML file
  67. * @param content is either the url of a file, or the root node of a MusicXML document, or the string content of a .xml/.mxl file
  68. */
  69. public load(content: string|Document): Promise<{}> {
  70. // Warning! This function is asynchronous! No error handling is done here.
  71. this.reset();
  72. if (typeof content === "string") {
  73. const str: string = <string>content;
  74. const self: OpenSheetMusicDisplay = this;
  75. if (str.substr(0, 4) === "\x50\x4b\x03\x04") {
  76. // This is a zip file, unpack it first
  77. return MXLHelper.MXLtoXMLstring(str).then(
  78. (x: string) => {
  79. return self.load(x);
  80. },
  81. (err: any) => {
  82. log.debug(err);
  83. throw new Error("OpenSheetMusicDisplay: Invalid MXL file");
  84. }
  85. );
  86. }
  87. // Javascript loads strings as utf-16, which is wonderful BS if you want to parse UTF-8 :S
  88. if (str.substr(0, 3) === "\uf7ef\uf7bb\uf7bf") {
  89. // UTF with BOM detected, truncate first three bytes and pass along
  90. return self.load(str.substr(3));
  91. }
  92. if (str.substr(0, 5) === "<?xml") {
  93. // Parse the string representing an xml file
  94. const parser: DOMParser = new DOMParser();
  95. content = parser.parseFromString(str, "application/xml");
  96. } else if (str.length < 2083) {
  97. // Assume now "str" is a URL
  98. // Retrieve the file at the given URL
  99. return AJAX.ajax(str).then(
  100. (s: string) => { return self.load(s); },
  101. (exc: Error) => { throw exc; }
  102. );
  103. }
  104. }
  105. if (!content || !(<any>content).nodeName) {
  106. return Promise.reject(new Error("OpenSheetMusicDisplay: The document which was provided is invalid"));
  107. }
  108. const children: NodeList = (<Document>content).childNodes;
  109. let elem: Element;
  110. for (let i: number = 0, length: number = children.length; i < length; i += 1) {
  111. const node: Node = children[i];
  112. if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === "score-partwise") {
  113. elem = <Element>node;
  114. break;
  115. }
  116. }
  117. if (!elem) {
  118. return Promise.reject(new Error("OpenSheetMusicDisplay: Document is not a valid 'partwise' MusicXML"));
  119. }
  120. const score: IXmlElement = new IXmlElement(elem);
  121. const calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator();
  122. const reader: MusicSheetReader = new MusicSheetReader();
  123. this.sheet = reader.createMusicSheet(score, "Untitled Score");
  124. if (this.sheet === undefined) {
  125. // error loading sheet, probably already logged, do nothing
  126. return Promise.reject(new Error("given music sheet was incomplete or could not be loaded."));
  127. }
  128. this.graphic = new GraphicalMusicSheet(this.sheet, calc);
  129. if (this.drawingParameters.drawCursors && this.cursor) {
  130. this.cursor.init(this.sheet.MusicPartManager, this.graphic);
  131. }
  132. log.info(`Loaded sheet ${this.sheet.TitleString} successfully.`);
  133. return Promise.resolve({});
  134. }
  135. /**
  136. * Render the music sheet in the container
  137. */
  138. public render(): void {
  139. if (!this.graphic) {
  140. throw new Error("OpenSheetMusicDisplay: Before rendering a music sheet, please load a MusicXML file");
  141. }
  142. this.drawer.clear(); // clear canvas before setting width
  143. // Set page width
  144. const width: number = this.container.offsetWidth;
  145. this.sheet.pageWidth = width / this.zoom / 10.0;
  146. // Before introducing the following optimization (maybe irrelevant), tests
  147. // have to be modified to ensure that width is > 0 when executed
  148. //if (isNaN(width) || width === 0) {
  149. // return;
  150. //}
  151. // Calculate again
  152. this.graphic.reCalculate();
  153. const height: number = this.graphic.MusicPages[0].PositionAndShape.BorderBottom * 10.0 * this.zoom;
  154. if (this.drawingParameters.drawCursors) {
  155. this.graphic.Cursors.length = 0;
  156. }
  157. // Update Sheet Page
  158. this.drawer.resize(width, height);
  159. this.drawer.scale(this.zoom);
  160. // Finally, draw
  161. this.drawer.drawSheet(this.graphic);
  162. if (this.drawingParameters.drawCursors && this.cursor) {
  163. // Update the cursor position
  164. this.cursor.update();
  165. }
  166. }
  167. /** States whether the render() function can be safely called. */
  168. public IsReadyToRender(): boolean {
  169. return this.graphic !== undefined;
  170. }
  171. /** Clears what OSMD has drawn on its canvas. */
  172. public clear(): void {
  173. this.drawer.clear();
  174. this.reset(); // without this, resize will draw loaded sheet again
  175. }
  176. /** Set OSMD rendering options using an IOSMDOptions object.
  177. * Can be called during runtime. Also called by constructor.
  178. * For example, setOptions({autoResize: false}) will disable autoResize even during runtime.
  179. */
  180. public setOptions(options: IOSMDOptions): void {
  181. this.drawingParameters = new DrawingParameters();
  182. if (options.drawingParameters) {
  183. this.drawingParameters.DrawingParametersEnum =
  184. (<any>DrawingParametersEnum)[options.drawingParameters.toLowerCase()];
  185. }
  186. const updateExistingBackend: boolean = this.backend !== undefined;
  187. if (options.backend !== undefined || this.backend === undefined) {
  188. if (updateExistingBackend) {
  189. // TODO doesn't work yet, still need to create a new OSMD object
  190. this.drawer.clear();
  191. // musicSheetCalculator.clearSystemsAndMeasures() // maybe? don't have reference though
  192. // musicSheetCalculator.clearRecreatedObjects();
  193. }
  194. if (options.backend === undefined || options.backend.toLowerCase() === "svg") {
  195. this.backend = new SvgVexFlowBackend();
  196. } else {
  197. this.backend = new CanvasVexFlowBackend();
  198. }
  199. this.backend.initialize(this.container);
  200. this.canvas = this.backend.getCanvas();
  201. this.innerElement = this.backend.getInnerElement();
  202. this.enableOrDisableCursor(this.drawingParameters.drawCursors);
  203. // Create the drawer
  204. this.drawer = new VexFlowMusicSheetDrawer(this.canvas, this.backend, this.drawingParameters);
  205. }
  206. // individual drawing parameters options
  207. if (options.autoBeam !== undefined) {
  208. EngravingRules.Rules.AutoBeamNotes = options.autoBeam;
  209. }
  210. const autoBeamOptions: AutoBeamOptions = options.autoBeamOptions;
  211. if (autoBeamOptions) {
  212. if (autoBeamOptions.maintain_stem_directions === undefined) {
  213. autoBeamOptions.maintain_stem_directions = false;
  214. }
  215. EngravingRules.Rules.AutoBeamOptions = autoBeamOptions;
  216. if (autoBeamOptions.groups && autoBeamOptions.groups.length) {
  217. for (const fraction of autoBeamOptions.groups) {
  218. if (fraction.length !== 2) {
  219. throw new Error("Each fraction in autoBeamOptions.groups must be of length 2, e.g. [3,4] for beaming three fourths");
  220. }
  221. }
  222. }
  223. }
  224. if (options.coloringEnabled !== undefined) {
  225. EngravingRules.Rules.ColoringEnabled = options.coloringEnabled;
  226. }
  227. if (options.disableCursor) {
  228. this.drawingParameters.drawCursors = false;
  229. this.enableOrDisableCursor(this.drawingParameters.drawCursors);
  230. }
  231. // alternative to if block: this.drawingsParameters.drawCursors = options.drawCursors !== false. No if, but always sets drawingParameters.
  232. // note that every option can be undefined, which doesn't mean the option should be set to false.
  233. if (options.drawHiddenNotes) {
  234. this.drawingParameters.drawHiddenNotes = true;
  235. }
  236. if (options.drawTitle !== undefined) {
  237. this.drawingParameters.DrawTitle = options.drawTitle;
  238. // TODO these settings are duplicate in drawingParameters and EngravingRules. Maybe we only need them in EngravingRules.
  239. // this sets the parameter in DrawingParameters, which in turn sets the parameter in EngravingRules.
  240. // see settings below that don't call drawingParameters for the immediate approach
  241. }
  242. if (options.drawSubtitle !== undefined) {
  243. this.drawingParameters.DrawSubtitle = options.drawSubtitle;
  244. }
  245. if (options.drawLyricist !== undefined) {
  246. this.drawingParameters.DrawLyricist = options.drawLyricist;
  247. }
  248. if (options.drawCredits !== undefined) {
  249. this.drawingParameters.drawCredits = options.drawCredits;
  250. }
  251. if (options.drawPartNames !== undefined) {
  252. this.drawingParameters.DrawPartNames = options.drawPartNames;
  253. }
  254. if (options.drawFingerings === false) {
  255. EngravingRules.Rules.RenderFingerings = false;
  256. }
  257. if (options.fingeringPosition !== undefined) {
  258. EngravingRules.Rules.FingeringPosition = AbstractExpression.PlacementEnumFromString(options.fingeringPosition);
  259. }
  260. if (options.fingeringInsideStafflines !== undefined) {
  261. EngravingRules.Rules.FingeringInsideStafflines = options.fingeringInsideStafflines;
  262. }
  263. if (options.setWantedStemDirectionByXml !== undefined) {
  264. EngravingRules.Rules.SetWantedStemDirectionByXml = options.setWantedStemDirectionByXml;
  265. }
  266. if (options.defaultColorNotehead) {
  267. EngravingRules.Rules.DefaultColorNotehead = options.defaultColorNotehead;
  268. }
  269. if (options.defaultColorRest) {
  270. EngravingRules.Rules.DefaultColorRest = options.defaultColorRest;
  271. }
  272. if (options.defaultColorStem) {
  273. EngravingRules.Rules.DefaultColorStem = options.defaultColorStem;
  274. }
  275. if (options.defaultColorLabel) {
  276. EngravingRules.Rules.DefaultColorLabel = options.defaultColorLabel;
  277. }
  278. if (options.defaultColorTitle) {
  279. EngravingRules.Rules.DefaultColorTitle = options.defaultColorTitle;
  280. }
  281. if (options.tupletsRatioed) {
  282. EngravingRules.Rules.TupletsRatioed = true;
  283. }
  284. if (options.tupletsBracketed) {
  285. EngravingRules.Rules.TupletsBracketed = true;
  286. }
  287. if (options.tripletsBracketed) {
  288. EngravingRules.Rules.TripletsBracketed = true;
  289. }
  290. if (options.autoResize) {
  291. if (!this.resizeHandlerAttached) {
  292. this.autoResize();
  293. }
  294. this.autoResizeEnabled = true;
  295. } else if (options.autoResize === false) { // not undefined
  296. this.autoResizeEnabled = false;
  297. // we could remove the window EventListener here, but not necessary.
  298. }
  299. }
  300. /**
  301. * Sets the logging level for this OSMD instance. By default, this is set to `warn`.
  302. *
  303. * @param: content can be `trace`, `debug`, `info`, `warn` or `error`.
  304. */
  305. public setLogLevel(level: string): void {
  306. switch (level) {
  307. case "trace":
  308. log.setLevel(log.levels.TRACE);
  309. break;
  310. case "debug":
  311. log.setLevel(log.levels.DEBUG);
  312. break;
  313. case "info":
  314. log.setLevel(log.levels.INFO);
  315. break;
  316. case "warn":
  317. log.setLevel(log.levels.WARN);
  318. break;
  319. case "error":
  320. log.setLevel(log.levels.ERROR);
  321. break;
  322. default:
  323. log.warn(`Could not set log level to ${level}. Using warn instead.`);
  324. log.setLevel(log.levels.WARN);
  325. break;
  326. }
  327. }
  328. /**
  329. * Initialize this object to default values
  330. * FIXME: Probably unnecessary
  331. */
  332. private reset(): void {
  333. if (this.drawingParameters.drawCursors && this.cursor) {
  334. this.cursor.hide();
  335. }
  336. this.sheet = undefined;
  337. this.graphic = undefined;
  338. this.zoom = 1.0;
  339. }
  340. /**
  341. * Attach the appropriate handler to the window.onResize event
  342. */
  343. private autoResize(): void {
  344. const self: OpenSheetMusicDisplay = this;
  345. this.handleResize(
  346. () => {
  347. // empty
  348. },
  349. () => {
  350. // The following code is probably not needed
  351. // (the width should adapt itself to the max allowed)
  352. //let width: number = Math.max(
  353. // document.documentElement.clientWidth,
  354. // document.body.scrollWidth,
  355. // document.documentElement.scrollWidth,
  356. // document.body.offsetWidth,
  357. // document.documentElement.offsetWidth
  358. //);
  359. //self.container.style.width = width + "px";
  360. if (self.IsReadyToRender()) {
  361. self.render();
  362. }
  363. }
  364. );
  365. }
  366. /**
  367. * Helper function for managing window's onResize events
  368. * @param startCallback is the function called when resizing starts
  369. * @param endCallback is the function called when resizing (kind-of) ends
  370. */
  371. private handleResize(startCallback: () => void, endCallback: () => void): void {
  372. let rtime: number;
  373. let timeout: number = undefined;
  374. const delta: number = 200;
  375. const self: OpenSheetMusicDisplay = this;
  376. function resizeStart(): void {
  377. if (!self.AutoResizeEnabled) {
  378. return;
  379. }
  380. rtime = (new Date()).getTime();
  381. if (!timeout) {
  382. startCallback();
  383. rtime = (new Date()).getTime();
  384. timeout = window.setTimeout(resizeEnd, delta);
  385. }
  386. }
  387. function resizeEnd(): void {
  388. timeout = undefined;
  389. window.clearTimeout(timeout);
  390. if ((new Date()).getTime() - rtime < delta) {
  391. timeout = window.setTimeout(resizeEnd, delta);
  392. } else {
  393. endCallback();
  394. }
  395. }
  396. if ((<any>window).attachEvent) {
  397. // Support IE<9
  398. (<any>window).attachEvent("onresize", resizeStart);
  399. } else {
  400. window.addEventListener("resize", resizeStart);
  401. }
  402. this.resizeHandlerAttached = true;
  403. window.setTimeout(startCallback, 0);
  404. window.setTimeout(endCallback, 1);
  405. }
  406. /** Enable or disable (hide) the cursor.
  407. * @param enable whether to enable (true) or disable (false) the cursor
  408. */
  409. public enableOrDisableCursor(enable: boolean): void {
  410. this.drawingParameters.drawCursors = enable;
  411. if (enable) {
  412. if (!this.cursor) {
  413. this.cursor = new Cursor(this.innerElement, this);
  414. if (this.sheet && this.graphic) { // else init is called in load()
  415. this.cursor.init(this.sheet.MusicPartManager, this.graphic);
  416. }
  417. }
  418. } else { // disable cursor
  419. if (!this.cursor) {
  420. return;
  421. }
  422. this.cursor.hide();
  423. // this.cursor = undefined;
  424. // TODO cursor should be disabled, not just hidden. otherwise user can just call osmd.cursor.hide().
  425. // however, this could cause null calls (cursor.next() etc), maybe that needs some solution.
  426. }
  427. }
  428. //#region GETTER / SETTER
  429. public set DrawSkyLine(value: boolean) {
  430. if (this.drawer) {
  431. this.drawer.skyLineVisible = value;
  432. this.render();
  433. }
  434. }
  435. public get DrawSkyLine(): boolean {
  436. return this.drawer.skyLineVisible;
  437. }
  438. public set DrawBottomLine(value: boolean) {
  439. if (this.drawer) {
  440. this.drawer.bottomLineVisible = value;
  441. this.render();
  442. }
  443. }
  444. public get DrawBottomLine(): boolean {
  445. return this.drawer.bottomLineVisible;
  446. }
  447. public set DrawBoundingBox(value: string) {
  448. this.drawer.drawableBoundingBoxElement = value;
  449. this.render();
  450. }
  451. public get DrawBoundingBox(): string {
  452. return this.drawer.drawableBoundingBoxElement;
  453. }
  454. public get AutoResizeEnabled(): boolean {
  455. return this.autoResizeEnabled;
  456. }
  457. public set AutoResizeEnabled(value: boolean) {
  458. this.autoResizeEnabled = value;
  459. }
  460. public get Sheet(): MusicSheet {
  461. return this.sheet;
  462. }
  463. public get Drawer(): VexFlowMusicSheetDrawer {
  464. return this.drawer;
  465. }
  466. public get GraphicSheet(): GraphicalMusicSheet {
  467. return this.graphic;
  468. }
  469. public get DrawingParameters(): DrawingParameters {
  470. return this.drawingParameters;
  471. }
  472. //#endregion
  473. }