186 lines
6.9 KiB
TypeScript
186 lines
6.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
beatsToBar,
|
|
pitchToNoteNameString,
|
|
pitchToNoteName,
|
|
pianoRollIndexToPitch,
|
|
noteNameToPitch
|
|
} from './midiUtil';
|
|
|
|
describe('midiUtil', () => {
|
|
describe('beatsToBar', () => {
|
|
it('should convert beats to bar position object', () => {
|
|
const result1 = beatsToBar(0, { numerator: 4, denominator: 4 });
|
|
expect(result1.bar).toBe(0);
|
|
expect(result1.beatInBar).toBe(0);
|
|
|
|
const result2 = beatsToBar(4, { numerator: 4, denominator: 4 });
|
|
expect(result2.bar).toBe(1);
|
|
expect(result2.beatInBar).toBe(0);
|
|
|
|
const result3 = beatsToBar(8, { numerator: 4, denominator: 4 });
|
|
expect(result3.bar).toBe(2);
|
|
expect(result3.beatInBar).toBe(0);
|
|
});
|
|
|
|
it('should handle different time signatures', () => {
|
|
const result1 = beatsToBar(0, { numerator: 3, denominator: 4 });
|
|
expect(result1.bar).toBe(0);
|
|
expect(result1.beatInBar).toBe(0);
|
|
|
|
const result2 = beatsToBar(3, { numerator: 3, denominator: 4 });
|
|
expect(result2.bar).toBe(1);
|
|
expect(result2.beatInBar).toBe(0);
|
|
});
|
|
|
|
it('should handle fractional beats', () => {
|
|
const result1 = beatsToBar(2.5, { numerator: 4, denominator: 4 });
|
|
expect(result1.bar).toBe(0);
|
|
expect(result1.beatInBar).toBe(2.5);
|
|
|
|
const result2 = beatsToBar(4.5, { numerator: 4, denominator: 4 });
|
|
expect(result2.bar).toBe(1);
|
|
expect(result2.beatInBar).toBe(0.5);
|
|
});
|
|
});
|
|
|
|
describe('pitchToNoteNameString', () => {
|
|
it('should convert MIDI pitch to note name with octave', () => {
|
|
expect(pitchToNoteNameString(60)).toBe('C4'); // Middle C
|
|
expect(pitchToNoteNameString(61)).toBe('C#4'); // C# above middle C
|
|
expect(pitchToNoteNameString(59)).toBe('B3'); // B below middle C
|
|
expect(pitchToNoteNameString(72)).toBe('C5'); // C one octave above middle C
|
|
expect(pitchToNoteNameString(48)).toBe('C3'); // C one octave below middle C
|
|
});
|
|
|
|
it('should handle edge cases', () => {
|
|
expect(pitchToNoteNameString(0)).toBe('C-1'); // Lowest MIDI note
|
|
expect(pitchToNoteNameString(127)).toBe('G9'); // Highest MIDI note
|
|
});
|
|
|
|
it('should handle all chromatic notes', () => {
|
|
const expectedNotes = ['C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4'];
|
|
|
|
for (let i = 0; i < 12; i++) {
|
|
expect(pitchToNoteNameString(60 + i)).toBe(expectedNotes[i]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('pitchToNoteName', () => {
|
|
it('should convert pitch to note name object', () => {
|
|
const result60 = pitchToNoteName(60); // Middle C
|
|
expect(result60.note).toBe('C');
|
|
expect(result60.octave).toBe(4);
|
|
|
|
const result61 = pitchToNoteName(61); // C#
|
|
expect(result61.note).toBe('C#');
|
|
expect(result61.octave).toBe(4);
|
|
});
|
|
|
|
it('should wrap around for different octaves', () => {
|
|
const result60 = pitchToNoteName(60);
|
|
const result72 = pitchToNoteName(72);
|
|
const result84 = pitchToNoteName(84);
|
|
|
|
expect(result60.note).toBe('C');
|
|
expect(result72.note).toBe('C');
|
|
expect(result84.note).toBe('C');
|
|
|
|
expect(result60.octave).toBe(4);
|
|
expect(result72.octave).toBe(5);
|
|
expect(result84.octave).toBe(6);
|
|
});
|
|
});
|
|
|
|
describe('pianoRollIndexToPitch', () => {
|
|
it('should convert piano roll row index to MIDI pitch', () => {
|
|
// This function likely maps visual rows to MIDI pitches
|
|
// The exact mapping depends on your implementation
|
|
const result = pianoRollIndexToPitch(10);
|
|
expect(typeof result).toBe('number');
|
|
expect(result).toBeGreaterThanOrEqual(0);
|
|
expect(result).toBeLessThanOrEqual(127);
|
|
});
|
|
|
|
it('should return different pitches for different indices', () => {
|
|
const pitch1 = pianoRollIndexToPitch(0);
|
|
const pitch2 = pianoRollIndexToPitch(1);
|
|
expect(pitch1).not.toBe(pitch2);
|
|
});
|
|
});
|
|
|
|
describe('noteNameToPitch', () => {
|
|
it('should convert note names to MIDI pitch', () => {
|
|
expect(noteNameToPitch('C4')).toBe(60); // Middle C
|
|
expect(noteNameToPitch('C#4')).toBe(61); // C# above middle C
|
|
expect(noteNameToPitch('D4')).toBe(62); // D above middle C
|
|
});
|
|
|
|
it('should handle different octaves', () => {
|
|
expect(noteNameToPitch('C3')).toBe(48); // C below middle C
|
|
expect(noteNameToPitch('C5')).toBe(72); // C above middle C
|
|
});
|
|
|
|
it('should handle sharps', () => {
|
|
expect(noteNameToPitch('C#4')).toBe(61);
|
|
expect(noteNameToPitch('F#4')).toBe(66);
|
|
expect(noteNameToPitch('G#4')).toBe(68);
|
|
});
|
|
|
|
it('should handle invalid note names', () => {
|
|
expect(() => noteNameToPitch('Db4')).toThrow('Invalid note name: Db4'); // Flats not supported
|
|
expect(() => noteNameToPitch('H4')).toThrow('Invalid note name: H4'); // Invalid note
|
|
expect(() => noteNameToPitch('C')).toThrow('Invalid note name: C'); // Missing octave
|
|
});
|
|
});
|
|
|
|
describe('edge cases and error handling', () => {
|
|
it('should handle negative values gracefully', () => {
|
|
expect(() => pitchToNoteNameString(-1)).not.toThrow();
|
|
expect(() => beatsToBar(-1, { numerator: 4, denominator: 4 })).not.toThrow();
|
|
});
|
|
|
|
it('should handle very large values', () => {
|
|
expect(() => pitchToNoteNameString(200)).not.toThrow();
|
|
expect(() => beatsToBar(1000, { numerator: 4, denominator: 4 })).not.toThrow();
|
|
});
|
|
|
|
it('should handle zero values', () => {
|
|
expect(pitchToNoteNameString(0)).toBeDefined();
|
|
expect(beatsToBar(0, { numerator: 4, denominator: 4 })).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('mathematical consistency', () => {
|
|
it('should maintain pitch relationships', () => {
|
|
// One octave = 12 semitones - note names should be the same
|
|
const baseNote = pitchToNoteName(60);
|
|
const octaveNote = pitchToNoteName(72);
|
|
expect(baseNote.note).toBe(octaveNote.note); // Both should be 'C'
|
|
expect(octaveNote.octave).toBe(baseNote.octave + 1); // Octave should be one higher
|
|
});
|
|
|
|
it('should maintain beat-to-bar relationships', () => {
|
|
const timeSignature = { numerator: 4, denominator: 4 };
|
|
|
|
// Should increment bar by 1 for each complete measure
|
|
for (let beat = 0; beat < 20; beat += 4) {
|
|
const expectedBar = Math.floor(beat / 4);
|
|
const result = beatsToBar(beat, timeSignature);
|
|
expect(result.bar).toBe(expectedBar);
|
|
expect(result.beatInBar).toBe(0); // Should be at start of bar
|
|
}
|
|
});
|
|
|
|
it('should maintain note name to pitch conversion consistency', () => {
|
|
// Converting pitch to note name and back should be consistent
|
|
const originalPitch = 60;
|
|
const noteObj = pitchToNoteName(originalPitch);
|
|
const noteName = `${noteObj.note}${noteObj.octave}`;
|
|
const convertedPitch = noteNameToPitch(noteName);
|
|
|
|
expect(convertedPitch).toBe(originalPitch);
|
|
});
|
|
});
|
|
}); |