midiwire 0.1.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/LICENSE +21 -0
- package/README.md +845 -0
- package/dist/midiwire.es.js +1987 -0
- package/dist/midiwire.umd.js +1 -0
- package/package.json +58 -0
- package/src/bindings/DataAttributeBinder.js +198 -0
- package/src/bindings/DataAttributeBinder.test.js +825 -0
- package/src/core/EventEmitter.js +93 -0
- package/src/core/EventEmitter.test.js +357 -0
- package/src/core/MIDIConnection.js +364 -0
- package/src/core/MIDIConnection.test.js +783 -0
- package/src/core/MIDIController.js +756 -0
- package/src/core/MIDIController.test.js +1958 -0
- package/src/core/MIDIDeviceManager.js +204 -0
- package/src/core/MIDIDeviceManager.test.js +638 -0
- package/src/core/errors.js +99 -0
- package/src/index.js +181 -0
- package/src/utils/dx7.js +1294 -0
- package/src/utils/dx7.test.js +1208 -0
- package/src/utils/midi.js +244 -0
- package/src/utils/midi.test.js +260 -0
- package/src/utils/sysex.js +98 -0
- package/src/utils/sysex.test.js +222 -0
- package/src/utils/validators.js +88 -0
- package/src/utils/validators.test.js +300 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { MIDIValidationError } from "../core/errors.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clamp a value between min and max
|
|
5
|
+
* @param {number} value - Value to clamp
|
|
6
|
+
* @param {number} min - Minimum value
|
|
7
|
+
* @param {number} max - Maximum value
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function clamp(value, min, max) {
|
|
11
|
+
return Math.max(min, Math.min(max, value))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a value from input range to MIDI range (0-127)
|
|
16
|
+
* @param {number} value - Input value
|
|
17
|
+
* @param {number} inputMin - Input minimum
|
|
18
|
+
* @param {number} inputMax - Input maximum
|
|
19
|
+
* @param {boolean} [invert=false] - Invert the output
|
|
20
|
+
* @returns {number} MIDI value (0-127)
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeValue(value, inputMin, inputMax, invert = false) {
|
|
23
|
+
// Normalize to 0-1
|
|
24
|
+
const normalized = (value - inputMin) / (inputMax - inputMin)
|
|
25
|
+
|
|
26
|
+
// Invert if requested
|
|
27
|
+
const final = invert ? 1 - normalized : normalized
|
|
28
|
+
|
|
29
|
+
// Scale to MIDI range (0-127)
|
|
30
|
+
const midiValue = final * 127
|
|
31
|
+
|
|
32
|
+
return clamp(Math.round(midiValue), 0, 127)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Denormalize a MIDI value (0-127) to a custom range
|
|
37
|
+
* @param {number} midiValue - MIDI value (0-127)
|
|
38
|
+
* @param {number} outputMin - Output minimum
|
|
39
|
+
* @param {number} outputMax - Output maximum
|
|
40
|
+
* @param {boolean} [invert=false] - Invert the input
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function denormalizeValue(midiValue, outputMin, outputMax, invert = false) {
|
|
44
|
+
// Normalize MIDI value to 0-1
|
|
45
|
+
let normalized = clamp(midiValue, 0, 127) / 127
|
|
46
|
+
|
|
47
|
+
// Invert if requested
|
|
48
|
+
if (invert) {
|
|
49
|
+
normalized = 1 - normalized
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Scale to output range
|
|
53
|
+
return outputMin + normalized * (outputMax - outputMin)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert a note name to MIDI note number
|
|
58
|
+
* @param {string} noteName - Note name (e.g., "C4", "A#3", "Bb5")
|
|
59
|
+
* @returns {number} MIDI note number (0-127)
|
|
60
|
+
* @throws {MIDIValidationError} If noteName is not a valid note format
|
|
61
|
+
*/
|
|
62
|
+
export function noteNameToNumber(noteName) {
|
|
63
|
+
const notes = {
|
|
64
|
+
C: 0,
|
|
65
|
+
"C#": 1,
|
|
66
|
+
DB: 1,
|
|
67
|
+
D: 2,
|
|
68
|
+
"D#": 3,
|
|
69
|
+
EB: 3,
|
|
70
|
+
E: 4,
|
|
71
|
+
F: 5,
|
|
72
|
+
"F#": 6,
|
|
73
|
+
GB: 6,
|
|
74
|
+
G: 7,
|
|
75
|
+
"G#": 8,
|
|
76
|
+
AB: 8,
|
|
77
|
+
A: 9,
|
|
78
|
+
"A#": 10,
|
|
79
|
+
BB: 10,
|
|
80
|
+
B: 11,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const match = noteName.match(/^([A-G][#b]?)(-?\d+)$/i)
|
|
84
|
+
if (!match) {
|
|
85
|
+
throw new MIDIValidationError(`Invalid note name: ${noteName}`, "note", noteName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [, note, octave] = match
|
|
89
|
+
const noteValue = notes[note.toUpperCase()]
|
|
90
|
+
|
|
91
|
+
if (noteValue === undefined) {
|
|
92
|
+
throw new MIDIValidationError(`Invalid note: ${note}`, "note", note)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const midiNote = (parseInt(octave, 10) + 1) * 12 + noteValue
|
|
96
|
+
return clamp(midiNote, 0, 127)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert MIDI note number to note name
|
|
101
|
+
* @param {number} noteNumber - MIDI note number (0-127)
|
|
102
|
+
* @param {boolean} [useFlats=false] - Use flats instead of sharps
|
|
103
|
+
* @returns {string} Note name (e.g., "C4")
|
|
104
|
+
*/
|
|
105
|
+
export function noteNumberToName(noteNumber, useFlats = false) {
|
|
106
|
+
const sharps = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
107
|
+
const flats = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
|
|
108
|
+
|
|
109
|
+
const notes = useFlats ? flats : sharps
|
|
110
|
+
const octave = Math.floor(noteNumber / 12) - 1
|
|
111
|
+
const note = notes[noteNumber % 12]
|
|
112
|
+
|
|
113
|
+
return `${note}${octave}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convert frequency (Hz) to nearest MIDI note number
|
|
118
|
+
* @param {number} frequency - Frequency in Hz
|
|
119
|
+
* @returns {number} MIDI note number
|
|
120
|
+
*/
|
|
121
|
+
export function frequencyToNote(frequency) {
|
|
122
|
+
const noteNumber = 69 + 12 * Math.log2(frequency / 440)
|
|
123
|
+
return clamp(Math.round(noteNumber), 0, 127)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert MIDI note number to frequency (Hz)
|
|
128
|
+
* @param {number} noteNumber - MIDI note number
|
|
129
|
+
* @returns {number} Frequency in Hz
|
|
130
|
+
*/
|
|
131
|
+
export function noteToFrequency(noteNumber) {
|
|
132
|
+
return 440 * 2 ** ((noteNumber - 69) / 12)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get CC name for common controller numbers
|
|
137
|
+
* @param {number} cc - CC number
|
|
138
|
+
* @returns {string} CC name or "CC {number}"
|
|
139
|
+
*/
|
|
140
|
+
export function getCCName(cc) {
|
|
141
|
+
const names = {
|
|
142
|
+
0: "Bank Select",
|
|
143
|
+
1: "Modulation",
|
|
144
|
+
2: "Breath Controller",
|
|
145
|
+
4: "Foot Controller",
|
|
146
|
+
5: "Portamento Time",
|
|
147
|
+
7: "Volume",
|
|
148
|
+
8: "Balance",
|
|
149
|
+
10: "Pan",
|
|
150
|
+
11: "Expression",
|
|
151
|
+
64: "Sustain Pedal",
|
|
152
|
+
65: "Portamento",
|
|
153
|
+
66: "Sostenuto",
|
|
154
|
+
67: "Soft Pedal",
|
|
155
|
+
68: "Legato",
|
|
156
|
+
71: "Resonance",
|
|
157
|
+
72: "Release Time",
|
|
158
|
+
73: "Attack Time",
|
|
159
|
+
74: "Cutoff",
|
|
160
|
+
75: "Decay Time",
|
|
161
|
+
76: "Vibrato Rate",
|
|
162
|
+
77: "Vibrato Depth",
|
|
163
|
+
78: "Vibrato Delay",
|
|
164
|
+
84: "Portamento Control",
|
|
165
|
+
91: "Reverb",
|
|
166
|
+
92: "Tremolo",
|
|
167
|
+
93: "Chorus",
|
|
168
|
+
94: "Detune",
|
|
169
|
+
95: "Phaser",
|
|
170
|
+
120: "All Sound Off",
|
|
171
|
+
121: "Reset All Controllers",
|
|
172
|
+
123: "All Notes Off",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return names[cc] || `CC ${cc}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Encode a 14-bit value into MSB and LSB (7-bit each)
|
|
180
|
+
* @param {number} value - 14-bit value (0-16383)
|
|
181
|
+
* @returns {{msb: number, lsb: number}} MSB and LSB values
|
|
182
|
+
*/
|
|
183
|
+
export function encode14BitValue(value) {
|
|
184
|
+
const clamped = clamp(Math.round(value), 0, 16383)
|
|
185
|
+
return {
|
|
186
|
+
msb: (clamped >> 7) & 0x7f,
|
|
187
|
+
lsb: clamped & 0x7f,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Decode MSB and LSB (7-bit each) into a 14-bit value
|
|
193
|
+
* @param {number} msb - Most significant byte (0-127)
|
|
194
|
+
* @param {number} lsb - Least significant byte (0-127)
|
|
195
|
+
* @returns {number} 14-bit value (0-16383)
|
|
196
|
+
*/
|
|
197
|
+
export function decode14BitValue(msb, lsb) {
|
|
198
|
+
return (clamp(msb, 0, 127) << 7) | clamp(lsb, 0, 127)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Normalize a 14-bit value from input range
|
|
203
|
+
* @param {number} value - Input value
|
|
204
|
+
* @param {number} inputMin - Input minimum
|
|
205
|
+
* @param {number} inputMax - Input maximum
|
|
206
|
+
* @param {boolean} [invert=false] - Invert the output
|
|
207
|
+
* @returns {{msb: number, lsb: number}} MSB and LSB values
|
|
208
|
+
*/
|
|
209
|
+
export function normalize14BitValue(value, inputMin, inputMax, invert = false) {
|
|
210
|
+
// Normalize to 0-1
|
|
211
|
+
const normalized = (value - inputMin) / (inputMax - inputMin)
|
|
212
|
+
|
|
213
|
+
// Invert if requested
|
|
214
|
+
const final = invert ? 1 - normalized : normalized
|
|
215
|
+
|
|
216
|
+
// Scale to 14-bit range (0-16383)
|
|
217
|
+
const value14Bit = final * 16383
|
|
218
|
+
|
|
219
|
+
return encode14BitValue(value14Bit)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Denormalize a 14-bit value to a custom range
|
|
224
|
+
* @param {number} msb - Most significant byte (0-127)
|
|
225
|
+
* @param {number} lsb - Least significant byte (0-127)
|
|
226
|
+
* @param {number} outputMin - Output minimum
|
|
227
|
+
* @param {number} outputMax - Output maximum
|
|
228
|
+
* @param {boolean} [invert=false] - Invert the input
|
|
229
|
+
* @returns {number} Denormalized value
|
|
230
|
+
*/
|
|
231
|
+
export function denormalize14BitValue(msb, lsb, outputMin, outputMax, invert = false) {
|
|
232
|
+
const value14Bit = decode14BitValue(msb, lsb)
|
|
233
|
+
|
|
234
|
+
// Normalize to 0-1
|
|
235
|
+
let normalized = value14Bit / 16383
|
|
236
|
+
|
|
237
|
+
// Invert if requested
|
|
238
|
+
if (invert) {
|
|
239
|
+
normalized = 1 - normalized
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Scale to output range
|
|
243
|
+
return outputMin + normalized * (outputMax - outputMin)
|
|
244
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
clamp,
|
|
4
|
+
decode14BitValue,
|
|
5
|
+
denormalize14BitValue,
|
|
6
|
+
denormalizeValue,
|
|
7
|
+
encode14BitValue,
|
|
8
|
+
frequencyToNote,
|
|
9
|
+
getCCName,
|
|
10
|
+
normalize14BitValue,
|
|
11
|
+
normalizeValue,
|
|
12
|
+
noteNameToNumber,
|
|
13
|
+
noteNumberToName,
|
|
14
|
+
noteToFrequency,
|
|
15
|
+
} from "./midi.js"
|
|
16
|
+
|
|
17
|
+
describe("MIDI Utils - Additional Tests", () => {
|
|
18
|
+
describe("denormalizeValue", () => {
|
|
19
|
+
it("should denormalize MIDI values to custom range", () => {
|
|
20
|
+
expect(denormalizeValue(64, 0, 100, false)).toBeCloseTo(50, 0) // 50% of 100
|
|
21
|
+
expect(denormalizeValue(0, 0, 100, false)).toBe(0)
|
|
22
|
+
expect(denormalizeValue(127, 0, 100, false)).toBe(100)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("should invert values when requested", () => {
|
|
26
|
+
expect(denormalizeValue(0, 0, 100, true)).toBe(100)
|
|
27
|
+
expect(denormalizeValue(127, 0, 100, true)).toBe(0)
|
|
28
|
+
expect(denormalizeValue(64, 0, 100, true)).toBeCloseTo(50, 0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should handle negative ranges", () => {
|
|
32
|
+
expect(denormalizeValue(0, -50, 50, false)).toBe(-50)
|
|
33
|
+
expect(denormalizeValue(127, -50, 50, false)).toBe(50)
|
|
34
|
+
expect(denormalizeValue(64, -50, 50, false)).toBeCloseTo(0, 0)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("frequencyToNote", () => {
|
|
39
|
+
it("should convert frequencies to MIDI notes", () => {
|
|
40
|
+
expect(frequencyToNote(440)).toBe(69) // A4
|
|
41
|
+
expect(frequencyToNote(261.63)).toBe(60) // C4
|
|
42
|
+
expect(frequencyToNote(220)).toBe(57) // A3
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should clamp to valid MIDI range", () => {
|
|
46
|
+
expect(frequencyToNote(20000)).toBe(127) // Very high frequency
|
|
47
|
+
expect(frequencyToNote(1)).toBe(0) // Very low frequency
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("should round to nearest note", () => {
|
|
51
|
+
expect(frequencyToNote(441)).toBe(69)
|
|
52
|
+
expect(frequencyToNote(439)).toBe(69)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("noteToFrequency", () => {
|
|
57
|
+
it("should convert MIDI notes to frequencies", () => {
|
|
58
|
+
expect(noteToFrequency(69)).toBeCloseTo(440, 2) // A4
|
|
59
|
+
expect(noteToFrequency(60)).toBeCloseTo(261.63, 2) // C4
|
|
60
|
+
expect(noteToFrequency(57)).toBeCloseTo(220, 2) // A3
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("should handle all valid notes", () => {
|
|
64
|
+
expect(noteToFrequency(0)).toBeCloseTo(8.18, 2)
|
|
65
|
+
expect(noteToFrequency(127)).toBeCloseTo(12543.85, 2)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("clamp - additional edge cases", () => {
|
|
70
|
+
it("should handle equal min and max", () => {
|
|
71
|
+
expect(clamp(5, 10, 10)).toBe(10)
|
|
72
|
+
expect(clamp(15, 10, 10)).toBe(10)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("should handle NaN values", () => {
|
|
76
|
+
expect(clamp(NaN, 0, 100)).toBeNaN()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should handle infinity", () => {
|
|
80
|
+
expect(clamp(Infinity, 0, 100)).toBe(100)
|
|
81
|
+
expect(clamp(-Infinity, 0, 100)).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("normalizeValue - additional edge cases", () => {
|
|
86
|
+
it("should handle equal input range", () => {
|
|
87
|
+
// Division by zero case - should handle gracefully
|
|
88
|
+
const result = normalizeValue(50, 50, 50, false)
|
|
89
|
+
expect(result).toBeNaN()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("should handle out of range values", () => {
|
|
93
|
+
expect(normalizeValue(200, 0, 100, false)).toBe(127)
|
|
94
|
+
expect(normalizeValue(-50, 0, 100, false)).toBe(0)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("noteNameToNumber - additional tests", () => {
|
|
99
|
+
it("should handle all valid note names", () => {
|
|
100
|
+
expect(noteNameToNumber("C0")).toBe(12)
|
|
101
|
+
expect(noteNameToNumber("C#0")).toBe(13)
|
|
102
|
+
expect(noteNameToNumber("Db0")).toBe(13)
|
|
103
|
+
expect(noteNameToNumber("G9")).toBe(127)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("should handle case variations", () => {
|
|
107
|
+
expect(noteNameToNumber("c4")).toBe(60)
|
|
108
|
+
expect(noteNameToNumber("A#4")).toBe(70)
|
|
109
|
+
expect(noteNameToNumber("bb4")).toBe(70)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should throw on invalid note names", () => {
|
|
113
|
+
expect(() => noteNameToNumber("C#")).toThrow("Invalid note name")
|
|
114
|
+
expect(() => noteNameToNumber("H4")).toThrow("Invalid note")
|
|
115
|
+
// C10 is actually valid (12 * 11 = 132, which gets clamped to 127)
|
|
116
|
+
expect(() => noteNameToNumber("C10")).not.toThrow()
|
|
117
|
+
expect(noteNameToNumber("C10")).toBe(127) // Clamped to max
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe("noteNumberToName - additional tests", () => {
|
|
122
|
+
it("should handle all octaves", () => {
|
|
123
|
+
expect(noteNumberToName(0)).toBe("C-1")
|
|
124
|
+
expect(noteNumberToName(127)).toBe("G9")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("should use flats when requested", () => {
|
|
128
|
+
expect(noteNumberToName(70, true)).toBe("Bb4")
|
|
129
|
+
expect(noteNumberToName(72, true)).toBe("C5") // No flat for C
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("getCCName - additional tests", () => {
|
|
134
|
+
it("should return all known CC names", () => {
|
|
135
|
+
expect(getCCName(0)).toBe("Bank Select")
|
|
136
|
+
expect(getCCName(1)).toBe("Modulation")
|
|
137
|
+
expect(getCCName(7)).toBe("Volume")
|
|
138
|
+
expect(getCCName(64)).toBe("Sustain Pedal")
|
|
139
|
+
expect(getCCName(120)).toBe("All Sound Off")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("should return generic format for unknown CCs", () => {
|
|
143
|
+
expect(getCCName(200)).toBe("CC 200")
|
|
144
|
+
expect(getCCName(-1)).toBe("CC -1")
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe("14-bit MIDI", () => {
|
|
149
|
+
describe("encode14BitValue", () => {
|
|
150
|
+
it("should encode 0 to MSB=0, LSB=0", () => {
|
|
151
|
+
expect(encode14BitValue(0)).toEqual({ msb: 0, lsb: 0 })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should encode 16383 to MSB=127, LSB=127", () => {
|
|
155
|
+
expect(encode14BitValue(16383)).toEqual({ msb: 127, lsb: 127 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should encode middle value correctly", () => {
|
|
159
|
+
const result = encode14BitValue(8192)
|
|
160
|
+
expect(result.msb).toBe(64)
|
|
161
|
+
expect(result.lsb).toBe(0)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it("should clamp values below 0", () => {
|
|
165
|
+
expect(encode14BitValue(-100)).toEqual({ msb: 0, lsb: 0 })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("should clamp values above 16383", () => {
|
|
169
|
+
expect(encode14BitValue(20000)).toEqual({ msb: 127, lsb: 127 })
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe("decode14BitValue", () => {
|
|
174
|
+
it("should decode MSB=0, LSB=0 to 0", () => {
|
|
175
|
+
expect(decode14BitValue(0, 0)).toBe(0)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("should decode MSB=127, LSB=127 to 16383", () => {
|
|
179
|
+
expect(decode14BitValue(127, 127)).toBe(16383)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("should decode middle values correctly", () => {
|
|
183
|
+
expect(decode14BitValue(64, 0)).toBe(8192)
|
|
184
|
+
expect(decode14BitValue(32, 0)).toBe(4096)
|
|
185
|
+
expect(decode14BitValue(96, 0)).toBe(12288)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe("encode14BitValue / decode14BitValue round-trip", () => {
|
|
190
|
+
it("should round-trip all edge values", () => {
|
|
191
|
+
const testValues = [0, 1, 127, 128, 16383]
|
|
192
|
+
testValues.forEach((value) => {
|
|
193
|
+
const encoded = encode14BitValue(value)
|
|
194
|
+
const decoded = decode14BitValue(encoded.msb, encoded.lsb)
|
|
195
|
+
expect(decoded).toBe(value)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("should round-trip random values", () => {
|
|
200
|
+
for (let i = 0; i < 100; i++) {
|
|
201
|
+
const value = Math.floor(Math.random() * 16384)
|
|
202
|
+
const encoded = encode14BitValue(value)
|
|
203
|
+
const decoded = decode14BitValue(encoded.msb, encoded.lsb)
|
|
204
|
+
expect(decoded).toBe(value)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe("normalize14BitValue", () => {
|
|
210
|
+
it("should normalize input range to 14-bit MSB/LSB", () => {
|
|
211
|
+
const result = normalize14BitValue(50, 0, 100, false)
|
|
212
|
+
expect(result).toHaveProperty("msb")
|
|
213
|
+
expect(result).toHaveProperty("lsb")
|
|
214
|
+
expect(result.msb).toBeGreaterThanOrEqual(0)
|
|
215
|
+
expect(result.msb).toBeLessThanOrEqual(127)
|
|
216
|
+
expect(result.lsb).toBeGreaterThanOrEqual(0)
|
|
217
|
+
expect(result.lsb).toBeLessThanOrEqual(127)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("should handle min=0, max=127", () => {
|
|
221
|
+
const result = normalize14BitValue(64, 0, 127, false)
|
|
222
|
+
const decoded = decode14BitValue(result.msb, result.lsb)
|
|
223
|
+
// 64/127 = 0.5039, so decoded should be ~8256 (0.5039 * 16383)
|
|
224
|
+
expect(decoded).toBeGreaterThan(8200)
|
|
225
|
+
expect(decoded).toBeLessThan(8300)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("should invert values when requested", () => {
|
|
229
|
+
const result = normalize14BitValue(0, 0, 100, true)
|
|
230
|
+
expect(result.msb).toBe(127)
|
|
231
|
+
expect(result.lsb).toBe(127)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("should handle negative ranges", () => {
|
|
235
|
+
const result = normalize14BitValue(0, -50, 50, false)
|
|
236
|
+
expect(result.msb).toBe(64)
|
|
237
|
+
expect(result.lsb).toBe(0)
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe("denormalize14BitValue", () => {
|
|
242
|
+
it("should denormalize 14-bit values to custom range", () => {
|
|
243
|
+
const msb = 64
|
|
244
|
+
const lsb = 0
|
|
245
|
+
const result = denormalize14BitValue(msb, lsb, 0, 100, false)
|
|
246
|
+
expect(result).toBeCloseTo(50, 1)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it("should handle min=0, max=16383", () => {
|
|
250
|
+
expect(denormalize14BitValue(0, 0, 0, 16383, false)).toBe(0)
|
|
251
|
+
expect(denormalize14BitValue(127, 127, 0, 16383, false)).toBe(16383)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it("should invert values when requested", () => {
|
|
255
|
+
const result = denormalize14BitValue(0, 0, 0, 100, true)
|
|
256
|
+
expect(result).toBeCloseTo(100, 0)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a SysEx message
|
|
3
|
+
* @param {Array<number>} data - Raw MIDI data
|
|
4
|
+
* @returns {Object|null} Parsed SysEx data or null if not SysEx
|
|
5
|
+
*/
|
|
6
|
+
export function parseSysEx(data) {
|
|
7
|
+
if (data[0] !== 0xf0 || data[data.length - 1] !== 0xf7) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
manufacturerId: data[1],
|
|
13
|
+
payload: data.slice(2, -1),
|
|
14
|
+
raw: data,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a SysEx message
|
|
20
|
+
* @param {number} manufacturerId - Manufacturer ID
|
|
21
|
+
* @param {Array<number>} payload - SysEx payload data
|
|
22
|
+
* @returns {Array<number>} Complete SysEx message with F0/F7
|
|
23
|
+
*/
|
|
24
|
+
export function createSysEx(manufacturerId, payload) {
|
|
25
|
+
return [0xf0, manufacturerId, ...payload, 0xf7]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if data is a SysEx message
|
|
30
|
+
* @param {Array<number>} data - MIDI data
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isSysEx(data) {
|
|
34
|
+
return data.length >= 2 && data[0] === 0xf0 && data[data.length - 1] === 0xf7
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Encode 8-bit data to 7-bit MIDI format
|
|
39
|
+
* @param {Array<number>} data - 8-bit data array
|
|
40
|
+
* @returns {Array<number>} 7-bit encoded data
|
|
41
|
+
*/
|
|
42
|
+
export function encode7Bit(data) {
|
|
43
|
+
const result = []
|
|
44
|
+
|
|
45
|
+
// Process data in groups of 7 bytes
|
|
46
|
+
for (let i = 0; i < data.length; i += 7) {
|
|
47
|
+
const group = data.slice(i, i + 7)
|
|
48
|
+
let headerByte = 0
|
|
49
|
+
const dataBytes = []
|
|
50
|
+
|
|
51
|
+
// Process each byte in the group
|
|
52
|
+
for (let j = 0; j < group.length; j++) {
|
|
53
|
+
const byte = group[j]
|
|
54
|
+
// Check if MSB (bit 7) is set
|
|
55
|
+
if (byte & 0x80) {
|
|
56
|
+
headerByte |= 1 << j
|
|
57
|
+
}
|
|
58
|
+
// Add the lower 7 bits as a data byte
|
|
59
|
+
dataBytes.push(byte & 0x7f)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add header byte followed by the data bytes
|
|
63
|
+
// Note: result length may not be a multiple of 8
|
|
64
|
+
result.push(headerByte, ...dataBytes)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decode 7-bit MIDI format to 8-bit data
|
|
72
|
+
* @param {Array<number>} data - 7-bit encoded data
|
|
73
|
+
* @returns {Array<number>} 8-bit decoded data
|
|
74
|
+
*/
|
|
75
|
+
export function decode7Bit(data) {
|
|
76
|
+
const result = []
|
|
77
|
+
|
|
78
|
+
// Process data in groups of 8 bytes (1 header + 7 data bytes)
|
|
79
|
+
for (let i = 0; i < data.length; i += 8) {
|
|
80
|
+
const headerByte = data[i]
|
|
81
|
+
// Calculate how many data bytes are in this group (1-7)
|
|
82
|
+
const groupSize = Math.min(7, data.length - i - 1)
|
|
83
|
+
|
|
84
|
+
// Process the data bytes in this group
|
|
85
|
+
for (let j = 0; j < groupSize; j++) {
|
|
86
|
+
let byte = data[i + 1 + j]
|
|
87
|
+
|
|
88
|
+
// Reconstruct the 8-bit value using header bit
|
|
89
|
+
if (headerByte & (1 << j)) {
|
|
90
|
+
byte |= 0x80
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
result.push(byte)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
}
|