diff --git a/test/util.test.mjs b/test/util.test.mjs new file mode 100644 index 00000000..679c922b --- /dev/null +++ b/test/util.test.mjs @@ -0,0 +1,60 @@ +import { strict as assert } from 'assert'; +import { isNote, tokenizeNote, toMidi } from '../util.mjs'; + +describe('isNote', () => { + it('should recognize notes without accidentals', function () { + 'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => { + assert.equal(isNote(note), true); + }); + }); + it('should recognize notes with accidentals', function () { + 'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => { + assert.equal(isNote(note), true); + }); + }); + it('should not recognize invalid notes', function () { + assert.equal(isNote('H5'), false); + assert.equal(isNote('C'), false); + assert.equal(isNote('X'), false); + assert.equal(isNote(1), false); + }); +}); + +describe('isNote', () => { + it('should tokenize notes without accidentals', function () { + assert.deepStrictEqual(tokenizeNote('C3'), ['C', '', '3']); + assert.deepStrictEqual(tokenizeNote('D3'), ['D', '', '3']); + assert.deepStrictEqual(tokenizeNote('E3'), ['E', '', '3']); + assert.deepStrictEqual(tokenizeNote('F3'), ['F', '', '3']); + assert.deepStrictEqual(tokenizeNote('G3'), ['G', '', '3']); + assert.deepStrictEqual(tokenizeNote('A3'), ['A', '', '3']); + assert.deepStrictEqual(tokenizeNote('B3'), ['B', '', '3']); + assert.deepStrictEqual(tokenizeNote('C4'), ['C', '', '4']); + assert.deepStrictEqual(tokenizeNote('D5'), ['D', '', '5']); + }); + it('should tokenize notes with accidentals', function () { + assert.deepStrictEqual(tokenizeNote('C#3'), ['C', '#', '3']); + assert.deepStrictEqual(tokenizeNote('D##3'), ['D', '##', '3']); + assert.deepStrictEqual(tokenizeNote('Eb3'), ['E', 'b', '3']); + assert.deepStrictEqual(tokenizeNote('Fbb3'), ['F', 'bb', '3']); + assert.deepStrictEqual(tokenizeNote('Bb5'), ['B', 'b', '5']); + }); + it('should note tokenize invalid notes', function () { + assert.deepStrictEqual(tokenizeNote('X'), []); + assert.deepStrictEqual(tokenizeNote('asfasf'), []); + assert.deepStrictEqual(tokenizeNote(123), []); + }); +}); +describe('toMidi', () => { + it('should turn notes into midi', function () { + assert.equal(toMidi('A4'), 69); + assert.equal(toMidi('C4'), 60); + assert.equal(toMidi('Db4'), 61); + assert.equal(toMidi('C3'), 48); + assert.equal(toMidi('Cb3'), 47); + assert.equal(toMidi('Cbb3'), 46); + assert.equal(toMidi('C#3'), 49); + assert.equal(toMidi('C#3'), 49); + assert.equal(toMidi('C##3'), 50); + }); +}); diff --git a/util.mjs b/util.mjs index 0702c23b..0f501a91 100644 --- a/util.mjs +++ b/util.mjs @@ -1,7 +1,16 @@ -export const isNote = (name) => /^[a-gA-G][#b]?[0-9]$/.test(name); -export const tokenizeNote = (note) => note.match(/^([a-gA-G])([#b])?([0-9])?$/).slice(1); +export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name); +export const tokenizeNote = (note) => + typeof note === 'string' + ? note + .match(/^([a-gA-G])([#b]*)([0-9])?$/) + ?.slice(1) + ?.map((x) => (x === undefined ? '' : x)) || [] + : []; export const toMidi = (note) => { const [pc, acc, oct] = tokenizeNote(note); + if (!pc) { + throw new Error('not a note: "' + note + '"'); + } const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()]; const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1 }[char], 0) || 0; return (Number(oct) + 1) * 12 + chroma + offset;