@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sudobility/music_codecs — the score encoders and decoders.
|
|
3
|
+
*
|
|
4
|
+
* Every file format ScoreSmith reads or writes lives here: MIDI, MusicXML and
|
|
5
|
+
* the tracker modules. Each one turns bytes-shaped input into a `Score`, or a
|
|
6
|
+
* `Score` back out, and does nothing else — no store, no rendering, no audio,
|
|
7
|
+
* no platform.
|
|
8
|
+
*
|
|
9
|
+
* That is why it is its own package. `music_api` (backend) and `music_lib`
|
|
10
|
+
* (frontend) both need to read a MIDI file, and neither may depend on the
|
|
11
|
+
* other; before this package existed the server imported the frontend library
|
|
12
|
+
* to get at its importer, and separately hand-copied helpers to avoid doing
|
|
13
|
+
* so. Both sides now depend on this, and on `@sudobility/music_types` for the
|
|
14
|
+
* score model and the domain primitives these operate on.
|
|
15
|
+
*
|
|
16
|
+
* Raw byte work — parsing a MIDI file into tracks and notes, or XML into a
|
|
17
|
+
* DOM — belongs to `@sudobility/music_io`, which is injected: these modules
|
|
18
|
+
* take a `MidiCodec` or an `XmlParser` rather than importing one, so they stay
|
|
19
|
+
* free of anything platform-shaped.
|
|
20
|
+
*/
|
|
21
|
+
export * from './midi/analyze.js';
|
|
22
|
+
export * from './midi/export.js';
|
|
23
|
+
export * from './midi/grid-detection.js';
|
|
24
|
+
export * from './midi/import.js';
|
|
25
|
+
export * from './midi/import-options.js';
|
|
26
|
+
export * from './midi/key-detection.js';
|
|
27
|
+
export * from './midi/measures.js';
|
|
28
|
+
export * from './musicxml/duration-map.js';
|
|
29
|
+
export * from './musicxml/import.js';
|
|
30
|
+
/**
|
|
31
|
+
* `safeFilename` is exported by both writers and they are not the same
|
|
32
|
+
* function — each sanitises for its own format's conventions. The MIDI one
|
|
33
|
+
* keeps the bare name it always had; the MusicXML one is named for its format
|
|
34
|
+
* rather than one of them silently shadowing the other.
|
|
35
|
+
*/
|
|
36
|
+
export { exportMusicXml, safeFilename as musicXmlSafeFilename, } from './musicxml/export.js';
|
|
37
|
+
export * from './mod/export.js';
|
|
38
|
+
export * from './mod/fill.js';
|
|
39
|
+
export * from './mod/import.js';
|
|
40
|
+
export * from './mod/limits.js';
|
|
41
|
+
export * from './mod/timing.js';
|
|
42
|
+
export * from './mod/types.js';
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC;;;;;GAKG;AACH,OAAO,EACL,cAAc,EACd,YAAY,IAAI,oBAAoB,GACrC,MAAM,sBAAsB,CAAC;AAG9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sudobility/music_codecs — the score encoders and decoders.
|
|
3
|
+
*
|
|
4
|
+
* Every file format ScoreSmith reads or writes lives here: MIDI, MusicXML and
|
|
5
|
+
* the tracker modules. Each one turns bytes-shaped input into a `Score`, or a
|
|
6
|
+
* `Score` back out, and does nothing else — no store, no rendering, no audio,
|
|
7
|
+
* no platform.
|
|
8
|
+
*
|
|
9
|
+
* That is why it is its own package. `music_api` (backend) and `music_lib`
|
|
10
|
+
* (frontend) both need to read a MIDI file, and neither may depend on the
|
|
11
|
+
* other; before this package existed the server imported the frontend library
|
|
12
|
+
* to get at its importer, and separately hand-copied helpers to avoid doing
|
|
13
|
+
* so. Both sides now depend on this, and on `@sudobility/music_types` for the
|
|
14
|
+
* score model and the domain primitives these operate on.
|
|
15
|
+
*
|
|
16
|
+
* Raw byte work — parsing a MIDI file into tracks and notes, or XML into a
|
|
17
|
+
* DOM — belongs to `@sudobility/music_io`, which is injected: these modules
|
|
18
|
+
* take a `MidiCodec` or an `XmlParser` rather than importing one, so they stay
|
|
19
|
+
* free of anything platform-shaped.
|
|
20
|
+
*/
|
|
21
|
+
// ---- MIDI -----------------------------------------------------------------
|
|
22
|
+
export * from './midi/analyze.js';
|
|
23
|
+
export * from './midi/export.js';
|
|
24
|
+
export * from './midi/grid-detection.js';
|
|
25
|
+
export * from './midi/import.js';
|
|
26
|
+
export * from './midi/import-options.js';
|
|
27
|
+
export * from './midi/key-detection.js';
|
|
28
|
+
export * from './midi/measures.js';
|
|
29
|
+
// ---- MusicXML -------------------------------------------------------------
|
|
30
|
+
export * from './musicxml/duration-map.js';
|
|
31
|
+
export * from './musicxml/import.js';
|
|
32
|
+
/**
|
|
33
|
+
* `safeFilename` is exported by both writers and they are not the same
|
|
34
|
+
* function — each sanitises for its own format's conventions. The MIDI one
|
|
35
|
+
* keeps the bare name it always had; the MusicXML one is named for its format
|
|
36
|
+
* rather than one of them silently shadowing the other.
|
|
37
|
+
*/
|
|
38
|
+
export { exportMusicXml, safeFilename as musicXmlSafeFilename, } from './musicxml/export.js';
|
|
39
|
+
// ---- Tracker modules (MOD, S3M, XM, IT, MPTM) -----------------------------
|
|
40
|
+
export * from './mod/export.js';
|
|
41
|
+
export * from './mod/fill.js';
|
|
42
|
+
export * from './mod/import.js';
|
|
43
|
+
export * from './mod/limits.js';
|
|
44
|
+
export * from './mod/timing.js';
|
|
45
|
+
export * from './mod/types.js';
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,8EAA8E;AAC9E,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AAEnC,8EAA8E;AAC9E,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC;;;;;GAKG;AACH,OAAO,EACL,cAAc,EACd,YAAY,IAAI,oBAAoB,GACrC,MAAM,sBAAsB,CAAC;AAE9B,8EAA8E;AAC9E,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only summary of a raw MIDI file, for the import wizard (spec §15):
|
|
3
|
+
* per-track name/channel/program/note-count/duration, tempo events, and
|
|
4
|
+
* time signatures, without importing anything into the score model yet.
|
|
5
|
+
*/
|
|
6
|
+
import type { MidiCodec, MidiFile } from '@sudobility/music_types';
|
|
7
|
+
import { type DetectedGrid } from './grid-detection.js';
|
|
8
|
+
export type MidiTrackSummary = {
|
|
9
|
+
index: number;
|
|
10
|
+
name: string;
|
|
11
|
+
channel: number;
|
|
12
|
+
program: number;
|
|
13
|
+
instrumentName: string;
|
|
14
|
+
noteCount: number;
|
|
15
|
+
durationSeconds: number;
|
|
16
|
+
/** True when the track's channel is 9 (GM channel 10), the standard percussion channel. */
|
|
17
|
+
isPercussion: boolean;
|
|
18
|
+
/** Mean MIDI note number of the track's notes, or `null` if it has none — used to default a target clef. */
|
|
19
|
+
averageMidi: number | null;
|
|
20
|
+
};
|
|
21
|
+
export type MidiTempoEventSummary = {
|
|
22
|
+
tick: number;
|
|
23
|
+
bpm: number;
|
|
24
|
+
};
|
|
25
|
+
export type MidiTimeSignatureSummary = {
|
|
26
|
+
tick: number;
|
|
27
|
+
numerator: number;
|
|
28
|
+
denominator: number;
|
|
29
|
+
};
|
|
30
|
+
export type MidiSummary = {
|
|
31
|
+
ppq: number;
|
|
32
|
+
durationSeconds: number;
|
|
33
|
+
tracks: MidiTrackSummary[];
|
|
34
|
+
tempoEvents: MidiTempoEventSummary[];
|
|
35
|
+
timeSignatures: MidiTimeSignatureSummary[];
|
|
36
|
+
/**
|
|
37
|
+
* The grid the file's onsets already sit on, which the wizard opens
|
|
38
|
+
* pre-filled with. Detected rather than assumed so that importing a file
|
|
39
|
+
* written in triplets or swing does not snap it onto a straight grid — see
|
|
40
|
+
* `grid-detection.ts`.
|
|
41
|
+
*/
|
|
42
|
+
detectedGrid: DetectedGrid;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Parses `data` as a Standard MIDI File and summarizes it for the import
|
|
46
|
+
* wizard's track list/preview. Throws if `data` isn't valid MIDI (parsing
|
|
47
|
+
* errors are a caller concern — the file picker/wizard is expected to
|
|
48
|
+
* surface them, per spec §28).
|
|
49
|
+
*/
|
|
50
|
+
export declare function analyzeMidiFile(midi: MidiFile): MidiSummary;
|
|
51
|
+
/** Decodes `data` and summarizes it. The codec is the only platform-bound part. */
|
|
52
|
+
export declare function analyzeMidi(data: ArrayBuffer, codec: MidiCodec): MidiSummary;
|
|
53
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/midi/analyze.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EAET,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,YAAY,EAAE,OAAO,CAAC;IACtB,4GAA4G;IAC5G,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,qBAAqB,EAAE,CAAC;IACrC,cAAc,EAAE,wBAAwB,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAsBF;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAiC3D;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,WAAW,CAE5E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { detectGrid } from './grid-detection.js';
|
|
2
|
+
function summarizeTrack(track, index) {
|
|
3
|
+
const noteCount = track.notes.length;
|
|
4
|
+
const averageMidi = noteCount > 0
|
|
5
|
+
? track.notes.reduce((sum, note) => sum + note.midi, 0) / noteCount
|
|
6
|
+
: null;
|
|
7
|
+
return {
|
|
8
|
+
index,
|
|
9
|
+
name: track.name.length > 0 ? track.name : `Track ${index + 1}`,
|
|
10
|
+
channel: track.channel,
|
|
11
|
+
program: track.instrument.number,
|
|
12
|
+
instrumentName: track.instrument.name ?? 'Instrument',
|
|
13
|
+
noteCount,
|
|
14
|
+
durationSeconds: track.durationSeconds,
|
|
15
|
+
isPercussion: track.channel === 9,
|
|
16
|
+
averageMidi,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parses `data` as a Standard MIDI File and summarizes it for the import
|
|
21
|
+
* wizard's track list/preview. Throws if `data` isn't valid MIDI (parsing
|
|
22
|
+
* errors are a caller concern — the file picker/wizard is expected to
|
|
23
|
+
* surface them, per spec §28).
|
|
24
|
+
*/
|
|
25
|
+
export function analyzeMidiFile(midi) {
|
|
26
|
+
// Every track counts, percussion included. Excluding the drums was a
|
|
27
|
+
// mistake: the grid chosen here is applied to *all* of them, so leaving a
|
|
28
|
+
// hi-hat laid on thirty-seconds out of the decision let the melody pick a
|
|
29
|
+
// quarter-note grid and then collapsed the whole kit onto the beat. Erring
|
|
30
|
+
// toward a finer grid costs only some tidiness in the notation; erring
|
|
31
|
+
// toward a coarser one destroys the groove.
|
|
32
|
+
//
|
|
33
|
+
// Note ends go in alongside onsets because the chosen grid quantizes
|
|
34
|
+
// durations too — see `detectGrid`. Percussion contributes onsets only: a
|
|
35
|
+
// drum hit's length is an artefact of how it was recorded, not something
|
|
36
|
+
// anyone plays or reads, and those arbitrary note-offs fit no grid at all,
|
|
37
|
+
// so counting them would send every file with drums to the fallback.
|
|
38
|
+
const positions = midi.tracks.flatMap(track => track.notes.flatMap(note => track.channel === 9
|
|
39
|
+
? [note.ticks]
|
|
40
|
+
: [note.ticks, note.ticks + note.durationTicks]));
|
|
41
|
+
return {
|
|
42
|
+
ppq: midi.header.ppq,
|
|
43
|
+
detectedGrid: detectGrid(positions, midi.header.ppq),
|
|
44
|
+
durationSeconds: midi.duration,
|
|
45
|
+
tracks: midi.tracks.map(summarizeTrack),
|
|
46
|
+
tempoEvents: midi.header.tempos.map(t => ({ tick: t.ticks, bpm: t.bpm })),
|
|
47
|
+
timeSignatures: midi.header.timeSignatures.map(t => ({
|
|
48
|
+
tick: t.ticks,
|
|
49
|
+
numerator: t.timeSignature[0],
|
|
50
|
+
denominator: t.timeSignature[1],
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Decodes `data` and summarizes it. The codec is the only platform-bound part. */
|
|
55
|
+
export function analyzeMidi(data, codec) {
|
|
56
|
+
return analyzeMidiFile(codec.decode(data));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/midi/analyze.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,UAAU,EAAqB,MAAM,qBAAqB,CAAC;AAuCpE,SAAS,cAAc,CAAC,KAAoB,EAAE,KAAa;IACzD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;IACrC,MAAM,WAAW,GACf,SAAS,GAAG,CAAC;QACX,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,SAAS;QACnE,CAAC,CAAC,IAAI,CAAC;IAEX,OAAO;QACL,KAAK;QACL,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,CAAC,EAAE;QAC/D,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,MAAM;QAChC,cAAc,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,YAAY;QACrD,SAAS;QACT,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,YAAY,EAAE,KAAK,CAAC,OAAO,KAAK,CAAC;QACjC,WAAW;KACZ,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,qEAAqE;IACrE,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,4CAA4C;IAC5C,EAAE;IACF,qEAAqE;IACrE,0EAA0E;IAC1E,yEAAyE;IACzE,2EAA2E;IAC3E,qEAAqE;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAC5C,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CACzB,KAAK,CAAC,OAAO,KAAK,CAAC;QACjB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;QACd,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,CAClD,CACF,CAAC;IAEF,OAAO;QACL,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG;QACpB,YAAY,EAAE,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;QACpD,eAAe,EAAE,IAAI,CAAC,QAAQ;QAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;QACvC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACzE,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnD,IAAI,EAAE,CAAC,CAAC,KAAK;YACb,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;YAC7B,WAAW,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;SAChC,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,IAAiB,EAAE,KAAgB;IAC7D,OAAO,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MidiCodec, Score } from '@sudobility/music_types';
|
|
2
|
+
/**
|
|
3
|
+
* Exports `score` as a Standard MIDI File byte array: PPQ, tempo map, time
|
|
4
|
+
* signatures, per-track name/program/channel (percussion-clef tracks use
|
|
5
|
+
* channel 9/GM channel 10), notes (with velocity), and volume (CC7) / pan
|
|
6
|
+
* (CC10) set at each track's start. Tied notes are rejoined into single MIDI
|
|
7
|
+
* notes before export (MIDI has no tie concept). Sustain pedal data isn't
|
|
8
|
+
* part of the internal `Score` model, so none is emitted (spec §16's
|
|
9
|
+
* "sustain where represented" — nothing is represented here yet).
|
|
10
|
+
*/
|
|
11
|
+
export declare function exportMidi(score: Score, codec: MidiCodec): Uint8Array;
|
|
12
|
+
/**
|
|
13
|
+
* Slugifies `title` into a filesystem/URL-safe download filename (no
|
|
14
|
+
* extension): lowercase, `[a-z0-9]` runs joined by single hyphens, leading/
|
|
15
|
+
* trailing hyphens trimmed. Falls back to `"untitled"` for a title with no
|
|
16
|
+
* alphanumeric characters at all (empty, whitespace-only, or
|
|
17
|
+
* punctuation-only).
|
|
18
|
+
*/
|
|
19
|
+
export declare function safeFilename(title: string): string;
|
|
20
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/midi/export.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,SAAS,EAMT,KAAK,EAGN,MAAM,yBAAyB,CAAC;AAmFjC;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG,UAAU,CAoCrE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score -> Standard MIDI File export (spec §16). Pure and read-only: never
|
|
3
|
+
* mutates `score`. `@tonejs/midi` is the designated MIDI library (spec §15)
|
|
4
|
+
* and its use here is an explicitly sanctioned exception to the
|
|
5
|
+
* adapters/services "no non-domain library" purity rule.
|
|
6
|
+
*/
|
|
7
|
+
import { pitchToMidi } from '@sudobility/music_types';
|
|
8
|
+
import { joinTiedNotes } from '@sudobility/music_types';
|
|
9
|
+
import { isNoteEvent } from '@sudobility/music_types';
|
|
10
|
+
/** General MIDI channel 10 (0-indexed 9), the standard percussion channel. */
|
|
11
|
+
const PERCUSSION_CHANNEL = 9;
|
|
12
|
+
const CC_VOLUME = 7;
|
|
13
|
+
const CC_PAN = 10;
|
|
14
|
+
const MIDI_VELOCITY_MAX = 127;
|
|
15
|
+
function clamp01(v) {
|
|
16
|
+
return Math.max(0, Math.min(1, v));
|
|
17
|
+
}
|
|
18
|
+
/** Maps a track's `pan` ([-1, 1], 0 = center) to MIDI CC10's normalized [0, 1] range (0.5 = center). */
|
|
19
|
+
function panToNormalized(pan) {
|
|
20
|
+
return clamp01((pan + 1) / 2);
|
|
21
|
+
}
|
|
22
|
+
/** The most voices any single measure of `track` has (so every "voice channel" ordinal position is covered). */
|
|
23
|
+
function maxVoiceCount(track) {
|
|
24
|
+
return track.measures.reduce((max, measure) => Math.max(max, measure.voices.length), 0);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* All of `track`'s note events, ordinal-voice ties rejoined across measure
|
|
28
|
+
* boundaries. Notes are gathered per "voice channel" — the same ordinal
|
|
29
|
+
* voice-index-across-measures approximation `domain/score/ties.ts` uses
|
|
30
|
+
* internally (see its `voiceChannel` doc comment) — since a measure's voice
|
|
31
|
+
* ids aren't stable across a barline, so a chain can only be tracked by
|
|
32
|
+
* position. Order across channels/within is by `startTick`.
|
|
33
|
+
*/
|
|
34
|
+
function collectTrackNotes(track) {
|
|
35
|
+
const voiceCount = maxVoiceCount(track);
|
|
36
|
+
const notes = [];
|
|
37
|
+
for (let voiceIndex = 0; voiceIndex < voiceCount; voiceIndex += 1) {
|
|
38
|
+
const channelEvents = [];
|
|
39
|
+
for (const measure of track.measures) {
|
|
40
|
+
const voice = measure.voices[voiceIndex];
|
|
41
|
+
if (voice)
|
|
42
|
+
channelEvents.push(...voice.events);
|
|
43
|
+
}
|
|
44
|
+
channelEvents.sort((a, b) => a.startTick - b.startTick);
|
|
45
|
+
notes.push(...joinTiedNotes(channelEvents).filter(isNoteEvent));
|
|
46
|
+
}
|
|
47
|
+
return notes;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Distinct `(tick, [numerator, denominator])` time-signature changes,
|
|
51
|
+
* derived from the first track's measures (spec §16 approximation: MIDI
|
|
52
|
+
* has one shared meter timeline, while this app's `Score` technically
|
|
53
|
+
* stores a `timeSignature` per measure per track — in practice every track
|
|
54
|
+
* shares the same meter sequence, so the first track's is authoritative).
|
|
55
|
+
* Falls back to a single 4/4-at-0 entry for a score with no tracks/measures.
|
|
56
|
+
*/
|
|
57
|
+
function collectTimeSignatureChanges(score) {
|
|
58
|
+
const track = score.tracks[0];
|
|
59
|
+
if (!track || track.measures.length === 0)
|
|
60
|
+
return [{ ticks: 0, timeSignature: [4, 4] }];
|
|
61
|
+
const changes = [];
|
|
62
|
+
let last = null;
|
|
63
|
+
for (const measure of track.measures) {
|
|
64
|
+
const ts = measure.timeSignature;
|
|
65
|
+
if (!last ||
|
|
66
|
+
last.numerator !== ts.numerator ||
|
|
67
|
+
last.denominator !== ts.denominator) {
|
|
68
|
+
changes.push({
|
|
69
|
+
ticks: measure.startTick,
|
|
70
|
+
timeSignature: [ts.numerator, ts.denominator],
|
|
71
|
+
});
|
|
72
|
+
last = ts;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return changes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Exports `score` as a Standard MIDI File byte array: PPQ, tempo map, time
|
|
79
|
+
* signatures, per-track name/program/channel (percussion-clef tracks use
|
|
80
|
+
* channel 9/GM channel 10), notes (with velocity), and volume (CC7) / pan
|
|
81
|
+
* (CC10) set at each track's start. Tied notes are rejoined into single MIDI
|
|
82
|
+
* notes before export (MIDI has no tie concept). Sustain pedal data isn't
|
|
83
|
+
* part of the internal `Score` model, so none is emitted (spec §16's
|
|
84
|
+
* "sustain where represented" — nothing is represented here yet).
|
|
85
|
+
*/
|
|
86
|
+
export function exportMidi(score, codec) {
|
|
87
|
+
const file = {
|
|
88
|
+
header: {
|
|
89
|
+
name: score.metadata.title,
|
|
90
|
+
ppq: score.ppq,
|
|
91
|
+
tempos: score.tempoMap.map(t => ({ ticks: t.tick, bpm: t.bpm })),
|
|
92
|
+
timeSignatures: collectTimeSignatureChanges(score),
|
|
93
|
+
},
|
|
94
|
+
tracks: score.tracks.map((track) => ({
|
|
95
|
+
name: track.name,
|
|
96
|
+
channel: track.clef === 'percussion' ? PERCUSSION_CHANNEL : track.midiChannel,
|
|
97
|
+
instrument: { number: track.midiProgram },
|
|
98
|
+
notes: collectTrackNotes(track).map(note => ({
|
|
99
|
+
midi: pitchToMidi(note.pitch),
|
|
100
|
+
ticks: note.startTick,
|
|
101
|
+
durationTicks: Math.max(1, note.durationTicks),
|
|
102
|
+
velocity: clamp01(note.velocity / MIDI_VELOCITY_MAX),
|
|
103
|
+
})),
|
|
104
|
+
controlChanges: {
|
|
105
|
+
[CC_VOLUME]: [
|
|
106
|
+
{ number: CC_VOLUME, ticks: 0, value: clamp01(track.volume) },
|
|
107
|
+
],
|
|
108
|
+
[CC_PAN]: [
|
|
109
|
+
{ number: CC_PAN, ticks: 0, value: panToNormalized(track.pan) },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
// Durations are outputs of decoding, not inputs to encoding; the codec
|
|
113
|
+
// ignores them here.
|
|
114
|
+
durationTicks: 0,
|
|
115
|
+
durationSeconds: 0,
|
|
116
|
+
})),
|
|
117
|
+
duration: 0,
|
|
118
|
+
};
|
|
119
|
+
return codec.encode(file);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Slugifies `title` into a filesystem/URL-safe download filename (no
|
|
123
|
+
* extension): lowercase, `[a-z0-9]` runs joined by single hyphens, leading/
|
|
124
|
+
* trailing hyphens trimmed. Falls back to `"untitled"` for a title with no
|
|
125
|
+
* alphanumeric characters at all (empty, whitespace-only, or
|
|
126
|
+
* punctuation-only).
|
|
127
|
+
*/
|
|
128
|
+
export function safeFilename(title) {
|
|
129
|
+
const slug = title
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
132
|
+
.replace(/^-+|-+$/g, '');
|
|
133
|
+
return slug.length > 0 ? slug : 'untitled';
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/midi/export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing the quantization grid a MIDI file actually needs.
|
|
3
|
+
*
|
|
4
|
+
* Import used to snap every file to a fixed sixteenth grid. That is
|
|
5
|
+
* destructive for files that were already sequenced on another grid: an
|
|
6
|
+
* eighth-note triplet sits at 1/3 of a beat, the nearest sixteenth is 1/4 or
|
|
7
|
+
* 1/2, and the even triplet comes out as a limping dotted-sixteenth figure.
|
|
8
|
+
* Measured on a 120bpm triplet line, every second and third note of each group
|
|
9
|
+
* moved by 42ms. Swung eighths break the same way.
|
|
10
|
+
* The notes are all still there, which is why this reads as "the timing is
|
|
11
|
+
* wrong" rather than as anything missing.
|
|
12
|
+
*
|
|
13
|
+
* So: find the coarsest grid the file's own onsets already sit on, and use
|
|
14
|
+
* that — quantizing to a grid the music is on cannot move anything. If nothing
|
|
15
|
+
* fits, keep quantization off by default. A performance MIDI is already the
|
|
16
|
+
* timing the listener expects, and snapping it is an editing choice, not a
|
|
17
|
+
* safe default.
|
|
18
|
+
*/
|
|
19
|
+
import type { DurationName } from '@sudobility/music_types';
|
|
20
|
+
export type DetectedGrid = {
|
|
21
|
+
grid: DurationName | null;
|
|
22
|
+
/** Triplet subdivision of `grid` — 2/3 of it, matching `quantize`'s `tripletGrid`. */
|
|
23
|
+
triplet: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** No grid is assumed when the onsets fit nothing; preserve performance timing. */
|
|
26
|
+
export declare const FALLBACK_GRID: DetectedGrid;
|
|
27
|
+
/**
|
|
28
|
+
* The coarsest grid `positions` already sit on, or no grid when none does.
|
|
29
|
+
* `ppq` is the file's own resolution — positions are compared in beats, so a
|
|
30
|
+
* 960-ppq file and a 480-ppq one holding the same music detect alike.
|
|
31
|
+
*
|
|
32
|
+
* Callers pass note *ends* as well as onsets. The chosen grid quantizes
|
|
33
|
+
* durations too, so a grid picked from onsets alone can still be too coarse to
|
|
34
|
+
* express the file: a piece played in staccato quarters has quarter-note
|
|
35
|
+
* onsets but eighth-note ends, and snapping those ends up to the quarter grid
|
|
36
|
+
* would smear every note into the next and make the whole thing sound
|
|
37
|
+
* sluggish. Feeding ends in makes the detector reject a grid that cannot hold
|
|
38
|
+
* them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectGrid(positions: number[], ppq: number): DetectedGrid;
|
|
41
|
+
//# sourceMappingURL=grid-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-detection.d.ts","sourceRoot":"","sources":["../../src/midi/grid-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1B,sFAAsF;IACtF,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAwBF,mFAAmF;AACnF,eAAO,MAAM,aAAa,EAAE,YAA6C,CAAC;AA0B1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAKzE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidates coarsest first, so the first fit wins: a file of straight eighths
|
|
3
|
+
* should be described as eighths, not as the thirtysecond grid that also
|
|
4
|
+
* happens to contain them. Both straight and triplet subdivisions are offered
|
|
5
|
+
* at each level; the finest entry (1/12 beat) is the grid that contains both
|
|
6
|
+
* sixteenths and eighth triplets, for files that use the two together.
|
|
7
|
+
*/
|
|
8
|
+
const CANDIDATES = [
|
|
9
|
+
{ grid: 'quarter', triplet: false, beats: 1 },
|
|
10
|
+
{ grid: 'quarter', triplet: true, beats: 2 / 3 },
|
|
11
|
+
{ grid: 'eighth', triplet: false, beats: 1 / 2 },
|
|
12
|
+
{ grid: 'eighth', triplet: true, beats: 1 / 3 },
|
|
13
|
+
{ grid: 'sixteenth', triplet: false, beats: 1 / 4 },
|
|
14
|
+
{ grid: 'sixteenth', triplet: true, beats: 1 / 6 },
|
|
15
|
+
{ grid: 'thirtysecond', triplet: false, beats: 1 / 8 },
|
|
16
|
+
{ grid: 'thirtysecond', triplet: true, beats: 1 / 12 },
|
|
17
|
+
];
|
|
18
|
+
/** No grid is assumed when the onsets fit nothing; preserve performance timing. */
|
|
19
|
+
export const FALLBACK_GRID = { grid: null, triplet: false };
|
|
20
|
+
/**
|
|
21
|
+
* How far off a grid line an onset may sit and still count as on it, in beats.
|
|
22
|
+
* ~10ms at 120bpm: wide enough for the tick-level rounding a sequencer leaves
|
|
23
|
+
* behind, far too narrow to swallow played-by-hand timing.
|
|
24
|
+
*/
|
|
25
|
+
const TOLERANCE_BEATS = 0.02;
|
|
26
|
+
/**
|
|
27
|
+
* The share of onsets that must fit. Not 100%: real files carry a few grace
|
|
28
|
+
* notes, flams and hand-nudged accents, and one of those should not force the
|
|
29
|
+
* whole file onto a finer grid than it is written on.
|
|
30
|
+
*/
|
|
31
|
+
const REQUIRED_FIT = 0.98;
|
|
32
|
+
function fits(onsetBeats, gridBeats) {
|
|
33
|
+
let on = 0;
|
|
34
|
+
for (const beat of onsetBeats) {
|
|
35
|
+
const offset = Math.abs(beat / gridBeats - Math.round(beat / gridBeats)) * gridBeats;
|
|
36
|
+
if (offset <= TOLERANCE_BEATS)
|
|
37
|
+
on += 1;
|
|
38
|
+
}
|
|
39
|
+
return on / onsetBeats.length >= REQUIRED_FIT;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The coarsest grid `positions` already sit on, or no grid when none does.
|
|
43
|
+
* `ppq` is the file's own resolution — positions are compared in beats, so a
|
|
44
|
+
* 960-ppq file and a 480-ppq one holding the same music detect alike.
|
|
45
|
+
*
|
|
46
|
+
* Callers pass note *ends* as well as onsets. The chosen grid quantizes
|
|
47
|
+
* durations too, so a grid picked from onsets alone can still be too coarse to
|
|
48
|
+
* express the file: a piece played in staccato quarters has quarter-note
|
|
49
|
+
* onsets but eighth-note ends, and snapping those ends up to the quarter grid
|
|
50
|
+
* would smear every note into the next and make the whole thing sound
|
|
51
|
+
* sluggish. Feeding ends in makes the detector reject a grid that cannot hold
|
|
52
|
+
* them.
|
|
53
|
+
*/
|
|
54
|
+
export function detectGrid(positions, ppq) {
|
|
55
|
+
if (positions.length === 0 || ppq <= 0)
|
|
56
|
+
return FALLBACK_GRID;
|
|
57
|
+
const onsetBeats = positions.map(tick => tick / ppq);
|
|
58
|
+
const match = CANDIDATES.find(candidate => fits(onsetBeats, candidate.beats));
|
|
59
|
+
return match ? { grid: match.grid, triplet: match.triplet } : FALLBACK_GRID;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=grid-detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-detection.js","sourceRoot":"","sources":["../../src/midi/grid-detection.ts"],"names":[],"mappings":"AA0BA;;;;;;GAMG;AACH,MAAM,UAAU,GAIX;IACH,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE;IAC7C,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IAChD,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IAChD,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IAC/C,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IACnD,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IAClD,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE;CACvD,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,aAAa,GAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAE1E;;;;GAIG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B;;;;GAIG;AACH,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,SAAS,IAAI,CAAC,UAAoB,EAAE,SAAiB;IACnD,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,MAAM,GACV,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;QACxE,IAAI,MAAM,IAAI,eAAe;YAAE,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,IAAI,YAAY,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,SAAmB,EAAE,GAAW;IACzD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,aAAa,CAAC;IAC7D,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9E,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIDI import wizard options (spec §15): what to include, how to clean up
|
|
3
|
+
* performance timing, and how to lay tracks out on staves. `MidiImportOptions`
|
|
4
|
+
* is the exact shape the Task 7 brief specifies; `defaultMidiImportOptions`
|
|
5
|
+
* derives sensible starting values from an `analyzeMidi` summary so the
|
|
6
|
+
* wizard can open pre-filled.
|
|
7
|
+
*/
|
|
8
|
+
import type { MidiSummary } from './analyze.js';
|
|
9
|
+
import type { Clef, DurationName } from '@sudobility/music_types';
|
|
10
|
+
export type MidiTrackSelection = {
|
|
11
|
+
sourceIndex: number;
|
|
12
|
+
include: boolean;
|
|
13
|
+
clef: Clef;
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
export type MidiImportOptions = {
|
|
17
|
+
trackSelections: MidiTrackSelection[];
|
|
18
|
+
/** Quantization grid value (spec §15's "whole" .. "sixteenth triplet" list); `null` skips start/duration quantization entirely. */
|
|
19
|
+
quantizeGrid: DurationName | null;
|
|
20
|
+
/** Use a triplet subdivision of `quantizeGrid` when quantizing. */
|
|
21
|
+
tripletDetection: boolean;
|
|
22
|
+
/** Notes shorter than this (at 480 ppq) are dropped as accidental/ornamental noise; `1` preserves every representable note. */
|
|
23
|
+
minDurationTicks: number;
|
|
24
|
+
/** Cluster near-simultaneous onsets within a small tolerance onto a shared start tick. */
|
|
25
|
+
mergeNearDuplicates: boolean;
|
|
26
|
+
/** `"extend"` lengthens notes to the sustain-pedal release point; `"ignore"` uses raw note-off timing. */
|
|
27
|
+
sustainPedal: 'extend' | 'ignore';
|
|
28
|
+
/** Split a single MIDI track into two linked grand-staff tracks ("Piano RH"/"Piano LH") instead of one track on `defaultClefFor`'s guessed clef. */
|
|
29
|
+
pianoStaffSplit: boolean;
|
|
30
|
+
/** MIDI note number at/above which a note is placed on the upper (RH/treble) staff when `pianoStaffSplit` is set. */
|
|
31
|
+
splitPointMidi: number;
|
|
32
|
+
/** Estimate a key signature from the included tracks' notes (Krumhansl-style) instead of defaulting to C major. */
|
|
33
|
+
detectKey: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `MidiImportOptions` with defaults pre-filled from `summary`: every
|
|
37
|
+
* non-empty track included, clef guessed per `defaultClefFor`, quantized to
|
|
38
|
+
* the grid the file itself is written on (`summary.detectedGrid`), or no
|
|
39
|
+
* quantization when no grid was confidently detected. Notes are preserved by
|
|
40
|
+
* default (`minDurationTicks: 1`); users can opt into cleanup from the wizard.
|
|
41
|
+
* Sustain pedal extension is honored, with no near-duplicate merging or piano
|
|
42
|
+
* staff split, and key detection on.
|
|
43
|
+
*/
|
|
44
|
+
export declare function defaultMidiImportOptions(summary: MidiSummary): MidiImportOptions;
|
|
45
|
+
//# sourceMappingURL=import-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-options.d.ts","sourceRoot":"","sources":["../../src/midi/import-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,kBAAkB,EAAE,CAAC;IACtC,mIAAmI;IACnI,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,mEAAmE;IACnE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,+HAA+H;IAC/H,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0GAA0G;IAC1G,YAAY,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAClC,oJAAoJ;IACpJ,eAAe,EAAE,OAAO,CAAC;IACzB,qHAAqH;IACrH,cAAc,EAAE,MAAM,CAAC;IACvB,mHAAmH;IACnH,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAYF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,GACnB,iBAAiB,CAiBnB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Middle C — the conventional grand-staff RH/LH split point. */
|
|
2
|
+
const DEFAULT_SPLIT_POINT_MIDI = 60;
|
|
3
|
+
/** Percussion tracks default to the percussion clef; otherwise treble/bass by note centroid (>= middle C -> treble), defaulting to treble for a note-free track. */
|
|
4
|
+
function defaultClefFor(track) {
|
|
5
|
+
if (track.isPercussion)
|
|
6
|
+
return 'percussion';
|
|
7
|
+
if (track.averageMidi === null)
|
|
8
|
+
return 'treble';
|
|
9
|
+
return track.averageMidi >= DEFAULT_SPLIT_POINT_MIDI ? 'treble' : 'bass';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `MidiImportOptions` with defaults pre-filled from `summary`: every
|
|
13
|
+
* non-empty track included, clef guessed per `defaultClefFor`, quantized to
|
|
14
|
+
* the grid the file itself is written on (`summary.detectedGrid`), or no
|
|
15
|
+
* quantization when no grid was confidently detected. Notes are preserved by
|
|
16
|
+
* default (`minDurationTicks: 1`); users can opt into cleanup from the wizard.
|
|
17
|
+
* Sustain pedal extension is honored, with no near-duplicate merging or piano
|
|
18
|
+
* staff split, and key detection on.
|
|
19
|
+
*/
|
|
20
|
+
export function defaultMidiImportOptions(summary) {
|
|
21
|
+
return {
|
|
22
|
+
trackSelections: summary.tracks.map(track => ({
|
|
23
|
+
sourceIndex: track.index,
|
|
24
|
+
include: track.noteCount > 0,
|
|
25
|
+
clef: defaultClefFor(track),
|
|
26
|
+
name: track.name,
|
|
27
|
+
})),
|
|
28
|
+
quantizeGrid: summary.detectedGrid.grid,
|
|
29
|
+
tripletDetection: summary.detectedGrid.triplet,
|
|
30
|
+
minDurationTicks: 1,
|
|
31
|
+
mergeNearDuplicates: false,
|
|
32
|
+
sustainPedal: 'extend',
|
|
33
|
+
pianoStaffSplit: false,
|
|
34
|
+
splitPointMidi: DEFAULT_SPLIT_POINT_MIDI,
|
|
35
|
+
detectKey: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=import-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-options.js","sourceRoot":"","sources":["../../src/midi/import-options.ts"],"names":[],"mappings":"AAqCA,iEAAiE;AACjE,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAEpC,oKAAoK;AACpK,SAAS,cAAc,CAAC,KAAuB;IAC7C,IAAI,KAAK,CAAC,YAAY;QAAE,OAAO,YAAY,CAAC;IAC5C,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IAChD,OAAO,KAAK,CAAC,WAAW,IAAI,wBAAwB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;AAC3E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAoB;IAEpB,OAAO;QACL,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC5C,WAAW,EAAE,KAAK,CAAC,KAAK;YACxB,OAAO,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC;YAC5B,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC;YAC3B,IAAI,EAAE,KAAK,CAAC,IAAI;SACjB,CAAC,CAAC;QACH,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,IAAI;QACvC,gBAAgB,EAAE,OAAO,CAAC,YAAY,CAAC,OAAO;QAC9C,gBAAgB,EAAE,CAAC;QACnB,mBAAmB,EAAE,KAAK;QAC1B,YAAY,EAAE,QAAQ;QACtB,eAAe,EAAE,KAAK;QACtB,cAAc,EAAE,wBAAwB;QACxC,SAAS,EAAE,IAAI;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard MIDI File -> `Score` import (spec §15). `@tonejs/midi` is the
|
|
3
|
+
* designated MIDI library (spec §15) and its use here is a sanctioned
|
|
4
|
+
* exception to the adapters/services "no non-domain library" purity rule.
|
|
5
|
+
*
|
|
6
|
+
* MIDI stores performance timing, not notation semantics — this import is
|
|
7
|
+
* necessarily an approximation (quantization, voice allocation, clef/staff
|
|
8
|
+
* assignment, and key estimation are all best-effort heuristics), which is
|
|
9
|
+
* why every result carries a `warnings` array the caller/wizard is expected
|
|
10
|
+
* to surface (spec §15: "Do not claim MIDI import perfectly reconstructs
|
|
11
|
+
* notation").
|
|
12
|
+
*/
|
|
13
|
+
import type { MidiCodec, MidiFile } from '@sudobility/music_types';
|
|
14
|
+
import type { MidiImportOptions } from './import-options.js';
|
|
15
|
+
import type { Score } from '@sudobility/music_types';
|
|
16
|
+
export type MidiImportResult = {
|
|
17
|
+
score: Score;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Imports a Standard MIDI File as a `Score` (spec §15): converts note times
|
|
22
|
+
* to 480-ppq score ticks, imports the tempo map and time signatures,
|
|
23
|
+
* applies sustain-pedal extension, Task 4 quantization (grid/triplet/
|
|
24
|
+
* minimum-duration/near-duplicate merging), key estimation and enharmonic
|
|
25
|
+
* respelling, Task 4 voice allocation, optional piano grand-staff split,
|
|
26
|
+
* and measure generation (notes split across measure boundaries with
|
|
27
|
+
* ties) — per track selected in `options.trackSelections`. Always returns a
|
|
28
|
+
* `warnings` array (never throws for merely-imperfect input); the caller is
|
|
29
|
+
* expected to surface it before the import is committed (spec §15: MIDI
|
|
30
|
+
* import must be reviewable, and one undoable operation once committed).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Converts an already-decoded MIDI file into a `Score`.
|
|
34
|
+
*
|
|
35
|
+
* Split out from `importMidi` so the heavy work — quantization and voice
|
|
36
|
+
* allocation — can be handed to a worker without the worker needing a codec.
|
|
37
|
+
* `MidiFile` is plain data and therefore structured-cloneable; a `MidiCodec` is
|
|
38
|
+
* not, and giving the worker one would make music_lib depend on music_io.
|
|
39
|
+
*/
|
|
40
|
+
export declare function importMidiFile(midi: MidiFile, options: MidiImportOptions): MidiImportResult;
|
|
41
|
+
/** Decodes `data` and imports it. The codec is the only platform-bound part. */
|
|
42
|
+
export declare function importMidi(data: ArrayBuffer, options: MidiImportOptions, codec: MidiCodec): MidiImportResult;
|
|
43
|
+
//# sourceMappingURL=import.d.ts.map
|