UNPKG

webmscore

Version:

MuseScore's libmscore in WebAssembly! Read mscz data, and generate audio/MIDI/MusicXML/SVG/PNG/PDF sheets right in browsers

358 lines (319 loc) 10.2 kB
// @ts-check // The main entry point to use webmscore as a web worker, // implements the same API set as './index.js' import { WebMscoreWorker } from '../.cache/worker.js' import { getSelfURL, shimDom } from './utils.js' const MSCORE_SCRIPT_URL = getSelfURL() /** * Reconstruct `Error` objects sent from the web worker * * Native `Error` types can't be cloned by structured clone algorithm */ class WorkerError extends Error { /** * @param {Error} err */ constructor(err) { super() this.name = err.name this.message = err.message this.originalStack = err.stack } } /** * Set the log level when the instance is created * default: 0 (Off) * @see WebMscore.setLogLevel */ let _logLevel = 0 /** * Use webmscore as a web worker * @implements {import('./index').default} */ class WebMscoreW { /** * @hideconstructor use `WebMscoreW.load` */ constructor() { const url = URL.createObjectURL( new Blob([ `(function () { var MSCORE_SCRIPT_URL = "${MSCORE_SCRIPT_URL}";` // set the environment variable for worker + '(' + shimDom.toString() + ')();' // %INJECTION_HINT_1% + '(' + WebMscoreWorker.toString() + ')()' + '})()' ]) ) /** @private */ this.worker = new Worker(url) /** @private */ this.workerURL = url } /** * @returns {Promise<void>} */ static get ready() { // not implemented return Promise.resolve() } /** * The maximum MSCZ/MSCX file format version supported by webmscore */ static async version() { // not implemented return -1 } /** * Set log level * @param {0 | 1 | 2} level - See https://github.com/LibreScore/webmscore/blob/v1.0.0/src/framework/global/thirdparty/haw_logger/logger/log_base.h#L30-L33 * - 0: Off * - 1: Normal (`ERRR` or `WARN` or `INFO`) * - 2: Debug (`DEBG`) * @returns {Promise<void>} */ static async setLogLevel(level) { _logLevel = level } /** * Load score data * @param {import('../schemas').InputFileFormat} format * @param {Uint8Array} data * @param {Uint8Array[] | Promise<Uint8Array[]>} fonts load extra font files (CJK characters support) * @param {boolean} doLayout set to false if you only need the score metadata or the midi file (Super Fast, 3x faster than the musescore software) */ static async load(format, data, fonts = [], doLayout = true) { const instance = new WebMscoreW() const [_fonts] = await Promise.all([ fonts, instance.rpc('ready') ]) await instance.rpc('setLogLevel', [_logLevel]) // default 0 (Off) await instance.rpc('load', [format, data, _fonts, doLayout], [data.buffer, ..._fonts.map(f => f.buffer)]) return instance } /** * Communicate with the worker thread with JSON-RPC * @private * @typedef {{ id: number; result?: any; error?: any; }} RPCRes * @param {keyof import('./index').default | '_synthAudio' | 'processSynth' | 'processSynthBatch' | 'load' | 'ready' | 'setLogLevel'} method * @param {any[]} params * @param {Transferable[]} transfer */ async rpc(method, params = [], transfer = []) { const id = Math.random() return new Promise((resolve, reject) => { const listener = (e) => { /** @type {RPCRes} */ const data = e.data if (data.id === id) { if (data.error) { reject(new WorkerError(data.error)) } this.worker.removeEventListener('message', listener) resolve(data.result) } } this.worker.addEventListener('message', listener) this.worker.postMessage({ id, method, params, }, transfer) }) } /** * Only save this excerpt (linked parts) of the score * * if no excerpts, generate excerpts from existing instrument parts * * @param {number} id `-1` means the full score * @returns {Promise<void>} */ setExcerptId(id) { return this.rpc('setExcerptId', [id]) } /** * @returns {Promise<number>} */ getExcerptId() { return this.rpc('getExcerptId') } /** * Generate excerpts from Parts (only parts that are visible) if no existing excerpts * @returns {Promise<void>} */ generateExcerpts() { return this.rpc('generateExcerpts') } /** * Get the score title * @returns {Promise<string>} */ title() { return this.rpc('title') } /** * Get the score title (filename safe, replaced some characters) * @returns {Promise<string>} */ titleFilenameSafe() { return this.rpc('titleFilenameSafe') } /** * Get the number of pages in the score (or the excerpt if `excerptId` is set) * @returns {Promise<number>} */ npages() { return this.rpc('npages') } /** * Get score metadata * @returns {Promise<import('../schemas').ScoreMetadata>} */ metadata() { return this.rpc('metadata') } /** * Get the positions of measures * @returns {Promise<import('../schemas').Positions>} */ measurePositions() { return this.rpc('measurePositions') } /** * Get the positions of segments * @returns {Promise<import('../schemas').Positions>} */ segmentPositions() { return this.rpc('segmentPositions') } /** * Export score as MusicXML file * @returns {Promise<string>} contents of the MusicXML file (plain text) */ saveXml() { return this.rpc('saveXml') } /** * Export score as compressed MusicXML file * @returns {Promise<Uint8Array>} */ saveMxl() { return this.rpc('saveMxl') } /** * Save part score as MSCZ/MSCX file * @param {'mscz' | 'mscx'} format * @returns {Promise<Uint8Array>} */ saveMsc(format = 'mscz') { return this.rpc('saveMsc', [format]) } /** * Export score as the SVG file of one page * @param {number} pageNumber integer * @param {boolean} drawPageBackground * @returns {Promise<string>} contents of the SVG file (plain text) */ saveSvg(pageNumber = 0, drawPageBackground = false) { return this.rpc('saveSvg', [pageNumber, drawPageBackground]) } /** * Export score as the PNG file of one page * @param {number} pageNumber integer * @param {boolean} drawPageBackground * @param {boolean} transparent * @returns {Promise<Uint8Array>} */ savePng(pageNumber = 0, drawPageBackground = false, transparent = true) { return this.rpc('savePng', [pageNumber, drawPageBackground, transparent]) } /** * Export score as PDF file * @returns {Promise<Uint8Array>} */ savePdf() { return this.rpc('savePdf') } /** * Export score as MIDI file * @param {boolean} midiExpandRepeats * @param {boolean} exportRPNs * @returns {Promise<Uint8Array>} */ saveMidi(midiExpandRepeats = true, exportRPNs = true) { return this.rpc('saveMidi', [midiExpandRepeats, exportRPNs]) } /** * Set the soundfont (sf2/sf3) data * @param {Uint8Array} data * @returns {Promise<void>} */ setSoundFont(data) { return this.rpc('setSoundFont', [data], [data.buffer]) } /** * Export score as audio file (wav/ogg/flac/mp3) * @param {'wav' | 'ogg' | 'flac' | 'mp3'} format * @returns {Promise<Uint8Array>} */ saveAudio(format) { return this.rpc('saveAudio', [format]) } /** * Export positions of measures or segments (if `ofSegments` == true) as JSON string * @param {boolean} ofSegments * @also `score.measurePositions()` and `score.segmentPositions()` * @returns {Promise<string>} */ savePositions(ofSegments) { return this.rpc('savePositions', [ofSegments]) } /** * Synthesize audio frames * @param {number} starttime The start time offset in seconds * @returns {Promise<(cancel?: boolean) => Promise<import('../schemas').SynthRes>>} The iterator function */ async synthAudio(starttime = 0) { const fnptr = await this.rpc('_synthAudio', [starttime]) return (cancel) => { return this.rpc('processSynth', [fnptr, cancel]) } } /** * Synthesize audio frames in bulk * @param {number} starttime - The start time offset in seconds * @param {number} batchSize - max number of result SynthRes' (n * 512 frames) * @returns {Promise<(cancel?: boolean) => Promise<import('../schemas').SynthRes[]>>} */ async synthAudioBatch(starttime, batchSize) { const fnptr = await this.rpc('_synthAudio', [starttime]) return (cancel) => { return this.rpc('processSynthBatch', [fnptr, batchSize, cancel]) } } /** * Export score metadata as JSON string * @also `score.metadata()` * @returns {Promise<string>} contents of the JSON file */ saveMetadata() { return this.rpc('saveMetadata') } /** * @param {boolean=} soft (default `true`) * * `true` destroy the score instance only, or * * `false` destroy the whole WebMscore webworker context * @returns {void} */ destroy(soft = true) { if (soft) { // destroy the score instance only this.rpc('destroy', [soft]) } else { // destroy the whole WebMscore webworker context // the default behaviour prior to v0.9.0 this.worker.terminate() URL.revokeObjectURL(this.workerURL) // GC } } } export default WebMscoreW