From 4011a5cece1ff8a364c7e014b2ea64072176c060 Mon Sep 17 00:00:00 2001 From: Vincenzo Sicurella Date: Sat, 23 Jul 2022 17:19:49 -0400 Subject: [PATCH 1/5] Add MTS pitch and SysEx format conversion methods --- src/__tests__/conversion.spec.ts | 30 ++++++++++++ src/conversion.ts | 84 +++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/__tests__/conversion.spec.ts b/src/__tests__/conversion.spec.ts index e50af77..cd72e01 100644 --- a/src/__tests__/conversion.spec.ts +++ b/src/__tests__/conversion.spec.ts @@ -3,9 +3,12 @@ import { centOffsetToFrequency, centsToValue, frequencyToCentOffset, + ftomts, + frequencyToMtsBytes, ftom, mtof, valueToCents, + mtsBytesToHex } from '../conversion'; describe('Ratio to cents converter', () => { @@ -38,6 +41,33 @@ describe('MIDI to frequency converter', () => { }); }); +describe('Frequency to MTS converter', () => { + it('converts a known value', () => { + expect(ftomts(261.625565)).toBeCloseTo(60); + expect(ftomts(0, false)).toBe(0); + expect(ftomts(14080, false)).toBe(127); + expect(ftomts(14080, true)).toBeCloseTo(129); + }) +}) + +describe("Frequency to MTS sysex value", () => { + it('converts a known value', () => { + expect(frequencyToMtsBytes(261.625565)).toMatchObject(new Uint8Array([60, 0, 0])); + expect(frequencyToMtsBytes(440)).toMatchObject(new Uint8Array([69, 0, 0])); + expect(frequencyToMtsBytes(442)).toMatchObject(new Uint8Array([69, 10, 6])); + expect(frequencyToMtsBytes(0)).toMatchObject(new Uint8Array([0, 0, 0])); + }) +}) + +describe("MTS data hex string converter", () => { + it('converts a known value', () => { + expect(mtsBytesToHex(new Uint8Array([60, 0, 0]))).toEqual("3c0000"); + expect(mtsBytesToHex(new Uint8Array([69, 0, 0]))).toEqual("450000"); + expect(mtsBytesToHex(new Uint8Array([69, 10, 6]))).toEqual("450a06"); + expect(mtsBytesToHex(new Uint8Array([69, 256, 6]))).toEqual("450006"); + }) +}) + describe('Frequency to MIDI converter', () => { it('converts a known value', () => { const [index, offset] = ftom(261.625565); diff --git a/src/conversion.ts b/src/conversion.ts index fa3a78a..02416c7 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -51,13 +51,32 @@ export function centOffsetToFrequency(offset: number, baseFrequency = 440) { const MIDI_NOTE_NUMBER_OF_A4 = 69; /** * Convert MIDI note number to frequency. - * @param index MIDI note number. + * @param index MIDI note number or MTS value. * @returns Frequency in Hertz. */ export function mtof(index: number) { return 440 * Math.pow(2, (index - MIDI_NOTE_NUMBER_OF_A4) / 12); } +function mtsRound(mtsValue: number): number { + return Math.round(mtsValue * 100000) / 100000; +} +/** + * Convert frequency to MTS number (semitones with A440=69). + * @param frequency Frequency in Hertz. + * @returns MTS value + */ +export function ftomts(frequency: number, ignoreLimit = false): number { + const mts = mtsRound( + MIDI_NOTE_NUMBER_OF_A4 + 12 * Math.log2(frequency / 440) + ); + if (mts < 0 && !ignoreLimit) return 0; + if (mts > 127.9999 && !ignoreLimit) + // Last accepted value - 7F 7F 7F is reserved + return 127.9999; + return mts; +} + /** * Convert frequency to MIDI note number and pitch offset measured in cents. * @param frequency Frequency in Hertz. @@ -70,6 +89,69 @@ export function ftom(frequency: number): [number, number] { return [midiNoteNumber, centsOffset]; } +/** + * Convert MTS pitch value to 3-byte representation + * @param number MTS pitch value + * @returns Uint8Array 3-byte of 7-bit MTS data + */ +export function mtsToMtsBytes(mtsValue: number): Uint8Array { + const noteNumber = Math.trunc(mtsValue); + const fine = Math.round(0x3fff * (mtsValue - noteNumber)); + + const data = new Uint8Array(3); + data[0] = noteNumber; + data[1] = (fine >> 7) & 0x7f; + data[2] = fine & 0x7f; + return data; +} + +/** + * Convert frequency to 3-byte MTS value + * @param frequency Frequency in Hertz. + * @returns Uint8Array of length 3 + */ +export function frequencyToMtsBytes(frequency: number): Uint8Array { + const mtsValue = ftomts(frequency); + return mtsToMtsBytes(mtsValue); +} + +/** + * Convert 3-byte MTS value to frequency + * @param Uint8Array of 3-bytes of 7-bit MTS values + * @returns frequency Frequency in Hertz + */ +export function mtsBytesToMts(mtsBytes: Uint8Array): number { + const noteNumber = mtsBytes[0]; + + const high = (mtsBytes[1] & 0x7f) << 7; + const low = mtsBytes[2] & 0x7f; + const fine = (high + low) / 0x3fff; + return noteNumber + fine; +} + +/** + * Convert 3-byte MTS value to frequency + * @param Uint8Array of 3-bytes of 7-bit MTS values + * @returns frequency Frequency in Hertz + */ +export function mtsBytesToFrequency(mtsBytes: Uint8Array): number { + const mts = mtsBytesToMts(mtsBytes); + return mtof(mts); +} + +/** Convert MTS Data value into readable hex string + * @param Uint8Array of 3-bytes of 7-bit MTS values + * @returns String representation of MTS value in hexadecimal + * can be used in MIDI messages + */ +export function mtsBytesToHex(mtsBytes: Uint8Array): String { + return ( + (mtsBytes[0] & 0x7f).toString(16).padStart(2, '0') + + (mtsBytes[1] & 0x7f).toString(16).padStart(2, '0') + + (mtsBytes[2] & 0x7f).toString(16).padStart(2, '0') + ); +} + /** * Convert cents to natural units. * @param cents Musical interval in cents. From 60c14a4373f97c528b7561e3b20e0709d7a4dd84 Mon Sep 17 00:00:00 2001 From: Vincenzo Sicurella Date: Thu, 16 Nov 2023 00:10:29 -0500 Subject: [PATCH 2/5] fix small rounding errors, improve out--of-range handling --- src/conversion.ts | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/conversion.ts b/src/conversion.ts index 02416c7..bee8a67 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -58,23 +58,16 @@ export function mtof(index: number) { return 440 * Math.pow(2, (index - MIDI_NOTE_NUMBER_OF_A4) / 12); } -function mtsRound(mtsValue: number): number { - return Math.round(mtsValue * 100000) / 100000; -} /** * Convert frequency to MTS number (semitones with A440=69). * @param frequency Frequency in Hertz. * @returns MTS value */ export function ftomts(frequency: number, ignoreLimit = false): number { - const mts = mtsRound( - MIDI_NOTE_NUMBER_OF_A4 + 12 * Math.log2(frequency / 440) - ); - if (mts < 0 && !ignoreLimit) return 0; - if (mts > 127.9999 && !ignoreLimit) - // Last accepted value - 7F 7F 7F is reserved - return 127.9999; - return mts; + if (frequency <= 0) return 0; + if (frequency > 13289.656616 && !ignoreLimit) return 127.999878; // Highest possible MTS value, corresponding to 7F 7F 7E + const mts = MIDI_NOTE_NUMBER_OF_A4 + 12 * Math.log2(frequency / 440); + return Math.round(mts * 1000000) / 1000000; } /** @@ -96,7 +89,7 @@ export function ftom(frequency: number): [number, number] { */ export function mtsToMtsBytes(mtsValue: number): Uint8Array { const noteNumber = Math.trunc(mtsValue); - const fine = Math.round(0x3fff * (mtsValue - noteNumber)); + const fine = Math.round(0x4000 * (mtsValue - noteNumber)); const data = new Uint8Array(3); data[0] = noteNumber; @@ -121,11 +114,15 @@ export function frequencyToMtsBytes(frequency: number): Uint8Array { * @returns frequency Frequency in Hertz */ export function mtsBytesToMts(mtsBytes: Uint8Array): number { - const noteNumber = mtsBytes[0]; + const msb = mtsBytes[1] > 0x7f ? 0x7f : mtsBytes[1]; + let lsb = mtsBytes[2]; + + const noteNumber = mtsBytes[0] > 0x7f ? 0x7f : mtsBytes[0]; + if (noteNumber == 0x7f) { + if (lsb >= 0x7f) lsb = 0x7e; + } else if (lsb > 0x7f) lsb = 0x7f; - const high = (mtsBytes[1] & 0x7f) << 7; - const low = mtsBytes[2] & 0x7f; - const fine = (high + low) / 0x3fff; + const fine = ((msb << 7) + lsb) / 0x4000; return noteNumber + fine; } @@ -136,7 +133,8 @@ export function mtsBytesToMts(mtsBytes: Uint8Array): number { */ export function mtsBytesToFrequency(mtsBytes: Uint8Array): number { const mts = mtsBytesToMts(mtsBytes); - return mtof(mts); + const frequency = mtof(mts); + return Math.round(frequency * 1000000) / 1000000; } /** Convert MTS Data value into readable hex string @@ -145,10 +143,13 @@ export function mtsBytesToFrequency(mtsBytes: Uint8Array): number { * can be used in MIDI messages */ export function mtsBytesToHex(mtsBytes: Uint8Array): String { + const noteNumber = mtsBytes[0] > 0x7f ? 0x7f : mtsBytes[0]; + const msb = mtsBytes[1] > 0x7f ? 0x7f : mtsBytes[1]; + const lsb = mtsBytes[2] > 0x7f ? 0x7f : mtsBytes[2]; return ( - (mtsBytes[0] & 0x7f).toString(16).padStart(2, '0') + - (mtsBytes[1] & 0x7f).toString(16).padStart(2, '0') + - (mtsBytes[2] & 0x7f).toString(16).padStart(2, '0') + (noteNumber).toString(16).padStart(2, '0') + + (msb).toString(16).padStart(2, '0') + + (lsb).toString(16).padStart(2, '0') ); } From b64b7c0430e4bd81c210e2501bce1f6a9f5eb4ac Mon Sep 17 00:00:00 2001 From: Vincenzo Sicurella Date: Thu, 16 Nov 2023 00:15:17 -0500 Subject: [PATCH 3/5] adhere to tests based on MIDI Tuning Spec values (with some fixed) --- src/__tests__/conversion.spec.ts | 201 +++++++++++++++++++++++++++---- 1 file changed, 179 insertions(+), 22 deletions(-) diff --git a/src/__tests__/conversion.spec.ts b/src/__tests__/conversion.spec.ts index cd72e01..7a6f5bb 100644 --- a/src/__tests__/conversion.spec.ts +++ b/src/__tests__/conversion.spec.ts @@ -8,7 +8,8 @@ import { ftom, mtof, valueToCents, - mtsBytesToHex + mtsBytesToHex, + mtsBytesToFrequency, } from '../conversion'; describe('Ratio to cents converter', () => { @@ -42,31 +43,187 @@ describe('MIDI to frequency converter', () => { }); describe('Frequency to MTS converter', () => { - it('converts a known value', () => { - expect(ftomts(261.625565)).toBeCloseTo(60); - expect(ftomts(0, false)).toBe(0); - expect(ftomts(14080, false)).toBe(127); - expect(ftomts(14080, true)).toBeCloseTo(129); - }) -}) + it('converts known frequency values (based on MIDI Tuning Spec)', () => { + expect(ftomts(8.175799)).toBe(0); + expect(ftomts(8.175828)).toBe(0.000062); + expect(ftomts(8.661957)).toBe(1); + expect(ftomts(16.351598)).toBe(12); + expect(ftomts(261.625565)).toBe(60); + expect(ftomts(277.182631)).toBe(61); + expect(ftomts(439.998449)).toBe(68.999939); + expect(ftomts(440)).toBe(69); + expect(ftomts(440.001551)).toBe(69.000061); + expect(ftomts(8372.01809)).toBe(120); + expect(ftomts(8372.047605)).toBe(120.000061); + expect(ftomts(12543.853951)).toBe(127); + expect(ftomts(12543.898175)).toBe(127.000061); + expect(ftomts(13289.656616)).toBe(127.999878); + expect(ftomts(13289.656616, true)).toBe(127.999878); + }); + it('clamps values beyond the specified MTS frequency range', () => { + expect(ftomts(-100, false)).toBe(0); + expect(ftomts(14080, false)).toBe(127.999878); + expect(ftomts(28980, false)).toBe(127.999878); + }); + it('does not clamp values above the specified MTS frequency range if ignoreLimit is true', () => { + expect(ftomts(-100, true)).toBe(0); + expect(ftomts(14080, true)).toBe(129); + expect(ftomts(28980, true)).toBeCloseTo(141.496923, 4); + }); +}); -describe("Frequency to MTS sysex value", () => { - it('converts a known value', () => { - expect(frequencyToMtsBytes(261.625565)).toMatchObject(new Uint8Array([60, 0, 0])); +describe('Frequency to MTS sysex value', () => { + it('converts known values from MIDI Tuning spec', () => { + expect(frequencyToMtsBytes(8.175799)).toMatchObject( + new Uint8Array([0, 0, 0]) + ); + expect(frequencyToMtsBytes(8.175828)).toMatchObject( + new Uint8Array([0, 0, 1]) + ); + expect(frequencyToMtsBytes(8.661957)).toMatchObject( + new Uint8Array([1, 0, 0]) + ); + expect(frequencyToMtsBytes(16.351598)).toMatchObject( + new Uint8Array([12, 0, 0]) + ); + expect(frequencyToMtsBytes(261.625565)).toMatchObject( + new Uint8Array([60, 0, 0]) + ); + expect(frequencyToMtsBytes(277.182631)).toMatchObject( + new Uint8Array([61, 0, 0]) + ); + expect(frequencyToMtsBytes(439.998449)).toMatchObject( + new Uint8Array([68, 127, 127]) + ); expect(frequencyToMtsBytes(440)).toMatchObject(new Uint8Array([69, 0, 0])); + expect(frequencyToMtsBytes(440.001551)).toMatchObject( + new Uint8Array([69, 0, 1]) + ); + expect(frequencyToMtsBytes(8372.01809)).toMatchObject( + new Uint8Array([120, 0, 0]) + ); + expect(frequencyToMtsBytes(8372.047605)).toMatchObject( + new Uint8Array([120, 0, 1]) + ); + expect(frequencyToMtsBytes(12543.853951)).toMatchObject( + new Uint8Array([127, 0, 0]) + ); + expect(frequencyToMtsBytes(12543.898175)).toMatchObject( + new Uint8Array([127, 0, 1]) + ); + expect(frequencyToMtsBytes(13289.656616)).toMatchObject( + new Uint8Array([127, 127, 126]) + ); + }); + it('converts other known values', () => { + expect(frequencyToMtsBytes(255.999612)).toMatchObject( + new Uint8Array([59, 79, 106]) + ); + expect(frequencyToMtsBytes(256)).toMatchObject( + new Uint8Array([59, 79, 106]) + ); + expect(frequencyToMtsBytes(441.999414)).toMatchObject( + new Uint8Array([69, 10, 6]) + ); expect(frequencyToMtsBytes(442)).toMatchObject(new Uint8Array([69, 10, 6])); - expect(frequencyToMtsBytes(0)).toMatchObject(new Uint8Array([0, 0, 0])); - }) -}) + }); + it('clamps values beyond supported MTS range', () => { + expect(frequencyToMtsBytes(-1)).toMatchObject(new Uint8Array([0, 0, 0])); + expect(frequencyToMtsBytes(14000)).toMatchObject( + new Uint8Array([127, 127, 126]) + ); + }); +}); -describe("MTS data hex string converter", () => { - it('converts a known value', () => { - expect(mtsBytesToHex(new Uint8Array([60, 0, 0]))).toEqual("3c0000"); - expect(mtsBytesToHex(new Uint8Array([69, 0, 0]))).toEqual("450000"); - expect(mtsBytesToHex(new Uint8Array([69, 10, 6]))).toEqual("450a06"); - expect(mtsBytesToHex(new Uint8Array([69, 256, 6]))).toEqual("450006"); - }) -}) +describe('MTS sysex value to frequency ', () => { + it('converts known values from MIDI Tuning spec', () => { + expect(mtsBytesToFrequency(new Uint8Array([0, 0, 0]))).toBeCloseTo( + 8.175799, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([0, 0, 1]))).toBeCloseTo( + 8.175828, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([1, 0, 0]))).toBeCloseTo( + 8.661957, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([12, 0, 0]))).toBeCloseTo( + 16.351598, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([60, 0, 0]))).toBeCloseTo( + 261.625565, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([61, 0, 0]))).toBeCloseTo( + 277.182631, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([68, 127, 127]))).toBeCloseTo( + 439.998449, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([69, 0, 0]))).toBe(440); + expect(mtsBytesToFrequency(new Uint8Array([69, 0, 1]))).toBeCloseTo( + 440.001551, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([120, 0, 0]))).toBeCloseTo( + 8372.01809, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([120, 0, 1]))).toBeCloseTo( + 8372.047605, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([127, 0, 0]))).toBeCloseTo( + 12543.853951, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([127, 0, 1]))).toBeCloseTo( + 12543.898175, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([127, 127, 126]))).toBeCloseTo( + 13289.656616, + 4 + ); + }); + it('converts other known values', () => { + expect(mtsBytesToFrequency(new Uint8Array([59, 79, 106]))).toBeCloseTo( + 255.999612, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([69, 10, 6]))).toBeCloseTo( + 441.999414, + 4 + ); + }); + it('clamps values beyond supported MTS range', () => { + expect(mtsBytesToFrequency(new Uint8Array([68, 199, 199]))).toBeCloseTo( + 439.998449, + 4 + ); + expect(mtsBytesToFrequency(new Uint8Array([199, 199, 199]))).toBeCloseTo( + 13289.656616, + 4 + ); + }); +}); + +describe('MTS data hex string converter', () => { + it('converts other known values', () => { + expect(mtsBytesToHex(new Uint8Array([60, 0, 0]))).toEqual('3c0000'); + expect(mtsBytesToHex(new Uint8Array([69, 0, 0]))).toEqual('450000'); + expect(mtsBytesToHex(new Uint8Array([69, 10, 6]))).toEqual('450a06'); + }); + it('masks int values above 0x7f by 0x7f', () => { + expect(mtsBytesToHex(new Uint8Array([69, 240, 6]))).toEqual('457006'); + expect(mtsBytesToHex(new Uint8Array([69, 255, 6]))).toEqual('457f06'); + }); +}); describe('Frequency to MIDI converter', () => { it('converts a known value', () => { From 10532a40baea8473a6fbe1963e1419047492375b Mon Sep 17 00:00:00 2001 From: Vincenzo Sicurella Date: Thu, 16 Nov 2023 12:25:08 -0500 Subject: [PATCH 4/5] fix mtsBytesToHex style and tests --- src/__tests__/conversion.spec.ts | 9 ++++++--- src/conversion.ts | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/__tests__/conversion.spec.ts b/src/__tests__/conversion.spec.ts index 7a6f5bb..1c6ff4e 100644 --- a/src/__tests__/conversion.spec.ts +++ b/src/__tests__/conversion.spec.ts @@ -219,9 +219,12 @@ describe('MTS data hex string converter', () => { expect(mtsBytesToHex(new Uint8Array([69, 0, 0]))).toEqual('450000'); expect(mtsBytesToHex(new Uint8Array([69, 10, 6]))).toEqual('450a06'); }); - it('masks int values above 0x7f by 0x7f', () => { - expect(mtsBytesToHex(new Uint8Array([69, 240, 6]))).toEqual('457006'); - expect(mtsBytesToHex(new Uint8Array([69, 255, 6]))).toEqual('457f06'); + it('clamps int values above 0x7f to 0x7f', () => { + expect(mtsBytesToHex(new Uint8Array([69, 240, 6]))).toEqual('457f06'); + expect(mtsBytesToHex(new Uint8Array([128, 255, 128]))).toEqual('7f7f7f'); + }); + it('allow value reserved for "no tuning change"', () => { + expect(mtsBytesToHex(new Uint8Array([127, 127, 127]))).toEqual('7f7f7f'); }); }); diff --git a/src/conversion.ts b/src/conversion.ts index bee8a67..e0703ce 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -118,7 +118,7 @@ export function mtsBytesToMts(mtsBytes: Uint8Array): number { let lsb = mtsBytes[2]; const noteNumber = mtsBytes[0] > 0x7f ? 0x7f : mtsBytes[0]; - if (noteNumber == 0x7f) { + if (noteNumber === 0x7f) { if (lsb >= 0x7f) lsb = 0x7e; } else if (lsb > 0x7f) lsb = 0x7f; @@ -147,9 +147,9 @@ export function mtsBytesToHex(mtsBytes: Uint8Array): String { const msb = mtsBytes[1] > 0x7f ? 0x7f : mtsBytes[1]; const lsb = mtsBytes[2] > 0x7f ? 0x7f : mtsBytes[2]; return ( - (noteNumber).toString(16).padStart(2, '0') + - (msb).toString(16).padStart(2, '0') + - (lsb).toString(16).padStart(2, '0') + noteNumber.toString(16).padStart(2, '0') + + msb.toString(16).padStart(2, '0') + + lsb.toString(16).padStart(2, '0') ); } From b953642c7af36a8d9ab90edaf81e8009de797339 Mon Sep 17 00:00:00 2001 From: Vincenzo Sicurella Date: Thu, 16 Nov 2023 14:03:04 -0500 Subject: [PATCH 5/5] add clamping to mtsToMtsBytes and associated tests --- src/__tests__/conversion.spec.ts | 50 ++++++++++++++++++++++++++++++++ src/conversion.ts | 3 ++ 2 files changed, 53 insertions(+) diff --git a/src/__tests__/conversion.spec.ts b/src/__tests__/conversion.spec.ts index 1c6ff4e..b089de3 100644 --- a/src/__tests__/conversion.spec.ts +++ b/src/__tests__/conversion.spec.ts @@ -8,6 +8,7 @@ import { ftom, mtof, valueToCents, + mtsToMtsBytes, mtsBytesToHex, mtsBytesToFrequency, } from '../conversion'; @@ -72,45 +73,91 @@ describe('Frequency to MTS converter', () => { }); }); +describe('MTS to MTS sysex value', () => { + it('converts known values from MIDI Tuning spec', () => { + expect(mtsToMtsBytes(0)).toMatchObject(new Uint8Array([0, 0, 0])); + expect(mtsToMtsBytes(0.00006)).toMatchObject(new Uint8Array([0, 0, 1])); + expect(mtsToMtsBytes(1)).toMatchObject(new Uint8Array([1, 0, 0])); + expect(mtsToMtsBytes(12)).toMatchObject(new Uint8Array([12, 0, 0])); + expect(mtsToMtsBytes(60)).toMatchObject(new Uint8Array([60, 0, 0])); + expect(mtsToMtsBytes(61)).toMatchObject(new Uint8Array([61, 0, 0])); + expect(mtsToMtsBytes(68.99994)).toMatchObject( + new Uint8Array([68, 127, 127]) + ); + expect(mtsToMtsBytes(69)).toMatchObject(new Uint8Array([69, 0, 0])); + expect(mtsToMtsBytes(69.000061)).toMatchObject(new Uint8Array([69, 0, 1])); + expect(mtsToMtsBytes(120)).toMatchObject(new Uint8Array([120, 0, 0])); + expect(mtsToMtsBytes(120.000061)).toMatchObject( + new Uint8Array([120, 0, 1]) + ); + expect(mtsToMtsBytes(127)).toMatchObject(new Uint8Array([127, 0, 0])); + expect(mtsToMtsBytes(127.000061)).toMatchObject( + new Uint8Array([127, 0, 1]) + ); + expect(mtsToMtsBytes(127.999878)).toMatchObject( + new Uint8Array([127, 127, 126]) + ); + }); + it('clamps values beyond the specified MTS frequency range', () => { + expect(mtsToMtsBytes(-1)).toMatchObject(new Uint8Array([0, 0, 0])); + expect(mtsToMtsBytes(127.9999)).toMatchObject( + new Uint8Array([127, 127, 126]) + ); + expect(mtsToMtsBytes(128)).toMatchObject(new Uint8Array([127, 127, 126])); + }); +}); + describe('Frequency to MTS sysex value', () => { it('converts known values from MIDI Tuning spec', () => { expect(frequencyToMtsBytes(8.175799)).toMatchObject( new Uint8Array([0, 0, 0]) ); + expect(frequencyToMtsBytes(8.175828)).toMatchObject( new Uint8Array([0, 0, 1]) ); + expect(frequencyToMtsBytes(8.661957)).toMatchObject( new Uint8Array([1, 0, 0]) ); + expect(frequencyToMtsBytes(16.351598)).toMatchObject( new Uint8Array([12, 0, 0]) ); + expect(frequencyToMtsBytes(261.625565)).toMatchObject( new Uint8Array([60, 0, 0]) ); + expect(frequencyToMtsBytes(277.182631)).toMatchObject( new Uint8Array([61, 0, 0]) ); + expect(frequencyToMtsBytes(439.998449)).toMatchObject( new Uint8Array([68, 127, 127]) ); + expect(frequencyToMtsBytes(440)).toMatchObject(new Uint8Array([69, 0, 0])); expect(frequencyToMtsBytes(440.001551)).toMatchObject( new Uint8Array([69, 0, 1]) ); + expect(frequencyToMtsBytes(8372.01809)).toMatchObject( new Uint8Array([120, 0, 0]) ); + expect(frequencyToMtsBytes(8372.047605)).toMatchObject( new Uint8Array([120, 0, 1]) ); + expect(frequencyToMtsBytes(12543.853951)).toMatchObject( new Uint8Array([127, 0, 0]) ); + expect(frequencyToMtsBytes(12543.898175)).toMatchObject( new Uint8Array([127, 0, 1]) ); + expect(frequencyToMtsBytes(13289.656616)).toMatchObject( new Uint8Array([127, 127, 126]) ); @@ -119,12 +166,15 @@ describe('Frequency to MTS sysex value', () => { expect(frequencyToMtsBytes(255.999612)).toMatchObject( new Uint8Array([59, 79, 106]) ); + expect(frequencyToMtsBytes(256)).toMatchObject( new Uint8Array([59, 79, 106]) ); + expect(frequencyToMtsBytes(441.999414)).toMatchObject( new Uint8Array([69, 10, 6]) ); + expect(frequencyToMtsBytes(442)).toMatchObject(new Uint8Array([69, 10, 6])); }); it('clamps values beyond supported MTS range', () => { diff --git a/src/conversion.ts b/src/conversion.ts index e0703ce..8bc555e 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -88,6 +88,9 @@ export function ftom(frequency: number): [number, number] { * @returns Uint8Array 3-byte of 7-bit MTS data */ export function mtsToMtsBytes(mtsValue: number): Uint8Array { + if (mtsValue <= 0) return new Uint8Array([0, 0, 0]); + if (mtsValue > 127.999878) return new Uint8Array([127, 127, 126]); + const noteNumber = Math.trunc(mtsValue); const fine = Math.round(0x4000 * (mtsValue - noteNumber));