Skip to content

Commit 0db14df

Browse files
committed
Fix Performance Leak in Tuner Lifecycle
Identified and resolved a performance bug where the requestAnimationFrame (RAF) loop for the tuner continued to execute even after the UI was closed. This resulted in unnecessary CPU and battery consumption due to persistent audio analysis and DOM manipulation in the background.
1 parent 5a34c5b commit 0db14df

File tree

2 files changed

+59
-19
lines changed

2 files changed

+59
-19
lines changed

js/utils/__tests__/synthutils.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,20 @@ describe("Utility Functions (logic-only)", () => {
11001100
stopTuner();
11011101
expect(mockClose).toHaveBeenCalledTimes(1);
11021102
});
1103+
1104+
it("should cancel any pending tuner animation frame", () => {
1105+
const originalCancel = global.cancelAnimationFrame;
1106+
const mockCancel = jest.fn();
1107+
global.cancelAnimationFrame = mockCancel;
1108+
Synth._tunerRafId = 123;
1109+
Synth._tunerActive = true;
1110+
Synth.tunerMic = null;
1111+
stopTuner();
1112+
expect(mockCancel).toHaveBeenCalledWith(123);
1113+
expect(Synth._tunerRafId).toBeNull();
1114+
expect(Synth._tunerActive).toBe(false);
1115+
global.cancelAnimationFrame = originalCancel;
1116+
});
11031117
});
11041118

11051119
describe("newTone", () => {

js/utils/synthutils.js

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,21 @@ function Synth() {
550550
* @type {function|null}
551551
*/
552552
this.detectPitch = null;
553+
/**
554+
* Flag to track whether tuner update loop is active.
555+
* @type {boolean}
556+
*/
557+
this._tunerActive = false;
558+
/**
559+
* Animation frame id for the tuner update loop.
560+
* @type {number|null}
561+
*/
562+
this._tunerRafId = null;
563+
/**
564+
* Cached tuner segments to avoid querying on every frame.
565+
* @type {NodeList|null}
566+
*/
567+
this._tunerSegments = null;
553568

554569
/**
555570
* Function to initialize a new Tone.js instance.
@@ -2449,6 +2464,12 @@ function Synth() {
24492464

24502465
this.tunerAnalyser = new Tone.Analyser("waveform", 2048);
24512466
this.tunerMic.connect(this.tunerAnalyser);
2467+
this._tunerActive = true;
2468+
this._tunerSegments = null;
2469+
if (this._tunerRafId !== null && typeof cancelAnimationFrame === "function") {
2470+
cancelAnimationFrame(this._tunerRafId);
2471+
}
2472+
this._tunerRafId = null;
24522473

24532474
const YIN = (sampleRate, bufferSize = 2048, threshold = 0.1) => {
24542475
// Low-Pass Filter to remove high-frequency noise
@@ -2530,6 +2551,15 @@ function Synth() {
25302551
let targetPitch = { note: "A4", frequency: 440 }; // Default target pitch
25312552

25322553
const updatePitch = () => {
2554+
if (!this._tunerActive) return;
2555+
2556+
const tunerContainer = document.getElementById("tunerContainer");
2557+
if (!tunerContainer || !this.tunerAnalyser || !this.detectPitch) {
2558+
this._tunerActive = false;
2559+
this._tunerRafId = null;
2560+
return;
2561+
}
2562+
25332563
const buffer = this.tunerAnalyser.getValue();
25342564
const pitch = this.detectPitch(buffer);
25352565

@@ -2548,14 +2578,6 @@ function Synth() {
25482578
// Show current note in display but calculate cents from target
25492579
note = currentNote.note; // Show the current note being played
25502580

2551-
// Debug logging
2552-
console.log("Debug values:", {
2553-
detectedPitch: pitch,
2554-
targetNote: targetPitch.note,
2555-
targetFrequency: targetPitch.frequency,
2556-
currentNote: note
2557-
});
2558-
25592581
// Ensure we have valid frequencies before calculation
25602582
if (pitch > 0 && targetPitch.frequency > 0) {
25612583
// Calculate cents from target frequency
@@ -2596,17 +2618,8 @@ function Synth() {
25962618
}
25972619
}
25982620

2599-
// Debug logging
2600-
console.log({
2601-
frequency: pitch.toFixed(1),
2602-
detectedNote: note,
2603-
centsDeviation: cents,
2604-
mode: tunerMode
2605-
});
2606-
26072621
// Initialize display elements if they don't exist
26082622
let noteDisplayContainer = document.getElementById("noteDisplayContainer");
2609-
const tunerContainer = document.getElementById("tunerContainer");
26102623

26112624
if (!noteDisplayContainer && tunerContainer) {
26122625
// Create container
@@ -3136,7 +3149,11 @@ function Synth() {
31363149
}
31373150

31383151
// Update tuner segments
3139-
const tunerSegments = document.querySelectorAll("#tunerContainer svg path");
3152+
let tunerSegments = this._tunerSegments;
3153+
if (!tunerSegments || tunerSegments.length === 0 || !tunerSegments[0].isConnected) {
3154+
tunerSegments = tunerContainer.querySelectorAll("svg path");
3155+
this._tunerSegments = tunerSegments;
3156+
}
31403157

31413158
// Define colors for the gradient
31423159
const colors = {
@@ -3292,16 +3309,25 @@ function Synth() {
32923309
});
32933310
}
32943311

3295-
requestAnimationFrame(updatePitch);
3312+
if (this._tunerActive && typeof requestAnimationFrame === "function") {
3313+
this._tunerRafId = requestAnimationFrame(updatePitch);
3314+
}
32963315
};
32973316

32983317
updatePitch();
32993318
};
33003319

33013320
this.stopTuner = () => {
3321+
this._tunerActive = false;
3322+
if (this._tunerRafId !== null && typeof cancelAnimationFrame === "function") {
3323+
cancelAnimationFrame(this._tunerRafId);
3324+
}
3325+
this._tunerRafId = null;
3326+
this._tunerSegments = null;
33023327
if (this.tunerMic) {
33033328
this.tunerMic.close();
33043329
}
3330+
this.tunerAnalyser = null;
33053331
};
33063332

33073333
const frequencyToNote = frequency => {

0 commit comments

Comments
 (0)