Skip to content

Commit 0ab8bae

Browse files
fix: B# and Cb interval math at octave boundaries (#6638)
* fix: B# and Cb interval math at octave boundaries GetIntervalNumber was messing up intervals like B to B# in the same octave — returning 12 or 13 semitones when it should be 1. The octave boundary wrap-around wasn't being handled. Fixed in IntervalsActions.js: detect when octave === 0 and use the shortest distance around the circle. Related to #6629 * fix: interval math for B# and Cb at octave boundaries GetIntervalNumber was giving 12 semitones for B→B# in the same octave. Should be 1. Fixed in IntervalsActions.js — modular arithmetic now finds the shortest distance across the octave boundary. Added tests for B→B#, B→Cb, C→Cb, and Cb→B.
1 parent 4ef1470 commit 0ab8bae

File tree

2 files changed

+82
-3
lines changed

2 files changed

+82
-3
lines changed

js/turtleactions/IntervalsActions.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,15 @@ function setupIntervalsActions(activity) {
9494
// Use dynamic temperament length for custom tunings
9595
const temperamentLength = this.getTemperamentLength();
9696

97-
if (ALLNOTESTEP[secondNote] < ALLNOTESTEP[firstNote] && octave !== 0)
98-
totalIntervals = temperamentLength - totalIntervals;
97+
// Handle octave boundary wrap-around for enharmonic equivalents
98+
// For cases like B (12) to B#/Cb (0), the raw difference is 12 but should be 1
99+
// Calculate forward distance across octave boundary using modular arithmetic
100+
const forwardDiff =
101+
(ALLNOTESTEP[secondNote] - ALLNOTESTEP[firstNote] + temperamentLength) %
102+
temperamentLength;
103+
// When notes are at octave boundary (forwardDiff === 0), use 1 semitone
104+
// Otherwise use the shorter of raw difference or forward distance
105+
totalIntervals = forwardDiff === 0 ? 1 : Math.min(totalIntervals, forwardDiff);
99106

100107
if (octave < 0 && totalIntervals !== 0 && totalIntervals !== temperamentLength)
101108
totalIntervals = temperamentLength - totalIntervals;

js/turtleactions/__tests__/IntervalsActions.test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,29 @@ describe("setupIntervalsActions", () => {
3838
minor: [2, 1, 2, 2, 1, 2, 2]
3939
};
4040

41-
global.ALLNOTESTEP = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
41+
global.ALLNOTESTEP = {
42+
"Cb": 0,
43+
"C": 1,
44+
"C#": 2,
45+
"Db": 2,
46+
"D": 3,
47+
"D#": 4,
48+
"Eb": 4,
49+
"E": 5,
50+
"E#": 6,
51+
"Fb": 5,
52+
"F": 6,
53+
"F#": 7,
54+
"Gb": 7,
55+
"G": 8,
56+
"G#": 9,
57+
"Ab": 9,
58+
"A": 10,
59+
"A#": 11,
60+
"Bb": 11,
61+
"B": 12,
62+
"B#": 0
63+
};
4264
global.NOTENAMES = ["C", "D", "E", "F", "G", "A", "B"];
4365

4466
global.SEMITONETOINTERVALMAP = Array(13)
@@ -179,6 +201,56 @@ describe("setupIntervalsActions", () => {
179201
expect(typeof Singer.IntervalsActions.GetIntervalNumber(0)).toBe("number");
180202
});
181203

204+
test("GetIntervalNumber handles B to B# enharmonic (same octave)", () => {
205+
// B (12) to B# (0) should be 1 semitone (minor second), not 12
206+
GetNotesForInterval.mockReturnValueOnce({
207+
firstNote: "B",
208+
secondNote: "B#",
209+
octave: 0
210+
});
211+
expect(Singer.IntervalsActions.GetIntervalNumber(0)).toBe(1);
212+
});
213+
214+
test("GetIntervalNumber handles B to Cb enharmonic (same octave)", () => {
215+
// B (12) to Cb (0) should be 1 semitone (minor second), not 12
216+
GetNotesForInterval.mockReturnValueOnce({
217+
firstNote: "B",
218+
secondNote: "Cb",
219+
octave: 0
220+
});
221+
expect(Singer.IntervalsActions.GetIntervalNumber(0)).toBe(1);
222+
});
223+
224+
test("GetIntervalNumber handles C to Cb (same octave)", () => {
225+
// C (1) to Cb (0) should be 1 semitone
226+
GetNotesForInterval.mockReturnValueOnce({
227+
firstNote: "C",
228+
secondNote: "Cb",
229+
octave: 0
230+
});
231+
expect(Singer.IntervalsActions.GetIntervalNumber(0)).toBe(1);
232+
});
233+
234+
test("GetIntervalNumber handles Cb to B (same octave)", () => {
235+
// Cb (0) to B (12) should be 1 semitone (wrap-around)
236+
GetNotesForInterval.mockReturnValueOnce({
237+
firstNote: "Cb",
238+
secondNote: "B",
239+
octave: 0
240+
});
241+
expect(Singer.IntervalsActions.GetIntervalNumber(0)).toBe(1);
242+
});
243+
244+
test("GetIntervalNumber handles normal interval correctly", () => {
245+
// C (1) to G (8) should be 7 semitones (perfect fifth)
246+
GetNotesForInterval.mockReturnValueOnce({
247+
firstNote: "C",
248+
secondNote: "G",
249+
octave: 0
250+
});
251+
expect(Singer.IntervalsActions.GetIntervalNumber(0)).toBe(7);
252+
});
253+
182254
test("GetCurrentInterval normal", () => {
183255
expect(typeof Singer.IntervalsActions.GetCurrentInterval(0)).toBe("string");
184256
});

0 commit comments

Comments
 (0)