Skip to content

Commit d182b42

Browse files
committed
test suite for midi
1 parent 22a3379 commit d182b42

File tree

3 files changed

+215
-31
lines changed

3 files changed

+215
-31
lines changed

js/__tests__/midi.test.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const { getClosestStandardNoteValue, transcribeMidi } = require('../midi');
2+
3+
const mockMidi = {
4+
header: {
5+
ppq: 480,
6+
tempos: [{ bpm: 120 }],
7+
timeSignatures: [{ timeSignature: [4, 4], ticks: 0 }]
8+
},
9+
tracks: [
10+
{
11+
instrument: { name: 'acoustic grand piano', family: 'piano', number: 0, percussion: false },
12+
channel: 1,
13+
notes: [
14+
{ name: 'C4', midi: 60, time: 0, duration: 0.5, velocity: 0.8 },
15+
{ name: 'E4', midi: 64, time: 0.5, duration: 0.75, velocity: 0.9 },
16+
{ name: 'G4', midi: 67, time: 1.25, duration: 0.5, velocity: 0.85 }
17+
]
18+
},
19+
{
20+
instrument: { name: 'acoustic guitar (nylon)', family: 'guitar', number: 24, percussion: false },
21+
channel: 2,
22+
notes: [
23+
{ name: 'G3', midi: 55, time: 0, duration: 0.6, velocity: 0.7 },
24+
{ name: 'C4', midi: 60, time: 0.6, duration: 0.8, velocity: 0.75 }
25+
]
26+
},
27+
{
28+
instrument: { name: 'drums', family: 'percussion', number: 128, percussion: true },
29+
channel: 9,
30+
notes: [
31+
{ name: 'Snare Drum', midi: 38, time: 0, duration: 0.3, velocity: 0.9 },
32+
{ name: 'Kick Drum', midi: 36, time: 0.5, duration: 0.3, velocity: 0.8 }
33+
]
34+
}
35+
]
36+
};
37+
38+
describe('getClosestStandardNoteValue', () => {
39+
it('should return the closest standard note duration for a given input', () => {
40+
expect(getClosestStandardNoteValue(1)).toEqual([1, 1]);
41+
expect(getClosestStandardNoteValue(0.0078125)).toEqual([1, 128]);
42+
});
43+
});
44+
45+
describe('transcribeMidi', () => {
46+
let loadNewBlocksSpy;
47+
48+
beforeEach(() => {
49+
// Mock dependencies
50+
global.getReverseDrumMidi = jest.fn(() => ({
51+
38: ["snare drum"],
52+
36: ["kick drum"],
53+
41: ["tom tom"]
54+
}));
55+
56+
global.VOICENAMES = [
57+
["piano", "acoustic grand piano"],
58+
["guitar", "acoustic guitar (nylon)"],
59+
];
60+
61+
global.activity = {
62+
textMsg: jest.fn(),
63+
blocks: {
64+
loadNewBlocks: jest.fn(),
65+
palettes: { _hideMenus: jest.fn() },
66+
trashStacks: []
67+
}
68+
};
69+
70+
// Spy on loadNewBlocks
71+
loadNewBlocksSpy = jest.spyOn(activity.blocks, 'loadNewBlocks');
72+
});
73+
74+
afterEach(() => {
75+
jest.clearAllMocks();
76+
});
77+
78+
it('should process all tracks and generate blocks', async () => {
79+
await transcribeMidi(mockMidi);
80+
expect(loadNewBlocksSpy).toHaveBeenCalled();
81+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
82+
expect(Array.isArray(loadedBlocks)).toBe(true);
83+
expect(loadedBlocks.length).toBeGreaterThan(0);
84+
});
85+
86+
it('should handle default tempo correctly', async () => {
87+
const midiWithoutTempo = {
88+
...mockMidi,
89+
header: {
90+
...mockMidi.header,
91+
tempos: []
92+
}
93+
};
94+
95+
await transcribeMidi(midiWithoutTempo);
96+
expect(loadNewBlocksSpy).toHaveBeenCalled();
97+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
98+
const bpmBlock = loadedBlocks.find(block =>
99+
Array.isArray(block[1]) && block[1][0] === 'setbpm3'
100+
);
101+
expect(bpmBlock).toBeDefined();
102+
const tempoValueBlock = loadedBlocks.find(block =>
103+
block[0] === bpmBlock[4][1]
104+
);
105+
expect(tempoValueBlock).toBeDefined();
106+
expect(tempoValueBlock[1][1].value).toBe(90);
107+
});
108+
109+
it('should skip tracks with no notes', async () => {
110+
const emptyTrackMidi = {
111+
...mockMidi,
112+
tracks: [{ ...mockMidi.tracks[0], notes: [] }]
113+
};
114+
115+
await transcribeMidi(emptyTrackMidi);
116+
expect(loadNewBlocksSpy).toHaveBeenCalled();
117+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
118+
const trackBlocks = loadedBlocks.filter(block =>
119+
Array.isArray(block[1]) && block[1][0] === 'setturtlename2'
120+
);
121+
expect(trackBlocks.length).toBe(0);
122+
});
123+
124+
it('should handle percussion instruments correctly', async () => {
125+
await transcribeMidi(mockMidi);
126+
expect(loadNewBlocksSpy).toHaveBeenCalled();
127+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
128+
const drumBlocks = loadedBlocks.filter(block =>
129+
block[1] === 'playdrum'
130+
);
131+
expect(drumBlocks.length).toBeGreaterThan(0);
132+
});
133+
134+
it('should assign correct instruments to tracks', async () => {
135+
await transcribeMidi(mockMidi);
136+
expect(loadNewBlocksSpy).toHaveBeenCalled();
137+
138+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
139+
const instrumentBlocks = loadedBlocks.filter(block =>
140+
Array.isArray(block[1]) && block[1][0] === 'settimbre'
141+
);
142+
const nonPercussionTracks = mockMidi.tracks.filter(track => !track.instrument.percussion);
143+
instrumentBlocks.forEach((block, index) => {
144+
const instrumentName = nonPercussionTracks[index].instrument.name;
145+
expect(block[1][1].value).toBe(instrumentName);
146+
});
147+
});
148+
149+
it('should generate correct note durations', async () => {
150+
await transcribeMidi(mockMidi);
151+
expect(loadNewBlocksSpy).toHaveBeenCalled();
152+
153+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
154+
const noteBlocks = loadedBlocks.filter(block =>
155+
Array.isArray(block[1]) && block[1][0] === 'newnote'
156+
);
157+
158+
noteBlocks.forEach(block => {
159+
const divideBlock = loadedBlocks.find(b => b[0] === block[4][1]);
160+
expect(divideBlock).toBeDefined();
161+
162+
const numeratorBlock = loadedBlocks.find(b => b[0] === divideBlock[4][1]);
163+
const denominatorBlock = loadedBlocks.find(b => b[0] === divideBlock[4][2]);
164+
165+
expect(numeratorBlock).toBeDefined();
166+
expect(denominatorBlock).toBeDefined();
167+
expect(numeratorBlock[1][1].value).toBeGreaterThan(0);
168+
expect(denominatorBlock[1][1].value).toBeGreaterThan(0);
169+
});
170+
});
171+
172+
it('should generate rest notes for gaps between notes', async () => {
173+
await transcribeMidi(mockMidi);
174+
expect(loadNewBlocksSpy).toHaveBeenCalled();
175+
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
176+
const restBlocks = loadedBlocks.filter(block =>
177+
block[1] === 'rest2'
178+
);
179+
expect(restBlocks.length).toBeGreaterThan(0);
180+
});
181+
});

js/activity.js

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4217,33 +4217,6 @@ class Activity {
42174217
}, 5000);
42184218
};
42194219

4220-
4221-
const standardDurations = [
4222-
{ value: "1/1", duration: 1 },
4223-
{ value: "1/2", duration: 0.5 },
4224-
{ value: "1/4", duration: 0.25 },
4225-
{ value: "1/8", duration: 0.125 },
4226-
{ value: "1/16", duration: 0.0625 },
4227-
{ value: "1/32", duration: 0.03125 },
4228-
{ value: "1/64", duration: 0.015625 },
4229-
{ value: "1/128", duration: 0.0078125 }
4230-
];
4231-
4232-
this.getClosestStandardNoteValue = function(duration) {
4233-
let closest = standardDurations[0];
4234-
let minDiff = Math.abs(duration - closest.duration);
4235-
4236-
for (let i = 1; i < standardDurations.length; i++) {
4237-
let diff = Math.abs(duration - standardDurations[i].duration);
4238-
if (diff < minDiff) {
4239-
closest = standardDurations[i];
4240-
minDiff = diff;
4241-
}
4242-
}
4243-
4244-
return closest.value.split('/').map(Number);
4245-
}
4246-
42474220
/**
42484221
* Loads MB project from Planet.
42494222
* @param projectID {Planet project ID}

js/midi.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,32 @@
1717
const MAX_NOTEBLOCKS = 200;
1818
const defaultTempo = 90;
1919

20+
const standardDurations = [
21+
{ value: "1/1", duration: 1 },
22+
{ value: "1/2", duration: 0.5 },
23+
{ value: "1/4", duration: 0.25 },
24+
{ value: "1/8", duration: 0.125 },
25+
{ value: "1/16", duration: 0.0625 },
26+
{ value: "1/32", duration: 0.03125 },
27+
{ value: "1/64", duration: 0.015625 },
28+
{ value: "1/128", duration: 0.0078125 }
29+
];
30+
31+
const getClosestStandardNoteValue = (duration) => {
32+
let closest = standardDurations[0];
33+
let minDiff = Math.abs(duration - closest.duration);
34+
35+
for (let i = 1; i < standardDurations.length; i++) {
36+
let diff = Math.abs(duration - standardDurations[i].duration);
37+
if (diff < minDiff) {
38+
closest = standardDurations[i];
39+
minDiff = diff;
40+
}
41+
}
42+
43+
return closest.value.split('/').map(Number);
44+
}
45+
2046
const transcribeMidi = async (midi) => {
2147
const currentMidi = midi;
2248
const drumMidi = getReverseDrumMidi();
@@ -155,7 +181,7 @@ const transcribeMidi = async (midi) => {
155181
//Using for loop for finding the shortest note value
156182
for (let j in sched) {
157183
let dur = sched[j].end - sched[j].start;;
158-
let temp = activity.getClosestStandardNoteValue(dur * 3 / 8);
184+
let temp = getClosestStandardNoteValue(dur * 3 / 8);
159185
shortestNoteDenominator = Math.max(shortestNoteDenominator, temp[1]);
160186
}
161187

@@ -202,7 +228,7 @@ const transcribeMidi = async (midi) => {
202228
}
203229
return ar;
204230
};
205-
let obj = activity.getClosestStandardNoteValue(duration * 3 / 8);
231+
let obj = getClosestStandardNoteValue(duration * 3 / 8);
206232
// let scalingFactor=1;
207233
// if(shortestNoteDenominator>32)
208234
// scalingFactor=shortestNoteDenominator/32;
@@ -213,7 +239,7 @@ const transcribeMidi = async (midi) => {
213239
// obj[0]=obj[0]*scalingFactor;
214240

215241
// To get the reduced fraction for 4/2 to 2/1
216-
obj = activity.getClosestStandardNoteValue(obj[0] / obj[1]);
242+
obj = getClosestStandardNoteValue(obj[0] / obj[1]);
217243

218244
// Since we are going to add action block in the front later
219245
if (k != 0) val = val + 2;
@@ -325,4 +351,8 @@ const transcribeMidi = async (midi) => {
325351
activity.blocks.loadNewBlocks(jsONON);
326352
// this.textMsg("MIDI import is not currently precise. Consider changing the speed with the Beats Per Minute block or modifying note value with the Multiply Note Value block");
327353
return null;
328-
};
354+
};
355+
356+
if (typeof module !== 'undefined' && module.exports) {
357+
module.exports = { getClosestStandardNoteValue, transcribeMidi };
358+
}

0 commit comments

Comments
 (0)