Skip to content

Commit c766294

Browse files
authored
fix: dispose Tone.js instruments and reset data structures to free audio memory (#5928)
- 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 fa1a50b commit c766294

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
}
@@ -1153,6 +1158,25 @@ class Logo {
11531158
this.notation.notationStaging = {};
11541159
this.notation.notationDrumStaging = {};
11551160

1161+
// Reset accumulated data structures to free memory between runs.
1162+
// These grow unboundedly across repeated executions.
1163+
this.turtleHeaps = {};
1164+
this.turtleDicts = {};
1165+
this.notationNotes = {};
1166+
this._midiData = {};
1167+
this.statusFields = [];
1168+
this.specialArgs = [];
1169+
this.connectionStore = {};
1170+
if (this.recordingBuffer && !this.recording) {
1171+
this.recordingBuffer = {
1172+
hasData: false,
1173+
notationOutput: "",
1174+
notationNotes: {},
1175+
notationStaging: {},
1176+
notationDrumStaging: {}
1177+
};
1178+
}
1179+
11561180
// Each turtle needs to keep its own wait time and music states.
11571181
for (const turtle in this.activity.turtles.turtleList) {
11581182
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

@@ -3495,6 +3516,68 @@ function Synth() {
34953516
this.centsSliderBtn.style.backgroundColor = "";
34963517
};
34973518

3519+
/**
3520+
* Disposes all Tone.js instruments, filters, and effects for every turtle
3521+
* to free audio memory (decoded AudioBuffers, Web Audio nodes, etc.).
3522+
* Instruments will be re-created by prepSynths() on the next run.
3523+
* @function
3524+
* @memberof Synth
3525+
* @returns {void}
3526+
*/
3527+
this.disposeAllInstruments = () => {
3528+
for (const turtle in instruments) {
3529+
for (const instrumentName in instruments[turtle]) {
3530+
if (
3531+
instruments[turtle][instrumentName] &&
3532+
typeof instruments[turtle][instrumentName].dispose === "function"
3533+
) {
3534+
try {
3535+
instruments[turtle][instrumentName].dispose();
3536+
} catch (e) {
3537+
console.debug("Error disposing instrument:", e);
3538+
}
3539+
}
3540+
delete instruments[turtle][instrumentName];
3541+
}
3542+
}
3543+
3544+
for (const turtle in instrumentsFilters) {
3545+
for (const instrumentName in instrumentsFilters[turtle]) {
3546+
const filters = instrumentsFilters[turtle][instrumentName];
3547+
if (Array.isArray(filters)) {
3548+
filters.forEach(f => {
3549+
if (f && typeof f.dispose === "function") {
3550+
try {
3551+
f.dispose();
3552+
} catch (e) {
3553+
console.debug("Error disposing filter:", e);
3554+
}
3555+
}
3556+
});
3557+
}
3558+
delete instrumentsFilters[turtle][instrumentName];
3559+
}
3560+
}
3561+
3562+
for (const turtle in instrumentsEffects) {
3563+
for (const instrumentName in instrumentsEffects[turtle]) {
3564+
const effects = instrumentsEffects[turtle][instrumentName];
3565+
if (Array.isArray(effects)) {
3566+
effects.forEach(fx => {
3567+
if (fx && typeof fx.dispose === "function") {
3568+
try {
3569+
fx.dispose();
3570+
} catch (e) {
3571+
console.debug("Error disposing effect:", e);
3572+
}
3573+
}
3574+
});
3575+
}
3576+
delete instrumentsEffects[turtle][instrumentName];
3577+
}
3578+
}
3579+
};
3580+
34983581
this.tone = null;
34993582
this.noteFrequencies = {};
35003583
this.startingPitch = "C4";

0 commit comments

Comments
 (0)