UNPKG

flat-embed

Version:

Interact and get events from Flat's Sheet Music Embed

1 lines 83.8 kB
{"version":3,"file":"flat-embed.mjs","sources":["../src/lib/compatibility.ts","../src/lib/callback.ts","../src/lib/communication.ts","../src/lib/dom.ts","../src/lib/embed.ts","../src/embed.ts"],"sourcesContent":["if (typeof window.postMessage === 'undefined') {\n throw new Error('The Flat Embed JS API is not supported in this browser');\n}\n","import type Embed from '../embed';\nimport type {\n EmbedMessageReceived,\n EmbedMessageReceivedEvent,\n EmbedMessageReceivedMethod,\n} from '../types';\nimport type { EmbedEventName } from '../types/events';\n\nclass EmbedCallback {\n embed: Embed;\n promises: Partial<\n Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }[]>\n >;\n eventCallbacks: Partial<Record<EmbedEventName, ((parameters: unknown) => void)[]>>;\n\n constructor(embed: Embed) {\n this.embed = embed;\n this.promises = {};\n this.eventCallbacks = {};\n }\n\n pushCall(\n // NOTE: We could type this with list of public methods\n name: string,\n resolve: (value: unknown) => void,\n reject: (reason?: unknown) => void,\n ) {\n this.promises[name] = this.promises[name] || [];\n this.promises[name]!.push({ resolve, reject });\n }\n\n /**\n * Register a callback for a specified event\n *\n * @param event The name of the event.\n * @param callback The function to call when receiving an event\n * @return `true` if it is the first subscriber, `false otherwise`\n */\n subscribeEvent(event: EmbedEventName, callback: (parameters: unknown) => void): boolean {\n this.eventCallbacks[event] = this.eventCallbacks[event] || [];\n this.eventCallbacks[event]!.push(callback);\n return this.eventCallbacks[event]!.length === 1;\n }\n\n /**\n * Unregister a callback for a specified event\n *\n * @param event The name of the event.\n * @param callback The function to call when receiving an event\n * @return `true` if it is the last subscriber, `false otherwise`\n */\n unsubscribeEvent(event: EmbedEventName, callback?: (parameters: unknown) => void): boolean {\n // Was not subscribed\n if (!this.eventCallbacks[event]) {\n return false;\n }\n\n // If a callback is specified, unsub this one\n if (callback) {\n const idx = this.eventCallbacks[event]!.indexOf(callback);\n if (idx >= 0) {\n this.eventCallbacks[event]!.splice(idx, 1);\n }\n }\n // Unsub all\n else {\n this.eventCallbacks[event] = [];\n }\n\n return !callback || this.eventCallbacks[event]!.length === 0;\n }\n\n /**\n * Process a message received from postMessage\n *\n * @param {object} data The data received from postMessage\n */\n process(data: EmbedMessageReceived) {\n if ('method' in data && data.method) {\n this.processMethodResponse(data);\n } else if ('event' in data && data.event) {\n this.processEvent(data);\n }\n }\n\n /**\n * Process a method response\n *\n * @param {object} data The data received from postMessage\n */\n processMethodResponse(data: EmbedMessageReceivedMethod) {\n if (!this.promises[data.method]) {\n return;\n }\n const promise = this.promises[data.method]!.shift();\n if (!promise) {\n return;\n }\n if (data.error) {\n promise.reject(data.error);\n } else {\n promise.resolve(data.response);\n }\n }\n\n /**\n * Process a receieved event\n *\n * @param {object} data The data received from postMessage\n */\n processEvent(data: EmbedMessageReceivedEvent) {\n if (!this.eventCallbacks[data.event] || this.eventCallbacks[data.event]!.length === 0) {\n return;\n }\n this.eventCallbacks[data.event]!.forEach(callback => {\n callback.call(this.embed, data.parameters);\n });\n }\n}\n\nexport default EmbedCallback;\n","import type Embed from '../embed';\nimport type { EmbedMessageReceived } from '../types';\n\n/**\n * Send a message to the embed via postMessage\n *\n * @param embed The instance of the embed where to send the message\n * @param method The name of the method to call\n * @param parameters The parameters to pass to the method\n */\nexport function postMessage(embed: Embed, method: string, parameters?: unknown): void {\n if (!embed.element.contentWindow || !embed.element.contentWindow.postMessage) {\n throw new Error('No `contentWindow` or `contentWindow.postMessage` available on the element');\n }\n\n const message = {\n method,\n parameters,\n };\n\n embed.element.contentWindow.postMessage(message, embed.origin);\n}\n\n/**\n * Parse a message received from postMessage\n *\n * @param data The data received from postMessage\n * @return Received message from the embed\n */\nexport function parseMessage(data: string | Record<string, unknown>): EmbedMessageReceived {\n if (typeof data === 'string') {\n data = JSON.parse(data);\n }\n return data as unknown as EmbedMessageReceived;\n}\n","/**\n * Select and normalize the DOM element input\n */\nexport function normalizeElement(\n element: HTMLIFrameElement | HTMLElement | string,\n): HTMLIFrameElement | HTMLElement {\n // Find an element by identifier\n if (typeof element === 'string') {\n const container = document.getElementById(element);\n if (!container) {\n throw new TypeError(`The DOM element with the identifier \"${element}\" was not found.`);\n }\n element = container;\n }\n\n // Check if a DOM element\n if (!(element instanceof window.HTMLElement)) {\n throw new TypeError('The first parameter must be an existing DOM element or an identifier.');\n }\n\n // The element is not an embed iframe?\n if (element.nodeName !== 'IFRAME') {\n // check if already present in the element\n const iframe = element.querySelector('iframe');\n if (iframe) {\n element = iframe;\n }\n }\n\n return element;\n}\n","import type { EmbedParameters } from '../types';\n\n/**\n * Build url for the new iframe\n *\n * @param {object} parameters\n */\nexport function buildIframeUrl(parameters: EmbedParameters) {\n let url = parameters.baseUrl || 'https://flat-embed.com';\n\n // Score id or blank embed\n if (!parameters.isCustomUrl) {\n url += `/${parameters.score || 'blank'}`;\n }\n\n // Build qs parameters\n const urlParameters: Record<string, string | number | boolean> = Object.assign(\n {\n jsapi: true,\n },\n parameters.embedParams as Record<string, string | number | boolean>,\n );\n\n const qs = Object.keys(urlParameters)\n .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(urlParameters[k])}`)\n .join('&');\n\n return `${url}?${qs}`;\n}\n\n/**\n * Create an iframe inside a specified element\n *\n * @param {HTMLElement} element\n * @param {object} parameters\n */\nexport function createEmbedIframe(\n element: HTMLElement,\n parameters: EmbedParameters,\n): HTMLIFrameElement {\n const url = buildIframeUrl(parameters);\n\n const iframe = document.createElement('iframe');\n iframe.setAttribute('src', url);\n iframe.setAttribute('width', parameters.width || '100%');\n iframe.setAttribute('height', parameters.height || '100%');\n iframe.setAttribute('allowfullscreen', 'true');\n iframe.setAttribute('allow', 'autoplay; midi');\n iframe.setAttribute('frameborder', '0');\n\n if (parameters.lazy) {\n iframe.setAttribute('loading', 'lazy');\n }\n\n element.appendChild(iframe);\n\n return iframe;\n}\n","import './lib/compatibility';\n\nimport EmbedCallback from './lib/callback';\nimport { parseMessage, postMessage } from './lib/communication';\nimport { normalizeElement } from './lib/dom';\nimport { createEmbedIframe } from './lib/embed';\nimport type {\n EmbedEventName,\n EmbedMessageReceived,\n EmbedMessageReceivedEvent,\n EmbedMessageReceivedMethod,\n EmbedParameters,\n MeasureDetails,\n MetronomeMode,\n NoteCursorPosition,\n NoteCursorPositionOptional,\n NoteDetails,\n PartConfiguration,\n PlaybackPosition,\n ScoreTrackConfiguration,\n} from './types';\nimport type { ScoreDetails } from './types/scoreDetails';\n\nconst embeds = new WeakMap<HTMLIFrameElement, Embed>();\nconst embedsReady = new WeakMap<HTMLIFrameElement, Promise<void>>();\n\nclass Embed {\n origin: string = '*';\n element!: HTMLIFrameElement;\n embedCallback!: EmbedCallback;\n\n /**\n * Create a new Flat Embed\n *\n * @param element A reference to a Flat Embed iframe or a container for the new iframe\n * @param parameters Parameters for the new iframe\n */\n constructor(element: HTMLIFrameElement | HTMLElement | string, parameters: EmbedParameters = {}) {\n const normalizedElement = normalizeElement(element);\n\n // Keep a single object instance per iframe\n if (normalizedElement instanceof HTMLIFrameElement && embeds.has(normalizedElement)) {\n return embeds.get(normalizedElement) as Embed;\n }\n\n // Create new element iframe if needed\n let iframeElement: HTMLIFrameElement;\n if (normalizedElement.nodeName !== 'IFRAME') {\n iframeElement = createEmbedIframe(normalizedElement, parameters);\n } else {\n iframeElement = normalizedElement as HTMLIFrameElement;\n }\n\n this.element = iframeElement;\n this.embedCallback = new EmbedCallback(this);\n\n const onReady = new Promise<void>(resolve => {\n // Handle incoming messages from embed\n const onMessage = (event: MessageEvent) => {\n if (this.element.contentWindow !== event.source) {\n return;\n }\n\n if (this.origin === '*') {\n this.origin = event.origin;\n }\n\n // Parse inbound message\n const data: EmbedMessageReceived = parseMessage(event.data);\n\n // Mark the embed as ready\n if (\n (data as EmbedMessageReceivedEvent).event === 'ready' ||\n (data as EmbedMessageReceivedMethod).method === 'ping'\n ) {\n resolve();\n return;\n }\n\n // Process regular messages from the embed\n this.embedCallback.process(data);\n };\n\n window.addEventListener('message', onMessage, false);\n postMessage(this, 'ping');\n });\n\n embeds.set(this.element, this);\n embedsReady.set(this.element, onReady);\n\n return this;\n }\n\n /**\n * Wait for the embed to be ready\n *\n * Returns a promise that resolves when the embed iframe has fully loaded and\n * communication with the Flat embed has been established. This method is automatically\n * called by all other embed methods, so you typically don't need to call it directly.\n * However, it can be useful when you want to know exactly when the embed is ready\n * without performing any other action.\n *\n * @returns A promise that resolves when the embed is ready\n *\n * @example\n * // Explicitly wait for embed to be ready\n * const embed = new Embed('container', {\n * score: '56ae21579a127715a02901a6'\n * });\n * await embed.ready();\n * console.log('Embed is now ready!');\n *\n * @example\n * // Note: Other methods automatically wait for ready state\n * const embed = new Embed('container');\n * // No need to call ready() - play() will wait automatically\n * await embed.play();\n *\n * @note All embed methods automatically call ready() internally, so explicit calls are optional\n */\n ready() {\n return embedsReady.get(this.element) || Promise.resolve();\n }\n\n /**\n * Call a method on the embed\n *\n * @param method Name of the method to call\n * @param parameters Method parameters\n * @returns Call result from Embed (if any)\n */\n call(\n method: string,\n parameters: Record<string, unknown> | string | string[] | number | boolean | Uint8Array = {},\n ) {\n return new Promise((resolve, reject) => {\n return this.ready().then(() => {\n this.embedCallback.pushCall(method, resolve, reject);\n postMessage(this, method, parameters);\n });\n });\n }\n\n /**\n * Subscribe to a specific event\n *\n * @param event The name of the event.\n * @param callback The function to call when receiving an event\n */\n on(event: EmbedEventName, callback: () => void) {\n if (typeof event !== 'string') {\n throw new TypeError('An event name (string) is required');\n }\n if (typeof callback !== 'function') {\n throw new TypeError('An callback (function) is required');\n }\n if (this.embedCallback.subscribeEvent(event, callback)) {\n this.call('addEventListener', event).catch(() => {});\n }\n }\n\n /**\n * Unsubscribe to a specific event\n *\n * @param event The name of the event.\n * @param callback The function to unsubscribe\n */\n off(event: EmbedEventName, callback?: () => void) {\n if (typeof event !== 'string') {\n throw new TypeError('An event name (string) is required');\n }\n if (this.embedCallback.unsubscribeEvent(event, callback)) {\n this.call('removeEventListener', event).catch(() => {});\n }\n }\n\n /**\n * Load a score hosted on Flat\n *\n * Loads a Flat score by its unique identifier. For private scores, you must provide\n * the sharing key obtained from a private link.\n *\n * @param score - The score identifier as a string, or an object containing:\n * - `score`: The unique identifier of the score (required)\n * - `sharingKey`: The sharing key for private scores (optional)\n * @returns A promise that resolves when the score is successfully loaded\n * @throws {TypeError} If the score parameter is invalid\n * @throws {Error} If the score cannot be loaded (e.g., not found, access denied)\n *\n * @example\n * // Load a public score\n * await embed.loadFlatScore('56ae21579a127715a02901a6');\n *\n * @example\n * // Load a private score with sharing key\n * await embed.loadFlatScore({\n * score: '56ae21579a127715a02901a6',\n * sharingKey: 'f79c3c0dd1fc76ed8b30d6f2c845c8c30f11fe88d9fc39ab96e8e407629d4885'\n * });\n */\n loadFlatScore(score: string | { score: string; sharingKey?: string }): Promise<void> {\n if (typeof score === 'string') {\n score = { score };\n }\n return this.call('loadFlatScore', score) as Promise<void>;\n }\n\n /**\n * Load a MusicXML score\n *\n * Loads a MusicXML score from a string or binary data. The score will be converted\n * to Flat's internal format and displayed in the embed.\n *\n * @param score - The MusicXML content as a string (XML) or Uint8Array (compressed MXL)\n * @returns A promise that resolves when the score is successfully loaded\n * @throws {TypeError} If the score format is invalid\n * @throws {Error} If the MusicXML cannot be parsed or loaded\n *\n * @example\n * // Load from XML string\n * const xmlString = '<?xml version=\"1.0\"?>...';\n * await embed.loadMusicXML(xmlString);\n *\n * @example\n * // Load from compressed MXL file\n * const response = await fetch('score.mxl');\n * const buffer = await response.arrayBuffer();\n * await embed.loadMusicXML(new Uint8Array(buffer));\n */\n loadMusicXML(score: string | Uint8Array) {\n return this.call('loadMusicXML', score) as Promise<void>;\n }\n\n /**\n * Load a MIDI score\n *\n * Loads a MIDI file and converts it to sheet music notation. Note that MIDI files\n * contain performance data rather than notation, so the conversion may not perfectly\n * represent the original musical intent.\n *\n * @param score - The MIDI file as a Uint8Array\n * @returns A promise that resolves when the score is successfully loaded\n * @throws {TypeError} If the score format is invalid\n * @throws {Error} If the MIDI file cannot be parsed or loaded\n *\n * @example\n * // Load MIDI file from URL\n * const response = await fetch('song.mid');\n * const buffer = await response.arrayBuffer();\n * await embed.loadMIDI(new Uint8Array(buffer));\n *\n * @example\n * // Load MIDI file from file input\n * const file = document.getElementById('fileInput').files[0];\n * const buffer = await file.arrayBuffer();\n * await embed.loadMIDI(new Uint8Array(buffer));\n */\n loadMIDI(score: Uint8Array): Promise<void> {\n return this.call('loadMIDI', score) as Promise<void>;\n }\n\n /**\n * Load a Flat JSON score\n *\n * Loads a score from Flat's native JSON format. This format preserves all score\n * data and is the most reliable way to transfer scores between Flat applications.\n *\n * @param score - The score data as a JSON string or JavaScript object\n * @returns A promise that resolves when the score is successfully loaded\n * @throws {TypeError} If the JSON is invalid or cannot be parsed\n * @throws {Error} If the score data is malformed or cannot be loaded\n *\n * @example\n * // Load from JSON object\n * const scoreData = await fetch('score.json').then(r => r.json());\n * await embed.loadJSON(scoreData);\n *\n * @example\n * // Load from JSON string\n * const jsonString = '{\"score-partwise\": {...}}';\n * await embed.loadJSON(jsonString);\n */\n loadJSON(score: string | Record<string, unknown>) {\n return this.call('loadJSON', score) as Promise<void>;\n }\n\n /**\n * Get the score in Flat JSON format\n *\n * Exports the currently loaded score as Flat's native JSON format. This format\n * preserves all score data including notation, layout, and metadata.\n *\n * @returns A promise that resolves with the score data as a JavaScript object\n * @throws {Error} If no score is currently loaded\n *\n * @example\n * // Export and save score data\n * const scoreData = await embed.getJSON();\n * const jsonString = JSON.stringify(scoreData);\n *\n * // Save to file\n * const blob = new Blob([jsonString], { type: 'application/json' });\n * const url = URL.createObjectURL(blob);\n * const a = document.createElement('a');\n * a.href = url;\n * a.download = 'score.json';\n * a.click();\n */\n getJSON() {\n return this.call('getJSON') as Promise<Record<string, unknown>>;\n }\n\n /**\n * Convert the displayed score to MusicXML\n *\n * Exports the currently loaded score as MusicXML, the standard format for sheet music\n * notation exchange. Supports both uncompressed XML and compressed MXL formats.\n *\n * @param options - Export options:\n * - `compressed`: If true, returns compressed MusicXML (.mxl) as Uint8Array.\n * If false (default), returns uncompressed XML as string.\n * @returns A promise that resolves with the MusicXML data\n * @throws {TypeError} If options parameter is invalid\n * @throws {Error} If no score is loaded or conversion fails\n *\n * @example\n * // Get uncompressed MusicXML\n * const xml = await embed.getMusicXML();\n * console.log(xml); // <?xml version=\"1.0\"...\n *\n * @example\n * // Get compressed MusicXML (.mxl)\n * const mxl = await embed.getMusicXML({ compressed: true });\n * const blob = new Blob([mxl], { type: 'application/vnd.recordare.musicxml' });\n * const url = URL.createObjectURL(blob);\n */\n getMusicXML(options?: { compressed?: boolean }): Promise<string | Uint8Array> {\n return new Promise((resolve, reject) => {\n options = options || {};\n if (typeof options !== 'object') {\n return reject(new TypeError('Options must be an object'));\n }\n this.call('getMusicXML', options)\n .then(data => {\n // Plain XML\n if (typeof data === 'string') {\n return resolve(data);\n }\n // Compressed, re-create Uint8Array\n return resolve(new Uint8Array(data as [number]));\n })\n .catch(reject);\n });\n }\n\n /**\n * Convert the displayed score to PNG image\n *\n * Exports the currently loaded score as a PNG image. Supports various export options\n * including resolution, layout mode, and output format.\n *\n * @param options - Export options:\n * - `result`: Output format - 'Uint8Array' (default) or 'dataURL'\n * - `dpi`: Resolution in dots per inch (50-300, default: 150)\n * - `layout`: Layout mode - 'track' (default, horizontal single system) or 'page'\n * @returns A promise that resolves with the PNG data\n * @throws {TypeError} If options parameter is invalid\n * @throws {Error} If no score is loaded or conversion fails\n *\n * @example\n * // Get PNG as Uint8Array\n * const pngData = await embed.getPNG();\n * const blob = new Blob([pngData], { type: 'image/png' });\n *\n * @example\n * // Get PNG as data URL for direct display\n * const dataUrl = await embed.getPNG({ result: 'dataURL' });\n * document.getElementById('preview').src = dataUrl;\n *\n * @example\n * // High resolution export with page layout\n * const hqPng = await embed.getPNG({ dpi: 300, layout: 'page' });\n */\n getPNG(options?: {\n /** Export result (either a PNG returned as Uint8Array or in dataURL) */\n result?: 'Uint8Array' | 'dataURL';\n /** DPI of exported image (min: 50, max: 300, default: 150) */\n dpi?: number;\n /** Layout of exported image */\n layout?: 'track' | 'page';\n }): Promise<Uint8Array | string> {\n return new Promise((resolve, reject) => {\n options = options || {};\n if (typeof options !== 'object') {\n return reject(new TypeError('Options must be an object'));\n }\n this.call('getPNG', options)\n .then(data => {\n if (typeof data === 'string') {\n return resolve(data);\n }\n resolve(new Uint8Array(data as [number]));\n })\n .catch(reject);\n });\n }\n\n /**\n * Convert the displayed score to MIDI\n *\n * Exports the currently loaded score as a MIDI file. The MIDI file will contain\n * performance data including notes, tempo, dynamics, and instrument information.\n * Note that some notation elements may not be represented in MIDI format.\n *\n * @returns A promise that resolves with a Uint8Array containing the MIDI file data\n * @throws {Error} If no score is loaded or conversion fails\n *\n * @example\n * // Export score as MIDI\n * const midiData = await embed.getMIDI();\n *\n * // Save as file\n * const blob = new Blob([midiData], { type: 'audio/midi' });\n * const url = URL.createObjectURL(blob);\n * const a = document.createElement('a');\n * a.href = url;\n * a.download = 'score.mid';\n * a.click();\n *\n * @example\n * // Play MIDI in browser (requires Web MIDI API)\n * const midiData = await embed.getMIDI();\n * // ... use with Web MIDI API or MIDI player library\n */\n getMIDI(): Promise<Uint8Array> {\n return this.call('getMIDI').then(data => new Uint8Array(data as [number]));\n }\n\n /**\n * Get the metadata of the score (for scores hosted on Flat)\n *\n * Retrieves metadata for scores that are hosted on Flat, including title, composer,\n * collaborators, creation date, and other information available through Flat's API.\n * This method only works for scores loaded via `loadFlatScore()`.\n *\n * @returns A promise that resolves with the score metadata object\n * @throws {Error} If no Flat score is loaded or metadata is unavailable\n *\n * @example\n * // Get metadata after loading a Flat score\n * await embed.loadFlatScore('56ae21579a127715a02901a6');\n * const metadata = await embed.getFlatScoreMetadata();\n * console.log(`Title: ${metadata.title}`);\n * console.log(`Created by: ${metadata.user.username}`);\n *\n * @see {@link https://flat.io/developers/api/reference/#operation/getScore}\n */\n getFlatScoreMetadata(): Promise<ScoreDetails> {\n return this.call('getFlatScoreMetadata') as Promise<ScoreDetails>;\n }\n\n /**\n * Get the embed configuration\n *\n * Retrieves the complete configuration object for the embed, including display\n * settings, permissions, editor configuration, and enabled features.\n *\n * @returns A promise that resolves with the embed configuration object\n * @throws {Error} If the configuration cannot be retrieved\n *\n * @example\n * // Get current embed configuration\n * const config = await embed.getEmbedConfig();\n * console.log(`Mode: ${config.mode}`);\n * console.log(`Controls enabled: ${config.controlsPlay}`);\n */\n getEmbedConfig(): Promise<Record<string, unknown>> {\n return this.call('getEmbedConfig') as Promise<Record<string, unknown>>;\n }\n\n /**\n * Set the editor configuration\n *\n * Updates the editor configuration for the embed. These settings control various\n * aspects of the editor interface and behavior. The configuration is applied\n * when the next score is loaded.\n *\n * @param editor - Editor configuration object that may include:\n * - displayMode: Score display mode\n * - toolsetId: Active toolset identifier\n * - hiddenTools: Array of tool identifiers to hide\n * - Additional editor-specific settings\n * @returns A promise that resolves when the configuration is updated\n * @throws {Error} If the configuration is invalid\n *\n * @example\n * // Configure editor before loading a score\n * await embed.setEditorConfig({\n * displayMode: 'responsive',\n * hiddenTools: ['note-duration', 'note-pitch']\n * });\n * await embed.loadFlatScore('56ae21579a127715a02901a6');\n *\n * @note This configuration persists across score loads until changed\n */\n setEditorConfig(editor: Record<string, unknown>): Promise<void> {\n return this.call('setEditorConfig', editor) as Promise<void>;\n }\n\n /**\n * Toggle fullscreen mode\n *\n * Switches the embed in or out of fullscreen mode. When in fullscreen, the embed\n * expands to fill the entire screen, providing an immersive view of the score.\n *\n * @param active - true to enter fullscreen, false to exit fullscreen\n * @returns A promise that resolves when the fullscreen state has changed\n * @throws {Error} If fullscreen mode cannot be toggled (e.g., browser restrictions)\n *\n * @example\n * // Enter fullscreen mode\n * await embed.fullscreen(true);\n *\n * @example\n * // Exit fullscreen mode\n * await embed.fullscreen(false);\n *\n * @example\n * // Toggle fullscreen with user interaction\n * button.addEventListener('click', () => {\n * embed.fullscreen(true);\n * });\n *\n * @note Fullscreen requests may require user interaction due to browser policies\n */\n fullscreen(active: boolean): Promise<void> {\n return this.call('fullscreen', active) as Promise<void>;\n }\n\n /**\n * Start playback\n *\n * Begins playing the score from the current cursor position. If playback was\n * previously paused, it resumes from the pause position. If stopped, it starts\n * from the beginning or the current cursor position.\n *\n * @returns A promise that resolves when playback has started\n * @throws {Error} If no score is loaded or playback cannot start\n *\n * @example\n * // Start playback\n * await embed.play();\n *\n * @example\n * // Play with event handling\n * embed.on('play', () => console.log('Playback started'));\n * await embed.play();\n *\n * @see {@link pause} - Pause playback\n * @see {@link stop} - Stop playback\n */\n play(): Promise<void> {\n return this.call('play') as Promise<void>;\n }\n\n /**\n * Pause playback\n *\n * Pauses the score playback at the current position. The playback position\n * is maintained, allowing you to resume from the same point using `play()`.\n *\n * @returns A promise that resolves when playback has been paused\n * @throws {Error} If no score is loaded or playback cannot be paused\n *\n * @example\n * // Pause playback\n * await embed.pause();\n *\n * @example\n * // Toggle play/pause\n * if (isPlaying) {\n * await embed.pause();\n * } else {\n * await embed.play();\n * }\n *\n * @see {@link play} - Start or resume playback\n * @see {@link stop} - Stop and reset playback\n */\n pause(): Promise<void> {\n return this.call('pause') as Promise<void>;\n }\n\n /**\n * Stop playback\n *\n * Stops the score playback and resets the playback position to the beginning\n * of the score. Unlike `pause()`, the playback position is not maintained.\n *\n * @returns A promise that resolves when playback has been stopped\n * @throws {Error} If no score is loaded or playback cannot be stopped\n *\n * @example\n * // Stop playback\n * await embed.stop();\n *\n * @example\n * // Stop and restart from beginning\n * await embed.stop();\n * await embed.play(); // Starts from beginning\n *\n * @see {@link play} - Start playback\n * @see {@link pause} - Pause playback\n */\n stop(): Promise<void> {\n return this.call('stop') as Promise<void>;\n }\n\n /**\n * Mute playback\n *\n * Mutes all audio output from the score playback. The playback continues\n * but without sound. This is equivalent to setting the master volume to 0\n * but preserves the previous volume setting.\n *\n * @returns A promise that resolves when audio has been muted\n * @throws {Error} If muting fails\n *\n * @example\n * // Mute audio\n * await embed.mute();\n *\n * @example\n * // Mute during playback\n * await embed.play();\n * await embed.mute();\n *\n * @see {@link setMasterVolume} - Set master volume level\n * @note There is no unmute method; use setMasterVolume to restore audio\n */\n mute(): Promise<void> {\n return this.call('mute') as Promise<void>;\n }\n\n /**\n * Get the current master volume\n *\n * Retrieves the current master volume level for score playback.\n *\n * @returns A promise that resolves with the volume level (0-100)\n * @throws {Error} If the volume cannot be retrieved\n *\n * @example\n * // Get current volume\n * const volume = await embed.getMasterVolume();\n * console.log(`Current volume: ${volume}%`);\n *\n * @see {@link setMasterVolume} - Set the master volume\n */\n getMasterVolume(): Promise<number> {\n return this.call('getMasterVolume') as Promise<number>;\n }\n\n /**\n * Set the master volume\n *\n * Sets the master volume level for score playback. This affects all parts\n * proportionally based on their individual volume settings.\n *\n * @param parameters - Volume settings\n * @returns A promise that resolves when the volume has been set\n * @throws {Error} If the volume value is invalid or cannot be set\n *\n * @example\n * // Set volume to 50%\n * await embed.setMasterVolume({ volume: 50 });\n *\n * @example\n * // Mute audio\n * await embed.setMasterVolume({ volume: 0 });\n *\n * @example\n * // Maximum volume\n * await embed.setMasterVolume({ volume: 100 });\n *\n * @see {@link getMasterVolume} - Get the current master volume\n */\n setMasterVolume(parameters: { volume: number }): Promise<void> {\n return this.call('setMasterVolume', parameters) as Promise<void>;\n }\n\n /**\n * Get the volume of a specific part\n *\n * Retrieves the current volume level for a specific instrument part in the score.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * @returns A promise that resolves with the part's volume level (0-100)\n * @throws {Error} If the part UUID is invalid or volume cannot be retrieved\n *\n * @example\n * // Get volume for a specific part\n * const parts = await embed.getParts();\n * const violinVolume = await embed.getPartVolume({\n * partUuid: parts[0].uuid\n * });\n *\n * @see {@link setPartVolume} - Set the volume for a part\n * @see {@link getParts} - Get all parts information\n */\n getPartVolume(parameters: { partUuid: string }): Promise<number> {\n return this.call('getPartVolume', parameters) as Promise<number>;\n }\n\n /**\n * Set the volume of a specific part\n *\n * Sets the volume level for a specific instrument part in the score. Part volumes\n * are independent but affected by the master volume.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * - `volume`: Volume level (0-100, where 0 is muted and 100 is maximum)\n * @returns A promise that resolves when the volume has been set\n * @throws {Error} If the part UUID or volume value is invalid\n *\n * @example\n * // Set violin part to 75% volume\n * const parts = await embed.getParts();\n * await embed.setPartVolume({\n * partUuid: parts[0].uuid,\n * volume: 75\n * });\n *\n * @example\n * // Mute the bass part\n * await embed.setPartVolume({\n * partUuid: bassPartUuid,\n * volume: 0\n * });\n *\n * @see {@link getPartVolume} - Get the volume for a part\n * @see {@link mutePart} - Mute a part\n */\n setPartVolume(parameters: { partUuid: string; volume: number }): Promise<void> {\n return this.call('setPartVolume', parameters) as Promise<void>;\n }\n\n /**\n * Mute a specific part\n *\n * Mutes the audio output for a specific instrument part. The part's volume\n * setting is preserved and can be restored using `unmutePart()`.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part to mute\n * @returns A promise that resolves when the part has been muted\n * @throws {Error} If the part UUID is invalid or muting fails\n *\n * @example\n * // Mute the drums part\n * const parts = await embed.getParts();\n * const drumsPart = parts.find(p => p.instrument === 'drums');\n * await embed.mutePart({ partUuid: drumsPart.uuid });\n *\n * @see {@link unmutePart} - Unmute a part\n * @see {@link setPartVolume} - Set part volume to 0 (alternative)\n */\n mutePart(parameters: { partUuid: string }): Promise<void> {\n return this.call('mutePart', parameters) as Promise<void>;\n }\n\n /**\n * Unmute a specific part\n *\n * Restores the audio output for a previously muted part. The part returns\n * to its previous volume level before it was muted.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part to unmute\n * @returns A promise that resolves when the part has been unmuted\n * @throws {Error} If the part UUID is invalid or unmuting fails\n *\n * @example\n * // Unmute a previously muted part\n * await embed.unmutePart({ partUuid: drumsPart.uuid });\n *\n * @see {@link mutePart} - Mute a part\n */\n unmutePart(parameters: { partUuid: string }): Promise<void> {\n return this.call('unmutePart', parameters) as Promise<void>;\n }\n\n /**\n * Enable solo mode for a part\n *\n * Enables solo mode for a specific part, which mutes all other parts while\n * keeping the selected part audible. Multiple parts can be in solo mode\n * simultaneously.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part to solo\n * @returns A promise that resolves when solo mode has been enabled\n * @throws {Error} If the part UUID is invalid or solo mode cannot be set\n *\n * @example\n * // Solo the violin part\n * const parts = await embed.getParts();\n * const violinPart = parts.find(p => p.instrument === 'violin');\n * await embed.setPartSoloMode({ partUuid: violinPart.uuid });\n *\n * @example\n * // Solo multiple parts\n * await embed.setPartSoloMode({ partUuid: violinUuid });\n * await embed.setPartSoloMode({ partUuid: celloUuid });\n *\n * @see {@link unsetPartSoloMode} - Disable solo mode\n * @see {@link getPartSoloMode} - Check solo mode status\n */\n setPartSoloMode(parameters: { partUuid: string }): Promise<void> {\n return this.call('setPartSoloMode', parameters) as Promise<void>;\n }\n\n /**\n * Disable solo mode for a part\n *\n * Disables solo mode for a specific part. If this was the only part in solo\n * mode, all parts return to their normal volume/mute states. If other parts\n * remain in solo mode, this part will be muted.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * @returns A promise that resolves when solo mode has been disabled\n * @throws {Error} If the part UUID is invalid or solo mode cannot be unset\n *\n * @example\n * // Remove solo from a part\n * await embed.unsetPartSoloMode({ partUuid: violinPart.uuid });\n *\n * @see {@link setPartSoloMode} - Enable solo mode\n * @see {@link getPartSoloMode} - Check solo mode status\n */\n unsetPartSoloMode(parameters: { partUuid: string }): Promise<void> {\n return this.call('unsetPartSoloMode', parameters) as Promise<void>;\n }\n\n /**\n * Get the solo mode status of a part\n *\n * Checks whether a specific part is currently in solo mode.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * @returns A promise that resolves with true if solo mode is enabled, false otherwise\n * @throws {Error} If the part UUID is invalid\n *\n * @example\n * // Check if violin is in solo mode\n * const isSolo = await embed.getPartSoloMode({\n * partUuid: violinPart.uuid\n * });\n * if (isSolo) {\n * console.log('Violin is in solo mode');\n * }\n *\n * @see {@link setPartSoloMode} - Enable solo mode\n * @see {@link unsetPartSoloMode} - Disable solo mode\n */\n getPartSoloMode(parameters: { partUuid: string }): Promise<boolean> {\n return this.call('getPartSoloMode', parameters) as Promise<boolean>;\n }\n\n /**\n * Get the reverb level of a part\n *\n * Retrieves the current reverb (reverberation) effect level for a specific\n * instrument part. Reverb adds spatial depth and ambience to the sound.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * @returns A promise that resolves with the reverb level (0-100)\n * @throws {Error} If the part UUID is invalid or reverb cannot be retrieved\n *\n * @example\n * // Get reverb level for piano part\n * const parts = await embed.getParts();\n * const pianoPart = parts.find(p => p.instrument === 'piano');\n * const reverb = await embed.getPartReverb({\n * partUuid: pianoPart.uuid\n * });\n * console.log(`Piano reverb: ${reverb}%`);\n *\n * @see {@link setPartReverb} - Set the reverb level\n */\n getPartReverb(parameters: { partUuid: string }): Promise<number> {\n return this.call('getPartReverb', parameters) as Promise<number>;\n }\n\n /**\n * Set the reverb level of a part\n *\n * Sets the reverb (reverberation) effect level for a specific instrument part.\n * Higher values create more spacious, ambient sound.\n *\n * @param parameters - Object containing:\n * - `partUuid`: The unique identifier of the part\n * - `reverberation`: Reverb level (0-100, where 0 is dry and 100 is maximum reverb)\n * @returns A promise that resolves when the reverb has been set\n * @throws {Error} If the part UUID or reverb value is invalid\n *\n * @example\n * // Add subtle reverb to piano\n * await embed.setPartReverb({\n * partUuid: pianoPart.uuid,\n * reverberation: 30\n * });\n *\n * @example\n * // Remove reverb (dry sound)\n * await embed.setPartReverb({\n * partUuid: pianoPart.uuid,\n * reverberation: 0\n * });\n *\n * @see {@link getPartReverb} - Get the current reverb level\n */\n setPartReverb(parameters: { partUuid: string; reverberation: number }): Promise<void> {\n return this.call('setPartReverb', parameters) as Promise<void>;\n }\n\n /**\n * Configure an audio or video track\n *\n * Sets up a new audio or video track to synchronize with the score playback.\n * This allows you to play backing tracks, reference recordings, or video\n * alongside the score.\n *\n * @param parameters - Track configuration object (see ScoreTrackConfiguration type)\n * @returns A promise that resolves when the track has been configured\n * @throws {Error} If the track configuration is invalid\n *\n * @example\n * // Configure an audio backing track\n * await embed.setTrack({\n * id: 'backing-track-1',\n * type: 'audio',\n * url: 'https://example.com/backing-track.mp3',\n * synchronizationPoints: [\n * { type: 'measure', measure: 0, time: 0 },\n * { type: 'measure', measure: 16, time: 32.5 }\n * ]\n * });\n *\n * @see {@link useTrack} - Enable a configured track\n * @see {@link seekTrackTo} - Seek to a position in the track\n */\n setTrack(parameters: ScoreTrackConfiguration): Promise<void> {\n return this.call('setTrack', parameters as unknown as Record<string, unknown>) as Promise<void>;\n }\n\n /**\n * Enable a previously configured track\n *\n * Activates a track that was previously configured with `setTrack()`. Only one\n * track can be active at a time.\n *\n * @param parameters - Object containing:\n * - `id`: The identifier of the track to enable\n * @returns A promise that resolves when the track has been enabled\n * @throws {Error} If the track ID is invalid or track cannot be enabled\n *\n * @example\n * // Enable a configured backing track\n * await embed.useTrack({ id: 'backing-track-1' });\n *\n * @example\n * // Switch between multiple tracks\n * await embed.useTrack({ id: 'practice-tempo' });\n * // Later...\n * await embed.useTrack({ id: 'full-tempo' });\n *\n * @see {@link setTrack} - Configure a new track\n */\n useTrack(parameters: { id: string }): Promise<void> {\n return this.call('useTrack', parameters) as Promise<void>;\n }\n\n /**\n * Seek to a position in the audio track\n *\n * Moves the playback position of the currently active audio/video track to\n * a specific time. This is useful for synchronizing with score playback or\n * jumping to specific sections.\n *\n * @param parameters - Object containing:\n * - `time`: Time position in seconds\n * @returns A promise that resolves when seeking is complete\n * @throws {Error} If no track is active or seeking fails\n *\n * @example\n * // Seek to 30 seconds\n * await embed.seekTrackTo({ time: 30 });\n *\n * @example\n * // Seek to beginning\n * await embed.seekTrackTo({ time: 0 });\n *\n * @see {@link setTrack} - Configure a track\n * @see {@link useTrack} - Enable a track\n */\n seekTrackTo(parameters: { time: number }): Promise<void> {\n return this.call('seekTrackTo', parameters) as Promise<void>;\n }\n\n /**\n * Print the score\n *\n * Opens the browser's print dialog to print the currently loaded score. The score\n * is formatted for optimal printing with proper page breaks and sizing.\n *\n * @returns A promise that resolves when the print dialog has been opened\n * @throws {Error} If no score is loaded or printing cannot be initiated\n *\n * @example\n * // Print the current score\n * await embed.print();\n *\n * @example\n * // Add print button\n * document.getElementById('printBtn').addEventListener('click', () => {\n * embed.print();\n * });\n *\n * @note The actual printing is controlled by the browser's print dialog\n */\n print(): Promise<void> {\n return this.call('print') as Promise<void>;\n }\n\n /**\n * Get the current zoom ratio\n *\n * Retrieves the current zoom level of the score display. The zoom level affects\n * how large the notation appears on screen.\n *\n * @returns A promise that resolves with the zoom ratio (0.5 to 3)\n * @throws {Error} If the zoom level cannot be retrieved\n *\n * @example\n * // Get current zoom level\n * const zoom = await embed.getZoom();\n * console.log(`Current zoom: ${zoom * 100}%`);\n *\n * @see {@link setZoom} - Set the zoom level\n * @see {@link getAutoZoom} - Check if auto-zoom is enabled\n */\n getZoom(): Promise<number> {\n return this.call('getZoom') as Promise<number>;\n }\n\n /**\n * Set the zoom ratio\n *\n * Sets a specific zoom level for the score display. Setting a manual zoom level\n * automatically disables auto-zoom mode.\n *\n * @param zoom - The zoom ratio (0.5 to 3)\n * - 0.5 = 50% (minimum zoom)\n * - 1 = 100% (normal size)\n * - 3 = 300% (maximum zoom)\n * @returns A promise that resolves with the actual zoom ratio applied\n * @throws {Error} If the zoom value is outside the valid range\n *\n * @example\n * // Set zoom to 150%\n * await embed.setZoom(1.5);\n *\n * @example\n * // Zoom in/out buttons\n * const zoomIn = async () => {\n * const current = await embed.getZoom();\n * await embed.setZoom(Math.min(current + 0.1, 3));\n * };\n *\n * @see {@link getZoom} - Get current zoom level\n * @see {@link setAutoZoom} - Enable automatic zoom\n */\n setZoom(zoom: number): Promise<number> {\n return this.call('setZoom', zoom) as Promise<number>;\n }\n\n /**\n * Get the auto-zoom status\n *\n * Checks whether auto-zoom mode is currently enabled. When auto-zoom is active,\n * the score automatically adjusts its zoom level to fit the available space.\n *\n * @returns A promise that resolves with true if auto-zoom is enabled, false otherwise\n * @throws {Error} If the status cannot be retrieved\n *\n * @example\n * // Check auto-zoom status\n * const isAutoZoom = await embed.getAutoZoom();\n * if (isAutoZoom) {\n * console.log('Score will auto-fit to container');\n * }\n *\n * @see {@link setAutoZoom} - Enable or disable auto-zoom\n */\n getAutoZoom(): Promise<boolean> {\n return this.call('getAutoZoom') as Promise<boolean>;\n }\n\n /**\n * Enable or disable auto-zoom\n *\n * Controls the auto-zoom feature. When enabled, the score automatically adjusts\n * its zoom level to fit the available container width. When disabled, the zoom\n * level remains fixed at the last set value.\n *\n * @param state - true to enable auto-zoom, false to disable\n * @returns A promise that resolves with the new auto-zoom state\n * @throws {Error} If auto-zoom cannot be toggled\n *\n * @example\n * // Enable auto-zoom\n * await embed.setAutoZoom(true);\n *\n * @example\n * // Disable auto-zoom and set fixed zoom\n * await embed.setAutoZoom(false);\n * await embed.setZoom(1.2);\n *\n * @see {@link getAutoZoom} - Check current auto-zoom status\n * @see {@link setZoom} - Set a specific zoom level\n */\n setAutoZoom(state: boolean): Promise<boolean> {\n return this.call('setAutoZoom', state) as Promise<boolean>;\n }\n\n /**\n * Set focus to the score\n *\n * Gives keyboard focus to the score iframe, allowing keyboard shortcuts and\n * navigation to work. This is useful when embedding multiple interactive elements\n * on a page.\n *\n * @returns A promise that resolves when focus has been set\n *\n * @example\n * // Focus score for keyboard interaction\n * await embed.focusScore();\n *\n * @example\n * // Focus score when user clicks a button\n * document.getElementById('editBtn').addEventListener('click', () => {\n * embed.focusScore();\n * });\n */\n focusScore(): Promise<void> {\n return this.call('focusScore') as Promise<void>;\n }\n\n /**\n * Get the cursor position\n *\n * Retrieves the current position of the cursor in the score, including the part,\n * measure, voice, and note location.\n *\n * @returns A promise that resolves with the cursor position object\n * @throws {Error} If cursor position cannot be retrieved\n *\n * @example\n * // Get current cursor position\n * const pos = await embed.getCursorPosition();\n * console.log(`Cursor at measure ${pos.measureIdx + 1}`);\n *\n * @see {@link setCursorPosition} - Set cursor position\n * @see {@link goLeft} - Move cursor left\n * @see {@link goRight} - Move cursor right\n */\n getCursorPosition(): Promise<NoteCursorPosition> {\n return this.call('getCursorPosition') as unknown as Promise<NoteCursorPosition>;\n }\n\n /**\n * Set the cursor position\n *\n * Moves the cursor to a specific position in the score. You can specify any\n * combination of part, measure, voice, staff, and note indices. Unspecified\n * values remain at their current position.\n *\n * @param position - New cursor position with optional fields:\n * - `partIdx`: Target part index (optional)\n * - `measureIdx`: Target measure index (optional)\n * - `voiceIdx`: Target voice index (optional)\n * - `noteIdx`: Target note index (optional)\n * - `staffIdx`: Target staff index (optional)\n * @returns A promise that resolves when the cursor has been moved\n * @throws {Error} If the position is invalid or cursor cannot be moved\n *\n * @example\n * // Move to beginning of measure 5\n * await embed.setCursorPosition({\n * measureIdx: 4, // 0-based index\n * noteIdx: 0\n * });\n *\n * @example\n * // Move to second voice of current measure\n * await embed.setCursorPosition({ voiceIdx: 1 });\n *\n * @see {@link getCursorPosition} - Get current position\n */\n setCursorPosition(position: NoteCursorPositionOptional): Promise<void> {\n return this.call('setCursorPosition', position as Record<string, unknown>) as Promise<void>;\n }\n\n /**\n * Get information about all parts\n *\n * Retrieves detailed information about all instrument parts in the score,\n * including their names, instruments, and unique identifiers.\n *\n * @returns A promise that resolves with an array of part configurations\n * @throws {Error} If no score is loaded or parts cannot be retrieved\n *\n