Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions js/__tests__/SaveInterface.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
const { Midi } = require('@tonejs/midi');
// Mocking the @tonejs/midi library
jest.mock('@tonejs/midi', () => {
return {
Midi: jest.fn().mockImplementation(() => ({
header: { ticksPerBeat: 480 },
addTrack: jest.fn(() => ({
addNote: jest.fn(),
name: '',
instrument: { number: 0 },
channel: 0
})),
toArray: jest.fn(() => new Uint8Array([1, 2, 3]))
}))
};
})
global.Midi = Midi;
global.Midi = jest.fn().mockImplementation(() => ({
header: { ticksPerBeat: 480 },
addTrack: jest.fn(() => ({
addNote: jest.fn(),
name: '',
instrument: { number: 0 },
channel: 0
})),
toArray: jest.fn(() => new Uint8Array([1, 2, 3]))
}));

global.jQuery = jest.fn(() => ({
on: jest.fn(),
trigger: jest.fn(),
Expand Down
181 changes: 181 additions & 0 deletions js/__tests__/midi.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const { getClosestStandardNoteValue, transcribeMidi } = require('../midi');

const mockMidi = {
header: {
ppq: 480,
tempos: [{ bpm: 120 }],
timeSignatures: [{ timeSignature: [4, 4], ticks: 0 }]
},
tracks: [
{
instrument: { name: 'acoustic grand piano', family: 'piano', number: 0, percussion: false },
channel: 1,
notes: [
{ name: 'C4', midi: 60, time: 0, duration: 0.5, velocity: 0.8 },
{ name: 'E4', midi: 64, time: 0.5, duration: 0.75, velocity: 0.9 },
{ name: 'G4', midi: 67, time: 1.25, duration: 0.5, velocity: 0.85 }
]
},
{
instrument: { name: 'acoustic guitar (nylon)', family: 'guitar', number: 24, percussion: false },
channel: 2,
notes: [
{ name: 'G3', midi: 55, time: 0, duration: 0.6, velocity: 0.7 },
{ name: 'C4', midi: 60, time: 0.6, duration: 0.8, velocity: 0.75 }
]
},
{
instrument: { name: 'drums', family: 'percussion', number: 128, percussion: true },
channel: 9,
notes: [
{ name: 'Snare Drum', midi: 38, time: 0, duration: 0.3, velocity: 0.9 },
{ name: 'Kick Drum', midi: 36, time: 0.5, duration: 0.3, velocity: 0.8 }
]
}
]
};

describe('getClosestStandardNoteValue', () => {
it('should return the closest standard note duration for a given input', () => {
expect(getClosestStandardNoteValue(1)).toEqual([1, 1]);
expect(getClosestStandardNoteValue(0.0078125)).toEqual([1, 128]);
});
});

describe('transcribeMidi', () => {
let loadNewBlocksSpy;

beforeEach(() => {
// Mock dependencies
global.getReverseDrumMidi = jest.fn(() => ({
38: ["snare drum"],
36: ["kick drum"],
41: ["tom tom"]
}));

global.VOICENAMES = [
["piano", "acoustic grand piano"],
["guitar", "acoustic guitar (nylon)"],
];

global.activity = {
textMsg: jest.fn(),
blocks: {
loadNewBlocks: jest.fn(),
palettes: { _hideMenus: jest.fn() },
trashStacks: []
}
};

// Spy on loadNewBlocks
loadNewBlocksSpy = jest.spyOn(activity.blocks, 'loadNewBlocks');
});

afterEach(() => {
jest.clearAllMocks();
});

it('should process all tracks and generate blocks', async () => {
await transcribeMidi(mockMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
expect(Array.isArray(loadedBlocks)).toBe(true);
expect(loadedBlocks.length).toBeGreaterThan(0);
});

it('should handle default tempo correctly', async () => {
const midiWithoutTempo = {
...mockMidi,
header: {
...mockMidi.header,
tempos: []
}
};

await transcribeMidi(midiWithoutTempo);
expect(loadNewBlocksSpy).toHaveBeenCalled();
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const bpmBlock = loadedBlocks.find(block =>
Array.isArray(block[1]) && block[1][0] === 'setbpm3'
);
expect(bpmBlock).toBeDefined();
const tempoValueBlock = loadedBlocks.find(block =>
block[0] === bpmBlock[4][1]
);
expect(tempoValueBlock).toBeDefined();
expect(tempoValueBlock[1][1].value).toBe(90);
});

it('should skip tracks with no notes', async () => {
const emptyTrackMidi = {
...mockMidi,
tracks: [{ ...mockMidi.tracks[0], notes: [] }]
};

await transcribeMidi(emptyTrackMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const trackBlocks = loadedBlocks.filter(block =>
Array.isArray(block[1]) && block[1][0] === 'setturtlename2'
);
expect(trackBlocks.length).toBe(0);
});

it('should handle percussion instruments correctly', async () => {
await transcribeMidi(mockMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const drumBlocks = loadedBlocks.filter(block =>
block[1] === 'playdrum'
);
expect(drumBlocks.length).toBeGreaterThan(0);
});

it('should assign correct instruments to tracks', async () => {
await transcribeMidi(mockMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();

const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const instrumentBlocks = loadedBlocks.filter(block =>
Array.isArray(block[1]) && block[1][0] === 'settimbre'
);
const nonPercussionTracks = mockMidi.tracks.filter(track => !track.instrument.percussion);
instrumentBlocks.forEach((block, index) => {
const instrumentName = nonPercussionTracks[index].instrument.name;
expect(block[1][1].value).toBe(instrumentName);
});
});

it('should generate correct note durations', async () => {
await transcribeMidi(mockMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();

const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const noteBlocks = loadedBlocks.filter(block =>
Array.isArray(block[1]) && block[1][0] === 'newnote'
);

noteBlocks.forEach(block => {
const divideBlock = loadedBlocks.find(b => b[0] === block[4][1]);
expect(divideBlock).toBeDefined();

const numeratorBlock = loadedBlocks.find(b => b[0] === divideBlock[4][1]);
const denominatorBlock = loadedBlocks.find(b => b[0] === divideBlock[4][2]);

expect(numeratorBlock).toBeDefined();
expect(denominatorBlock).toBeDefined();
expect(numeratorBlock[1][1].value).toBeGreaterThan(0);
expect(denominatorBlock[1][1].value).toBeGreaterThan(0);
});
});

it('should generate rest notes for gaps between notes', async () => {
await transcribeMidi(mockMidi);
expect(loadNewBlocksSpy).toHaveBeenCalled();
const loadedBlocks = loadNewBlocksSpy.mock.calls[0][0];
const restBlocks = loadedBlocks.filter(block =>
block[1] === 'rest2'
);
expect(restBlocks.length).toBeGreaterThan(0);
});
});
27 changes: 0 additions & 27 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -4217,33 +4217,6 @@ class Activity {
}, 5000);
};


const standardDurations = [
{ value: "1/1", duration: 1 },
{ value: "1/2", duration: 0.5 },
{ value: "1/4", duration: 0.25 },
{ value: "1/8", duration: 0.125 },
{ value: "1/16", duration: 0.0625 },
{ value: "1/32", duration: 0.03125 },
{ value: "1/64", duration: 0.015625 },
{ value: "1/128", duration: 0.0078125 }
];

this.getClosestStandardNoteValue = function(duration) {
let closest = standardDurations[0];
let minDiff = Math.abs(duration - closest.duration);

for (let i = 1; i < standardDurations.length; i++) {
let diff = Math.abs(duration - standardDurations[i].duration);
if (diff < minDiff) {
closest = standardDurations[i];
minDiff = diff;
}
}

return closest.value.split('/').map(Number);
}

/**
* Loads MB project from Planet.
* @param projectID {Planet project ID}
Expand Down
40 changes: 35 additions & 5 deletions js/midi.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,35 @@
/* exported transcribeMidi*/


const MAX_NOTEBLOCKS = 200;
const MAX_NOTEBLOCKS = 100;
const defaultTempo = 90;

const standardDurations = [
{ value: "1/1", duration: 1 },
{ value: "1/2", duration: 0.5 },
{ value: "1/4", duration: 0.25 },
{ value: "1/8", duration: 0.125 },
{ value: "1/16", duration: 0.0625 },
{ value: "1/32", duration: 0.03125 },
{ value: "1/64", duration: 0.015625 },
{ value: "1/128", duration: 0.0078125 }
];

const getClosestStandardNoteValue = (duration) => {
let closest = standardDurations[0];
let minDiff = Math.abs(duration - closest.duration);

for (let i = 1; i < standardDurations.length; i++) {
let diff = Math.abs(duration - standardDurations[i].duration);
if (diff < minDiff) {
closest = standardDurations[i];
minDiff = diff;
}
}

return closest.value.split('/').map(Number);
}

const transcribeMidi = async (midi) => {
const currentMidi = midi;
const drumMidi = getReverseDrumMidi();
Expand Down Expand Up @@ -155,7 +181,7 @@ const transcribeMidi = async (midi) => {
//Using for loop for finding the shortest note value
for (let j in sched) {
let dur = sched[j].end - sched[j].start;;
let temp = activity.getClosestStandardNoteValue(dur * 3 / 8);
let temp = getClosestStandardNoteValue(dur * 3 / 8);
shortestNoteDenominator = Math.max(shortestNoteDenominator, temp[1]);
}

Expand Down Expand Up @@ -202,7 +228,7 @@ const transcribeMidi = async (midi) => {
}
return ar;
};
let obj = activity.getClosestStandardNoteValue(duration * 3 / 8);
let obj = getClosestStandardNoteValue(duration * 3 / 8);
// let scalingFactor=1;
// if(shortestNoteDenominator>32)
// scalingFactor=shortestNoteDenominator/32;
Expand All @@ -213,7 +239,7 @@ const transcribeMidi = async (midi) => {
// obj[0]=obj[0]*scalingFactor;

// To get the reduced fraction for 4/2 to 2/1
obj = activity.getClosestStandardNoteValue(obj[0] / obj[1]);
obj = getClosestStandardNoteValue(obj[0] / obj[1]);

// Since we are going to add action block in the front later
if (k != 0) val = val + 2;
Expand Down Expand Up @@ -325,4 +351,8 @@ const transcribeMidi = async (midi) => {
activity.blocks.loadNewBlocks(jsONON);
// 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");
return null;
};
};

if (typeof module !== 'undefined' && module.exports) {
module.exports = { getClosestStandardNoteValue, transcribeMidi };
}
Loading