@ac-kit/format-midi 0.1.0 → 0.2.0
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/CHANGELOG.md +37 -0
- package/README.md +36 -0
- package/dist/file/_time-division.d.ts +9 -0
- package/dist/file/_time-division.d.ts.map +1 -0
- package/dist/file/_time-division.js +9 -0
- package/dist/file/_time-division.js.map +1 -0
- package/dist/file/detect-midi-text-encoding.d.ts +42 -0
- package/dist/file/detect-midi-text-encoding.d.ts.map +1 -0
- package/dist/file/detect-midi-text-encoding.js +55 -0
- package/dist/file/detect-midi-text-encoding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/message/is-midi-note-release.d.ts +12 -3
- package/dist/message/is-midi-note-release.d.ts.map +1 -1
- package/dist/message/is-midi-note-release.js +3 -0
- package/dist/message/is-midi-note-release.js.map +1 -1
- package/dist/song/compose-midi-song.d.ts +55 -0
- package/dist/song/compose-midi-song.d.ts.map +1 -0
- package/dist/song/compose-midi-song.js +250 -0
- package/dist/song/compose-midi-song.js.map +1 -0
- package/dist/song/index.d.ts +2 -0
- package/dist/song/index.d.ts.map +1 -0
- package/dist/song/index.js +2 -0
- package/dist/song/index.js.map +1 -0
- package/dist/timeline/compose-midi-karaoke.d.ts +61 -0
- package/dist/timeline/compose-midi-karaoke.d.ts.map +1 -0
- package/dist/timeline/compose-midi-karaoke.js +98 -0
- package/dist/timeline/compose-midi-karaoke.js.map +1 -0
- package/dist/timeline/compose-midi-notes.d.ts +54 -0
- package/dist/timeline/compose-midi-notes.d.ts.map +1 -0
- package/dist/timeline/compose-midi-notes.js +97 -0
- package/dist/timeline/compose-midi-notes.js.map +1 -0
- package/dist/timeline/compose-midi-timeline.d.ts.map +1 -1
- package/dist/timeline/compose-midi-timeline.js +2 -4
- package/dist/timeline/compose-midi-timeline.js.map +1 -1
- package/package.json +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @ac-kit/format-midi
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- eb2aa3d: Reads a Standard MIDI File as music, not only as events:
|
|
8
|
+
|
|
9
|
+
- `composeMidiNotes` pairs every note-on with its release into a `MidiNote`
|
|
10
|
+
with its start and end in ticks and seconds.
|
|
11
|
+
- `detectMidiTextEncoding` guesses whether a file's text meta events are
|
|
12
|
+
UTF-8, Shift JIS or windows-1252, to decode them with `@ac-kit/core`'s
|
|
13
|
+
`decodeText`.
|
|
14
|
+
- `composeMidiKaraoke` reads the tags and timed syllables of a Soft Karaoke
|
|
15
|
+
(`.kar`) file.
|
|
16
|
+
- `composeMidiSong`, on the new `@ac-kit/format-midi/song` subpath, converts a
|
|
17
|
+
file into an `@ac-kit/model-music` `Song`. The package now depends on
|
|
18
|
+
`@ac-kit/algo` and `@ac-kit/model-music`; the main entry does not load
|
|
19
|
+
`@ac-kit/model-music`.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [aca1cb1]
|
|
24
|
+
- Updated dependencies [eb2aa3d]
|
|
25
|
+
- @ac-kit/core@0.4.0
|
|
26
|
+
- @ac-kit/model-music@0.1.0
|
|
27
|
+
- @ac-kit/algo@0.2.1
|
|
28
|
+
- @ac-kit/format-core@0.1.4
|
|
29
|
+
- @ac-kit/format-varint@0.1.3
|
|
30
|
+
|
|
31
|
+
## 0.1.1
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- cdbcb8a: `isMidiNoteRelease` no longer narrows every `noteOn` away when it answers
|
|
36
|
+
`false`: its guard is now the new `MidiNoteRelease` type, a `noteOff` or a
|
|
37
|
+
`noteOn` with `velocity: 0`, so a note-on with a velocity stays a `noteOn` in
|
|
38
|
+
the `false` branch.
|
|
39
|
+
|
|
3
40
|
## 0.1.0
|
|
4
41
|
|
|
5
42
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -99,6 +99,42 @@ independent sequences: each is timed on its own and they follow one another.
|
|
|
99
99
|
An SMPTE division gives each tick a fixed length, with frame rate 29 meaning
|
|
100
100
|
29.97.
|
|
101
101
|
|
|
102
|
+
## Notes
|
|
103
|
+
|
|
104
|
+
`composeMidiNotes` pairs every note-on of a timeline with the release that ends
|
|
105
|
+
it — a `noteOff`, or a `noteOn` of velocity 0, on the same track, channel and
|
|
106
|
+
note number — and gives each note its start and end in ticks and in seconds.
|
|
107
|
+
Overlapping strikes of one note close first in, first out, and a note never
|
|
108
|
+
released ends where its track ends. The sustain pedal is not applied: its
|
|
109
|
+
controller events stay in the timeline.
|
|
110
|
+
|
|
111
|
+
## Text
|
|
112
|
+
|
|
113
|
+
Text meta events keep their bytes. `detectMidiTextEncoding` guesses one
|
|
114
|
+
`MidiTextEncoding` for a whole file from all its text events — `utf-8`,
|
|
115
|
+
`shift_jis` or `windows-1252` — and `decodeText` from `@ac-kit/core` decodes
|
|
116
|
+
each event with it, or with strict ISO-8859-1 (`latin1`) when that is known.
|
|
117
|
+
|
|
118
|
+
`composeMidiKaraoke` reads a Soft Karaoke (`.kar`) file: its `@V`, `@L`, `@T`
|
|
119
|
+
and `@I` tags, and the syllables of its lyrics track with their times and their
|
|
120
|
+
`/` line and `\` paragraph breaks. A file without the `@K` tag is not karaoke.
|
|
121
|
+
|
|
122
|
+
## Songs
|
|
123
|
+
|
|
124
|
+
`composeMidiSong`, from the `@ac-kit/format-midi/song` subpath, converts a file
|
|
125
|
+
into a `Song` of `@ac-kit/model-music`: notes with their part, pitch, velocity
|
|
126
|
+
and times, one part per track and channel with its name and program, the tempo
|
|
127
|
+
map, time and key signatures, measures, the sustain, sostenuto and soft pedals,
|
|
128
|
+
markers, and lyrics from karaoke or `lyric` events. A format 2 file is read one
|
|
129
|
+
sequence at a time.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { parseMidiFile } from "@ac-kit/format-midi";
|
|
133
|
+
import { composeMidiSong } from "@ac-kit/format-midi/song";
|
|
134
|
+
|
|
135
|
+
const song = composeMidiSong(parseMidiFile(bytes));
|
|
136
|
+
```
|
|
137
|
+
|
|
102
138
|
See the
|
|
103
139
|
[generated API docs](https://anthochamp.github.io/node-essentials/api/format-midi/)
|
|
104
140
|
for the full reference.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MidiTimeDivision } from "./midi-file.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ticks per second of an SMPTE time division, with frame rate 29 read as 30
|
|
4
|
+
* drop-frame, 30000/1001 frames per second.
|
|
5
|
+
*/
|
|
6
|
+
export declare function timecodeTicksPerSecond_(division: Extract<MidiTimeDivision, {
|
|
7
|
+
type: "timecode";
|
|
8
|
+
}>): number;
|
|
9
|
+
//# sourceMappingURL=_time-division.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_time-division.d.ts","sourceRoot":"","sources":["../../src/file/_time-division.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,EAAE;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,GACvD,MAAM,CAKR"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticks per second of an SMPTE time division, with frame rate 29 read as 30
|
|
3
|
+
* drop-frame, 30000/1001 frames per second.
|
|
4
|
+
*/
|
|
5
|
+
export function timecodeTicksPerSecond_(division) {
|
|
6
|
+
const framesPerSecond = division.framesPerSecond === 29 ? 30000 / 1001 : division.framesPerSecond;
|
|
7
|
+
return framesPerSecond * division.ticksPerFrame;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=_time-division.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_time-division.js","sourceRoot":"","sources":["../../src/file/_time-division.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACtC,QAAyD;IAEzD,MAAM,eAAe,GACpB,QAAQ,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,KAAM,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC;IAE5E,OAAO,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type TextDecodingName } from "@ac-kit/core";
|
|
2
|
+
import type { MidiFile } from "./midi-file.js";
|
|
3
|
+
/**
|
|
4
|
+
* A character encoding found in the text meta events of Standard MIDI Files, to
|
|
5
|
+
* decode them with `@ac-kit/core`'s `decodeText`.
|
|
6
|
+
*
|
|
7
|
+
* - `utf-8` — recent tools; ASCII text is valid UTF-8 too.
|
|
8
|
+
* - `shift_jis` — Japanese sequencers.
|
|
9
|
+
* - `windows-1252` — Western European text from most other tools, and what Web
|
|
10
|
+
* APIs mean by "Latin-1".
|
|
11
|
+
* - `latin1` — strict ISO-8859-1, mapping 0x80–0x9F to the C1 control codes where
|
|
12
|
+
* `windows-1252` has printable characters.
|
|
13
|
+
*/
|
|
14
|
+
export type MidiTextEncoding = Extract<TextDecodingName, "utf-8" | "shift_jis" | "windows-1252" | "latin1">;
|
|
15
|
+
/**
|
|
16
|
+
* Guesses the encoding of a file's text meta events, all of them together,
|
|
17
|
+
* since one tool wrote them.
|
|
18
|
+
*
|
|
19
|
+
* Use it when nothing else says which encoding a file uses, then decode each
|
|
20
|
+
* text event with `decodeText(data, encoding)` from `@ac-kit/core`, or pass the
|
|
21
|
+
* result to {@link composeMidiKaraoke}. It is a heuristic: Japanese text
|
|
22
|
+
* written in kanji only reads as `windows-1252`.
|
|
23
|
+
*
|
|
24
|
+
* - `utf-8` when every text is valid UTF-8, which includes plain ASCII and a file
|
|
25
|
+
* with no text at all;
|
|
26
|
+
* - Otherwise `shift_jis` when every text is valid Shift JIS and some of it is
|
|
27
|
+
* kana;
|
|
28
|
+
* - Otherwise `windows-1252`, which decodes any byte.
|
|
29
|
+
*
|
|
30
|
+
* Time complexity: O(n) in the total size of the text meta events. Memory:
|
|
31
|
+
* O(n).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* detectMidiTextEncoding(parseMidiFile(bytes)); // "utf-8"
|
|
35
|
+
*
|
|
36
|
+
* @param file - A parsed file.
|
|
37
|
+
* @returns The most plausible encoding.
|
|
38
|
+
* @throws {RangeError} When the text is not UTF-8 and the runtime's
|
|
39
|
+
* `TextDecoder` lacks Shift JIS.
|
|
40
|
+
*/
|
|
41
|
+
export declare function detectMidiTextEncoding(file: MidiFile): MidiTextEncoding;
|
|
42
|
+
//# sourceMappingURL=detect-midi-text-encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-midi-text-encoding.d.ts","sourceRoot":"","sources":["../../src/file/detect-midi-text-encoding.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CACrC,gBAAgB,EAChB,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CACjD,CAAC;AAMF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,GAAG,gBAAgB,CA8BvE"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { decodeText, isValidText } from "@ac-kit/core";
|
|
2
|
+
// Full-width hiragana and katakana: Japanese text nearly always has some, and
|
|
3
|
+
// the Shift JIS pairs that Latin-1 letters happen to form are kanji instead.
|
|
4
|
+
const KANA_PATTERN_ = /[\u3041-\u30ff]/u;
|
|
5
|
+
/**
|
|
6
|
+
* Guesses the encoding of a file's text meta events, all of them together,
|
|
7
|
+
* since one tool wrote them.
|
|
8
|
+
*
|
|
9
|
+
* Use it when nothing else says which encoding a file uses, then decode each
|
|
10
|
+
* text event with `decodeText(data, encoding)` from `@ac-kit/core`, or pass the
|
|
11
|
+
* result to {@link composeMidiKaraoke}. It is a heuristic: Japanese text
|
|
12
|
+
* written in kanji only reads as `windows-1252`.
|
|
13
|
+
*
|
|
14
|
+
* - `utf-8` when every text is valid UTF-8, which includes plain ASCII and a file
|
|
15
|
+
* with no text at all;
|
|
16
|
+
* - Otherwise `shift_jis` when every text is valid Shift JIS and some of it is
|
|
17
|
+
* kana;
|
|
18
|
+
* - Otherwise `windows-1252`, which decodes any byte.
|
|
19
|
+
*
|
|
20
|
+
* Time complexity: O(n) in the total size of the text meta events. Memory:
|
|
21
|
+
* O(n).
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* detectMidiTextEncoding(parseMidiFile(bytes)); // "utf-8"
|
|
25
|
+
*
|
|
26
|
+
* @param file - A parsed file.
|
|
27
|
+
* @returns The most plausible encoding.
|
|
28
|
+
* @throws {RangeError} When the text is not UTF-8 and the runtime's
|
|
29
|
+
* `TextDecoder` lacks Shift JIS.
|
|
30
|
+
*/
|
|
31
|
+
export function detectMidiTextEncoding(file) {
|
|
32
|
+
const texts = [];
|
|
33
|
+
for (const { events } of file.tracks) {
|
|
34
|
+
for (const { event } of events) {
|
|
35
|
+
if (event.kind === "meta" &&
|
|
36
|
+
"data" in event.meta &&
|
|
37
|
+
event.meta.type !== "sequencerSpecific" &&
|
|
38
|
+
event.meta.type !== "unknown") {
|
|
39
|
+
texts.push(event.meta.data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (texts.every((text) => isValidText(text, "utf-8"))) {
|
|
44
|
+
return "utf-8";
|
|
45
|
+
}
|
|
46
|
+
if (!texts.every((text) => isValidText(text, "shift_jis"))) {
|
|
47
|
+
return "windows-1252";
|
|
48
|
+
}
|
|
49
|
+
const hasKana = texts.some((text) => KANA_PATTERN_.test(decodeText(text, "shift_jis")));
|
|
50
|
+
if (hasKana) {
|
|
51
|
+
return "shift_jis";
|
|
52
|
+
}
|
|
53
|
+
return "windows-1252";
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=detect-midi-text-encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-midi-text-encoding.js","sourceRoot":"","sources":["../../src/file/detect-midi-text-encoding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAyB,MAAM,cAAc,CAAC;AAoB9E,8EAA8E;AAC9E,6EAA6E;AAC7E,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAc;IACpD,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACtC,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,MAAM,EAAE,CAAC;YAChC,IACC,KAAK,CAAC,IAAI,KAAK,MAAM;gBACrB,MAAM,IAAI,KAAK,CAAC,IAAI;gBACpB,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,mBAAmB;gBACvC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,EAC5B,CAAC;gBACF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;QACF,CAAC;IACF,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;QACvD,OAAO,OAAO,CAAC;IAChB,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,cAAc,CAAC;IACvB,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CACnC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CACjD,CAAC;IACF,IAAI,OAAO,EAAE,CAAC;QACb,OAAO,WAAW,CAAC;IACpB,CAAC;IAED,OAAO,cAAc,CAAC;AACvB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./errors.js";
|
|
2
|
+
export * from "./file/detect-midi-text-encoding.js";
|
|
2
3
|
export * from "./file/midi-file.js";
|
|
3
4
|
export * from "./file/midi-meta-event.js";
|
|
4
5
|
export * from "./file/parse-midi-file.js";
|
|
@@ -8,5 +9,7 @@ export * from "./message/is-midi-note-release.js";
|
|
|
8
9
|
export * from "./message/midi-message-decoder.js";
|
|
9
10
|
export * from "./message/midi-message-encoder.js";
|
|
10
11
|
export * from "./message/midi-message.js";
|
|
12
|
+
export * from "./timeline/compose-midi-karaoke.js";
|
|
13
|
+
export * from "./timeline/compose-midi-notes.js";
|
|
11
14
|
export * from "./timeline/compose-midi-timeline.js";
|
|
12
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qCAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qCAAqC,CAAC;AACpD,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./errors.js";
|
|
2
|
+
export * from "./file/detect-midi-text-encoding.js";
|
|
2
3
|
export * from "./file/midi-file.js";
|
|
3
4
|
export * from "./file/midi-meta-event.js";
|
|
4
5
|
export * from "./file/parse-midi-file.js";
|
|
@@ -8,5 +9,7 @@ export * from "./message/is-midi-note-release.js";
|
|
|
8
9
|
export * from "./message/midi-message-decoder.js";
|
|
9
10
|
export * from "./message/midi-message-encoder.js";
|
|
10
11
|
export * from "./message/midi-message.js";
|
|
12
|
+
export * from "./timeline/compose-midi-karaoke.js";
|
|
13
|
+
export * from "./timeline/compose-midi-notes.js";
|
|
11
14
|
export * from "./timeline/compose-midi-timeline.js";
|
|
12
15
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qCAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qCAAqC,CAAC;AACpD,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC"}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { MidiMessage } from "./midi-message.js";
|
|
2
|
+
/** A message that releases a note: any `noteOff`, or a `noteOn` with velocity 0. */
|
|
3
|
+
export type MidiNoteRelease = Extract<MidiMessage, {
|
|
4
|
+
type: "noteOff";
|
|
5
|
+
}> | (Extract<MidiMessage, {
|
|
6
|
+
type: "noteOn";
|
|
7
|
+
}> & {
|
|
8
|
+
velocity: 0;
|
|
9
|
+
});
|
|
2
10
|
/**
|
|
3
11
|
* Whether `message` releases a note: a `noteOff`, or a `noteOn` with velocity
|
|
4
12
|
* 0, which every MIDI receiver treats as a note-off.
|
|
@@ -6,10 +14,11 @@ import type { MidiMessage } from "./midi-message.js";
|
|
|
6
14
|
* Use it instead of testing `type === "noteOff"`, which misses the note-on form
|
|
7
15
|
* most senders use so that running status covers a whole phrase.
|
|
8
16
|
*
|
|
17
|
+
* When it answers `false`, `message` may still be a `noteOn`: only a `noteOff`
|
|
18
|
+
* is ruled out.
|
|
19
|
+
*
|
|
9
20
|
* @example
|
|
10
21
|
* isMidiNoteRelease({ type: "noteOn", channel: 0, note: 60, velocity: 0 }); // true
|
|
11
22
|
*/
|
|
12
|
-
export declare function isMidiNoteRelease(message: MidiMessage): message is
|
|
13
|
-
type: "noteOff" | "noteOn";
|
|
14
|
-
}>;
|
|
23
|
+
export declare function isMidiNoteRelease(message: MidiMessage): message is MidiNoteRelease;
|
|
15
24
|
//# sourceMappingURL=is-midi-note-release.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-midi-note-release.d.ts","sourceRoot":"","sources":["../../src/message/is-midi-note-release.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD
|
|
1
|
+
{"version":3,"file":"is-midi-note-release.d.ts","sourceRoot":"","sources":["../../src/message/is-midi-note-release.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,oFAAoF;AACpF,MAAM,MAAM,eAAe,GACxB,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GACzC,CAAC,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAAG;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,WAAW,GAClB,OAAO,IAAI,eAAe,CAK5B"}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Use it instead of testing `type === "noteOff"`, which misses the note-on form
|
|
6
6
|
* most senders use so that running status covers a whole phrase.
|
|
7
7
|
*
|
|
8
|
+
* When it answers `false`, `message` may still be a `noteOn`: only a `noteOff`
|
|
9
|
+
* is ruled out.
|
|
10
|
+
*
|
|
8
11
|
* @example
|
|
9
12
|
* isMidiNoteRelease({ type: "noteOn", channel: 0, note: 60, velocity: 0 }); // true
|
|
10
13
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-midi-note-release.js","sourceRoot":"","sources":["../../src/message/is-midi-note-release.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"is-midi-note-release.js","sourceRoot":"","sources":["../../src/message/is-midi-note-release.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAChC,OAAoB;IAEpB,OAAO,CACN,OAAO,CAAC,IAAI,KAAK,SAAS;QAC1B,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,CAAC,CAAC,CACrD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Song } from "@ac-kit/model-music";
|
|
2
|
+
import { type MidiTextEncoding } from "../file/detect-midi-text-encoding.js";
|
|
3
|
+
import type { MidiFile } from "../file/midi-file.js";
|
|
4
|
+
/** Options of {@link composeMidiSong}. */
|
|
5
|
+
export type ComposeMidiSongOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* The encoding of the file's text; detected with `detectMidiTextEncoding`
|
|
8
|
+
* when omitted.
|
|
9
|
+
*/
|
|
10
|
+
encoding?: MidiTextEncoding;
|
|
11
|
+
/**
|
|
12
|
+
* Which track of a format 2 file to read, each being a song of its own: 0 by
|
|
13
|
+
* default. Ignored for formats 0 and 1.
|
|
14
|
+
*/
|
|
15
|
+
sequence?: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Converts a Standard MIDI File into a `Song` of `@ac-kit/model-music`: every
|
|
19
|
+
* note with its start and end in ticks and seconds, under one tempo map, with
|
|
20
|
+
* parts, meter, key, measures, pedals, markers and lyrics.
|
|
21
|
+
*
|
|
22
|
+
* Use it to read a file as music; use {@link composeMidiTimeline} and
|
|
23
|
+
* {@link composeMidiNotes} for its events as stored.
|
|
24
|
+
*
|
|
25
|
+
* - **Parts**: one per track and channel carrying notes, in the order they first
|
|
26
|
+
* sound. Each is named by its track's name and given the channel's last
|
|
27
|
+
* program change before its first note; channel 10 is percussion.
|
|
28
|
+
* - **Title**: the karaoke `@T` title, else the name of track 0 (of the sequence,
|
|
29
|
+
* for format 2).
|
|
30
|
+
* - **Time**: a metrical file keeps its ticks and its Set Tempo events. An
|
|
31
|
+
* SMPTE-timed file is given 480 ticks per quarter note and the one tempo that
|
|
32
|
+
* keeps every event at its time.
|
|
33
|
+
* - **Measures** come from the time signatures, 4/4 before the first; a time
|
|
34
|
+
* signature of numerator 0 is skipped.
|
|
35
|
+
* - **Pedals**: sustain (CC 64), sostenuto (CC 66) and soft (CC 67) on a channel
|
|
36
|
+
* with notes, as 0–1.
|
|
37
|
+
* - **Lyrics**: the syllables of a Soft Karaoke file, else the `lyric` meta
|
|
38
|
+
* events. **Markers**: `marker` and `cuePoint` meta events.
|
|
39
|
+
*
|
|
40
|
+
* Time complexity: O(n log n) in the number of events, for the timeline's sort.
|
|
41
|
+
* Memory: O(n).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* composeMidiSong(parseMidiFile(bytes)).notes[0];
|
|
45
|
+
* // { part: 0, staff: undefined, voice: undefined, pitch: 48, velocity: 96,
|
|
46
|
+
* // startTick: 0, endTick: 384, startSeconds: 0, endSeconds: 2 }
|
|
47
|
+
*
|
|
48
|
+
* @param file - A parsed file.
|
|
49
|
+
* @param options - The text encoding and, for format 2, the sequence.
|
|
50
|
+
* @returns The song.
|
|
51
|
+
* @throws {RangeError} When `options.sequence` names no track of a format 2
|
|
52
|
+
* file.
|
|
53
|
+
*/
|
|
54
|
+
export declare function composeMidiSong(file: MidiFile, options?: ComposeMidiSongOptions): Song;
|
|
55
|
+
//# sourceMappingURL=compose-midi-song.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-song.d.ts","sourceRoot":"","sources":["../../src/song/compose-midi-song.ts"],"names":[],"mappings":"AACA,OAAO,EAKN,KAAK,IAAI,EAST,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAEN,KAAK,gBAAgB,EACrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAQrD,0CAA0C;AAC1C,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;OAGG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAaF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,QAAQ,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC9B,IAAI,CAoKN"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { decodeText } from "@ac-kit/core";
|
|
2
|
+
import { composeMeasures, composeTempoMap, tempoMapSeconds, } from "@ac-kit/model-music";
|
|
3
|
+
import { timecodeTicksPerSecond_ } from "../file/_time-division.js";
|
|
4
|
+
import { detectMidiTextEncoding, } from "../file/detect-midi-text-encoding.js";
|
|
5
|
+
import { composeMidiKaraoke } from "../timeline/compose-midi-karaoke.js";
|
|
6
|
+
import { composeMidiNotes } from "../timeline/compose-midi-notes.js";
|
|
7
|
+
import { composeMidiTimeline, } from "../timeline/compose-midi-timeline.js";
|
|
8
|
+
// The resolution given to an SMPTE-timed file, whose ticks have no musical length.
|
|
9
|
+
const TIMECODE_TICKS_PER_QUARTER_NOTE_ = 480;
|
|
10
|
+
const PEDAL_CONTROLLERS_ = new Map([
|
|
11
|
+
[64, "sustain"],
|
|
12
|
+
[66, "sostenuto"],
|
|
13
|
+
[67, "soft"],
|
|
14
|
+
]);
|
|
15
|
+
const PERCUSSION_CHANNEL_ = 9;
|
|
16
|
+
/**
|
|
17
|
+
* Converts a Standard MIDI File into a `Song` of `@ac-kit/model-music`: every
|
|
18
|
+
* note with its start and end in ticks and seconds, under one tempo map, with
|
|
19
|
+
* parts, meter, key, measures, pedals, markers and lyrics.
|
|
20
|
+
*
|
|
21
|
+
* Use it to read a file as music; use {@link composeMidiTimeline} and
|
|
22
|
+
* {@link composeMidiNotes} for its events as stored.
|
|
23
|
+
*
|
|
24
|
+
* - **Parts**: one per track and channel carrying notes, in the order they first
|
|
25
|
+
* sound. Each is named by its track's name and given the channel's last
|
|
26
|
+
* program change before its first note; channel 10 is percussion.
|
|
27
|
+
* - **Title**: the karaoke `@T` title, else the name of track 0 (of the sequence,
|
|
28
|
+
* for format 2).
|
|
29
|
+
* - **Time**: a metrical file keeps its ticks and its Set Tempo events. An
|
|
30
|
+
* SMPTE-timed file is given 480 ticks per quarter note and the one tempo that
|
|
31
|
+
* keeps every event at its time.
|
|
32
|
+
* - **Measures** come from the time signatures, 4/4 before the first; a time
|
|
33
|
+
* signature of numerator 0 is skipped.
|
|
34
|
+
* - **Pedals**: sustain (CC 64), sostenuto (CC 66) and soft (CC 67) on a channel
|
|
35
|
+
* with notes, as 0–1.
|
|
36
|
+
* - **Lyrics**: the syllables of a Soft Karaoke file, else the `lyric` meta
|
|
37
|
+
* events. **Markers**: `marker` and `cuePoint` meta events.
|
|
38
|
+
*
|
|
39
|
+
* Time complexity: O(n log n) in the number of events, for the timeline's sort.
|
|
40
|
+
* Memory: O(n).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* composeMidiSong(parseMidiFile(bytes)).notes[0];
|
|
44
|
+
* // { part: 0, staff: undefined, voice: undefined, pitch: 48, velocity: 96,
|
|
45
|
+
* // startTick: 0, endTick: 384, startSeconds: 0, endSeconds: 2 }
|
|
46
|
+
*
|
|
47
|
+
* @param file - A parsed file.
|
|
48
|
+
* @param options - The text encoding and, for format 2, the sequence.
|
|
49
|
+
* @returns The song.
|
|
50
|
+
* @throws {RangeError} When `options.sequence` names no track of a format 2
|
|
51
|
+
* file.
|
|
52
|
+
*/
|
|
53
|
+
export function composeMidiSong(file, options) {
|
|
54
|
+
const encoding = options?.encoding ?? detectMidiTextEncoding(file);
|
|
55
|
+
const sequence = options?.sequence ?? 0;
|
|
56
|
+
const timeline = selectSequence_(composeMidiTimeline(file), file, sequence);
|
|
57
|
+
const tempoMap = composeTempoMap_(file, timeline);
|
|
58
|
+
const secondsAt = (tick) => tempoMapSeconds(tempoMap, tick);
|
|
59
|
+
const midiNotes = composeMidiNotes(timeline);
|
|
60
|
+
const trackNames = new Map();
|
|
61
|
+
const programs = new Map();
|
|
62
|
+
const firstPrograms = new Map();
|
|
63
|
+
const partIndices = new Map();
|
|
64
|
+
const parts = [];
|
|
65
|
+
const timeSignatures = [];
|
|
66
|
+
const keySignatures = [];
|
|
67
|
+
const markers = [];
|
|
68
|
+
const lyricEvents = [];
|
|
69
|
+
let endTick = 0;
|
|
70
|
+
for (const { trackIndex, tick, event } of timeline) {
|
|
71
|
+
endTick = Math.max(endTick, tick);
|
|
72
|
+
if (event.kind === "channel") {
|
|
73
|
+
if (event.message.type === "programChange") {
|
|
74
|
+
programs.set(partKey_(trackIndex, event.message.channel), event.message.program);
|
|
75
|
+
}
|
|
76
|
+
else if (event.message.type === "noteOn" &&
|
|
77
|
+
event.message.velocity > 0) {
|
|
78
|
+
const key = partKey_(trackIndex, event.message.channel);
|
|
79
|
+
if (!partIndices.has(key)) {
|
|
80
|
+
partIndices.set(key, partIndices.size);
|
|
81
|
+
const program = programs.get(key);
|
|
82
|
+
if (program !== undefined) {
|
|
83
|
+
firstPrograms.set(key, program);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (event.kind !== "meta") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const { meta } = event;
|
|
93
|
+
switch (meta.type) {
|
|
94
|
+
case "trackName":
|
|
95
|
+
if (!trackNames.has(trackIndex)) {
|
|
96
|
+
trackNames.set(trackIndex, decodeText(meta.data, encoding));
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "timeSignature":
|
|
100
|
+
if (meta.numerator >= 1) {
|
|
101
|
+
pushAtTick_(timeSignatures, {
|
|
102
|
+
tick,
|
|
103
|
+
numerator: meta.numerator,
|
|
104
|
+
denominator: 2 ** meta.denominatorPower,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case "keySignature":
|
|
109
|
+
pushAtTick_(keySignatures, {
|
|
110
|
+
tick,
|
|
111
|
+
fifths: meta.sharpsFlats,
|
|
112
|
+
mode: meta.minor ? "minor" : "major",
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
case "marker":
|
|
116
|
+
case "cuePoint":
|
|
117
|
+
markers.push({
|
|
118
|
+
tick,
|
|
119
|
+
seconds: secondsAt(tick),
|
|
120
|
+
text: decodeText(meta.data, encoding),
|
|
121
|
+
kind: meta.type === "marker" ? "marker" : "cue",
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case "lyric":
|
|
125
|
+
lyricEvents.push({
|
|
126
|
+
part: undefined,
|
|
127
|
+
tick,
|
|
128
|
+
seconds: secondsAt(tick),
|
|
129
|
+
text: decodeText(meta.data, encoding),
|
|
130
|
+
break: "none",
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
// Tempo is read by composeTempoMap_; the other meta events carry nothing a Song holds.
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const [key, part] of partIndices) {
|
|
139
|
+
const trackIndex = Math.floor(key / 16);
|
|
140
|
+
const channel = key % 16;
|
|
141
|
+
parts[part] = {
|
|
142
|
+
name: trackNames.get(trackIndex),
|
|
143
|
+
program: firstPrograms.get(key),
|
|
144
|
+
percussion: channel === PERCUSSION_CHANNEL_,
|
|
145
|
+
staves: 1,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const notes = midiNotes.map((note) => {
|
|
149
|
+
endTick = Math.max(endTick, note.endTick);
|
|
150
|
+
return {
|
|
151
|
+
part: partIndices.get(partKey_(note.trackIndex, note.channel)),
|
|
152
|
+
staff: undefined,
|
|
153
|
+
voice: undefined,
|
|
154
|
+
pitch: note.note,
|
|
155
|
+
velocity: note.velocity,
|
|
156
|
+
startTick: note.startTick,
|
|
157
|
+
endTick: note.endTick,
|
|
158
|
+
startSeconds: secondsAt(note.startTick),
|
|
159
|
+
endSeconds: secondsAt(note.endTick),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
notes.sort((a, b) => a.startTick - b.startTick || a.pitch - b.pitch);
|
|
163
|
+
const pedals = [];
|
|
164
|
+
for (const { trackIndex, tick, event } of timeline) {
|
|
165
|
+
if (event.kind !== "channel" || event.message.type !== "controlChange") {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const pedal = PEDAL_CONTROLLERS_.get(event.message.controller);
|
|
169
|
+
const part = partIndices.get(partKey_(trackIndex, event.message.channel));
|
|
170
|
+
if (pedal !== undefined && part !== undefined) {
|
|
171
|
+
pedals.push({
|
|
172
|
+
part,
|
|
173
|
+
tick,
|
|
174
|
+
seconds: secondsAt(tick),
|
|
175
|
+
pedal,
|
|
176
|
+
value: event.message.value / 127,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const karaoke = composeMidiKaraoke(timeline, encoding);
|
|
181
|
+
const lyrics = karaoke?.syllables.map(({ tick, text, break: lineBreak }) => ({
|
|
182
|
+
part: undefined,
|
|
183
|
+
tick,
|
|
184
|
+
seconds: secondsAt(tick),
|
|
185
|
+
text,
|
|
186
|
+
break: lineBreak,
|
|
187
|
+
})) ?? lyricEvents;
|
|
188
|
+
return {
|
|
189
|
+
title: karaoke?.titles[0] ?? trackNames.get(file.format === 2 ? sequence : 0),
|
|
190
|
+
tempoMap,
|
|
191
|
+
endTick,
|
|
192
|
+
endSeconds: secondsAt(endTick),
|
|
193
|
+
parts,
|
|
194
|
+
notes,
|
|
195
|
+
timeSignatures,
|
|
196
|
+
keySignatures,
|
|
197
|
+
measures: composeMeasures(timeSignatures, tempoMap, endTick),
|
|
198
|
+
markers,
|
|
199
|
+
pedals,
|
|
200
|
+
lyrics,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function partKey_(trackIndex, channel) {
|
|
204
|
+
return trackIndex * 16 + channel;
|
|
205
|
+
}
|
|
206
|
+
// Several signatures at one tick: the last one stands, as for a tempo.
|
|
207
|
+
function pushAtTick_(items, item) {
|
|
208
|
+
if (items.at(-1)?.tick === item.tick) {
|
|
209
|
+
items[items.length - 1] = item;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
items.push(item);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function selectSequence_(timeline, file, sequence) {
|
|
216
|
+
if (file.format !== 2) {
|
|
217
|
+
return timeline;
|
|
218
|
+
}
|
|
219
|
+
if (!(Number.isInteger(sequence) &&
|
|
220
|
+
sequence >= 0 &&
|
|
221
|
+
sequence < file.tracks.length)) {
|
|
222
|
+
throw new RangeError(`sequence ${sequence} is not a track of this ${file.tracks.length}-track file`);
|
|
223
|
+
}
|
|
224
|
+
return timeline.filter(({ trackIndex }) => trackIndex === sequence);
|
|
225
|
+
}
|
|
226
|
+
function composeTempoMap_(file, timeline) {
|
|
227
|
+
const division = file.timeDivision;
|
|
228
|
+
if (division.type === "timecode") {
|
|
229
|
+
return composeTempoMap(TIMECODE_TICKS_PER_QUARTER_NOTE_, [
|
|
230
|
+
{
|
|
231
|
+
tick: 0,
|
|
232
|
+
microsecondsPerQuarterNote: (1000000 * TIMECODE_TICKS_PER_QUARTER_NOTE_) /
|
|
233
|
+
timecodeTicksPerSecond_(division),
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
const marks = [];
|
|
238
|
+
for (const { tick, event } of timeline) {
|
|
239
|
+
if (event.kind === "meta" &&
|
|
240
|
+
event.meta.type === "setTempo" &&
|
|
241
|
+
event.meta.microsecondsPerQuarterNote > 0) {
|
|
242
|
+
marks.push({
|
|
243
|
+
tick,
|
|
244
|
+
microsecondsPerQuarterNote: event.meta.microsecondsPerQuarterNote,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return composeTempoMap(division.ticksPerQuarterNote, marks);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=compose-midi-song.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-song.js","sourceRoot":"","sources":["../../src/song/compose-midi-song.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/song/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/song/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MidiTextEncoding } from "../file/detect-midi-text-encoding.js";
|
|
2
|
+
import type { MidiTimelineEvent } from "./compose-midi-timeline.js";
|
|
3
|
+
/** One lyric syllable of a karaoke file, at the time it is sung. */
|
|
4
|
+
export type MidiKaraokeSyllable = {
|
|
5
|
+
trackIndex: number;
|
|
6
|
+
tick: number;
|
|
7
|
+
seconds: number;
|
|
8
|
+
/** The syllable without its break mark; spaces between words included. */
|
|
9
|
+
text: string;
|
|
10
|
+
/**
|
|
11
|
+
* What comes before the syllable: `paragraph` for a `\` mark (a new screen),
|
|
12
|
+
* `line` for a `/` mark, `none` otherwise.
|
|
13
|
+
*/
|
|
14
|
+
break: "none" | "line" | "paragraph";
|
|
15
|
+
};
|
|
16
|
+
/** The lyrics and tags of a Soft Karaoke (`.kar`) file. */
|
|
17
|
+
export type MidiKaraoke = {
|
|
18
|
+
/** The `@V` tag: the format version, `0100` for 1.0. */
|
|
19
|
+
version: string | undefined;
|
|
20
|
+
/** The `@L` tag: the lyrics' language, such as `ENGL`. */
|
|
21
|
+
language: string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The `@T` tags in order: by convention the title, the performer, then who
|
|
24
|
+
* sequenced it.
|
|
25
|
+
*/
|
|
26
|
+
titles: string[];
|
|
27
|
+
/** The `@I` tags in order: free information lines. */
|
|
28
|
+
infos: string[];
|
|
29
|
+
syllables: MidiKaraokeSyllable[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Reads the lyrics of a Soft Karaoke file, the `.kar` convention that carries
|
|
33
|
+
* them as text events with `@`-prefixed tags.
|
|
34
|
+
*
|
|
35
|
+
* Use it on files that carry `@K` (`@KMIDI KARAOKE FILE`); for lyrics stored as
|
|
36
|
+
* `lyric` meta events, read those from the timeline directly.
|
|
37
|
+
*
|
|
38
|
+
* - The tags are text events starting with `@K`, `@V`, `@L`, `@T` or `@I`, in any
|
|
39
|
+
* track.
|
|
40
|
+
* - The syllables are the other text events of the lyrics track: the track
|
|
41
|
+
* holding the first `@L` or `@T` tag, or else the track with the most
|
|
42
|
+
* untagged text events.
|
|
43
|
+
* - A syllable starting with `\` opens a paragraph and one starting with `/`
|
|
44
|
+
* opens a line; the mark is removed from `text`.
|
|
45
|
+
*
|
|
46
|
+
* Time complexity: O(n) in `timeline.length`. Memory: O(k) in the number of
|
|
47
|
+
* text events.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* composeMidiKaraoke(composeMidiTimeline(file), "windows-1252")?.titles;
|
|
51
|
+
* // ["Twinkle, Twinkle", "Traditional"]
|
|
52
|
+
*
|
|
53
|
+
* @param timeline - A file's events in playing order, from
|
|
54
|
+
* {@link composeMidiTimeline}.
|
|
55
|
+
* @param encoding - The file's text encoding, typically from
|
|
56
|
+
* {@link detectMidiTextEncoding}.
|
|
57
|
+
* @returns The karaoke content, or `undefined` when no `@K` tag marks the file
|
|
58
|
+
* as Soft Karaoke.
|
|
59
|
+
*/
|
|
60
|
+
export declare function composeMidiKaraoke(timeline: readonly MidiTimelineEvent[], encoding: MidiTextEncoding): MidiKaraoke | undefined;
|
|
61
|
+
//# sourceMappingURL=compose-midi-karaoke.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-karaoke.d.ts","sourceRoot":"","sources":["../../src/timeline/compose-midi-karaoke.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;CACrC,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,WAAW,GAAG;IACzB,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sDAAsD;IACtD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,mBAAmB,EAAE,CAAC;CACjC,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EACtC,QAAQ,EAAE,gBAAgB,GACxB,WAAW,GAAG,SAAS,CAyDzB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { maxBy } from "@ac-kit/algo";
|
|
2
|
+
import { decodeText } from "@ac-kit/core";
|
|
3
|
+
const TAG_PATTERN_ = /^@([KVLTI])(.*)$/su;
|
|
4
|
+
/**
|
|
5
|
+
* Reads the lyrics of a Soft Karaoke file, the `.kar` convention that carries
|
|
6
|
+
* them as text events with `@`-prefixed tags.
|
|
7
|
+
*
|
|
8
|
+
* Use it on files that carry `@K` (`@KMIDI KARAOKE FILE`); for lyrics stored as
|
|
9
|
+
* `lyric` meta events, read those from the timeline directly.
|
|
10
|
+
*
|
|
11
|
+
* - The tags are text events starting with `@K`, `@V`, `@L`, `@T` or `@I`, in any
|
|
12
|
+
* track.
|
|
13
|
+
* - The syllables are the other text events of the lyrics track: the track
|
|
14
|
+
* holding the first `@L` or `@T` tag, or else the track with the most
|
|
15
|
+
* untagged text events.
|
|
16
|
+
* - A syllable starting with `\` opens a paragraph and one starting with `/`
|
|
17
|
+
* opens a line; the mark is removed from `text`.
|
|
18
|
+
*
|
|
19
|
+
* Time complexity: O(n) in `timeline.length`. Memory: O(k) in the number of
|
|
20
|
+
* text events.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* composeMidiKaraoke(composeMidiTimeline(file), "windows-1252")?.titles;
|
|
24
|
+
* // ["Twinkle, Twinkle", "Traditional"]
|
|
25
|
+
*
|
|
26
|
+
* @param timeline - A file's events in playing order, from
|
|
27
|
+
* {@link composeMidiTimeline}.
|
|
28
|
+
* @param encoding - The file's text encoding, typically from
|
|
29
|
+
* {@link detectMidiTextEncoding}.
|
|
30
|
+
* @returns The karaoke content, or `undefined` when no `@K` tag marks the file
|
|
31
|
+
* as Soft Karaoke.
|
|
32
|
+
*/
|
|
33
|
+
export function composeMidiKaraoke(timeline, encoding) {
|
|
34
|
+
let isKaraoke = false;
|
|
35
|
+
const karaoke = {
|
|
36
|
+
version: undefined,
|
|
37
|
+
language: undefined,
|
|
38
|
+
titles: [],
|
|
39
|
+
infos: [],
|
|
40
|
+
syllables: [],
|
|
41
|
+
};
|
|
42
|
+
let lyricsTrack;
|
|
43
|
+
const untagged = [];
|
|
44
|
+
const untaggedCounts = new Map();
|
|
45
|
+
for (const { trackIndex, tick, seconds, event } of timeline) {
|
|
46
|
+
if (event.kind !== "meta" || event.meta.type !== "text") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const text = decodeText(event.meta.data, encoding);
|
|
50
|
+
const tag = TAG_PATTERN_.exec(text);
|
|
51
|
+
if (tag === null) {
|
|
52
|
+
untagged.push(composeSyllable_(trackIndex, tick, seconds, text));
|
|
53
|
+
untaggedCounts.set(trackIndex, (untaggedCounts.get(trackIndex) ?? 0) + 1);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = tag[2];
|
|
57
|
+
switch (tag[1]) {
|
|
58
|
+
case "K":
|
|
59
|
+
isKaraoke = true;
|
|
60
|
+
break;
|
|
61
|
+
case "V":
|
|
62
|
+
karaoke.version ??= value;
|
|
63
|
+
break;
|
|
64
|
+
case "L":
|
|
65
|
+
karaoke.language ??= value;
|
|
66
|
+
lyricsTrack ??= trackIndex;
|
|
67
|
+
break;
|
|
68
|
+
case "T":
|
|
69
|
+
karaoke.titles.push(value);
|
|
70
|
+
lyricsTrack ??= trackIndex;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
karaoke.infos.push(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!isKaraoke) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
lyricsTrack ??= maxBy(untaggedCounts, ([, count]) => count)?.[0];
|
|
80
|
+
karaoke.syllables = untagged.filter(({ trackIndex }) => trackIndex === lyricsTrack);
|
|
81
|
+
return karaoke;
|
|
82
|
+
}
|
|
83
|
+
function composeSyllable_(trackIndex, tick, seconds, text) {
|
|
84
|
+
if (text.startsWith("\\")) {
|
|
85
|
+
return {
|
|
86
|
+
trackIndex,
|
|
87
|
+
tick,
|
|
88
|
+
seconds,
|
|
89
|
+
text: text.slice(1),
|
|
90
|
+
break: "paragraph",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (text.startsWith("/")) {
|
|
94
|
+
return { trackIndex, tick, seconds, text: text.slice(1), break: "line" };
|
|
95
|
+
}
|
|
96
|
+
return { trackIndex, tick, seconds, text, break: "none" };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=compose-midi-karaoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-karaoke.js","sourceRoot":"","sources":["../../src/timeline/compose-midi-karaoke.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MidiTimelineEvent } from "./compose-midi-timeline.js";
|
|
2
|
+
/** A sounding note of a Standard MIDI File: a note-on paired with its release. */
|
|
3
|
+
export type MidiNote = {
|
|
4
|
+
/** Index of the track the note-on came from. */
|
|
5
|
+
trackIndex: number;
|
|
6
|
+
/** 0–15. */
|
|
7
|
+
channel: number;
|
|
8
|
+
/** 0–127; 60 is middle C. */
|
|
9
|
+
note: number;
|
|
10
|
+
/** The note-on velocity, 1–127. */
|
|
11
|
+
velocity: number;
|
|
12
|
+
/**
|
|
13
|
+
* The note-off velocity, 0–127, or `undefined` when the note was released by
|
|
14
|
+
* a note-on of velocity 0 or by the end of its track.
|
|
15
|
+
*/
|
|
16
|
+
releaseVelocity: number | undefined;
|
|
17
|
+
startTick: number;
|
|
18
|
+
endTick: number;
|
|
19
|
+
startSeconds: number;
|
|
20
|
+
endSeconds: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Pairs every note-on of a timeline with the release that ends it, giving each
|
|
24
|
+
* note its start and end in ticks and in seconds — what a piano roll, a
|
|
25
|
+
* practice tool or a converter to notation reads, rather than raw events.
|
|
26
|
+
*
|
|
27
|
+
* Use {@link composeMidiTimeline} directly when the other events matter as they
|
|
28
|
+
* are, controllers and the sustain pedal included: this keeps note events only,
|
|
29
|
+
* and does not lengthen a note held by the pedal.
|
|
30
|
+
*
|
|
31
|
+
* - A release is a `noteOff` or a `noteOn` of velocity 0 on the same track,
|
|
32
|
+
* channel and note number.
|
|
33
|
+
* - A note struck again before its release stays open beside the first, and
|
|
34
|
+
* releases close them first in, first out.
|
|
35
|
+
* - A note never released ends where its track ends: its End of Track event, or
|
|
36
|
+
* its last event when it has none.
|
|
37
|
+
* - A release with no open note is ignored.
|
|
38
|
+
*
|
|
39
|
+
* Time complexity: O(n) in `timeline.length`. Memory: O(m) in the number of
|
|
40
|
+
* notes.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // RP-001's format 0 example, 96 ticks per quarter note at 120 bpm:
|
|
44
|
+
* composeMidiNotes(composeMidiTimeline(file))[0];
|
|
45
|
+
* // { trackIndex: 0, channel: 2, note: 48, velocity: 96, releaseVelocity: 64,
|
|
46
|
+
* // startTick: 0, endTick: 384, startSeconds: 0, endSeconds: 2 }
|
|
47
|
+
*
|
|
48
|
+
* @param timeline - A file's events in playing order, from
|
|
49
|
+
* {@link composeMidiTimeline}.
|
|
50
|
+
* @returns The notes in the order of their note-ons in `timeline`: by start for
|
|
51
|
+
* formats 0 and 1, track after track for format 2.
|
|
52
|
+
*/
|
|
53
|
+
export declare function composeMidiNotes(timeline: readonly MidiTimelineEvent[]): MidiNote[];
|
|
54
|
+
//# sourceMappingURL=compose-midi-notes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-notes.d.ts","sourceRoot":"","sources":["../../src/timeline/compose-midi-notes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG;IACtB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC;AAQF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GACpC,QAAQ,EAAE,CAgEZ"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isMidiNoteRelease } from "../message/is-midi-note-release.js";
|
|
2
|
+
function noteKey_(trackIndex, channel, note) {
|
|
3
|
+
return (trackIndex * 16 + channel) * 128 + note;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Pairs every note-on of a timeline with the release that ends it, giving each
|
|
7
|
+
* note its start and end in ticks and in seconds — what a piano roll, a
|
|
8
|
+
* practice tool or a converter to notation reads, rather than raw events.
|
|
9
|
+
*
|
|
10
|
+
* Use {@link composeMidiTimeline} directly when the other events matter as they
|
|
11
|
+
* are, controllers and the sustain pedal included: this keeps note events only,
|
|
12
|
+
* and does not lengthen a note held by the pedal.
|
|
13
|
+
*
|
|
14
|
+
* - A release is a `noteOff` or a `noteOn` of velocity 0 on the same track,
|
|
15
|
+
* channel and note number.
|
|
16
|
+
* - A note struck again before its release stays open beside the first, and
|
|
17
|
+
* releases close them first in, first out.
|
|
18
|
+
* - A note never released ends where its track ends: its End of Track event, or
|
|
19
|
+
* its last event when it has none.
|
|
20
|
+
* - A release with no open note is ignored.
|
|
21
|
+
*
|
|
22
|
+
* Time complexity: O(n) in `timeline.length`. Memory: O(m) in the number of
|
|
23
|
+
* notes.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // RP-001's format 0 example, 96 ticks per quarter note at 120 bpm:
|
|
27
|
+
* composeMidiNotes(composeMidiTimeline(file))[0];
|
|
28
|
+
* // { trackIndex: 0, channel: 2, note: 48, velocity: 96, releaseVelocity: 64,
|
|
29
|
+
* // startTick: 0, endTick: 384, startSeconds: 0, endSeconds: 2 }
|
|
30
|
+
*
|
|
31
|
+
* @param timeline - A file's events in playing order, from
|
|
32
|
+
* {@link composeMidiTimeline}.
|
|
33
|
+
* @returns The notes in the order of their note-ons in `timeline`: by start for
|
|
34
|
+
* formats 0 and 1, track after track for format 2.
|
|
35
|
+
*/
|
|
36
|
+
export function composeMidiNotes(timeline) {
|
|
37
|
+
const notes = [];
|
|
38
|
+
const openNotes = new Map();
|
|
39
|
+
const trackEnds = new Map();
|
|
40
|
+
for (const { trackIndex, tick, seconds, event } of timeline) {
|
|
41
|
+
// A track's events reach the timeline in tick order, so its last one ends it.
|
|
42
|
+
const trackEnd = trackEnds.get(trackIndex);
|
|
43
|
+
if (trackEnd === undefined) {
|
|
44
|
+
trackEnds.set(trackIndex, { tick, seconds });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
trackEnd.tick = tick;
|
|
48
|
+
trackEnd.seconds = seconds;
|
|
49
|
+
}
|
|
50
|
+
if (event.kind !== "channel") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const { message } = event;
|
|
54
|
+
if (message.type !== "noteOn" && message.type !== "noteOff") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const key = noteKey_(trackIndex, message.channel, message.note);
|
|
58
|
+
if (isMidiNoteRelease(message)) {
|
|
59
|
+
const note = openNotes.get(key)?.shift();
|
|
60
|
+
if (note !== undefined) {
|
|
61
|
+
note.endTick = tick;
|
|
62
|
+
note.endSeconds = seconds;
|
|
63
|
+
note.releaseVelocity =
|
|
64
|
+
message.type === "noteOff" ? message.velocity : undefined;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const note = {
|
|
69
|
+
trackIndex,
|
|
70
|
+
channel: message.channel,
|
|
71
|
+
note: message.note,
|
|
72
|
+
velocity: message.velocity,
|
|
73
|
+
releaseVelocity: undefined,
|
|
74
|
+
startTick: tick,
|
|
75
|
+
endTick: tick,
|
|
76
|
+
startSeconds: seconds,
|
|
77
|
+
endSeconds: seconds,
|
|
78
|
+
};
|
|
79
|
+
notes.push(note);
|
|
80
|
+
const queue = openNotes.get(key);
|
|
81
|
+
if (queue === undefined) {
|
|
82
|
+
openNotes.set(key, [note]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
queue.push(note);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const queue of openNotes.values()) {
|
|
89
|
+
for (const note of queue) {
|
|
90
|
+
const trackEnd = trackEnds.get(note.trackIndex);
|
|
91
|
+
note.endTick = trackEnd.tick;
|
|
92
|
+
note.endSeconds = trackEnd.seconds;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return notes;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=compose-midi-notes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-midi-notes.js","sourceRoot":"","sources":["../../src/timeline/compose-midi-notes.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose-midi-timeline.d.ts","sourceRoot":"","sources":["../../src/timeline/compose-midi-timeline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compose-midi-timeline.d.ts","sourceRoot":"","sources":["../../src/timeline/compose-midi-timeline.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,QAAQ,EACR,aAAa,EAEb,MAAM,sBAAsB,CAAC;AAE9B,uEAAuE;AACvE,MAAM,MAAM,iBAAiB,GAAG;IAC/B,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,aAAa,CAAC;CACrB,CAAC;AAuDF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,EAAE,CAqBvE"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timecodeTicksPerSecond_ } from "../file/_time-division.js";
|
|
1
2
|
/** RP-001: a file with no Set Tempo event before a tick plays at 120 bpm. */
|
|
2
3
|
const DEFAULT_TEMPO_ = 500000;
|
|
3
4
|
function appendTrackEvents_(target, file, trackIndex) {
|
|
@@ -14,10 +15,7 @@ function appendTrackEvents_(target, file, trackIndex) {
|
|
|
14
15
|
*/
|
|
15
16
|
function assignSeconds_(events, from, division) {
|
|
16
17
|
if (division.type === "timecode") {
|
|
17
|
-
const
|
|
18
|
-
? 30000 / 1001
|
|
19
|
-
: division.framesPerSecond;
|
|
20
|
-
const ticksPerSecond = framesPerSecond * division.ticksPerFrame;
|
|
18
|
+
const ticksPerSecond = timecodeTicksPerSecond_(division);
|
|
21
19
|
for (let index = from; index < events.length; index++) {
|
|
22
20
|
events[index].seconds = events[index].tick / ticksPerSecond;
|
|
23
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose-midi-timeline.js","sourceRoot":"","sources":["../../src/timeline/compose-midi-timeline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compose-midi-timeline.js","sourceRoot":"","sources":["../../src/timeline/compose-midi-timeline.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ac-kit/format-midi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MIDI 1.0: the message model with a live byte-stream Decoder/Encoder, Standard MIDI File parse/print,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MIDI 1.0: the message model with a live byte-stream Decoder/Encoder, Standard MIDI File parse/print, tick-to-seconds timelines, notes, text decoding, and conversion to a model-music Song.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"midi",
|
|
7
7
|
"midi-file",
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"main": "./dist/index.js",
|
|
29
29
|
"exports": {
|
|
30
|
-
".": "./dist/index.js"
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./song": "./dist/song/index.js"
|
|
31
32
|
},
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"main": "./dist/index.js",
|
|
34
35
|
"exports": {
|
|
35
|
-
".": "./dist/index.js"
|
|
36
|
+
".": "./dist/index.js",
|
|
37
|
+
"./song": "./dist/song/index.js"
|
|
36
38
|
},
|
|
37
39
|
"access": "public"
|
|
38
40
|
},
|
|
@@ -45,9 +47,11 @@
|
|
|
45
47
|
"prepack": "yarn build"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
|
-
"@ac-kit/
|
|
49
|
-
"@ac-kit/
|
|
50
|
-
"@ac-kit/format-
|
|
50
|
+
"@ac-kit/algo": "^0.2.1",
|
|
51
|
+
"@ac-kit/core": "^0.4.0",
|
|
52
|
+
"@ac-kit/format-core": "^0.1.4",
|
|
53
|
+
"@ac-kit/format-varint": "^0.1.3",
|
|
54
|
+
"@ac-kit/model-music": "^0.1.0",
|
|
51
55
|
"@types/web": "^0.0.359"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|