diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index a85a8d6..6c8f02c 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -5,6 +5,8 @@ import { approximatePrimeLimitWithErrors, arraysEqual, binomial, + circleDifference, + circleDistance, clamp, div, dot, @@ -220,3 +222,45 @@ describe('Norm', () => { expect(norm(a, 'maximum')).toBeCloseTo(4); }); }); + +describe('Pitch difference with circle equivalence', () => { + it('calculates the difference between 700.0 and 701.955', () => { + const diff = circleDifference(700.0, 701.955); + expect(diff).toBeCloseTo(-1.955); + }); + + it('calculates the octave-equivalent difference between 5/1 and 4\\12', () => { + const diff = circleDifference(valueToCents(5), 400.0); + expect(diff).toBeCloseTo(-13.686); + }); + + it('calculates the tritave-equivalent difference between 5/1 and 13/1', () => { + const diff = circleDifference( + valueToCents(5), + valueToCents(13), + valueToCents(3) + ); + expect(diff).toBeCloseTo(247.741); + }); +}); + +describe('Pitch distance with circle equivalence', () => { + it('calculates the distance between 700.0 and 701.955', () => { + const diff = circleDistance(700.0, 701.955); + expect(diff).toBeCloseTo(1.955); + }); + + it('calculates the octave-equivalent distance between 5/1 and 4\\12', () => { + const diff = circleDistance(valueToCents(5), 400.0); + expect(diff).toBeCloseTo(13.686); + }); + + it('calculates the tritave-equivalent distance between 5/1 and 13/1', () => { + const diff = circleDistance( + valueToCents(5), + valueToCents(13), + valueToCents(3) + ); + expect(diff).toBeCloseTo(247.741); + }); +}); diff --git a/src/index.ts b/src/index.ts index b792fe1..a9eaabc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -530,3 +530,26 @@ export function norm( } return result; } + +/** + * Calculate the difference between two cents values such that equave equivalence is taken into account. + * @param a The first pitch measured in cents. + * @param b The second pitch measured in cents. + * @param equaveCents The interval of equivalence measured in cents. + * @returns The first pitch minus the second pitch but on a circle such that large differences wrap around. + */ +export function circleDifference(a: number, b: number, equaveCents = 1200.0) { + const half = 0.5 * equaveCents; + return mmod(a - b + half, equaveCents) - half; +} + +/** + * Calculate the distance between two cents values such that equave equivalence is taken into account. + * @param a The first pitch measured in cents. + * @param b The second pitch measured in cents. + * @param equaveCents The interval of equivalence measured in cents. + * @returns The absolute distance between the two pitches measured in cents but on a circle such that large distances wrap around. + */ +export function circleDistance(a: number, b: number, equaveCents = 1200.0) { + return Math.abs(circleDifference(a, b, equaveCents)); +}