@huffduff/midi-writer-ts 3.2.0 → 3.2.3
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/README.md +27 -28
- package/build/index.cjs +24 -26
- package/build/index.mjs +1 -26
- package/build/types/main.d.ts +2 -35
- package/package.json +10 -4
- package/src/abstract-event.ts +8 -0
- package/src/chunks/chunk.ts +6 -0
- package/src/chunks/header.ts +29 -0
- package/src/chunks/track.ts +345 -0
- package/src/constants.ts +18 -0
- package/src/main.ts +48 -0
- package/src/meta-events/copyright-event.ts +35 -0
- package/src/meta-events/cue-point-event.ts +35 -0
- package/src/meta-events/end-track-event.ts +29 -0
- package/src/meta-events/instrument-name-event.ts +35 -0
- package/src/meta-events/key-signature-event.ts +73 -0
- package/src/meta-events/lyric-event.ts +35 -0
- package/src/meta-events/marker-event.ts +35 -0
- package/src/meta-events/meta-event.ts +7 -0
- package/src/meta-events/tempo-event.ts +37 -0
- package/src/meta-events/text-event.ts +35 -0
- package/src/meta-events/time-signature-event.ts +32 -0
- package/src/meta-events/track-name-event.ts +35 -0
- package/src/midi-events/controller-change-event.ts +30 -0
- package/src/midi-events/midi-event.ts +11 -0
- package/src/midi-events/note-event.ts +164 -0
- package/src/midi-events/note-off-event.ts +55 -0
- package/src/midi-events/note-on-event.ts +69 -0
- package/src/midi-events/pitch-bend-event.ts +40 -0
- package/src/midi-events/program-change-event.ts +28 -0
- package/src/utils.ts +263 -0
- package/src/vexflow.ts +96 -0
- package/src/writer.ts +99 -0
- package/.editorconfig +0 -24
- package/.eslintignore +0 -3
- package/.eslintrc.js +0 -18
- package/.nvmrc +0 -1
- package/.travis.yml +0 -3
- package/browser/midiwriter.js +0 -1367
- package/jsdoc.json +0 -5
- package/postinstall.js +0 -1
- package/rollup.config.js +0 -22
- package/runkit.js +0 -18
- package/tsconfig.json +0 -13
- package/typedoc.json +0 -5
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {AbstractEvent} from '../abstract-event';
|
|
2
|
+
import {MidiEvent} from './midi-event';
|
|
3
|
+
import {NoteOnEvent} from './note-on-event';
|
|
4
|
+
import {NoteOffEvent} from './note-off-event';
|
|
5
|
+
import {Utils} from '../utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wrapper for noteOnEvent/noteOffEvent objects that builds both events.
|
|
9
|
+
* @param {object} fields - {pitch: '[C4]', duration: '4', wait: '4', velocity: 1-100}
|
|
10
|
+
* @return {NoteEvent}
|
|
11
|
+
*/
|
|
12
|
+
class NoteEvent implements AbstractEvent {
|
|
13
|
+
data: number[];
|
|
14
|
+
delta: number;
|
|
15
|
+
events: MidiEvent[];
|
|
16
|
+
name: string;
|
|
17
|
+
pitch: string[];
|
|
18
|
+
grace: string|string[];
|
|
19
|
+
channel: number;
|
|
20
|
+
repeat: number;
|
|
21
|
+
tick: number;
|
|
22
|
+
duration: string;
|
|
23
|
+
sequential: boolean;
|
|
24
|
+
wait: string;
|
|
25
|
+
velocity: number;
|
|
26
|
+
tickDuration: number;
|
|
27
|
+
restDuration: number;
|
|
28
|
+
|
|
29
|
+
constructor(fields) {
|
|
30
|
+
this.data = [];
|
|
31
|
+
this.name = 'NoteEvent';
|
|
32
|
+
this.pitch = Utils.toArray(fields.pitch);
|
|
33
|
+
|
|
34
|
+
this.channel = fields.channel || 1;
|
|
35
|
+
this.duration = fields.duration || '4';
|
|
36
|
+
this.grace = fields.grace;
|
|
37
|
+
this.repeat = fields.repeat || 1;
|
|
38
|
+
this.sequential = fields.sequential || false;
|
|
39
|
+
this.tick = fields.startTick || fields.tick || null;
|
|
40
|
+
this.velocity = fields.velocity || 50;
|
|
41
|
+
this.wait = fields.wait || 0;
|
|
42
|
+
|
|
43
|
+
this.tickDuration = Utils.getTickDuration(this.duration);
|
|
44
|
+
this.restDuration = Utils.getTickDuration(this.wait);
|
|
45
|
+
|
|
46
|
+
this.events = []; // Hold actual NoteOn/NoteOff events
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds int array for this event.
|
|
51
|
+
* @return {NoteEvent}
|
|
52
|
+
*/
|
|
53
|
+
buildData(): NoteEvent {
|
|
54
|
+
// Reset data array
|
|
55
|
+
this.data = [];
|
|
56
|
+
|
|
57
|
+
// Apply grace note(s) and subtract ticks (currently 1 tick per grace note) from tickDuration so net value is the same
|
|
58
|
+
if (this.grace) {
|
|
59
|
+
const graceDuration = 1;
|
|
60
|
+
this.grace = Utils.toArray(this.grace);
|
|
61
|
+
this.grace.forEach(() => {
|
|
62
|
+
const noteEvent = new NoteEvent({pitch: this.grace, duration:'T' + graceDuration});
|
|
63
|
+
this.data = this.data.concat(noteEvent.data);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// fields.pitch could be an array of pitches.
|
|
68
|
+
// If so create note events for each and apply the same duration.
|
|
69
|
+
|
|
70
|
+
// By default this is a chord if it's an array of notes that requires one NoteOnEvent.
|
|
71
|
+
// If this.sequential === true then it's a sequential string of notes that requires separate NoteOnEvents.
|
|
72
|
+
if ( ! this.sequential) {
|
|
73
|
+
// Handle repeat
|
|
74
|
+
for (let j = 0; j < this.repeat; j++) {
|
|
75
|
+
// Note on
|
|
76
|
+
this.pitch.forEach((p, i) => {
|
|
77
|
+
let noteOnNew;
|
|
78
|
+
|
|
79
|
+
if (i == 0) {
|
|
80
|
+
noteOnNew = new NoteOnEvent({
|
|
81
|
+
channel: this.channel,
|
|
82
|
+
wait: this.wait,
|
|
83
|
+
delta: Utils.getTickDuration(this.wait),
|
|
84
|
+
velocity: this.velocity,
|
|
85
|
+
pitch: p,
|
|
86
|
+
tick: this.tick,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
} else {
|
|
90
|
+
// Running status (can ommit the note on status)
|
|
91
|
+
//noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
|
|
92
|
+
noteOnNew = new NoteOnEvent({
|
|
93
|
+
channel: this.channel,
|
|
94
|
+
wait: 0,
|
|
95
|
+
delta: 0,
|
|
96
|
+
velocity: this.velocity,
|
|
97
|
+
pitch: p,
|
|
98
|
+
tick: this.tick,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.events.push(noteOnNew);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Note off
|
|
106
|
+
this.pitch.forEach((p, i) => {
|
|
107
|
+
let noteOffNew;
|
|
108
|
+
|
|
109
|
+
if (i == 0) {
|
|
110
|
+
//noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))});
|
|
111
|
+
noteOffNew = new NoteOffEvent({
|
|
112
|
+
channel: this.channel,
|
|
113
|
+
duration: this.duration,
|
|
114
|
+
velocity: this.velocity,
|
|
115
|
+
pitch: p,
|
|
116
|
+
tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
} else {
|
|
120
|
+
// Running status (can omit the note off status)
|
|
121
|
+
//noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
|
|
122
|
+
noteOffNew = new NoteOffEvent({
|
|
123
|
+
channel: this.channel,
|
|
124
|
+
duration: 0,
|
|
125
|
+
velocity: this.velocity,
|
|
126
|
+
pitch: p,
|
|
127
|
+
tick: this.tick !== null ? Utils.getTickDuration(this.duration) + this.tick : null,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.events.push(noteOffNew);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
} else {
|
|
136
|
+
// Handle repeat
|
|
137
|
+
for (let j = 0; j < this.repeat; j++) {
|
|
138
|
+
this.pitch.forEach((p, i) => {
|
|
139
|
+
const noteOnNew = new NoteOnEvent({
|
|
140
|
+
channel: this.channel,
|
|
141
|
+
wait: (i > 0 ? 0 : this.wait), // wait only applies to first note in repetition
|
|
142
|
+
delta: (i > 0 ? 0 : Utils.getTickDuration(this.wait)),
|
|
143
|
+
velocity: this.velocity,
|
|
144
|
+
pitch: p,
|
|
145
|
+
tick: this.tick,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const noteOffNew = new NoteOffEvent({
|
|
149
|
+
channel: this.channel,
|
|
150
|
+
duration: this.duration,
|
|
151
|
+
velocity: this.velocity,
|
|
152
|
+
pitch: p,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.events.push(noteOnNew, noteOffNew);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {NoteEvent};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {MidiEvent} from './midi-event';
|
|
2
|
+
import {Utils} from '../utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Holds all data for a "note off" MIDI event
|
|
6
|
+
* @param {object} fields {data: []}
|
|
7
|
+
* @return {NoteOffEvent}
|
|
8
|
+
*/
|
|
9
|
+
class NoteOffEvent implements MidiEvent {
|
|
10
|
+
channel: number;
|
|
11
|
+
data: number[];
|
|
12
|
+
delta: number;
|
|
13
|
+
deltaWithPrecisionCorrection: number;
|
|
14
|
+
status: 0x80;
|
|
15
|
+
name: string;
|
|
16
|
+
velocity: number;
|
|
17
|
+
pitch: string|number;
|
|
18
|
+
duration: string|number;
|
|
19
|
+
tick: number;
|
|
20
|
+
|
|
21
|
+
constructor(fields: { channel: number; duration: string|number; velocity: number; pitch: string|number; tick?: number; data?: number[]; delta?: number }) {
|
|
22
|
+
this.name = 'NoteOffEvent';
|
|
23
|
+
this.channel = fields.channel || 1;
|
|
24
|
+
this.pitch = fields.pitch;
|
|
25
|
+
this.velocity = fields.velocity || 50;
|
|
26
|
+
this.tick = fields.tick || null;
|
|
27
|
+
this.data = fields.data;
|
|
28
|
+
this.delta = fields.delta || Utils.getTickDuration(fields.duration);
|
|
29
|
+
this.status = 0x80;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds int array for this event.
|
|
34
|
+
* @param {Track} track - parent track
|
|
35
|
+
* @return {NoteOffEvent}
|
|
36
|
+
*/
|
|
37
|
+
buildData(track, precisionDelta: number, options: {middleC?: string} = {}) {
|
|
38
|
+
if (this.tick === null) {
|
|
39
|
+
this.tick = Utils.getRoundedIfClose(this.delta + track.tickPointer);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
|
|
43
|
+
|
|
44
|
+
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
|
|
45
|
+
.concat(
|
|
46
|
+
this.status | this.channel - 1,
|
|
47
|
+
Utils.getPitch(this.pitch, options.middleC),
|
|
48
|
+
Utils.convertVelocity(this.velocity)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {NoteOffEvent};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { MidiEvent } from './midi-event';
|
|
2
|
+
import { Utils } from '../utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Holds all data for a "note on" MIDI event
|
|
6
|
+
* @param {object} fields {data: []}
|
|
7
|
+
* @return {NoteOnEvent}
|
|
8
|
+
*/
|
|
9
|
+
class NoteOnEvent implements MidiEvent {
|
|
10
|
+
channel: number;
|
|
11
|
+
data: number[];
|
|
12
|
+
delta: number;
|
|
13
|
+
status: 0x90;
|
|
14
|
+
name: string;
|
|
15
|
+
pitch: string | number;
|
|
16
|
+
velocity: number;
|
|
17
|
+
wait: string | number;
|
|
18
|
+
tick: number;
|
|
19
|
+
deltaWithPrecisionCorrection: number;
|
|
20
|
+
|
|
21
|
+
constructor(fields: { channel?: number; wait?: string | number; velocity?: number; pitch?: string | number; tick?: number; data?: number[]; delta?: number }) {
|
|
22
|
+
this.name = 'NoteOnEvent';
|
|
23
|
+
this.channel = fields.channel || 1;
|
|
24
|
+
this.pitch = fields.pitch;
|
|
25
|
+
this.wait = fields.wait || 0;
|
|
26
|
+
this.velocity = fields.velocity || 50;
|
|
27
|
+
|
|
28
|
+
this.tick = fields.tick || null;
|
|
29
|
+
this.delta = null;
|
|
30
|
+
this.data = fields.data;
|
|
31
|
+
this.status = 0x90;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds int array for this event.
|
|
36
|
+
* @param {Track} track - parent track
|
|
37
|
+
* @return {NoteOnEvent}
|
|
38
|
+
*/
|
|
39
|
+
buildData(track, precisionDelta, options: { middleC?: string } = {}) {
|
|
40
|
+
this.data = [];
|
|
41
|
+
|
|
42
|
+
// Explicitly defined startTick event
|
|
43
|
+
if (this.tick) {
|
|
44
|
+
this.tick = Utils.getRoundedIfClose(this.tick);
|
|
45
|
+
|
|
46
|
+
// If this is the first event in the track then use event's starting tick as delta.
|
|
47
|
+
if (track.tickPointer == 0) {
|
|
48
|
+
this.delta = this.tick;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} else {
|
|
52
|
+
this.delta = Utils.getTickDuration(this.wait);
|
|
53
|
+
this.tick = Utils.getRoundedIfClose(track.tickPointer + this.delta);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
|
|
57
|
+
|
|
58
|
+
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
|
|
59
|
+
.concat(
|
|
60
|
+
this.status | this.channel - 1,
|
|
61
|
+
Utils.getPitch(this.pitch, options.middleC),
|
|
62
|
+
Utils.convertVelocity(this.velocity)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { NoteOnEvent };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {MidiEvent} from './midi-event';
|
|
2
|
+
import {Utils} from '../utils';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Holds all data for a "Pitch Bend" MIDI event
|
|
7
|
+
* [ -1.0, 0, 1.0 ] -> [ 0, 8192, 16383]
|
|
8
|
+
* @param {object} fields { bend : float, channel : int, delta: int }
|
|
9
|
+
* @return {PitchBendEvent}
|
|
10
|
+
*/
|
|
11
|
+
class PitchBendEvent implements MidiEvent {
|
|
12
|
+
channel: number;
|
|
13
|
+
data: number[];
|
|
14
|
+
delta: number;
|
|
15
|
+
name: string;
|
|
16
|
+
status: 0xE0;
|
|
17
|
+
|
|
18
|
+
constructor(fields) {
|
|
19
|
+
this.channel = fields.channel || 0;
|
|
20
|
+
this.delta = fields.delta || 0x00;
|
|
21
|
+
this.name = 'PitchBendEvent';
|
|
22
|
+
this.status = 0xE0;
|
|
23
|
+
|
|
24
|
+
const bend14 = this.scale14bits(fields.bend);
|
|
25
|
+
|
|
26
|
+
const lsbValue = bend14 & 0x7f;
|
|
27
|
+
const msbValue = ( bend14 >> 7 ) & 0x7f;
|
|
28
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, lsbValue, msbValue);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
scale14bits(zeroOne) {
|
|
32
|
+
if ( zeroOne <= 0 ) {
|
|
33
|
+
return Math.floor( 16384 * ( zeroOne + 1 ) / 2 );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Math.floor( 16383 * ( zeroOne + 1 ) / 2 );
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {PitchBendEvent};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {MidiEvent} from './midi-event';
|
|
2
|
+
import {Utils} from '../utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Holds all data for a "program change" MIDI event
|
|
6
|
+
* @param {object} fields {instrument: integer, delta: integer}
|
|
7
|
+
* @return {ProgramChangeEvent}
|
|
8
|
+
*/
|
|
9
|
+
class ProgramChangeEvent implements MidiEvent {
|
|
10
|
+
channel: number;
|
|
11
|
+
data: number[];
|
|
12
|
+
delta: number;
|
|
13
|
+
status: 0xC0;
|
|
14
|
+
name: string;
|
|
15
|
+
instrument: number;
|
|
16
|
+
|
|
17
|
+
constructor(fields: { channel?: number; delta?: number; instrument: number; }) {
|
|
18
|
+
this.channel = fields.channel || 0;
|
|
19
|
+
this.delta = fields.delta || 0x00;
|
|
20
|
+
this.instrument = fields.instrument;
|
|
21
|
+
this.status = 0xC0;
|
|
22
|
+
this.name = 'ProgramChangeEvent';
|
|
23
|
+
// delta time defaults to 0.
|
|
24
|
+
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, this.instrument);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {ProgramChangeEvent};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { Constants } from './constants';
|
|
2
|
+
import { toMidi } from '@tonaljs/midi';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Static utility functions used throughout the library.
|
|
6
|
+
*/
|
|
7
|
+
class Utils {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets MidiWriterJS version number.
|
|
11
|
+
* @return {string}
|
|
12
|
+
*/
|
|
13
|
+
static version(): string {
|
|
14
|
+
return Constants.VERSION;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert a string to an array of bytes
|
|
19
|
+
* @param {string} string
|
|
20
|
+
* @return {array}
|
|
21
|
+
*/
|
|
22
|
+
static stringToBytes(string: string): number[] {
|
|
23
|
+
return string.split('').map(char => char.charCodeAt(0))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if argument is a valid number.
|
|
28
|
+
* @param {*} n - Value to check
|
|
29
|
+
* @return {boolean}
|
|
30
|
+
*/
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
static isNumeric(n: any): boolean {
|
|
33
|
+
return !isNaN(parseFloat(n)) && isFinite(n)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the correct MIDI number for the specified pitch.
|
|
38
|
+
* Uses Tonal Midi - https://github.com/danigb/tonal/tree/master/packages/midi
|
|
39
|
+
* @param {(string|number)} pitch - 'C#4' or midi note code
|
|
40
|
+
* @param {string} middleC
|
|
41
|
+
* @return {number}
|
|
42
|
+
*/
|
|
43
|
+
static getPitch(pitch: string | number, middleC: string | number = 'C4'): number {
|
|
44
|
+
return 60 - toMidi(middleC) + toMidi(pitch);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Translates number of ticks to MIDI timestamp format, returning an array of
|
|
49
|
+
* hex strings with the time values. Midi has a very particular time to express time,
|
|
50
|
+
* take a good look at the spec before ever touching this function.
|
|
51
|
+
* Thanks to https://github.com/sergi/jsmidi
|
|
52
|
+
*
|
|
53
|
+
* @param {number} ticks - Number of ticks to be translated
|
|
54
|
+
* @return {array} - Bytes that form the MIDI time value
|
|
55
|
+
*/
|
|
56
|
+
static numberToVariableLength(ticks: number): number[] {
|
|
57
|
+
ticks = Math.round(ticks);
|
|
58
|
+
let buffer = ticks & 0x7F;
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line no-cond-assign
|
|
61
|
+
while (ticks = ticks >> 7) {
|
|
62
|
+
buffer <<= 8;
|
|
63
|
+
buffer |= ((ticks & 0x7F) | 0x80);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const bList = [];
|
|
67
|
+
// eslint-disable-next-line no-constant-condition
|
|
68
|
+
while (true) {
|
|
69
|
+
bList.push(buffer & 0xff);
|
|
70
|
+
|
|
71
|
+
if (buffer & 0x80) buffer >>= 8
|
|
72
|
+
else { break; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return bList;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Counts number of bytes in string
|
|
80
|
+
* @param {string} s
|
|
81
|
+
* @return {number}
|
|
82
|
+
*/
|
|
83
|
+
static stringByteCount(s: string): number {
|
|
84
|
+
return encodeURI(s).split(/%..|./).length - 1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get an int from an array of bytes.
|
|
89
|
+
* @param {array} bytes
|
|
90
|
+
* @return {number}
|
|
91
|
+
*/
|
|
92
|
+
static numberFromBytes(bytes: number[]): number {
|
|
93
|
+
let hex = '';
|
|
94
|
+
let stringResult;
|
|
95
|
+
|
|
96
|
+
bytes.forEach((byte) => {
|
|
97
|
+
stringResult = byte.toString(16);
|
|
98
|
+
|
|
99
|
+
// ensure string is 2 chars
|
|
100
|
+
if (stringResult.length == 1) stringResult = "0" + stringResult
|
|
101
|
+
|
|
102
|
+
hex += stringResult;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return parseInt(hex, 16);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Takes a number and splits it up into an array of bytes. Can be padded by passing a number to bytesNeeded
|
|
110
|
+
* @param {number} number
|
|
111
|
+
* @param {number} bytesNeeded
|
|
112
|
+
* @return {array} - Array of bytes
|
|
113
|
+
*/
|
|
114
|
+
static numberToBytes(number: number, bytesNeeded: number): number[] {
|
|
115
|
+
bytesNeeded = bytesNeeded || 1;
|
|
116
|
+
|
|
117
|
+
let hexString = number.toString(16);
|
|
118
|
+
|
|
119
|
+
if (hexString.length & 1) { // Make sure hex string is even number of chars
|
|
120
|
+
hexString = '0' + hexString;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Split hex string into an array of two char elements
|
|
124
|
+
const hexArray = hexString.match(/.{2}/g);
|
|
125
|
+
|
|
126
|
+
// Now parse them out as integers
|
|
127
|
+
const intArray = hexArray.map(item => parseInt(item, 16))
|
|
128
|
+
|
|
129
|
+
// Prepend empty bytes if we don't have enough
|
|
130
|
+
if (intArray.length < bytesNeeded) {
|
|
131
|
+
while (bytesNeeded - intArray.length > 0) {
|
|
132
|
+
intArray.unshift(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return intArray;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Converts value to array if needed.
|
|
141
|
+
* @param {any} value
|
|
142
|
+
* @return {array}
|
|
143
|
+
*/
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
|
+
static toArray(value: any): any[] {
|
|
146
|
+
if (Array.isArray(value)) return value;
|
|
147
|
+
return [value];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Converts velocity to value 0-127
|
|
152
|
+
* @param {number} velocity - Velocity value 1-100
|
|
153
|
+
* @return {number}
|
|
154
|
+
*/
|
|
155
|
+
static convertVelocity(velocity: number): number {
|
|
156
|
+
// Max passed value limited to 100
|
|
157
|
+
velocity = velocity > 100 ? 100 : velocity;
|
|
158
|
+
return Math.round(velocity / 100 * 127);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Gets the total number of ticks of a specified duration.
|
|
163
|
+
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
|
|
164
|
+
* @param {(string|array)} duration
|
|
165
|
+
* @return {number}
|
|
166
|
+
*/
|
|
167
|
+
static getTickDuration(duration: (string | string[] | number)): number {
|
|
168
|
+
if (Array.isArray(duration)) {
|
|
169
|
+
// Recursively execute this method for each item in the array and return the sum of tick durations.
|
|
170
|
+
return duration.map((value) => {
|
|
171
|
+
return Utils.getTickDuration(value);
|
|
172
|
+
}).reduce((a, b) => {
|
|
173
|
+
return a + b;
|
|
174
|
+
}, 0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
duration = duration.toString();
|
|
178
|
+
|
|
179
|
+
if (duration.toLowerCase().charAt(0) === 't') {
|
|
180
|
+
// If duration starts with 't' then the number that follows is an explicit tick count
|
|
181
|
+
const ticks = parseInt(duration.substring(1));
|
|
182
|
+
|
|
183
|
+
if (isNaN(ticks) || ticks < 0) {
|
|
184
|
+
throw new Error(duration + ' is not a valid duration.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return ticks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Need to apply duration here. Quarter note == Constants.HEADER_CHUNK_DIVISION
|
|
191
|
+
const quarterTicks = Utils.numberFromBytes(Constants.HEADER_CHUNK_DIVISION);
|
|
192
|
+
const tickDuration = quarterTicks * Utils.getDurationMultiplier(duration);
|
|
193
|
+
return Utils.getRoundedIfClose(tickDuration)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Due to rounding errors in JavaScript engines,
|
|
198
|
+
* it's safe to round when we're very close to the actual tick number
|
|
199
|
+
*
|
|
200
|
+
* @static
|
|
201
|
+
* @param {number} tick
|
|
202
|
+
* @return {number}
|
|
203
|
+
*/
|
|
204
|
+
static getRoundedIfClose(tick: number): number {
|
|
205
|
+
const roundedTick = Math.round(tick);
|
|
206
|
+
return Math.abs(roundedTick - tick) < 0.000001 ? roundedTick : tick;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Due to low precision of MIDI,
|
|
211
|
+
* we need to keep track of rounding errors in deltas.
|
|
212
|
+
* This function will calculate the rounding error for a given duration.
|
|
213
|
+
*
|
|
214
|
+
* @static
|
|
215
|
+
* @param {number} tick
|
|
216
|
+
* @return {number}
|
|
217
|
+
*/
|
|
218
|
+
static getPrecisionLoss(tick: number): number {
|
|
219
|
+
const roundedTick = Math.round(tick);
|
|
220
|
+
return roundedTick - tick;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Gets what to multiple ticks/quarter note by to get the specified duration.
|
|
225
|
+
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
|
|
226
|
+
* @param {string} duration
|
|
227
|
+
* @return {number}
|
|
228
|
+
*/
|
|
229
|
+
static getDurationMultiplier(duration: string): number {
|
|
230
|
+
// Need to apply duration here.
|
|
231
|
+
// Quarter note == Constants.HEADER_CHUNK_DIVISION ticks.
|
|
232
|
+
|
|
233
|
+
if (duration === '0') return 0;
|
|
234
|
+
|
|
235
|
+
const match = duration.match(/^(?<dotted>d+)?(?<base>\d+)(?:t(?<tuplet>\d*))?/);
|
|
236
|
+
if (match) {
|
|
237
|
+
const base = Number(match.groups.base);
|
|
238
|
+
// 1 or any power of two:
|
|
239
|
+
const isValidBase = base === 1 || ((base & (base - 1)) === 0);
|
|
240
|
+
if (isValidBase) {
|
|
241
|
+
// how much faster or slower is this note compared to a quarter?
|
|
242
|
+
const ratio = base / 4;
|
|
243
|
+
let durationInQuarters = 1 / ratio;
|
|
244
|
+
const { dotted, tuplet } = match.groups;
|
|
245
|
+
if (dotted) {
|
|
246
|
+
const thisManyDots = dotted.length;
|
|
247
|
+
const divisor = Math.pow(2, thisManyDots);
|
|
248
|
+
durationInQuarters = durationInQuarters + (durationInQuarters * ((divisor - 1) / divisor));
|
|
249
|
+
}
|
|
250
|
+
if (typeof tuplet === 'string') {
|
|
251
|
+
const fitInto = durationInQuarters * 2;
|
|
252
|
+
// default to triplet:
|
|
253
|
+
const thisManyNotes = Number(tuplet || '3');
|
|
254
|
+
durationInQuarters = fitInto / thisManyNotes;
|
|
255
|
+
}
|
|
256
|
+
return durationInQuarters
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw new Error(duration + ' is not a valid duration.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { Utils };
|