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 +42 -9
- package/package.json +1 -1
- package/src/index.js +3 -0
- package/src/lib/metadata-utils.js +140 -0
- package/src/lib/midi-parser.js +6 -113
- package/src/lib/musicxml-converter.js +78 -2
- package/src/lib/playback-manager.js +47 -10
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 {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
76
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|
package/src/lib/midi-parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
245
|
+
throw new Error('Invalid input type. Expected MidiPlayer, parsed data object, ArrayBuffer (MIDI/MXL), or MusicXML string');
|
|
209
246
|
}
|
|
210
247
|
}
|
|
211
248
|
|