audio-mixer-engine 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,21 +66,36 @@ await manager.play({ leadIn: true, metronome: true });
66
66
 
67
67
  ### Loading MusicXML Files
68
68
 
69
+ PlaybackManager auto-detects MusicXML strings and MXL/ZIP ArrayBuffers:
70
+
69
71
  ```javascript
70
- import { MusicXmlConverter, loadMusicXml, PlaybackManager } from 'audio-mixer-engine';
72
+ import { loadMusicXml } from 'audio-mixer-engine';
73
+
74
+ // MXL file (ArrayBuffer) — auto-detected from ZIP magic bytes
75
+ await manager.load(mxlArrayBuffer);
71
76
 
72
- // Load MusicXML or MXL file (auto-detects format)
77
+ // MusicXML string auto-detected from <?xml or <score-partwise> prefix
73
78
  const xmlString = await loadMusicXml(fileArrayBuffer);
79
+ await manager.load(xmlString);
74
80
 
75
- // Convert to parsedData and load into PlaybackManager
76
- const converter = new MusicXmlConverter();
77
- const parsedData = converter.convert(xmlString);
78
- await manager.load(parsedData);
81
+ // With metadata overrides (title, composer, per-part instrument mapping)
82
+ await manager.load(xmlString, metadataOverrides);
79
83
 
80
84
  // From here on, playback works exactly the same as MIDI
81
85
  await manager.play({ leadIn: true, metronome: true });
82
86
  ```
83
87
 
88
+ You can also convert manually for more control:
89
+
90
+ ```javascript
91
+ import { MusicXmlConverter, loadMusicXml } from 'audio-mixer-engine';
92
+
93
+ const xmlString = await loadMusicXml(fileArrayBuffer);
94
+ const converter = new MusicXmlConverter();
95
+ const parsedData = converter.convert(xmlString, { defaultTempo: 120 }, metadataOverrides);
96
+ await manager.load(parsedData);
97
+ ```
98
+
84
99
  ## Key Concepts
85
100
 
86
101
  **Part-centric design**: Each musical part gets an independent `ChannelHandle` with its own `AudioNode` output. Your application connects these outputs to gain controls, analyzers, and effects.
@@ -98,8 +113,12 @@ await manager.play({ leadIn: true, metronome: true });
98
113
  ### PlaybackManager
99
114
 
100
115
  ```javascript
101
- // Lifecycle
102
- await manager.load(midiArrayBuffer, metadata, instrumentMap);
116
+ // Lifecycle — load() auto-detects input type
117
+ await manager.load(midiArrayBuffer); // MIDI ArrayBuffer
118
+ await manager.load(mxlArrayBuffer); // MXL/ZIP ArrayBuffer (auto-detected)
119
+ await manager.load(musicXmlString); // MusicXML string (auto-detected)
120
+ await manager.load(parsedData); // Pre-parsed data object
121
+ await manager.load(input, metadata, instrumentMap); // With optional overrides
103
122
  manager.reset();
104
123
 
105
124
  // Transport
@@ -232,12 +251,26 @@ const converter = new MusicXmlConverter();
232
251
  const parsedData = converter.convert(xmlString, {
233
252
  defaultTempo: 120, // Fallback tempo if none in score
234
253
  defaultVelocity: 80 // Fallback note velocity
235
- });
254
+ }, metadataOverrides); // Optional: override title, composer, per-part instruments
236
255
 
237
256
  // parsedData includes parts, barStructure, metadata, and structureMetadata
238
257
  // Use with PlaybackManager or MidiPlayer just like MIDI-parsed data
239
258
  ```
240
259
 
260
+ ### Metadata Utilities
261
+
262
+ ```javascript
263
+ import { resolveInstrument, normalizeLegacyMetadata } from 'audio-mixer-engine';
264
+
265
+ // Map instrument names/numbers to MIDI program numbers
266
+ resolveInstrument('piano'); // 0
267
+ resolveInstrument('choir_aahs'); // 52
268
+ resolveInstrument(40); // 40
269
+
270
+ // Normalize legacy v1 metadata format to v2
271
+ const normalized = normalizeLegacyMetadata(legacyMetadata);
272
+ ```
273
+
241
274
  ## Mixer Example
242
275
 
243
276
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-mixer-engine",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Audio engine library for audio mixer applications with MIDI parsing, playback, and synthesis",
5
5
  "main": "dist/audio-mixer-engine.cjs.js",
6
6
  "module": "dist/audio-mixer-engine.es.js",
package/src/index.js CHANGED
@@ -25,6 +25,9 @@ export { default as MidiPlayer } from './lib/midi-player.js';
25
25
  export { default as MusicXmlConverter } from './lib/musicxml-converter.js';
26
26
  export { loadMusicXml, loadMusicXmlSync } from './lib/musicxml/mxl-loader.js';
27
27
 
28
+ // Shared metadata utilities
29
+ export { resolveInstrument, normalizeLegacyMetadata } from './lib/metadata-utils.js';
30
+
28
31
  // Playback manager
29
32
  export { default as PlaybackManager } from './lib/playback-manager.js';
30
33
 
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Shared metadata utilities extracted from MidiParser.
3
+ * Pure functions for resolving instruments and normalizing legacy metadata formats.
4
+ */
5
+
6
+ /**
7
+ * Resolve instrument to MIDI program number.
8
+ * Accepts a number (returned as-is), a string instrument name (looked up), or defaults to 0 (piano).
9
+ * @param {number|string} instrument - Instrument identifier
10
+ * @returns {number} MIDI program number
11
+ */
12
+ export function resolveInstrument(instrument) {
13
+ if (typeof instrument === 'number') {
14
+ return instrument;
15
+ }
16
+
17
+ if (typeof instrument === 'string') {
18
+ const instrumentMap = {
19
+ 'choir_aahs': 52,
20
+ 'piano': 0,
21
+ 'acoustic_grand_piano': 0,
22
+ 'bright_acoustic_piano': 1,
23
+ 'electric_grand_piano': 2,
24
+ 'strings': 48,
25
+ 'string_ensemble_1': 48,
26
+ 'violin': 40,
27
+ 'viola': 41,
28
+ 'cello': 42,
29
+ 'contrabass': 43
30
+ };
31
+
32
+ const normalized = instrument.toLowerCase().replace(/ /g, '_');
33
+ return instrumentMap[normalized] !== undefined ? instrumentMap[normalized] : 0;
34
+ }
35
+
36
+ return 0; // Default to piano
37
+ }
38
+
39
+ /**
40
+ * Parse URL query parameters.
41
+ * @param {string} url - URL string with query parameters
42
+ * @returns {Object} Object with parameter key-value pairs
43
+ */
44
+ export function parseUrlParams(url) {
45
+ const params = {};
46
+
47
+ const queryStart = url.indexOf('?');
48
+ if (queryStart === -1) {
49
+ return params;
50
+ }
51
+
52
+ const queryString = url.substring(queryStart + 1);
53
+
54
+ const pairs = queryString.split('&');
55
+ for (const pair of pairs) {
56
+ const [key, value] = pair.split('=');
57
+ if (key && value !== undefined) {
58
+ params[key] = value;
59
+ }
60
+ }
61
+
62
+ return params;
63
+ }
64
+
65
+ /**
66
+ * Convert legacy parts array to current format object.
67
+ * Parses URL query parameters (track, prog) from legacy format.
68
+ * @param {Array} legacyParts - Array of part objects with url, name, volume
69
+ * @returns {Object} Parts object with channel and instrument
70
+ */
71
+ export function convertLegacyParts(legacyParts) {
72
+ const parts = {};
73
+
74
+ for (const legacyPart of legacyParts) {
75
+ if (!legacyPart.name || !legacyPart.url) {
76
+ continue;
77
+ }
78
+
79
+ const partKey = legacyPart.name.toLowerCase();
80
+ const urlParams = parseUrlParams(legacyPart.url);
81
+ const partConfig = {};
82
+
83
+ if (urlParams.track !== undefined) {
84
+ partConfig.channel = parseInt(urlParams.track, 10);
85
+ }
86
+
87
+ if (urlParams.prog !== undefined) {
88
+ partConfig.instrument = parseInt(urlParams.prog, 10);
89
+ }
90
+
91
+ if (partConfig.channel !== undefined) {
92
+ parts[partKey] = partConfig;
93
+ }
94
+ }
95
+
96
+ return parts;
97
+ }
98
+
99
+ /**
100
+ * Normalize legacy metadata format (v1) to current format (v2).
101
+ * Detects and converts:
102
+ * - scores[] array wrapper -> unwraps scores[0]
103
+ * - parts array with URLs -> parts object with trackIndex/instrument
104
+ * - bars array -> preserved for beat mapping
105
+ *
106
+ * Mutates and returns the metadata object, or returns an empty object if input is falsy/empty.
107
+ * @param {Object|null} metadata - Raw metadata overrides
108
+ * @returns {Object} Normalized metadata object (same reference if mutated, or new empty object)
109
+ */
110
+ export function normalizeLegacyMetadata(metadata) {
111
+ if (!metadata || Object.keys(metadata).length === 0) {
112
+ return metadata || {};
113
+ }
114
+
115
+ // Detect legacy format: has 'scores' array wrapper
116
+ if (metadata.scores && Array.isArray(metadata.scores) && metadata.scores.length > 0) {
117
+ const score = metadata.scores[0];
118
+
119
+ if (score.parts) {
120
+ metadata.parts = convertLegacyParts(score.parts);
121
+ }
122
+
123
+ if (score.bars) {
124
+ metadata.bars = score.bars;
125
+ }
126
+
127
+ delete metadata.scores;
128
+ }
129
+ // If no 'scores' wrapper but has legacy parts array (array instead of object)
130
+ else if (metadata.parts && Array.isArray(metadata.parts)) {
131
+ metadata.parts = convertLegacyParts(metadata.parts);
132
+ }
133
+
134
+ // Remove legacy fields that should be ignored
135
+ delete metadata.type;
136
+ delete metadata.version;
137
+ delete metadata.subtitle;
138
+
139
+ return metadata;
140
+ }
@@ -1,3 +1,5 @@
1
+ import { resolveInstrument, normalizeLegacyMetadata, convertLegacyParts, parseUrlParams } from './metadata-utils.js';
2
+
1
3
  /**
2
4
  * MidiParser - A class for parsing MIDI files
3
5
  * Extracts parts, bar structure, and metadata
@@ -63,39 +65,7 @@ class MidiParser {
63
65
  * @private
64
66
  */
65
67
  _normalizeLegacyMetadata() {
66
- if (!this.metadataOverrides || Object.keys(this.metadataOverrides).length === 0) {
67
- return;
68
- }
69
-
70
- const metadata = this.metadataOverrides;
71
-
72
- // Detect legacy format: has 'scores' array wrapper
73
- if (metadata.scores && Array.isArray(metadata.scores) && metadata.scores.length > 0) {
74
- const score = metadata.scores[0];
75
-
76
- // Unwrap scores[0] - merge it with top-level metadata
77
- if (score.parts) {
78
- // Convert legacy parts array to current format object
79
- metadata.parts = this._convertLegacyParts(score.parts);
80
- }
81
-
82
- // Preserve bars array if present
83
- if (score.bars) {
84
- metadata.bars = score.bars;
85
- }
86
-
87
- // Remove the scores wrapper
88
- delete metadata.scores;
89
- }
90
- // If no 'scores' wrapper but has legacy parts array (array instead of object)
91
- else if (metadata.parts && Array.isArray(metadata.parts)) {
92
- metadata.parts = this._convertLegacyParts(metadata.parts);
93
- }
94
-
95
- // Remove legacy fields that should be ignored
96
- delete metadata.type;
97
- delete metadata.version;
98
- delete metadata.subtitle;
68
+ this.metadataOverrides = normalizeLegacyMetadata(this.metadataOverrides);
99
69
  }
100
70
 
101
71
  /**
@@ -106,39 +76,7 @@ class MidiParser {
106
76
  * @returns {Object} Parts object with channel and instrument
107
77
  */
108
78
  _convertLegacyParts(legacyParts) {
109
- const parts = {};
110
-
111
- for (const legacyPart of legacyParts) {
112
- if (!legacyPart.name || !legacyPart.url) {
113
- continue;
114
- }
115
-
116
- // Convert part name to lowercase for consistency
117
- const partKey = legacyPart.name.toLowerCase();
118
-
119
- // Parse URL query parameters
120
- const urlParams = this._parseUrlParams(legacyPart.url);
121
-
122
- // Create part config
123
- const partConfig = {};
124
-
125
- // Extract channel from 'track' parameter (legacy 'track' actually meant MIDI channel)
126
- if (urlParams.track !== undefined) {
127
- partConfig.channel = parseInt(urlParams.track, 10);
128
- }
129
-
130
- // Extract instrument from 'prog' parameter
131
- if (urlParams.prog !== undefined) {
132
- partConfig.instrument = parseInt(urlParams.prog, 10);
133
- }
134
-
135
- // Only add part if it has a channel
136
- if (partConfig.channel !== undefined) {
137
- parts[partKey] = partConfig;
138
- }
139
- }
140
-
141
- return parts;
79
+ return convertLegacyParts(legacyParts);
142
80
  }
143
81
 
144
82
  /**
@@ -148,26 +86,7 @@ class MidiParser {
148
86
  * @returns {Object} Object with parameter key-value pairs
149
87
  */
150
88
  _parseUrlParams(url) {
151
- const params = {};
152
-
153
- // Find query string (everything after '?')
154
- const queryStart = url.indexOf('?');
155
- if (queryStart === -1) {
156
- return params;
157
- }
158
-
159
- const queryString = url.substring(queryStart + 1);
160
-
161
- // Parse parameters
162
- const pairs = queryString.split('&');
163
- for (const pair of pairs) {
164
- const [key, value] = pair.split('=');
165
- if (key && value !== undefined) {
166
- params[key] = value;
167
- }
168
- }
169
-
170
- return params;
89
+ return parseUrlParams(url);
171
90
  }
172
91
 
173
92
  /**
@@ -895,33 +814,7 @@ class MidiParser {
895
814
  * @private
896
815
  */
897
816
  _resolveInstrument(instrument) {
898
- if (typeof instrument === 'number') {
899
- return instrument;
900
- }
901
-
902
- // String instrument name - convert to program number
903
- // Import getInstrumentProgram if available, otherwise use simple mapping
904
- if (typeof instrument === 'string') {
905
- // Simple mapping for common instruments (can be expanded)
906
- const instrumentMap = {
907
- 'choir_aahs': 52,
908
- 'piano': 0,
909
- 'acoustic_grand_piano': 0,
910
- 'bright_acoustic_piano': 1,
911
- 'electric_grand_piano': 2,
912
- 'strings': 48,
913
- 'string_ensemble_1': 48,
914
- 'violin': 40,
915
- 'viola': 41,
916
- 'cello': 42,
917
- 'contrabass': 43
918
- };
919
-
920
- const normalized = instrument.toLowerCase().replace(/ /g, '_');
921
- return instrumentMap[normalized] !== undefined ? instrumentMap[normalized] : 0;
922
- }
923
-
924
- return 0; // Default to piano
817
+ return resolveInstrument(instrument);
925
818
  }
926
819
 
927
820
  /**
@@ -20,6 +20,7 @@ import {
20
20
  generateBarsArray,
21
21
  extractRehearsalMarks
22
22
  } from './musicxml/structure-analyzer.js';
23
+ import { resolveInstrument, normalizeLegacyMetadata } from './metadata-utils.js';
23
24
 
24
25
  class MusicXmlConverter {
25
26
  constructor() {
@@ -33,9 +34,10 @@ class MusicXmlConverter {
33
34
  * @param {Object} [options]
34
35
  * @param {number} [options.defaultTempo=120] - Default tempo if none specified in score
35
36
  * @param {number} [options.defaultVelocity=80] - Default note velocity
37
+ * @param {Object|null} [metadataOverrides=null] - Optional metadata overrides (title, composer, parts instrument mapping)
36
38
  * @returns {Object} parsedData with parts, barStructure, metadata, structureMetadata
37
39
  */
38
- convert(input, options = {}) {
40
+ convert(input, options = {}, metadataOverrides = null) {
39
41
  const doc = typeof input === 'string' ? parseXml(input) : input;
40
42
  const root = doc.documentElement;
41
43
 
@@ -238,7 +240,8 @@ class MusicXmlConverter {
238
240
  };
239
241
  }
240
242
 
241
- return {
243
+ // Build result object
244
+ const result = {
242
245
  parts,
243
246
  barStructure,
244
247
  metadata: {
@@ -258,6 +261,79 @@ class MusicXmlConverter {
258
261
  parts: structureParts
259
262
  }
260
263
  };
264
+
265
+ // Apply metadata overrides if provided
266
+ if (metadataOverrides) {
267
+ this._applyMetadataOverrides(result, metadataOverrides);
268
+ }
269
+
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Apply metadata overrides to the converted result.
275
+ * Handles legacy format normalization, title/composer overrides,
276
+ * and per-part instrument/order overrides via case-insensitive name matching.
277
+ * @param {Object} result - The parsedData result to modify in place
278
+ * @param {Object} overrides - Raw metadata overrides
279
+ * @private
280
+ */
281
+ _applyMetadataOverrides(result, overrides) {
282
+ // Normalize legacy metadata format (scores[] wrapper, parts array, etc.)
283
+ const normalized = normalizeLegacyMetadata(
284
+ typeof overrides === 'string' ? JSON.parse(overrides) : { ...overrides }
285
+ );
286
+
287
+ // Override title/composer/arranger/copyright
288
+ if (normalized.title !== undefined) {
289
+ result.metadata.title = normalized.title;
290
+ result.structureMetadata.title = normalized.title;
291
+ }
292
+ if (normalized.composer !== undefined) {
293
+ result.metadata.composer = normalized.composer;
294
+ result.structureMetadata.composer = normalized.composer;
295
+ }
296
+ if (normalized.arranger !== undefined) {
297
+ result.metadata.arranger = normalized.arranger;
298
+ }
299
+ if (normalized.copyright !== undefined) {
300
+ result.metadata.copyright = normalized.copyright;
301
+ }
302
+
303
+ // Apply per-part overrides (instrument, order) via case-insensitive name matching
304
+ if (normalized.parts && typeof normalized.parts === 'object') {
305
+ // Build lowercase→originalName lookup from XML parts
306
+ const lowerToOriginal = {};
307
+ for (const xmlPartName of Object.keys(result.parts)) {
308
+ lowerToOriginal[xmlPartName.toLowerCase()] = xmlPartName;
309
+ }
310
+
311
+ for (const [metaKey, partConfig] of Object.entries(normalized.parts)) {
312
+ const matchedName = lowerToOriginal[metaKey.toLowerCase()];
313
+ if (!matchedName) {
314
+ console.warn(`Metadata override: part "${metaKey}" does not match any XML part. Available: ${Object.keys(result.parts).join(', ')}`);
315
+ continue;
316
+ }
317
+
318
+ // Apply instrument override
319
+ if (partConfig.instrument !== undefined && partConfig.instrument !== null) {
320
+ const resolvedInstrument = resolveInstrument(partConfig.instrument);
321
+ result.parts[matchedName].defaultInstrument = resolvedInstrument;
322
+ result.parts[matchedName].programChanges = [];
323
+ if (result.structureMetadata.parts[matchedName]) {
324
+ result.structureMetadata.parts[matchedName].instrument = resolvedInstrument;
325
+ }
326
+ }
327
+
328
+ // Apply order override
329
+ if (partConfig.order !== undefined && typeof partConfig.order === 'number') {
330
+ if (result.structureMetadata.parts[matchedName]) {
331
+ result.structureMetadata.parts[matchedName].order = partConfig.order;
332
+ }
333
+ }
334
+ }
335
+ }
336
+
261
337
  }
262
338
 
263
339
  /**
@@ -1,6 +1,8 @@
1
1
  import mitt from 'mitt';
2
2
  import MidiParser from './midi-parser.js';
3
3
  import MidiPlayer from './midi-player.js';
4
+ import MusicXmlConverter from './musicxml-converter.js';
5
+ import { loadMusicXmlSync } from './musicxml/mxl-loader.js';
4
6
 
5
7
  /**
6
8
  * PlaybackManager - High-level orchestration for MIDI playback, metronome, and lead-in
@@ -44,6 +46,7 @@ export default class PlaybackManager {
44
46
 
45
47
  this.eventBus = mitt();
46
48
  this._parser = new MidiParser(); // Internal MIDI parser
49
+ this._musicXmlConverter = new MusicXmlConverter(); // Internal MusicXML converter
47
50
  this._partOutputsMap = new Map(); // Track part outputs for iteration
48
51
 
49
52
  // Store instrument map separately (allows parsing before audio engine ready)
@@ -171,19 +174,53 @@ export default class PlaybackManager {
171
174
  }
172
175
  // Otherwise, player will be created when setAudioEngine() is called
173
176
 
177
+ } else if (typeof input === 'string') {
178
+ // String input - check if it looks like MusicXML
179
+ const trimmed = input.trimStart();
180
+ if (trimmed.startsWith('<?xml') || trimmed.startsWith('<score-partwise') || trimmed.startsWith('<score-timewise')) {
181
+ // MusicXML string - convert directly (use trimmed to avoid XML parse errors from leading whitespace)
182
+ this.parsedData = this._musicXmlConverter.convert(trimmed, {}, metadata);
183
+ this.instrumentMap = instrumentMap || this._createDefaultInstrumentMap(this.parsedData.parts);
184
+
185
+ if (this._audioEngineReady) {
186
+ this._setupPlayerWithAudio();
187
+ }
188
+ } else {
189
+ throw new Error('Invalid input string. Expected MusicXML content (must start with <?xml, <score-partwise>, or <score-timewise>)');
190
+ }
191
+
174
192
  } else if (input instanceof ArrayBuffer) {
175
- // Parse raw MIDI data (with metadata for normalization)
176
- // This works WITHOUT audio engine - key benefit!
177
- this.parsedData = await this._parser.parse(input, metadata);
193
+ // ArrayBuffer input - detect format from magic bytes
194
+ const data = new Uint8Array(input);
178
195
 
179
- // Create instrument mapping if not provided
180
- this.instrumentMap = instrumentMap || this._createDefaultInstrumentMap(this.parsedData.parts);
196
+ if (data.length >= 4 && data[0] === 0x4D && data[1] === 0x54 && data[2] === 0x68 && data[3] === 0x64) {
197
+ // MThd magic bytes - MIDI file
198
+ this.parsedData = await this._parser.parse(input, metadata);
199
+ this.instrumentMap = instrumentMap || this._createDefaultInstrumentMap(this.parsedData.parts);
181
200
 
182
- // If audio engine ready, create player now
183
- if (this._audioEngineReady) {
184
- this._setupPlayerWithAudio();
201
+ if (this._audioEngineReady) {
202
+ this._setupPlayerWithAudio();
203
+ }
204
+
205
+ } else if (data.length >= 4 && data[0] === 0x50 && data[1] === 0x4B && data[2] === 0x03 && data[3] === 0x04) {
206
+ // PK\x03\x04 magic bytes - ZIP/MXL file
207
+ const xmlString = loadMusicXmlSync(input);
208
+ this.parsedData = this._musicXmlConverter.convert(xmlString, {}, metadata);
209
+ this.instrumentMap = instrumentMap || this._createDefaultInstrumentMap(this.parsedData.parts);
210
+
211
+ if (this._audioEngineReady) {
212
+ this._setupPlayerWithAudio();
213
+ }
214
+
215
+ } else {
216
+ // Unknown format - try MIDI parser for backwards compatibility
217
+ this.parsedData = await this._parser.parse(input, metadata);
218
+ this.instrumentMap = instrumentMap || this._createDefaultInstrumentMap(this.parsedData.parts);
219
+
220
+ if (this._audioEngineReady) {
221
+ this._setupPlayerWithAudio();
222
+ }
185
223
  }
186
- // Otherwise, player will be created when setAudioEngine() is called
187
224
 
188
225
  } else if (input && typeof input === 'object' && input.parts) {
189
226
  // Parsed MIDI data object
@@ -205,7 +242,7 @@ export default class PlaybackManager {
205
242
  // Otherwise, player will be created when setAudioEngine() is called
206
243
 
207
244
  } else {
208
- throw new Error('Invalid input type. Expected MidiPlayer, parsed MIDI data, or ArrayBuffer');
245
+ throw new Error('Invalid input type. Expected MidiPlayer, parsed data object, ArrayBuffer (MIDI/MXL), or MusicXML string');
209
246
  }
210
247
  }
211
248