@sudobility/music_codecs 0.1.1
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/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/midi/analyze.d.ts +53 -0
- package/dist/midi/analyze.d.ts.map +1 -0
- package/dist/midi/analyze.js +58 -0
- package/dist/midi/analyze.js.map +1 -0
- package/dist/midi/export.d.ts +20 -0
- package/dist/midi/export.d.ts.map +1 -0
- package/dist/midi/export.js +135 -0
- package/dist/midi/export.js.map +1 -0
- package/dist/midi/grid-detection.d.ts +41 -0
- package/dist/midi/grid-detection.d.ts.map +1 -0
- package/dist/midi/grid-detection.js +61 -0
- package/dist/midi/grid-detection.js.map +1 -0
- package/dist/midi/import-options.d.ts +45 -0
- package/dist/midi/import-options.d.ts.map +1 -0
- package/dist/midi/import-options.js +38 -0
- package/dist/midi/import-options.js.map +1 -0
- package/dist/midi/import.d.ts +43 -0
- package/dist/midi/import.d.ts.map +1 -0
- package/dist/midi/import.js +441 -0
- package/dist/midi/import.js.map +1 -0
- package/dist/midi/key-detection.d.ts +11 -0
- package/dist/midi/key-detection.d.ts.map +1 -0
- package/dist/midi/key-detection.js +96 -0
- package/dist/midi/key-detection.js.map +1 -0
- package/dist/midi/measures.d.ts +39 -0
- package/dist/midi/measures.d.ts.map +1 -0
- package/dist/midi/measures.js +141 -0
- package/dist/midi/measures.js.map +1 -0
- package/dist/mod/export.d.ts +54 -0
- package/dist/mod/export.d.ts.map +1 -0
- package/dist/mod/export.js +172 -0
- package/dist/mod/export.js.map +1 -0
- package/dist/mod/fill.d.ts +11 -0
- package/dist/mod/fill.d.ts.map +1 -0
- package/dist/mod/fill.js +50 -0
- package/dist/mod/fill.js.map +1 -0
- package/dist/mod/import.d.ts +14 -0
- package/dist/mod/import.d.ts.map +1 -0
- package/dist/mod/import.js +182 -0
- package/dist/mod/import.js.map +1 -0
- package/dist/mod/limits.d.ts +26 -0
- package/dist/mod/limits.d.ts.map +1 -0
- package/dist/mod/limits.js +14 -0
- package/dist/mod/limits.js.map +1 -0
- package/dist/mod/timing.d.ts +29 -0
- package/dist/mod/timing.d.ts.map +1 -0
- package/dist/mod/timing.js +61 -0
- package/dist/mod/timing.js.map +1 -0
- package/dist/mod/types.d.ts +8 -0
- package/dist/mod/types.d.ts.map +1 -0
- package/dist/mod/types.js +2 -0
- package/dist/mod/types.js.map +1 -0
- package/dist/musicxml/duration-map.d.ts +34 -0
- package/dist/musicxml/duration-map.d.ts.map +1 -0
- package/dist/musicxml/duration-map.js +79 -0
- package/dist/musicxml/duration-map.js.map +1 -0
- package/dist/musicxml/export.d.ts +35 -0
- package/dist/musicxml/export.d.ts.map +1 -0
- package/dist/musicxml/export.js +271 -0
- package/dist/musicxml/export.js.map +1 -0
- package/dist/musicxml/import.d.ts +52 -0
- package/dist/musicxml/import.d.ts.map +1 -0
- package/dist/musicxml/import.js +637 -0
- package/dist/musicxml/import.js.map +1 -0
- package/dist/test/fixtures.d.ts +45 -0
- package/dist/test/fixtures.d.ts.map +1 -0
- package/dist/test/fixtures.js +420 -0
- package/dist/test/fixtures.js.map +1 -0
- package/dist/test/musicxml-warnings.d.ts +11 -0
- package/dist/test/musicxml-warnings.d.ts.map +1 -0
- package/dist/test/musicxml-warnings.js +26 -0
- package/dist/test/musicxml-warnings.js.map +1 -0
- package/dist/test/platform.d.ts +13 -0
- package/dist/test/platform.d.ts.map +1 -0
- package/dist/test/platform.js +191 -0
- package/dist/test/platform.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/midi/import.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EAET,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7D,OAAO,KAAK,EAGV,KAAK,EAGN,MAAM,yBAAyB,CAAC;AA+cjC,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEpE;;;;;;;;;;;GAWG;AACH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,iBAAiB,GACzB,gBAAgB,CAmGlB;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CACxB,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,SAAS,GACf,gBAAgB,CAElB"}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { detectKeySignature } from './key-detection.js';
|
|
2
|
+
import { assembleTrackMeasures, buildMeasureSpans } from './measures.js';
|
|
3
|
+
import { midiToPitch, pitchToMidi } from '@sudobility/music_types';
|
|
4
|
+
import { createId } from '@sudobility/music_types';
|
|
5
|
+
import { isNoteEvent } from '@sudobility/music_types';
|
|
6
|
+
import { quantizeEvents } from '@sudobility/music_types';
|
|
7
|
+
import { ticksFor } from '@sudobility/music_types';
|
|
8
|
+
import { allocateVoices } from '@sudobility/music_types';
|
|
9
|
+
/** The score model's fixed internal PPQ (spec §4/§15: every import is normalized to 480). */
|
|
10
|
+
const SCORE_PPQ = 480;
|
|
11
|
+
const DEFAULT_TEMPO_BPM = 120;
|
|
12
|
+
const MIN_BPM = 20;
|
|
13
|
+
const MAX_BPM = 400;
|
|
14
|
+
const MAX_VELOCITY = 127;
|
|
15
|
+
const SUSTAIN_CC_NUMBER = 64;
|
|
16
|
+
const SUSTAIN_DOWN_THRESHOLD = 0.5;
|
|
17
|
+
/** Max simultaneous notated voices `allocateVoices` may open per staff. */
|
|
18
|
+
const DEFAULT_MAX_VOICES = 4;
|
|
19
|
+
const DEFAULT_KEY_SIGNATURE = { fifths: 0, mode: 'major' };
|
|
20
|
+
const PERCUSSION_CHANNEL = 9;
|
|
21
|
+
/**
|
|
22
|
+
* Onsets of the same pitch within this many ticks of each other are treated
|
|
23
|
+
* as accidental duplicate triggers (not a deliberate ornament/grace note) by
|
|
24
|
+
* `mergeNearDuplicates`. Deliberately independent of `quantizeGrid` — this
|
|
25
|
+
* is about performance-timing noise, not the notation grid — roughly a
|
|
26
|
+
* 128th note at 480 ppq. A near-duplicate this close is almost always a
|
|
27
|
+
* double trigger rather than an intentional repeated note.
|
|
28
|
+
*/
|
|
29
|
+
const NEAR_DUPLICATE_TOLERANCE_TICKS = 15;
|
|
30
|
+
const ALWAYS_WARNING = 'MIDI import approximates performance timing as notation: quantization, voice/staff assignment, and key detection are best-effort. Review the result before use.';
|
|
31
|
+
// ---- Tempo / time signature -----------------------------------------------
|
|
32
|
+
function buildTempoMap(sourceTempos, ratio, warnings) {
|
|
33
|
+
const converted = sourceTempos
|
|
34
|
+
.map(t => ({ tick: Math.round(t.ticks * ratio), bpm: t.bpm }))
|
|
35
|
+
.sort((a, b) => a.tick - b.tick);
|
|
36
|
+
const withOrigin = converted.length > 0 && converted[0].tick === 0
|
|
37
|
+
? converted
|
|
38
|
+
: [{ tick: 0, bpm: DEFAULT_TEMPO_BPM }, ...converted];
|
|
39
|
+
return withOrigin.map(t => {
|
|
40
|
+
const bpm = Math.min(MAX_BPM, Math.max(MIN_BPM, t.bpm));
|
|
41
|
+
if (bpm !== t.bpm) {
|
|
42
|
+
warnings.push(`Tempo event at tick ${t.tick} (${t.bpm.toFixed(1)} bpm) was outside the supported ${MIN_BPM}-${MAX_BPM} bpm range and was clamped to ${bpm} bpm.`);
|
|
43
|
+
}
|
|
44
|
+
return { id: createId(), tick: t.tick, bpm };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function convertTimeSignatures(sourceTimeSignatures, ratio) {
|
|
48
|
+
return sourceTimeSignatures.map(t => ({
|
|
49
|
+
tick: Math.round(t.ticks * ratio),
|
|
50
|
+
timeSignature: {
|
|
51
|
+
numerator: t.timeSignature[0],
|
|
52
|
+
denominator: t.timeSignature[1],
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
/** Pedal-down `[start, end)` intervals (target ticks) from a track's CC64 events; an unreleased pedal closes at `trackEndTick`. */
|
|
57
|
+
function sustainIntervals(sourceTrack, ratio, trackEndTick) {
|
|
58
|
+
const events = [
|
|
59
|
+
...(sourceTrack.controlChanges[SUSTAIN_CC_NUMBER] ?? []),
|
|
60
|
+
].sort((a, b) => a.ticks - b.ticks);
|
|
61
|
+
const intervals = [];
|
|
62
|
+
let downStart = null;
|
|
63
|
+
for (const event of events) {
|
|
64
|
+
const tick = Math.round(event.ticks * ratio);
|
|
65
|
+
const down = event.value >= SUSTAIN_DOWN_THRESHOLD;
|
|
66
|
+
if (down && downStart === null) {
|
|
67
|
+
downStart = tick;
|
|
68
|
+
}
|
|
69
|
+
else if (!down && downStart !== null) {
|
|
70
|
+
intervals.push({ start: downStart, end: tick });
|
|
71
|
+
downStart = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (downStart !== null)
|
|
75
|
+
intervals.push({ start: downStart, end: trackEndTick });
|
|
76
|
+
return intervals;
|
|
77
|
+
}
|
|
78
|
+
/** Extends `rawEnd` to the release tick of whichever pedal-down interval it falls inside, if any. */
|
|
79
|
+
function extendThroughSustain(rawEnd, intervals) {
|
|
80
|
+
for (const interval of intervals) {
|
|
81
|
+
if (rawEnd >= interval.start && rawEnd < interval.end)
|
|
82
|
+
return interval.end;
|
|
83
|
+
}
|
|
84
|
+
return rawEnd;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Converts a source track's notes to target-tick `RawNote`s, optionally
|
|
88
|
+
* extending each note's end through a sustain-pedal-down interval it falls
|
|
89
|
+
* in (`sustainPedal: "extend"`). Sustain-created same-pitch extensions are
|
|
90
|
+
* clamped so they do not run into the next onset of that same pitch. Raw MIDI
|
|
91
|
+
* overlaps are left intact; preserving performance timing means not shortening
|
|
92
|
+
* source notes unless a cleanup stage explicitly asks for it.
|
|
93
|
+
*/
|
|
94
|
+
function extractRawNotes(sourceTrack, ratio, sustainPedal) {
|
|
95
|
+
const trackEndTick = Math.round(sourceTrack.durationTicks * ratio);
|
|
96
|
+
const intervals = sustainPedal === 'extend'
|
|
97
|
+
? sustainIntervals(sourceTrack, ratio, trackEndTick)
|
|
98
|
+
: [];
|
|
99
|
+
const spans = sourceTrack.notes.map(note => {
|
|
100
|
+
const startTick = Math.round(note.ticks * ratio);
|
|
101
|
+
const rawEnd = Math.round((note.ticks + note.durationTicks) * ratio);
|
|
102
|
+
const endTick = intervals.length > 0 ? extendThroughSustain(rawEnd, intervals) : rawEnd;
|
|
103
|
+
return {
|
|
104
|
+
midi: note.midi,
|
|
105
|
+
startTick,
|
|
106
|
+
rawEnd,
|
|
107
|
+
endTick,
|
|
108
|
+
velocity: note.velocity,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const byPitch = new Map();
|
|
112
|
+
for (const span of spans) {
|
|
113
|
+
const bucket = byPitch.get(span.midi);
|
|
114
|
+
if (bucket)
|
|
115
|
+
bucket.push(span);
|
|
116
|
+
else
|
|
117
|
+
byPitch.set(span.midi, [span]);
|
|
118
|
+
}
|
|
119
|
+
for (const bucket of byPitch.values()) {
|
|
120
|
+
bucket.sort((a, b) => a.startTick - b.startTick);
|
|
121
|
+
for (let i = 0; i < bucket.length - 1; i += 1) {
|
|
122
|
+
if (bucket[i].rawEnd <= bucket[i + 1].startTick &&
|
|
123
|
+
bucket[i].endTick > bucket[i + 1].startTick) {
|
|
124
|
+
bucket[i].endTick = bucket[i + 1].startTick;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return spans.map(span => ({
|
|
129
|
+
midi: span.midi,
|
|
130
|
+
startTick: span.startTick,
|
|
131
|
+
durationTicks: Math.max(1, span.endTick - span.startTick),
|
|
132
|
+
velocity: Math.min(MAX_VELOCITY, Math.max(0, Math.round(span.velocity * MAX_VELOCITY))),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
// ---- Quantization -------------------------------------------------------------
|
|
136
|
+
/** Grid unit in ticks for `options.quantizeGrid`, or `1` (a no-op grid) when quantization is off. */
|
|
137
|
+
function gridTicksFor(options) {
|
|
138
|
+
return options.quantizeGrid !== null
|
|
139
|
+
? ticksFor(options.quantizeGrid, SCORE_PPQ)
|
|
140
|
+
: 1;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Merges runs of same-pitch onsets within `toleranceTicks` of each other
|
|
144
|
+
* into a single note (earliest start, spanning to the latest end, keeping
|
|
145
|
+
* the louder velocity) — genuine accidental-duplicate-trigger cleanup, not
|
|
146
|
+
* just onset repositioning (contrast `quantizeEvents`' `chordToleranceTicks`,
|
|
147
|
+
* which only snaps near-simultaneous onsets onto a shared tick and is
|
|
148
|
+
* meant for *chords*, i.e. different pitches — it never reduces note
|
|
149
|
+
* count, so it can't by itself implement "merge near-duplicate notes").
|
|
150
|
+
* Only ever compares notes of the *same* pitch, so legitimate chords
|
|
151
|
+
* (different pitches, same or near onset) are never affected. Returns the
|
|
152
|
+
* merged notes (sorted by `startTick`) and how many source notes were
|
|
153
|
+
* folded into another (i.e. `input.length - notes.length`).
|
|
154
|
+
*/
|
|
155
|
+
/** Folds `next` into `base`: keeps `base`'s start, extends to cover both spans, keeps the louder velocity. */
|
|
156
|
+
function mergeInto(base, next) {
|
|
157
|
+
const end = Math.max(base.startTick + base.durationTicks, next.startTick + next.durationTicks);
|
|
158
|
+
return {
|
|
159
|
+
...base,
|
|
160
|
+
durationTicks: end - base.startTick,
|
|
161
|
+
velocity: Math.max(base.velocity, next.velocity),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function mergeNearDuplicateNotes(notes, toleranceTicks) {
|
|
165
|
+
const byPitch = new Map();
|
|
166
|
+
for (const note of notes) {
|
|
167
|
+
const midi = pitchToMidi(note.pitch);
|
|
168
|
+
const bucket = byPitch.get(midi);
|
|
169
|
+
if (bucket)
|
|
170
|
+
bucket.push(note);
|
|
171
|
+
else
|
|
172
|
+
byPitch.set(midi, [note]);
|
|
173
|
+
}
|
|
174
|
+
const kept = [];
|
|
175
|
+
let mergedCount = 0;
|
|
176
|
+
for (const bucket of byPitch.values()) {
|
|
177
|
+
const sorted = [...bucket].sort((a, b) => a.startTick - b.startTick);
|
|
178
|
+
let current = null;
|
|
179
|
+
for (const note of sorted) {
|
|
180
|
+
if (current !== null &&
|
|
181
|
+
note.startTick - current.startTick <= toleranceTicks) {
|
|
182
|
+
current = mergeInto(current, note);
|
|
183
|
+
mergedCount += 1;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (current)
|
|
187
|
+
kept.push(current);
|
|
188
|
+
current = note;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (current)
|
|
192
|
+
kept.push(current);
|
|
193
|
+
}
|
|
194
|
+
kept.sort((a, b) => a.startTick - b.startTick);
|
|
195
|
+
return { notes: kept, mergedCount };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Turns a selection's raw notes into cleaned-up `NoteEvent`s per `options`,
|
|
199
|
+
* as three explicit, independently-measured stages so each cause of a note
|
|
200
|
+
* being dropped/merged/trimmed can be reported accurately (rather than one
|
|
201
|
+
* combined before/after count blamed entirely on `minDurationTicks`,
|
|
202
|
+
* regardless of which stage actually changed anything):
|
|
203
|
+
*
|
|
204
|
+
* 1. Drop notes shorter than `minDurationTicks` + snap starts/durations to
|
|
205
|
+
* the quantization grid (`quantizeGrid`/`tripletDetection`). Only this
|
|
206
|
+
* stage can remove notes, so any count delta here is `droppedShort`.
|
|
207
|
+
* 2. Merge near-duplicate same-pitch onsets (`mergeNearDuplicates`) via
|
|
208
|
+
* `mergeNearDuplicateNotes`. Any count delta here is `mergedDuplicates`.
|
|
209
|
+
* 3. When an explicit cleanup stage can create residual same-pitch overlaps,
|
|
210
|
+
* trim a repeated pitch down to the next same-pitch onset. Different-pitch
|
|
211
|
+
* overlaps are real polyphony and must survive into voice allocation.
|
|
212
|
+
*/
|
|
213
|
+
function trimSamePitchOverlaps(notes) {
|
|
214
|
+
const byId = new Map(notes.map(note => [note.id, note]));
|
|
215
|
+
const byPitch = new Map();
|
|
216
|
+
for (const note of notes) {
|
|
217
|
+
const midi = pitchToMidi(note.pitch);
|
|
218
|
+
const bucket = byPitch.get(midi);
|
|
219
|
+
if (bucket)
|
|
220
|
+
bucket.push(note);
|
|
221
|
+
else
|
|
222
|
+
byPitch.set(midi, [note]);
|
|
223
|
+
}
|
|
224
|
+
let trimmedCount = 0;
|
|
225
|
+
for (const bucket of byPitch.values()) {
|
|
226
|
+
const sorted = [...bucket].sort((a, b) => a.startTick - b.startTick || a.durationTicks - b.durationTicks);
|
|
227
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
228
|
+
let nextIndex = i + 1;
|
|
229
|
+
while (nextIndex < sorted.length &&
|
|
230
|
+
sorted[nextIndex].startTick === sorted[i].startTick) {
|
|
231
|
+
nextIndex += 1;
|
|
232
|
+
}
|
|
233
|
+
const next = sorted[nextIndex];
|
|
234
|
+
if (!next)
|
|
235
|
+
continue;
|
|
236
|
+
const current = byId.get(sorted[i].id);
|
|
237
|
+
const currentEnd = current.startTick + current.durationTicks;
|
|
238
|
+
if (currentEnd > next.startTick) {
|
|
239
|
+
const durationTicks = Math.max(1, next.startTick - current.startTick);
|
|
240
|
+
if (durationTicks !== current.durationTicks) {
|
|
241
|
+
byId.set(current.id, { ...current, durationTicks });
|
|
242
|
+
trimmedCount += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { notes: notes.map(note => byId.get(note.id)), trimmedCount };
|
|
248
|
+
}
|
|
249
|
+
function prepareSelectionNotes(raw, tempTrackId, options) {
|
|
250
|
+
const initialEvents = raw.map(n => ({
|
|
251
|
+
id: createId(),
|
|
252
|
+
pitch: midiToPitch(n.midi),
|
|
253
|
+
startTick: n.startTick,
|
|
254
|
+
durationTicks: n.durationTicks,
|
|
255
|
+
velocity: n.velocity,
|
|
256
|
+
voiceId: 'import',
|
|
257
|
+
trackId: tempTrackId,
|
|
258
|
+
}));
|
|
259
|
+
const quantizing = options.quantizeGrid !== null;
|
|
260
|
+
const grid = gridTicksFor(options);
|
|
261
|
+
const snapQuantizeOptions = {
|
|
262
|
+
grid,
|
|
263
|
+
quantizeStarts: quantizing,
|
|
264
|
+
quantizeDurations: quantizing,
|
|
265
|
+
tripletGrid: quantizing && options.tripletDetection,
|
|
266
|
+
minDurationTicks: options.minDurationTicks,
|
|
267
|
+
};
|
|
268
|
+
const snapped = quantizeEvents(initialEvents, snapQuantizeOptions).filter(isNoteEvent);
|
|
269
|
+
const droppedShort = initialEvents.length - snapped.length;
|
|
270
|
+
const { notes: deduped, mergedCount: mergedDuplicates } = options.mergeNearDuplicates
|
|
271
|
+
? mergeNearDuplicateNotes(snapped, NEAR_DUPLICATE_TOLERANCE_TICKS)
|
|
272
|
+
: { notes: snapped, mergedCount: 0 };
|
|
273
|
+
const shouldTrimSamePitchOverlaps = quantizing || options.mergeNearDuplicates;
|
|
274
|
+
const { notes: resolved, trimmedCount: trimmedOverlaps } = shouldTrimSamePitchOverlaps
|
|
275
|
+
? trimSamePitchOverlaps(deduped)
|
|
276
|
+
: { notes: deduped, trimmedCount: 0 };
|
|
277
|
+
return { notes: resolved, droppedShort, mergedDuplicates, trimmedOverlaps };
|
|
278
|
+
}
|
|
279
|
+
// ---- Track assembly -------------------------------------------------------------
|
|
280
|
+
function trackVolume(sourceTrack) {
|
|
281
|
+
const cc = sourceTrack.controlChanges[7]?.[0];
|
|
282
|
+
return cc ? cc.value : 1;
|
|
283
|
+
}
|
|
284
|
+
function trackPan(sourceTrack) {
|
|
285
|
+
const cc = sourceTrack.controlChanges[10]?.[0];
|
|
286
|
+
return cc ? cc.value * 2 - 1 : 0;
|
|
287
|
+
}
|
|
288
|
+
function buildSingleTrack(prepared, spans, keySignature) {
|
|
289
|
+
const trackId = createId();
|
|
290
|
+
const groups = allocateVoices(prepared.notes, {
|
|
291
|
+
maxVoices: DEFAULT_MAX_VOICES,
|
|
292
|
+
splitPoint: Number.NEGATIVE_INFINITY,
|
|
293
|
+
});
|
|
294
|
+
const lanes = groups.map(g => g.notes);
|
|
295
|
+
const measures = assembleTrackMeasures(lanes, spans, keySignature, trackId);
|
|
296
|
+
const clef = prepared.selection.clef;
|
|
297
|
+
return {
|
|
298
|
+
id: trackId,
|
|
299
|
+
name: prepared.selection.name,
|
|
300
|
+
instrumentName: prepared.sourceTrack.instrument.name || 'Instrument',
|
|
301
|
+
midiProgram: prepared.sourceTrack.instrument.number,
|
|
302
|
+
midiChannel: clef === 'percussion'
|
|
303
|
+
? PERCUSSION_CHANNEL
|
|
304
|
+
: Math.min(15, Math.max(0, prepared.sourceTrack.channel)),
|
|
305
|
+
clef,
|
|
306
|
+
volume: trackVolume(prepared.sourceTrack),
|
|
307
|
+
pan: trackPan(prepared.sourceTrack),
|
|
308
|
+
muted: false,
|
|
309
|
+
solo: false,
|
|
310
|
+
measures,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** Splits `prepared` into two linked grand-staff tracks ("Piano RH" upper/treble, "Piano LH" lower/bass) at `options.splitPointMidi`. */
|
|
314
|
+
function buildSplitTracks(prepared, spans, keySignature, splitPointMidi) {
|
|
315
|
+
const groups = allocateVoices(prepared.notes, {
|
|
316
|
+
maxVoices: DEFAULT_MAX_VOICES,
|
|
317
|
+
splitPoint: splitPointMidi,
|
|
318
|
+
});
|
|
319
|
+
const upperLanes = groups.filter(g => g.staff === 'upper').map(g => g.notes);
|
|
320
|
+
const lowerLanes = groups.filter(g => g.staff === 'lower').map(g => g.notes);
|
|
321
|
+
const rhId = createId();
|
|
322
|
+
const lhId = createId();
|
|
323
|
+
const shared = {
|
|
324
|
+
instrumentName: prepared.sourceTrack.instrument.name || 'Instrument',
|
|
325
|
+
midiProgram: prepared.sourceTrack.instrument.number,
|
|
326
|
+
midiChannel: Math.min(15, Math.max(0, prepared.sourceTrack.channel)),
|
|
327
|
+
volume: trackVolume(prepared.sourceTrack),
|
|
328
|
+
pan: trackPan(prepared.sourceTrack),
|
|
329
|
+
muted: false,
|
|
330
|
+
solo: false,
|
|
331
|
+
};
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
id: rhId,
|
|
335
|
+
name: 'Piano RH',
|
|
336
|
+
clef: 'treble',
|
|
337
|
+
...shared,
|
|
338
|
+
measures: assembleTrackMeasures(upperLanes, spans, keySignature, rhId),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: lhId,
|
|
342
|
+
name: 'Piano LH',
|
|
343
|
+
clef: 'bass',
|
|
344
|
+
...shared,
|
|
345
|
+
measures: assembleTrackMeasures(lowerLanes, spans, keySignature, lhId),
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Imports a Standard MIDI File as a `Score` (spec §15): converts note times
|
|
351
|
+
* to 480-ppq score ticks, imports the tempo map and time signatures,
|
|
352
|
+
* applies sustain-pedal extension, Task 4 quantization (grid/triplet/
|
|
353
|
+
* minimum-duration/near-duplicate merging), key estimation and enharmonic
|
|
354
|
+
* respelling, Task 4 voice allocation, optional piano grand-staff split,
|
|
355
|
+
* and measure generation (notes split across measure boundaries with
|
|
356
|
+
* ties) — per track selected in `options.trackSelections`. Always returns a
|
|
357
|
+
* `warnings` array (never throws for merely-imperfect input); the caller is
|
|
358
|
+
* expected to surface it before the import is committed (spec §15: MIDI
|
|
359
|
+
* import must be reviewable, and one undoable operation once committed).
|
|
360
|
+
*/
|
|
361
|
+
/**
|
|
362
|
+
* Converts an already-decoded MIDI file into a `Score`.
|
|
363
|
+
*
|
|
364
|
+
* Split out from `importMidi` so the heavy work — quantization and voice
|
|
365
|
+
* allocation — can be handed to a worker without the worker needing a codec.
|
|
366
|
+
* `MidiFile` is plain data and therefore structured-cloneable; a `MidiCodec` is
|
|
367
|
+
* not, and giving the worker one would make music_lib depend on music_io.
|
|
368
|
+
*/
|
|
369
|
+
export function importMidiFile(midi, options) {
|
|
370
|
+
const warnings = [ALWAYS_WARNING];
|
|
371
|
+
const ratio = SCORE_PPQ / midi.header.ppq;
|
|
372
|
+
const tempoMap = buildTempoMap(midi.header.tempos, ratio, warnings);
|
|
373
|
+
const timeSignatureChanges = convertTimeSignatures(midi.header.timeSignatures, ratio);
|
|
374
|
+
const preparedSelections = [];
|
|
375
|
+
for (const selection of options.trackSelections) {
|
|
376
|
+
if (!selection.include)
|
|
377
|
+
continue;
|
|
378
|
+
const sourceTrack = midi.tracks[selection.sourceIndex];
|
|
379
|
+
if (!sourceTrack) {
|
|
380
|
+
warnings.push(`Track selection references source track ${selection.sourceIndex}, which doesn't exist in this file; it was skipped.`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const raw = extractRawNotes(sourceTrack, ratio, options.sustainPedal);
|
|
384
|
+
const prepared = prepareSelectionNotes(raw, `import-${selection.sourceIndex}`, options);
|
|
385
|
+
if (prepared.droppedShort > 0) {
|
|
386
|
+
warnings.push(`Track "${selection.name}": dropped ${prepared.droppedShort} note(s) shorter than ${options.minDurationTicks} ticks.`);
|
|
387
|
+
}
|
|
388
|
+
if (prepared.mergedDuplicates > 0) {
|
|
389
|
+
warnings.push(`Track "${selection.name}": merged ${prepared.mergedDuplicates} near-duplicate note(s).`);
|
|
390
|
+
}
|
|
391
|
+
if (prepared.trimmedOverlaps > 0) {
|
|
392
|
+
warnings.push(`Track "${selection.name}": trimmed ${prepared.trimmedOverlaps} overlapping repeated-pitch note(s).`);
|
|
393
|
+
}
|
|
394
|
+
preparedSelections.push({ selection, sourceTrack, notes: prepared.notes });
|
|
395
|
+
}
|
|
396
|
+
if (preparedSelections.length === 0) {
|
|
397
|
+
warnings.push('No tracks were selected for import; the imported score has no tracks.');
|
|
398
|
+
}
|
|
399
|
+
const allNotes = preparedSelections.flatMap(p => p.notes);
|
|
400
|
+
const keySignature = options.detectKey
|
|
401
|
+
? detectKeySignature(allNotes)
|
|
402
|
+
: DEFAULT_KEY_SIGNATURE;
|
|
403
|
+
const respelledSelections = preparedSelections.map(p => ({
|
|
404
|
+
...p,
|
|
405
|
+
notes: p.notes.map(n => ({
|
|
406
|
+
...n,
|
|
407
|
+
pitch: midiToPitch(pitchToMidi(n.pitch), keySignature),
|
|
408
|
+
})),
|
|
409
|
+
}));
|
|
410
|
+
const endTick = respelledSelections.reduce((max, p) => {
|
|
411
|
+
const localMax = p.notes.reduce((m, n) => Math.max(m, n.startTick + n.durationTicks), 0);
|
|
412
|
+
return Math.max(max, localMax);
|
|
413
|
+
}, 0);
|
|
414
|
+
const spans = buildMeasureSpans(timeSignatureChanges, SCORE_PPQ, endTick);
|
|
415
|
+
const tracks = respelledSelections.flatMap(prepared => options.pianoStaffSplit
|
|
416
|
+
? buildSplitTracks(prepared, spans, keySignature, options.splitPointMidi)
|
|
417
|
+
: [buildSingleTrack(prepared, spans, keySignature)]);
|
|
418
|
+
const now = new Date().toISOString();
|
|
419
|
+
const score = {
|
|
420
|
+
id: createId(),
|
|
421
|
+
version: 1,
|
|
422
|
+
ppq: SCORE_PPQ,
|
|
423
|
+
metadata: {
|
|
424
|
+
// The neutral model makes `name` optional, since not every Standard MIDI
|
|
425
|
+
// File carries one; @tonejs/midi papered over that with an empty string.
|
|
426
|
+
title: (midi.header.name ?? '').trim().length > 0
|
|
427
|
+
? midi.header.name.trim()
|
|
428
|
+
: 'Imported MIDI',
|
|
429
|
+
createdAt: now,
|
|
430
|
+
updatedAt: now,
|
|
431
|
+
},
|
|
432
|
+
tempoMap,
|
|
433
|
+
tracks,
|
|
434
|
+
};
|
|
435
|
+
return { score, warnings };
|
|
436
|
+
}
|
|
437
|
+
/** Decodes `data` and imports it. The codec is the only platform-bound part. */
|
|
438
|
+
export function importMidi(data, options, codec) {
|
|
439
|
+
return importMidiFile(codec.decode(data), options);
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/midi/import.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KeySignature, NoteEvent } from '@sudobility/music_types';
|
|
2
|
+
/**
|
|
3
|
+
* Estimates the best-fitting `KeySignature` for `notes` by correlating
|
|
4
|
+
* their duration-weighted pitch-class histogram against the
|
|
5
|
+
* Krumhansl-Kessler profiles at all 12 tonics, both major and minor, and
|
|
6
|
+
* returning the highest-correlating (tonic, mode) pair. Falls back to C
|
|
7
|
+
* major for an empty or entirely-silent `notes` list (histogram all-zero,
|
|
8
|
+
* for which correlation is undefined).
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectKeySignature(notes: NoteEvent[]): KeySignature;
|
|
11
|
+
//# sourceMappingURL=key-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-detection.d.ts","sourceRoot":"","sources":["../../src/midi/key-detection.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAmEvE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,YAAY,CA6BnE"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Krumhansl-style key estimation for MIDI import (spec §15): builds a
|
|
3
|
+
* duration-weighted pitch-class histogram from a set of notes and
|
|
4
|
+
* correlates it, at every tonic/mode rotation, against the classic
|
|
5
|
+
* Krumhansl-Kessler major/minor key profiles, returning the best-matching
|
|
6
|
+
* `KeySignature`. A simple template match, not a full Krumhansl-Schmuckler
|
|
7
|
+
* implementation (spec explicitly allows this: "simple template match is
|
|
8
|
+
* fine") — ambiguous or atonal input has no guaranteed "correct" answer
|
|
9
|
+
* either way.
|
|
10
|
+
*/
|
|
11
|
+
import { pitchToMidi } from '@sudobility/music_types';
|
|
12
|
+
// Inlined from the deleted services/generation/music-theory.ts (mock stack
|
|
13
|
+
// removed in Phase 2); pitch-class math only.
|
|
14
|
+
function majorTonicPitchClass(fifths) {
|
|
15
|
+
return (((7 * fifths) % 12) + 12) % 12;
|
|
16
|
+
}
|
|
17
|
+
/** The `KeySignature` (fifths -7..7) whose tonic is `pitchClass` in `mode`. */
|
|
18
|
+
function keySignatureForTonicPitchClass(pitchClass, mode) {
|
|
19
|
+
const majorPitchClass = mode === 'major' ? pitchClass : (pitchClass + 3) % 12;
|
|
20
|
+
for (let fifths = -7; fifths <= 7; fifths += 1) {
|
|
21
|
+
if (majorTonicPitchClass(fifths) === majorPitchClass) {
|
|
22
|
+
return { fifths, mode };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`keySignatureForTonicPitchClass: no key signature found for pitch class ${pitchClass}`);
|
|
26
|
+
}
|
|
27
|
+
/** Krumhansl-Kessler major-key profile (tonic at index 0), a canonical perceptual "fit" weighting per scale degree. */
|
|
28
|
+
const MAJOR_PROFILE = [
|
|
29
|
+
6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88,
|
|
30
|
+
];
|
|
31
|
+
/** Krumhansl-Kessler minor-key profile (tonic at index 0). */
|
|
32
|
+
const MINOR_PROFILE = [
|
|
33
|
+
6.33, 2.68, 3.52, 5.38, 2.6, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17,
|
|
34
|
+
];
|
|
35
|
+
const PITCH_CLASSES = 12;
|
|
36
|
+
/** Pearson correlation coefficient between two equal-length numeric vectors; 0 if either has zero variance. */
|
|
37
|
+
function correlate(a, b) {
|
|
38
|
+
const n = a.length;
|
|
39
|
+
const meanA = a.reduce((sum, v) => sum + v, 0) / n;
|
|
40
|
+
const meanB = b.reduce((sum, v) => sum + v, 0) / n;
|
|
41
|
+
let numerator = 0;
|
|
42
|
+
let sumSqA = 0;
|
|
43
|
+
let sumSqB = 0;
|
|
44
|
+
for (let i = 0; i < n; i += 1) {
|
|
45
|
+
const da = a[i] - meanA;
|
|
46
|
+
const db = b[i] - meanB;
|
|
47
|
+
numerator += da * db;
|
|
48
|
+
sumSqA += da * da;
|
|
49
|
+
sumSqB += db * db;
|
|
50
|
+
}
|
|
51
|
+
const denominator = Math.sqrt(sumSqA * sumSqB);
|
|
52
|
+
return denominator === 0 ? 0 : numerator / denominator;
|
|
53
|
+
}
|
|
54
|
+
/** Builds a duration-weighted pitch-class (0-11) histogram from `notes`. */
|
|
55
|
+
function pitchClassHistogram(notes) {
|
|
56
|
+
const histogram = new Array(PITCH_CLASSES).fill(0);
|
|
57
|
+
for (const note of notes) {
|
|
58
|
+
const pitchClass = ((pitchToMidi(note.pitch) % PITCH_CLASSES) + PITCH_CLASSES) %
|
|
59
|
+
PITCH_CLASSES;
|
|
60
|
+
histogram[pitchClass] += note.durationTicks;
|
|
61
|
+
}
|
|
62
|
+
return histogram;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Estimates the best-fitting `KeySignature` for `notes` by correlating
|
|
66
|
+
* their duration-weighted pitch-class histogram against the
|
|
67
|
+
* Krumhansl-Kessler profiles at all 12 tonics, both major and minor, and
|
|
68
|
+
* returning the highest-correlating (tonic, mode) pair. Falls back to C
|
|
69
|
+
* major for an empty or entirely-silent `notes` list (histogram all-zero,
|
|
70
|
+
* for which correlation is undefined).
|
|
71
|
+
*/
|
|
72
|
+
export function detectKeySignature(notes) {
|
|
73
|
+
const histogram = pitchClassHistogram(notes);
|
|
74
|
+
if (histogram.every(v => v === 0)) {
|
|
75
|
+
return { fifths: 0, mode: 'major' };
|
|
76
|
+
}
|
|
77
|
+
let bestScore = -Infinity;
|
|
78
|
+
let bestTonic = 0;
|
|
79
|
+
let bestMode = 'major';
|
|
80
|
+
for (let tonic = 0; tonic < PITCH_CLASSES; tonic += 1) {
|
|
81
|
+
for (const [mode, profile] of [
|
|
82
|
+
['major', MAJOR_PROFILE],
|
|
83
|
+
['minor', MINOR_PROFILE],
|
|
84
|
+
]) {
|
|
85
|
+
const expected = Array.from({ length: PITCH_CLASSES }, (_, pc) => profile[(pc - tonic + PITCH_CLASSES) % PITCH_CLASSES]);
|
|
86
|
+
const score = correlate(histogram, expected);
|
|
87
|
+
if (score > bestScore) {
|
|
88
|
+
bestScore = score;
|
|
89
|
+
bestTonic = tonic;
|
|
90
|
+
bestMode = mode;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return keySignatureForTonicPitchClass(bestTonic, bestMode);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=key-detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-detection.js","sourceRoot":"","sources":["../../src/midi/key-detection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { KeySignature, Measure, NoteEvent, TimeSignature } from '@sudobility/music_types';
|
|
2
|
+
export declare const DEFAULT_TIME_SIGNATURE: TimeSignature;
|
|
3
|
+
export type TimeSignatureChange = {
|
|
4
|
+
tick: number;
|
|
5
|
+
timeSignature: TimeSignature;
|
|
6
|
+
};
|
|
7
|
+
export type MeasureSpan = {
|
|
8
|
+
index: number;
|
|
9
|
+
startTick: number;
|
|
10
|
+
durationTicks: number;
|
|
11
|
+
timeSignature: TimeSignature;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Computes consecutive measure spans covering `[0, endTick)` from a set of
|
|
15
|
+
* time-signature changes (need not include a tick-0 entry — one is
|
|
16
|
+
* synthesized at `DEFAULT_TIME_SIGNATURE` if missing; need not be sorted).
|
|
17
|
+
* Always returns at least one span (a silent/empty file still gets a single
|
|
18
|
+
* default-meter measure, mirroring `createEmptyScore`'s "at least one
|
|
19
|
+
* measure" convention).
|
|
20
|
+
*
|
|
21
|
+
* A change tick that doesn't land exactly on a measure boundary of the
|
|
22
|
+
* preceding meter (uncommon, but not disallowed by the MIDI format) is
|
|
23
|
+
* absorbed into whichever measure is in progress when it's reached, rather
|
|
24
|
+
* than truncating a partial measure — a documented simplification, not a
|
|
25
|
+
* full mid-measure meter change.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildMeasureSpans(changes: TimeSignatureChange[], ppq: number, endTick: number): MeasureSpan[];
|
|
28
|
+
/**
|
|
29
|
+
* Assembles one track's `Measure[]` from `voiceLanes` — each inner array a
|
|
30
|
+
* "notated voice" of mutually non-overlapping, absolute-tick `NoteEvent`s
|
|
31
|
+
* (as produced by `allocateVoices`) — over `spans`. A note spanning more
|
|
32
|
+
* than one span is split at every internal span boundary via
|
|
33
|
+
* `splitNoteAcrossMeasures` (producing a tied chain); each span's voice
|
|
34
|
+
* content is then gap-filled with rests. An empty `voiceLanes` (a track
|
|
35
|
+
* with no notes at all) still produces one fully-rested voice per measure,
|
|
36
|
+
* satisfying the "every measure has >=1 voice per track" invariant.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assembleTrackMeasures(voiceLanes: NoteEvent[][], spans: MeasureSpan[], keySignature: KeySignature, trackId: string): Measure[];
|
|
39
|
+
//# sourceMappingURL=measures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measures.d.ts","sourceRoot":"","sources":["../../src/midi/measures.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,YAAY,EACZ,OAAO,EAEP,SAAS,EACT,aAAa,EACd,MAAM,yBAAyB,CAAC;AAGjC,eAAO,MAAM,sBAAsB,EAAE,aAGpC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,EAAE,EAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd,WAAW,EAAE,CA4Cf;AAgDD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,SAAS,EAAE,EAAE,EACzB,KAAK,EAAE,WAAW,EAAE,EACpB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,GACd,OAAO,EAAE,CAqCX"}
|