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