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 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 Options
20
+ ### Choosing an Audio Engine
21
21
 
22
- - **SpessaSynthAudioEngine**: Soundfont-based synthesis, requires separate soundfont file (~10-50MB)
23
- - **LightweightAudioEngine**: Sample-based with smaller bundle (~574KB). See [LIGHTWEIGHT_ENGINE.md](./LIGHTWEIGHT_ENGINE.md) for setup.
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 { SpessaSynthAudioEngine, PlaybackManager } from 'audio-mixer-engine';
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 SpessaSynthAudioEngine(ac);
34
- await audioEngine.initialize('/path/to/soundfont.sf2');
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 legacy v1 metadata format to v2
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 configuration, and playback modifiers
330
- - **[BEATMAPPING.md](./BEATMAPPING.md)** - Beat mapping and non-linear playback structures
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 and best practices
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",
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
- if (timingEngine.swingMap.length > 0) {
254
- for (const part of Object.values(parts)) {
255
- for (const note of part.notes) {
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
- for (const lyric of part.lyrics) {
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