Skip to content

Commit

Permalink
Merge pull request #7 from xenharmonic-devs/mts-methods
Browse files Browse the repository at this point in the history
Add MTS pitch and SysEx format conversion methods
  • Loading branch information
vsicurella authored Nov 16, 2023
2 parents d90a348 + b953642 commit a3c0987
Show file tree
Hide file tree
Showing 2 changed files with 327 additions and 1 deletion.
240 changes: 240 additions & 0 deletions src/__tests__/conversion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import {
centOffsetToFrequency,
centsToValue,
frequencyToCentOffset,
ftomts,
frequencyToMtsBytes,
ftom,
mtof,
valueToCents,
mtsToMtsBytes,
mtsBytesToHex,
mtsBytesToFrequency,
} from '../conversion';

describe('Ratio to cents converter', () => {
Expand Down Expand Up @@ -38,6 +43,241 @@ describe('MIDI to frequency converter', () => {
});
});

describe('Frequency to MTS converter', () => {
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('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])
);
});
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]));
});
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 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('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');
});
});

describe('Frequency to MIDI converter', () => {
it('converts a known value', () => {
const [index, offset] = ftom(261.625565);
Expand Down
88 changes: 87 additions & 1 deletion src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,25 @@ 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);
}

/**
* 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 {
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;
}

/**
* Convert frequency to MIDI note number and pitch offset measured in cents.
* @param frequency Frequency in Hertz.
Expand All @@ -70,6 +82,80 @@ 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 {
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));

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 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 fine = ((msb << 7) + lsb) / 0x4000;
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);
const frequency = mtof(mts);
return Math.round(frequency * 1000000) / 1000000;
}

/** 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 {
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 (
noteNumber.toString(16).padStart(2, '0') +
msb.toString(16).padStart(2, '0') +
lsb.toString(16).padStart(2, '0')
);
}

/**
* Convert cents to natural units.
* @param cents Musical interval in cents.
Expand Down

0 comments on commit a3c0987

Please sign in to comment.