Skip to content

Commit 2d5c134

Browse files
committed
Improve audio performance and timer handling
1 parent 895b769 commit 2d5c134

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

js/turtle-singer.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2480,10 +2480,20 @@ class Singer {
24802480
tur.singer.embeddedGraphics[blk] = [];
24812481

24822482
// Ensure note value block unhighlights after note plays.
2483-
setTimeout(() => {
2483+
// Cancel any previously pending unhighlight timer for this block to
2484+
// prevent unbounded timer accumulation in tight infinite loops, which
2485+
// would otherwise saturate the JS timer queue and stall the main thread.
2486+
if (!tur.singer._unhighlightTimers) {
2487+
tur.singer._unhighlightTimers = {};
2488+
}
2489+
if (tur.singer._unhighlightTimers[blk]) {
2490+
clearTimeout(tur.singer._unhighlightTimers[blk]);
2491+
}
2492+
tur.singer._unhighlightTimers[blk] = setTimeout(() => {
24842493
if (activity.blocks.visible && blk in activity.blocks.blockList) {
24852494
activity.blocks.unhighlight(blk);
24862495
}
2496+
delete tur.singer._unhighlightTimers[blk];
24872497
}, beatValue * 1000);
24882498
};
24892499

js/utils/synthutils.js

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@
4848

4949
/**
5050
* The number of voices in polyphony.
51+
* Raised from 3 to 6 to prevent voice exhaustion and audio engine crashes
52+
* during infinite-loop playback with chords or multiple turtles.
5153
* @constant
5254
* @type {number}
53-
* @default 3
55+
* @default 6
5456
*/
55-
const POLYCOUNT = 3;
57+
const POLYCOUNT = 6;
5658

5759
/**
5860
* Array of names and details for various noise synthesizers.
@@ -1727,6 +1729,59 @@ function Synth() {
17271729
console.debug("Error triggering note:", e);
17281730
}
17291731
} else {
1732+
// ── Perf fix: fast-path for notes with no real graph-level effect nodes ──
1733+
// doPartials and doPortamento only mutate synth properties in-place and
1734+
// do NOT require new audio graph nodes. Skipping disconnect/reconnect
1735+
// for every plain note eliminates per-note audio-graph rewiring, which
1736+
// is the primary cause of buffer underruns in long/infinite sessions.
1737+
const _needsGraphRewire =
1738+
(paramsFilters !== null &&
1739+
paramsFilters !== undefined &&
1740+
paramsFilters.length > 0) ||
1741+
(paramsEffects !== null &&
1742+
paramsEffects !== undefined &&
1743+
(paramsEffects.doVibrato ||
1744+
paramsEffects.doDistortion ||
1745+
paramsEffects.doTremolo ||
1746+
paramsEffects.doPhaser ||
1747+
paramsEffects.doChorus ||
1748+
paramsEffects.doNeighbor));
1749+
1750+
if (!_needsGraphRewire) {
1751+
// Apply in-place property mutations then take the fast path.
1752+
if (paramsEffects !== null && paramsEffects !== undefined) {
1753+
if (paramsEffects.doPartials) {
1754+
if (synth.oscillator !== undefined) {
1755+
synth.oscillator.partials = paramsEffects.partials;
1756+
} else if (synth.voices !== undefined) {
1757+
for (let i = 0; i < synth.voices.length; i++) {
1758+
if (synth.voices[i].oscillator) {
1759+
synth.voices[i].oscillator.partials =
1760+
paramsEffects.partials;
1761+
}
1762+
}
1763+
}
1764+
}
1765+
if (paramsEffects.doPortamento) {
1766+
if (synth.oscillator !== undefined) {
1767+
synth.portamento = paramsEffects.portamento;
1768+
} else if (synth.voices !== undefined) {
1769+
for (let i = 0; i < synth.voices.length; i++) {
1770+
synth.voices[i].portamento = paramsEffects.portamento;
1771+
}
1772+
}
1773+
}
1774+
}
1775+
try {
1776+
await Tone.ToneAudioBuffer.loaded();
1777+
synth.triggerAttackRelease(notes, beatValue, Tone.now() + future);
1778+
} catch (e) {
1779+
console.debug("Error triggering note (no-graph-rewire fast path):", e);
1780+
}
1781+
return;
1782+
}
1783+
// ─────────────────────────────────────────────────────────────────────
1784+
17301785
// Remove the dry path so effects are routed serially, not in parallel
17311786
synth.disconnect(Tone.Destination);
17321787
const chainNodes = [];
@@ -1871,7 +1926,10 @@ function Synth() {
18711926
}
18721927
}
18731928

1874-
// Schedule cleanup after the note duration
1929+
// Schedule cleanup after the note duration.
1930+
// A 500 ms safety buffer is added beyond the note duration to prevent
1931+
// premature disposal caused by audio-clock drift or scheduler jitter,
1932+
// which would otherwise produce crackling artefacts in long sessions.
18751933
setTimeout(() => {
18761934
try {
18771935
// Dispose of effects
@@ -1892,7 +1950,7 @@ function Synth() {
18921950
} catch (e) {
18931951
console.debug("Error disposing effects:", e);
18941952
}
1895-
}, beatValue * 1000);
1953+
}, beatValue * 1000 + 500);
18961954
}
18971955
} catch (e) {
18981956
console.error("Error in _performNotes:", e);

0 commit comments

Comments
 (0)