audio-mixer-engine 1.3.1 → 1.3.2
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.
|
|
3
|
+
"version": "1.3.2",
|
|
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",
|
|
@@ -38,6 +38,10 @@ export class TimingEngine {
|
|
|
38
38
|
|
|
39
39
|
// Time signature changes: array of { tick, numerator, denominator }
|
|
40
40
|
this.timeSigChanges = [{ tick: 0, numerator: 4, denominator: 4 }];
|
|
41
|
+
|
|
42
|
+
// Swing map: array of { tick, first, second, swingType } or { tick, straight: true }
|
|
43
|
+
// Built during the main walk; always starts empty and is populated measure by measure.
|
|
44
|
+
this.swingMap = [];
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -87,6 +91,69 @@ export class TimingEngine {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Record a swing directive at the current tick position.
|
|
96
|
+
* Called when a <swing> element is found inside a <sound> direction.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} params
|
|
99
|
+
* @param {boolean} [params.straight] - If true, turns swing off (play straight)
|
|
100
|
+
* @param {number} [params.first] - Ratio numerator for the on-beat note
|
|
101
|
+
* @param {number} [params.second] - Ratio denominator for the off-beat note
|
|
102
|
+
* @param {string} [params.swingType] - 'eighth' (default) or 'sixteenth'
|
|
103
|
+
*/
|
|
104
|
+
processSwing(params) {
|
|
105
|
+
this.swingMap.push({ tick: this.currentTick, ...params });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Apply swing timing transformation to a straight tick position.
|
|
110
|
+
*
|
|
111
|
+
* For each beat (defined by the swing type), the straight midpoint is shifted
|
|
112
|
+
* to the swing ratio point:
|
|
113
|
+
* swingPoint = swingUnit × first / (first + second)
|
|
114
|
+
* Notes before the midpoint are stretched; notes after are compressed.
|
|
115
|
+
* Beat boundaries (multiples of swingUnit) are never moved.
|
|
116
|
+
*
|
|
117
|
+
* @param {number} tick - Original (straight) canonical tick
|
|
118
|
+
* @returns {number} Swung canonical tick
|
|
119
|
+
*/
|
|
120
|
+
applySwingToTick(tick) {
|
|
121
|
+
// Find the active swing parameters at this tick
|
|
122
|
+
let active = null;
|
|
123
|
+
for (const entry of this.swingMap) {
|
|
124
|
+
if (entry.tick > tick) break;
|
|
125
|
+
active = entry.straight ? null : entry;
|
|
126
|
+
}
|
|
127
|
+
if (!active) return tick; // No swing active — return unchanged
|
|
128
|
+
|
|
129
|
+
const { first, second, swingType } = active;
|
|
130
|
+
|
|
131
|
+
// Swing unit: the note-pair duration that gets swung
|
|
132
|
+
// 'eighth' → quarter note = ticksPerBeat; 'sixteenth' → eighth note = ticksPerBeat/2
|
|
133
|
+
const swingUnit = swingType === 'sixteenth'
|
|
134
|
+
? this.ticksPerBeat / 2
|
|
135
|
+
: this.ticksPerBeat;
|
|
136
|
+
|
|
137
|
+
const straightHalf = swingUnit / 2;
|
|
138
|
+
const swingPoint = swingUnit * first / (first + second);
|
|
139
|
+
|
|
140
|
+
const beatIndex = Math.floor(tick / swingUnit);
|
|
141
|
+
const beatOffset = tick - beatIndex * swingUnit;
|
|
142
|
+
|
|
143
|
+
let mappedOffset;
|
|
144
|
+
if (beatOffset <= straightHalf) {
|
|
145
|
+
// First half: stretch from [0, straightHalf] → [0, swingPoint]
|
|
146
|
+
mappedOffset = beatOffset * swingPoint / straightHalf;
|
|
147
|
+
} else {
|
|
148
|
+
// Second half: compress from (straightHalf, swingUnit] → (swingPoint, swingUnit]
|
|
149
|
+
mappedOffset = swingPoint +
|
|
150
|
+
(beatOffset - straightHalf) * (swingUnit - swingPoint) / straightHalf;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Math.round(beatIndex * swingUnit + mappedOffset);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
90
157
|
/**
|
|
91
158
|
* Convert a MusicXML duration value (in source divisions) to canonical ticks.
|
|
92
159
|
* @param {number} xmlDuration - Duration from <duration> element
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* const parsedData = converter.convert(musicXmlString, { defaultTempo: 120 });
|
|
11
11
|
* await playbackManager.load(parsedData);
|
|
12
12
|
*/
|
|
13
|
-
import { parseXml, getChild, getChildren, getAttr } from './musicxml/xml-helpers.js';
|
|
13
|
+
import { parseXml, getChild, getChildren, getAttr, getChildText, getChildNumber } from './musicxml/xml-helpers.js';
|
|
14
14
|
import { extractMetadata } from './musicxml/metadata-extractor.js';
|
|
15
15
|
import { TimingEngine, DEFAULT_TICKS_PER_BEAT } from './musicxml/timing-engine.js';
|
|
16
16
|
import { NoteExtractor } from './musicxml/note-extractor.js';
|
|
@@ -147,13 +147,28 @@ class MusicXmlConverter {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
// Check for tempo in directions
|
|
150
|
+
// Check for tempo and swing in directions
|
|
151
151
|
const directions = getChildren(m, 'direction');
|
|
152
152
|
for (const dir of directions) {
|
|
153
153
|
const sound = getChild(dir, 'sound');
|
|
154
154
|
if (sound) {
|
|
155
155
|
const tempo = parseFloat(sound.getAttribute('tempo'));
|
|
156
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 };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
measureTimingCache.set(mi, cache);
|
|
@@ -182,6 +197,9 @@ class MusicXmlConverter {
|
|
|
182
197
|
if (cachedTiming.tempo) {
|
|
183
198
|
timingEngine.processTempo(cachedTiming.tempo);
|
|
184
199
|
}
|
|
200
|
+
if (cachedTiming.swing) {
|
|
201
|
+
timingEngine.processSwing(cachedTiming.swing);
|
|
202
|
+
}
|
|
185
203
|
|
|
186
204
|
// Generate barStructure entry at current tick position
|
|
187
205
|
// Use actual beats from unrolled entry (handles partial bars)
|
|
@@ -223,6 +241,34 @@ class MusicXmlConverter {
|
|
|
223
241
|
parts[partName].notes.push(...remainingNotes);
|
|
224
242
|
}
|
|
225
243
|
|
|
244
|
+
// Sort notes by startTick within each part.
|
|
245
|
+
// Multi-staff parts (e.g. piano) use <backup> elements which produce
|
|
246
|
+
// unsorted notes when staves/voices are interleaved in the XML.
|
|
247
|
+
for (const part of Object.values(parts)) {
|
|
248
|
+
part.notes.sort((a, b) => a.startTick - b.startTick || a.pitch - b.pitch);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Apply swing timing if any swing directives were found.
|
|
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) {
|
|
256
|
+
const s = timingEngine.applySwingToTick(note.startTick);
|
|
257
|
+
const e = timingEngine.applySwingToTick(note.endTick);
|
|
258
|
+
note.startTick = s;
|
|
259
|
+
note.endTick = e;
|
|
260
|
+
note.duration = e - s;
|
|
261
|
+
note.startTime = timingEngine.tickToTime(s);
|
|
262
|
+
note.endTime = timingEngine.tickToTime(e);
|
|
263
|
+
}
|
|
264
|
+
for (const lyric of part.lyrics) {
|
|
265
|
+
const t = timingEngine.applySwingToTick(lyric.tick);
|
|
266
|
+
lyric.tick = t;
|
|
267
|
+
lyric.time = timingEngine.tickToTime(t);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
226
272
|
// 7. Build the parsedData output
|
|
227
273
|
const partNames = metadata.partInfos.map((p, i) => ({
|
|
228
274
|
index: i,
|