Skip to content

Commit 26ec0af

Browse files
committed
fix: dispose Tone.js instruments and reset data structures to free audio memory
- Add .dispose() calls before deleting instruments in ___createSynth() for BUILTIN_SYNTHS, CUSTOM_SYNTHS, and CUSTOMSAMPLES code paths - Add disposeAllInstruments() method to Synth that properly disposes all instruments, filters, and effects for every turtle - Call disposeAllInstruments() in Logo.doStopTurtles() to free decoded AudioBuffers and Web Audio nodes when the stop button is pressed - Close AudioContext in testTuner() and testSpecificFrequency() to prevent orphaned audio resource leaks (~4-8 MB each) - Reset unbounded data structures (turtleHeaps, turtleDicts, notationNotes, _midiData, statusFields, specialArgs, connectionStore, recordingBuffer) at the start of runLogoCommands() to free memory between repeated runs - Update test mocks to include disposeAllInstruments Estimated RAM savings: ~65-145 MB depending on session length and number of instruments loaded.
1 parent f488f0b commit 26ec0af

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

js/__tests__/logo.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ global.Synth = jest.fn().mockImplementation(() => ({
3434
start: jest.fn(),
3535
stop: jest.fn(),
3636
stopSound: jest.fn(),
37+
disposeAllInstruments: jest.fn(),
3738
changeInTemperament: false,
3839
recorder: null
3940
}));
@@ -424,6 +425,7 @@ describe("Logo Class", () => {
424425
logo.synth = {
425426
stop: jest.fn(),
426427
stopSound: jest.fn(),
428+
disposeAllInstruments: jest.fn(),
427429
recorder: null
428430
};
429431

@@ -437,6 +439,7 @@ describe("Logo Class", () => {
437439
logo.synth = {
438440
stop: jest.fn(),
439441
stopSound: jest.fn(),
442+
disposeAllInstruments: jest.fn(),
440443
recorder: null
441444
};
442445

@@ -451,6 +454,7 @@ describe("Logo Class", () => {
451454
logo.synth = {
452455
stop: jest.fn(),
453456
stopSound: jest.fn(),
457+
disposeAllInstruments: jest.fn(),
454458
recorder: null
455459
};
456460

@@ -465,6 +469,7 @@ describe("Logo Class", () => {
465469
logo.synth = {
466470
stop: jest.fn(),
467471
stopSound: jest.fn(),
472+
disposeAllInstruments: jest.fn(),
468473
recorder: null
469474
};
470475
logo.stepQueue = { 0: [1, 2, 3] };
@@ -1069,6 +1074,7 @@ describe("Logo comprehensive method coverage", () => {
10691074
logo.synth = {
10701075
stop: jest.fn(),
10711076
stopSound: jest.fn(),
1077+
disposeAllInstruments: jest.fn(),
10721078
recorder: { state: "recording", stop: jest.fn() }
10731079
};
10741080
logo._restoreConnections = jest.fn();

js/logo.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,11 @@ class Logo {
10401040
if (this.synth.recorder && this.synth.recorder.state == "recording")
10411041
this.synth.recorder.stop();
10421042

1043+
// Dispose all Tone.js instruments to free decoded AudioBuffers
1044+
// and Web Audio nodes. They will be re-created by prepSynths()
1045+
// on the next run.
1046+
this.synth.disposeAllInstruments();
1047+
10431048
if (this.cameraID != null) {
10441049
this.deps.utils.doStopVideoCam(this.cameraID, this.setCameraID);
10451050
}
@@ -1148,6 +1153,25 @@ class Logo {
11481153
this.notation.notationStaging = {};
11491154
this.notation.notationDrumStaging = {};
11501155

1156+
// Reset accumulated data structures to free memory between runs.
1157+
// These grow unboundedly across repeated executions.
1158+
this.turtleHeaps = {};
1159+
this.turtleDicts = {};
1160+
this.notationNotes = {};
1161+
this._midiData = {};
1162+
this.statusFields = [];
1163+
this.specialArgs = [];
1164+
this.connectionStore = {};
1165+
if (this.recordingBuffer && !this.recording) {
1166+
this.recordingBuffer = {
1167+
hasData: false,
1168+
notationOutput: "",
1169+
notationNotes: {},
1170+
notationStaging: {},
1171+
notationDrumStaging: {}
1172+
};
1173+
}
1174+
11511175
// Each turtle needs to keep its own wait time and music states.
11521176
for (const turtle in this.activity.turtles.turtleList) {
11531177
this.initTurtle(turtle);

js/utils/synthutils.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,13 @@ function Synth() {
15621562
}
15631563
} else if (sourceName in BUILTIN_SYNTHS) {
15641564
if (instruments[turtle] && instruments[turtle][instrumentName]) {
1565+
if (typeof instruments[turtle][instrumentName].dispose === "function") {
1566+
try {
1567+
instruments[turtle][instrumentName].dispose();
1568+
} catch (e) {
1569+
console.debug("Error disposing instrument:", e);
1570+
}
1571+
}
15651572
delete instruments[turtle][instrumentName];
15661573
}
15671574

@@ -1575,6 +1582,13 @@ function Synth() {
15751582
}
15761583
} else if (sourceName in CUSTOM_SYNTHS) {
15771584
if (instruments[turtle] && instruments[turtle][instrumentName]) {
1585+
if (typeof instruments[turtle][instrumentName].dispose === "function") {
1586+
try {
1587+
instruments[turtle][instrumentName].dispose();
1588+
} catch (e) {
1589+
console.debug("Error disposing instrument:", e);
1590+
}
1591+
}
15781592
delete instruments[turtle][instrumentName];
15791593
}
15801594

@@ -1585,6 +1599,13 @@ function Synth() {
15851599
instrumentsSource[instrumentName] = [0, "poly"];
15861600
} else if (sourceName in CUSTOMSAMPLES) {
15871601
if (instruments[turtle] && instruments[turtle][instrumentName]) {
1602+
if (typeof instruments[turtle][instrumentName].dispose === "function") {
1603+
try {
1604+
instruments[turtle][instrumentName].dispose();
1605+
} catch (e) {
1606+
console.debug("Error disposing instrument:", e);
1607+
}
1608+
}
15881609
delete instruments[turtle][instrumentName];
15891610
}
15901611

@@ -3509,6 +3530,68 @@ function Synth() {
35093530
this.centsSliderBtn.style.backgroundColor = "";
35103531
};
35113532

3533+
/**
3534+
* Disposes all Tone.js instruments, filters, and effects for every turtle
3535+
* to free audio memory (decoded AudioBuffers, Web Audio nodes, etc.).
3536+
* Instruments will be re-created by prepSynths() on the next run.
3537+
* @function
3538+
* @memberof Synth
3539+
* @returns {void}
3540+
*/
3541+
this.disposeAllInstruments = () => {
3542+
for (const turtle in instruments) {
3543+
for (const instrumentName in instruments[turtle]) {
3544+
if (
3545+
instruments[turtle][instrumentName] &&
3546+
typeof instruments[turtle][instrumentName].dispose === "function"
3547+
) {
3548+
try {
3549+
instruments[turtle][instrumentName].dispose();
3550+
} catch (e) {
3551+
console.debug("Error disposing instrument:", e);
3552+
}
3553+
}
3554+
delete instruments[turtle][instrumentName];
3555+
}
3556+
}
3557+
3558+
for (const turtle in instrumentsFilters) {
3559+
for (const instrumentName in instrumentsFilters[turtle]) {
3560+
const filters = instrumentsFilters[turtle][instrumentName];
3561+
if (Array.isArray(filters)) {
3562+
filters.forEach(f => {
3563+
if (f && typeof f.dispose === "function") {
3564+
try {
3565+
f.dispose();
3566+
} catch (e) {
3567+
console.debug("Error disposing filter:", e);
3568+
}
3569+
}
3570+
});
3571+
}
3572+
delete instrumentsFilters[turtle][instrumentName];
3573+
}
3574+
}
3575+
3576+
for (const turtle in instrumentsEffects) {
3577+
for (const instrumentName in instrumentsEffects[turtle]) {
3578+
const effects = instrumentsEffects[turtle][instrumentName];
3579+
if (Array.isArray(effects)) {
3580+
effects.forEach(fx => {
3581+
if (fx && typeof fx.dispose === "function") {
3582+
try {
3583+
fx.dispose();
3584+
} catch (e) {
3585+
console.debug("Error disposing effect:", e);
3586+
}
3587+
}
3588+
});
3589+
}
3590+
delete instrumentsEffects[turtle][instrumentName];
3591+
}
3592+
}
3593+
};
3594+
35123595
this.tone = null;
35133596
this.noteFrequencies = {};
35143597
this.startingPitch = "C4";

0 commit comments

Comments
 (0)