@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.
Files changed (45) hide show
  1. package/README.md +27 -28
  2. package/build/index.cjs +24 -26
  3. package/build/index.mjs +1 -26
  4. package/build/types/main.d.ts +2 -35
  5. package/package.json +10 -4
  6. package/src/abstract-event.ts +8 -0
  7. package/src/chunks/chunk.ts +6 -0
  8. package/src/chunks/header.ts +29 -0
  9. package/src/chunks/track.ts +345 -0
  10. package/src/constants.ts +18 -0
  11. package/src/main.ts +48 -0
  12. package/src/meta-events/copyright-event.ts +35 -0
  13. package/src/meta-events/cue-point-event.ts +35 -0
  14. package/src/meta-events/end-track-event.ts +29 -0
  15. package/src/meta-events/instrument-name-event.ts +35 -0
  16. package/src/meta-events/key-signature-event.ts +73 -0
  17. package/src/meta-events/lyric-event.ts +35 -0
  18. package/src/meta-events/marker-event.ts +35 -0
  19. package/src/meta-events/meta-event.ts +7 -0
  20. package/src/meta-events/tempo-event.ts +37 -0
  21. package/src/meta-events/text-event.ts +35 -0
  22. package/src/meta-events/time-signature-event.ts +32 -0
  23. package/src/meta-events/track-name-event.ts +35 -0
  24. package/src/midi-events/controller-change-event.ts +30 -0
  25. package/src/midi-events/midi-event.ts +11 -0
  26. package/src/midi-events/note-event.ts +164 -0
  27. package/src/midi-events/note-off-event.ts +55 -0
  28. package/src/midi-events/note-on-event.ts +69 -0
  29. package/src/midi-events/pitch-bend-event.ts +40 -0
  30. package/src/midi-events/program-change-event.ts +28 -0
  31. package/src/utils.ts +263 -0
  32. package/src/vexflow.ts +96 -0
  33. package/src/writer.ts +99 -0
  34. package/.editorconfig +0 -24
  35. package/.eslintignore +0 -3
  36. package/.eslintrc.js +0 -18
  37. package/.nvmrc +0 -1
  38. package/.travis.yml +0 -3
  39. package/browser/midiwriter.js +0 -1367
  40. package/jsdoc.json +0 -5
  41. package/postinstall.js +0 -1
  42. package/rollup.config.js +0 -22
  43. package/runkit.js +0 -18
  44. package/tsconfig.json +0 -13
  45. 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 };