audio-mixer-engine 1.3.3 → 1.3.4
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 +18 -11
- package/package.json +1 -1
- package/src/lib/musicxml/note-extractor.js +11 -4
- package/src/lib/musicxml-converter.js +12 -4
package/README.md
CHANGED
|
@@ -17,21 +17,27 @@ A part-centric JavaScript audio library for mixer applications. Provides individ
|
|
|
17
17
|
npm install audio-mixer-engine
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
### Audio Engine
|
|
20
|
+
### Choosing an Audio Engine
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
| Engine | Bundle | Instruments | Best for |
|
|
23
|
+
|--------|--------|-------------|----------|
|
|
24
|
+
| **`LightweightAudioEngine`** | ~574KB | Piano, choir | Mobile, general web use |
|
|
25
|
+
| **`SpessaSynthAudioEngine`** | +10-50MB soundfont | Full General MIDI | Desktop, full instrument range |
|
|
26
|
+
|
|
27
|
+
For most deployments — especially mobile — use `LightweightAudioEngine`. See [LIGHTWEIGHT_ENGINE.md](./LIGHTWEIGHT_ENGINE.md) for setup details.
|
|
28
|
+
|
|
29
|
+
`SpessaSynthAudioEngine` requires a separate soundfont download and has known audio performance issues on mobile (see [LATENCY_CALIBRATION.md](./LATENCY_CALIBRATION.md)).
|
|
24
30
|
|
|
25
31
|
## Quick Start
|
|
26
32
|
|
|
27
33
|
```javascript
|
|
28
|
-
import {
|
|
34
|
+
import { LightweightAudioEngine, PlaybackManager } from 'audio-mixer-engine';
|
|
29
35
|
|
|
30
36
|
const ac = new AudioContext();
|
|
31
37
|
|
|
32
|
-
// Initialize audio engine
|
|
33
|
-
const audioEngine = new
|
|
34
|
-
await audioEngine.initialize(
|
|
38
|
+
// Initialize audio engine (no soundfont needed)
|
|
39
|
+
const audioEngine = new LightweightAudioEngine(ac);
|
|
40
|
+
await audioEngine.initialize();
|
|
35
41
|
|
|
36
42
|
// Create PlaybackManager
|
|
37
43
|
const manager = new PlaybackManager(audioEngine, {
|
|
@@ -267,7 +273,7 @@ resolveInstrument('piano'); // 0
|
|
|
267
273
|
resolveInstrument('choir_aahs'); // 52
|
|
268
274
|
resolveInstrument(40); // 40
|
|
269
275
|
|
|
270
|
-
// Normalize
|
|
276
|
+
// Normalize metadata with scores[] wrapper to flat format
|
|
271
277
|
const normalized = normalizeLegacyMetadata(legacyMetadata);
|
|
272
278
|
```
|
|
273
279
|
|
|
@@ -326,11 +332,12 @@ class AudioMixer {
|
|
|
326
332
|
|
|
327
333
|
- **[MUSICXML.md](./MUSICXML.md)** - MusicXML/MXL file loading, supported elements, and API reference
|
|
328
334
|
- **[LIGHTWEIGHT_ENGINE.md](./LIGHTWEIGHT_ENGINE.md)** - Lightweight engine setup and sample file configuration
|
|
329
|
-
- **[METADATA.md](./METADATA.md)** - Score metadata, part
|
|
330
|
-
- **[
|
|
335
|
+
- **[METADATA.md](./METADATA.md)** - Score metadata, part/instrument overrides, and playback modifiers
|
|
336
|
+
- **[MIDI_METADATA.md](./MIDI_METADATA.md)** - MIDI bar structure format (repeats, jumps, pickup bars)
|
|
331
337
|
- **[INTERFACE.md](./INTERFACE.md)** - Complete interface contract for UI integration
|
|
332
|
-
- **[INIT_PROGRESS.md](./INIT_PROGRESS.md)** - Initialization progress tracking
|
|
338
|
+
- **[INIT_PROGRESS.md](./INIT_PROGRESS.md)** - Initialization progress tracking (SpessaSynth only)
|
|
333
339
|
- **[IOS_AUDIO_RESUMPTION.md](./IOS_AUDIO_RESUMPTION.md)** - Handling iOS screen lock/background audio suspension
|
|
340
|
+
- **[LATENCY_CALIBRATION.md](./LATENCY_CALIBRATION.md)** - Audio latency measurement and drift detection (SpessaSynth)
|
|
334
341
|
|
|
335
342
|
## Development
|
|
336
343
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audio-mixer-engine",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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",
|
|
@@ -131,6 +131,7 @@ export class NoteExtractor {
|
|
|
131
131
|
const isGrace = getChild(child, 'grace') !== null;
|
|
132
132
|
const durationXml = getChildNumber(child, 'duration', 0);
|
|
133
133
|
const durationTicks = this.timing.xmlDurationToTicks(durationXml);
|
|
134
|
+
const isTuplet = getChild(child, 'time-modification') !== null;
|
|
134
135
|
|
|
135
136
|
// Grace notes: skip (no duration effect)
|
|
136
137
|
if (isGrace) continue;
|
|
@@ -175,9 +176,11 @@ export class NoteExtractor {
|
|
|
175
176
|
if (tieStart) {
|
|
176
177
|
// Middle of a tie chain: extend the end tick
|
|
177
178
|
pending.endTick = noteStart + durationTicks;
|
|
179
|
+
pending.isTuplet = pending.isTuplet || isTuplet;
|
|
178
180
|
} else {
|
|
179
181
|
// End of tie: emit the completed note
|
|
180
182
|
pending.endTick = noteStart + durationTicks;
|
|
183
|
+
pending.isTuplet = pending.isTuplet || isTuplet;
|
|
181
184
|
notes.push(this._buildNote(pending, partChannel, sourceTrackIndex));
|
|
182
185
|
this.pendingTies.delete(midiPitch);
|
|
183
186
|
}
|
|
@@ -187,7 +190,8 @@ export class NoteExtractor {
|
|
|
187
190
|
pitch: midiPitch,
|
|
188
191
|
startTick: noteStart,
|
|
189
192
|
endTick: noteStart + durationTicks,
|
|
190
|
-
velocity: this.currentVelocity
|
|
193
|
+
velocity: this.currentVelocity,
|
|
194
|
+
isTuplet
|
|
191
195
|
});
|
|
192
196
|
} else {
|
|
193
197
|
// Regular note (not tied)
|
|
@@ -195,7 +199,8 @@ export class NoteExtractor {
|
|
|
195
199
|
pitch: midiPitch,
|
|
196
200
|
startTick: noteStart,
|
|
197
201
|
endTick: noteStart + durationTicks,
|
|
198
|
-
velocity: this.currentVelocity
|
|
202
|
+
velocity: this.currentVelocity,
|
|
203
|
+
isTuplet
|
|
199
204
|
}, partChannel, sourceTrackIndex));
|
|
200
205
|
}
|
|
201
206
|
|
|
@@ -209,7 +214,8 @@ export class NoteExtractor {
|
|
|
209
214
|
text,
|
|
210
215
|
syllabic: syllabic || 'single',
|
|
211
216
|
tick: noteStart,
|
|
212
|
-
time: this.timing.tickToTime(noteStart)
|
|
217
|
+
time: this.timing.tickToTime(noteStart),
|
|
218
|
+
isTuplet
|
|
213
219
|
});
|
|
214
220
|
}
|
|
215
221
|
}
|
|
@@ -257,7 +263,8 @@ export class NoteExtractor {
|
|
|
257
263
|
endTime: this.timing.tickToTime(data.endTick),
|
|
258
264
|
velocity: data.velocity,
|
|
259
265
|
channel,
|
|
260
|
-
sourceTrackIndex
|
|
266
|
+
sourceTrackIndex,
|
|
267
|
+
isTuplet: data.isTuplet || false
|
|
261
268
|
};
|
|
262
269
|
}
|
|
263
270
|
|
|
@@ -250,9 +250,13 @@ class MusicXmlConverter {
|
|
|
250
250
|
|
|
251
251
|
// Apply swing timing if any swing directives were found.
|
|
252
252
|
// Transforms note and lyric tick positions; does not affect barStructure.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// Tuplet notes (isTuplet=true) are excluded: their timing already embodies the
|
|
254
|
+
// subdivision that swing represents, so applying swing would double-count it.
|
|
255
|
+
// The isTuplet flag is an internal marker and is stripped here before output.
|
|
256
|
+
const hasSwing = timingEngine.swingMap.length > 0;
|
|
257
|
+
for (const part of Object.values(parts)) {
|
|
258
|
+
for (const note of part.notes) {
|
|
259
|
+
if (hasSwing && !note.isTuplet) {
|
|
256
260
|
const s = timingEngine.applySwingToTick(note.startTick);
|
|
257
261
|
const e = timingEngine.applySwingToTick(note.endTick);
|
|
258
262
|
note.startTick = s;
|
|
@@ -261,11 +265,15 @@ class MusicXmlConverter {
|
|
|
261
265
|
note.startTime = timingEngine.tickToTime(s);
|
|
262
266
|
note.endTime = timingEngine.tickToTime(e);
|
|
263
267
|
}
|
|
264
|
-
|
|
268
|
+
delete note.isTuplet;
|
|
269
|
+
}
|
|
270
|
+
for (const lyric of part.lyrics) {
|
|
271
|
+
if (hasSwing && !lyric.isTuplet) {
|
|
265
272
|
const t = timingEngine.applySwingToTick(lyric.tick);
|
|
266
273
|
lyric.tick = t;
|
|
267
274
|
lyric.time = timingEngine.tickToTime(t);
|
|
268
275
|
}
|
|
276
|
+
delete lyric.isTuplet;
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
279
|
|