Skip to content

Commit c4d3374

Browse files
committed
fix: resolve nthModalPitch broken for sharp keys like D#
buildScale converts notes like F→E♯ and C→B♯ to avoid duplicate letter names, but playNthModalPitch and ScaleDegreeBlock lookup these notes in NOTESSHARP which doesn't contain them, returning -1 and breaking the semitone calculation. Fix: resolve special notes (E♯→F, B♯→C, C♭→B, F♭→E) via EQUIVALENTNATURALS before looking up in NOTESFLAT/NOTESSHARP. Affected: playNthModalPitch (PitchActions.js:246) and ScaleDegreeBlock (PitchBlocks.js:2008) Related to #7171
1 parent a090f86 commit c4d3374

5 files changed

Lines changed: 102 additions & 9 deletions

File tree

js/blocks/PitchBlocks.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
YSTAFFNOTEHEIGHT, MUSICALMODES, keySignatureToMode, ALLNOTENAMES,
2121
nthDegreeToPitch, A0, C8, calcOctave, SOLFEGECONVERSIONTABLE,
2222
NOTESFLAT, NOTESSHARP, NOTESTEP, scaleDegreeToPitchMapping,
23-
INTERVALVALUES
23+
INTERVALVALUES, EQUIVALENTNATURALS
2424
*/
2525

2626
/* exported setupPitchBlocks */
@@ -2005,9 +2005,11 @@ function setupPitchBlocks(activity) {
20052005
);
20062006
let semitones = ref;
20072007

2008-
semitones += NOTESFLAT.includes(note)
2009-
? NOTESFLAT.indexOf(note) - ref
2010-
: NOTESSHARP.indexOf(note) - ref;
2008+
const resolvedNote =
2009+
note in EQUIVALENTNATURALS ? EQUIVALENTNATURALS[note] : note;
2010+
semitones += NOTESFLAT.includes(resolvedNote)
2011+
? NOTESFLAT.indexOf(resolvedNote) - ref
2012+
: NOTESSHARP.indexOf(resolvedNote) - ref;
20112013

20122014
/** calculates changes in reference octave which occur a semitone before the reference key */
20132015
const deltaOctave = isNegativeArg

js/blocks/__tests__/PitchBlocks.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ describe("setupPitchBlocks", () => {
160160
global.NOTESSHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
161161
global.NOTESFLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
162162
global.NOTESTEP = { C: 1, D: 3, E: 5, F: 6, G: 8, A: 10, B: 12 };
163+
global.EQUIVALENTNATURALS = {
164+
"E\u266F": "F",
165+
"B\u266F": "C",
166+
"C\u266D": "B",
167+
"F\u266D": "E"
168+
};
163169
global.SOLFEGECONVERSIONTABLE = {
164170
C: "do",
165171
D: "re",

js/turtleactions/PitchActions.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
nthDegreeToPitch, SHARP, FLAT, pitchToFrequency, SOLFEGENAMES1, SOLFEGECONVERSIONTABLE,
2626
numberToPitch, ACCIDENTALNAMES, ACCIDENTALVALUES, NOTESFLAT, NOTESSHARP, NOTESTEP, MUSICALMODES,
2727
keySignatureToMode, getInterval, EFFECTSNAMES, NANERRORMSG, frequencyToPitch,
28-
MusicBlocks, Mouse, isCustomTemperament
28+
MusicBlocks, Mouse, isCustomTemperament, EQUIVALENTNATURALS
2929
*/
3030

3131
/*
@@ -243,9 +243,10 @@ function setupPitchActions(activity) {
243243

244244
const [note, _offset] = nthDegreeToPitch(tur.singer.keySignature, scaleDegree);
245245
let semitones = ref;
246-
semitones += NOTESFLAT.includes(note)
247-
? NOTESFLAT.indexOf(note) - ref
248-
: NOTESSHARP.indexOf(note) - ref;
246+
const resolvedNote = note in EQUIVALENTNATURALS ? EQUIVALENTNATURALS[note] : note;
247+
semitones += NOTESFLAT.includes(resolvedNote)
248+
? NOTESFLAT.indexOf(resolvedNote) - ref
249+
: NOTESSHARP.indexOf(resolvedNote) - ref;
249250
/** calculates changes in reference octave which occur a semitone before the reference key */
250251
const deltaOctave = Math.floor(number / modeLength);
251252
/** calculates changes in octave when crossing B */

js/turtleactions/__tests__/PitchActions.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ Object.assign(global, {
4343
NOTESTEP: musicUtils.NOTESTEP,
4444
MUSICALMODES: musicUtils.MUSICALMODES,
4545
SHARP: musicUtils.SHARP,
46-
FLAT: musicUtils.FLAT
46+
FLAT: musicUtils.FLAT,
47+
EQUIVALENTNATURALS: { "E\u266F": "F", "B\u266F": "C", "C\u266D": "B", "F\u266D": "E" }
4748
});
4849

4950
global.NANERRORMSG = require("../../logo").NANERRORMSG;

js/utils/__tests__/musicutils.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,89 @@ describe("pitchToFrequency", () => {
20462046
});
20472047
});
20482048

2049+
describe("pitchToFrequency cents edge cases", () => {
2050+
global.TWELTHROOT2 = 1.0594630943592953;
2051+
global.TWELVEHUNDRETHROOT2 = Number("1.0005777895065549");
2052+
global.A0 = 27.5;
2053+
2054+
it("12-EDO regression: cents formula matches standard 12-EDO", () => {
2055+
// When temperament is 12-EDO and cents is 0, result must equal standard 12-EDO frequency
2056+
const result0 = pitchToFrequency("A", 4, 0, "C");
2057+
expect(result0).toBe(A0 * Math.pow(TWELTHROOT2, 48));
2058+
2059+
// With cents and 12-EDO, uses 1200-EDO formula
2060+
const result50 = pitchToFrequency("A", 4, 50, "C");
2061+
expect(result50).toBe(A0 * Math.pow(TWELVEHUNDRETHROOT2, 48 * 100 + 50));
2062+
});
2063+
2064+
it("19-EDO cents: explicit formula matches implementation", () => {
2065+
// A4 in 19-EDO = pitchNumber 76 (4 * 19)
2066+
// freq = A0 * 2^(1/(19*100))^(76*100 + cents)
2067+
const result0 = pitchToFrequency("A", 4, 0, "C", "equal19");
2068+
const expected0 = A0 * Math.pow(2, 1 / (19 * 100)) ** (76 * 100);
2069+
expect(result0).toBeCloseTo(expected0, 4);
2070+
2071+
const result50 = pitchToFrequency("A", 4, 50, "C", "equal19");
2072+
const expected50 = A0 * Math.pow(2, 1 / (19 * 100)) ** (76 * 100 + 50);
2073+
expect(result50).toBeCloseTo(expected50, 4);
2074+
});
2075+
2076+
it("31-EDO cents: explicit formula matches implementation", () => {
2077+
// A4 in 31-EDO = pitchNumber 124 (4 * 31)
2078+
const result0 = pitchToFrequency("A", 4, 0, "C", "equal31");
2079+
const expected0 = A0 * Math.pow(2, 1 / (31 * 100)) ** (124 * 100);
2080+
expect(result0).toBeCloseTo(expected0, 4);
2081+
2082+
const result100 = pitchToFrequency("A", 4, 100, "C", "equal31");
2083+
const expected100 = A0 * Math.pow(2, 1 / (31 * 100)) ** (124 * 100 + 100);
2084+
expect(result100).toBeCloseTo(expected100, 4);
2085+
});
2086+
2087+
it("handles large positive cents in 12-EDO", () => {
2088+
// 1199 cents ~ almost one octave above
2089+
const result = pitchToFrequency("A", 4, 1199, "C");
2090+
const expected = A0 * Math.pow(TWELVEHUNDRETHROOT2, 48 * 100 + 1199);
2091+
expect(result).toBeCloseTo(expected, 4);
2092+
});
2093+
2094+
it("handles large negative cents in 12-EDO", () => {
2095+
// -100 cents = one semitone flat
2096+
const result = pitchToFrequency("A", 4, -100, "C");
2097+
const expected = A0 * Math.pow(TWELVEHUNDRETHROOT2, 48 * 100 - 100);
2098+
expect(result).toBeCloseTo(expected, 4);
2099+
});
2100+
2101+
it("handles cents at different octaves with temperament", () => {
2102+
// C5 in 19-EDO: pitchNumber = 5*19 + 0(C) - 9(A offset) = 86
2103+
const result = pitchToFrequency("C", 5, 30, "C", "equal19");
2104+
const expected = A0 * Math.pow(2, 1 / (19 * 100)) ** (86 * 100 + 30);
2105+
expect(result).toBeCloseTo(expected, 4);
2106+
});
2107+
2108+
it("12-EDO regression: cents with unequal temperaments", () => {
2109+
// Default equal temperament with cents should match 12-EDO cents formula
2110+
const result = pitchToFrequency("A", 4, 75, "C");
2111+
const expected = A0 * Math.pow(TWELVEHUNDRETHROOT2, 48 * 100 + 75);
2112+
expect(result).toBeCloseTo(expected, 4);
2113+
});
2114+
});
2115+
2116+
describe("frequencyToPitch cents with 12-EDO regression", () => {
2117+
it("returns 0 cents for exact 12-EDO pitches", () => {
2118+
expect(frequencyToPitch(440)[2]).toBe(0);
2119+
expect(frequencyToPitch(261.63)[2]).toBe(0);
2120+
expect(frequencyToPitch(523.25)[2]).toBe(0);
2121+
});
2122+
2123+
it("detects small cents deviation in 12-EDO", () => {
2124+
// 441 Hz = 440 * 2^(1/1200) ≈ A4 + 1 cent
2125+
const freq = 440 * Math.pow(2, 1 / 1200);
2126+
const result = frequencyToPitch(freq);
2127+
expect(result[2]).toBeGreaterThan(0);
2128+
expect(result[2]).toBeLessThan(2);
2129+
});
2130+
});
2131+
20492132
describe("noteToFrequency", () => {
20502133
global.TWELTHROOT2 = 1.0594630943592953;
20512134
global.TWELVEHUNDRETHROOT2 = Number("1.0005777895065549");

0 commit comments

Comments
 (0)