audio-mixer-engine 1.3.7 → 1.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audio-mixer-engine",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
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",
@@ -147,30 +147,51 @@ class MusicXmlConverter {
147
147
  }
148
148
  }
149
149
  }
150
- // Check for tempo and swing in directions
151
- const directions = getChildren(m, 'direction');
152
- for (const dir of directions) {
153
- const sound = getChild(dir, 'sound');
154
- if (sound) {
155
- const tempo = parseFloat(sound.getAttribute('tempo'));
156
- if (!isNaN(tempo) && tempo > 0) cache.tempo = tempo;
157
-
158
- // Detect <swing> element
159
- const swingEl = getChild(sound, 'swing');
160
- if (swingEl) {
161
- if (getChild(swingEl, 'straight')) {
162
- cache.swing = { straight: true };
163
- } else {
164
- const first = getChildNumber(swingEl, 'first');
165
- const second = getChildNumber(swingEl, 'second');
166
- const swingType = getChildText(swingEl, 'swing-type') || 'eighth';
167
- if (first !== null && second !== null && first > 0 && second > 0) {
168
- cache.swing = { first, second, swingType };
150
+ // Walk children in order to collect position-aware tempo/swing changes.
151
+ // Tracking divCursor lets us record each tempo change's beat-level offset
152
+ // within the measure, so mid-measure rit/accel steps land at the right tick.
153
+ let divCursor = 0;
154
+ cache.tempos = []; // [{ divisionOffset, tempo }]
155
+ for (const child of m.children) {
156
+ if (child.tagName === 'note') {
157
+ if (child.querySelector('chord') === null) {
158
+ const durEl = child.querySelector('duration');
159
+ divCursor += durEl ? parseInt(durEl.textContent, 10) || 0 : 0;
160
+ }
161
+ } else if (child.tagName === 'forward') {
162
+ const durEl = child.querySelector('duration');
163
+ divCursor += durEl ? parseInt(durEl.textContent, 10) || 0 : 0;
164
+ } else if (child.tagName === 'backup') {
165
+ const durEl = child.querySelector('duration');
166
+ divCursor -= durEl ? parseInt(durEl.textContent, 10) || 0 : 0;
167
+ if (divCursor < 0) divCursor = 0;
168
+ } else if (child.tagName === 'direction') {
169
+ const sound = getChild(child, 'sound');
170
+ if (sound) {
171
+ const tempo = parseFloat(sound.getAttribute('tempo'));
172
+ if (!isNaN(tempo) && tempo > 0) {
173
+ cache.tempos.push({ divisionOffset: divCursor, tempo });
174
+ }
175
+
176
+ // Detect <swing> element
177
+ const swingEl = getChild(sound, 'swing');
178
+ if (swingEl) {
179
+ if (getChild(swingEl, 'straight')) {
180
+ cache.swing = { straight: true };
181
+ } else {
182
+ const first = getChildNumber(swingEl, 'first');
183
+ const second = getChildNumber(swingEl, 'second');
184
+ const swingType = getChildText(swingEl, 'swing-type') || 'eighth';
185
+ if (first !== null && second !== null && first > 0 && second > 0) {
186
+ cache.swing = { first, second, swingType };
187
+ }
169
188
  }
170
189
  }
171
190
  }
172
191
  }
173
192
  }
193
+ // Keep cache.tempo for structure-display purposes (first tempo seen in measure)
194
+ if (cache.tempos.length > 0) cache.tempo = cache.tempos[0].tempo;
174
195
  measureTimingCache.set(mi, cache);
175
196
  }
176
197
 
@@ -187,6 +208,10 @@ class MusicXmlConverter {
187
208
  const mi = entry.measureIndex;
188
209
  const cachedTiming = measureTimingCache.get(mi) || {};
189
210
 
211
+ // Read measure start before mutating the timing cursor for tempo placement
212
+ const measureStartTick = timingEngine.currentTick;
213
+ const actualBeats = entry.beatsInMeasure;
214
+
190
215
  // Apply timing state for this measure
191
216
  if (cachedTiming.divisions) {
192
217
  timingEngine.currentDivisions = cachedTiming.divisions;
@@ -194,17 +219,22 @@ class MusicXmlConverter {
194
219
  if (cachedTiming.timeSig) {
195
220
  timingEngine.currentTimeSig = { ...cachedTiming.timeSig };
196
221
  }
197
- if (cachedTiming.tempo) {
198
- timingEngine.processTempo(cachedTiming.tempo);
222
+
223
+ // Apply each tempo change at its exact tick offset within the measure so that
224
+ // mid-measure rit/accel steps (e.g. one change per beat) are preserved.
225
+ if (cachedTiming.tempos && cachedTiming.tempos.length > 0) {
226
+ const divisions = cachedTiming.divisions || timingEngine.currentDivisions;
227
+ for (const { divisionOffset, tempo } of cachedTiming.tempos) {
228
+ const tickOffset = Math.round((divisionOffset / divisions) * timingEngine.ticksPerBeat);
229
+ timingEngine.currentTick = measureStartTick + tickOffset;
230
+ timingEngine.processTempo(tempo);
231
+ }
232
+ timingEngine.currentTick = measureStartTick; // Restore cursor for bar-structure generation
199
233
  }
234
+
200
235
  if (cachedTiming.swing) {
201
236
  timingEngine.processSwing(cachedTiming.swing);
202
237
  }
203
-
204
- // Generate barStructure entry at current tick position
205
- // Use actual beats from unrolled entry (handles partial bars)
206
- const measureStartTick = timingEngine.currentTick;
207
- const actualBeats = entry.beatsInMeasure;
208
238
  const barEntry = timingEngine.generateBarStructureEntry(measureStartTick, actualBeats);
209
239
  barStructure.push(barEntry);
210
240