Phrase script glitching audio when changing input parameters

I have this phrase script here;

-- OTHELLO / REVERSI MELODY GENERATOR
-- The defining mechanic: placing a disc that OUTFLANKS a line of
-- opponent discs FLIPS all of them to your colour in one move.
-- A single placement can trigger cascading flips in up to 8
-- directions simultaneously — this ripple is the musical heart
-- of the piece.
--
-- ══════════════════════════════════════════════════════════════════
-- BOARD AS PITCH SPACE:
-- ══════════════════════════════════════════════════════════════════
--   Column (a-h) → scale degree.  Row (1-8) → register (register_span
--   sets how many semitones separate row 1 from row 8 — narrow = an
--   intimate cluster, wide = a dramatic sweep across octaves).
--   Panning follows column: a=left, h=right.
--
-- ══════════════════════════════════════════════════════════════════
-- THE CASCADE:
-- ══════════════════════════════════════════════════════════════════
--   Each legal move outflanks 1-4+ lines of opponent discs at once.
--   Every outflanked line becomes its own outward-rippling arpeggio,
--   staggered by CASCADE_SPEED — fast settings sound like a frantic
--   chain reaction, slow settings feel like a majestic unfurling wave.
--   Three cascade characters are available (CASCADE_STYLE):
--     Diatonic run   — flipped squares sound their own natural pitch
--     Octave echo    — each flipped square doubles with an octave-up echo
--     Chromatic slide — a smooth glissando from anchor to new piece
--
-- ══════════════════════════════════════════════════════════════════
-- CORNERS:
-- ══════════════════════════════════════════════════════════════════
--   Corners can never be flipped — the single most valuable square
--   in real Othello strategy. CORNER_EMPHASIS makes corner placements
--   sound with extra volume and a sustained low pedal echo — an
--   anchor point the ear can always locate.
--
-- ══════════════════════════════════════════════════════════════════
-- TENSION & ENDGAME:
-- ══════════════════════════════════════════════════════════════════
--   TENSION_CURVE gradually introduces soft dissonant grace notes as
--   the board fills — the mounting pressure of a shrinking board.
--   When neither player has a legal move, the game ends and the
--   FINAL TALLY resolves into a chord — darker/lower if Black holds
--   the majority of squares, brighter/higher if White does.

local scale_names = {
  "major","minor","dorian","phrygian","lydian","mixolydian",
  "whole tone","harmonic minor","pentatonic major","pentatonic minor",
  "enigmatic","chromatic",
}

local cascade_styles = {
  "Diatonic run (natural scale pitches)",
  "Octave echo (doubled at the octave)",
  "Chromatic slide (smooth glissando)",
}

local DIRS = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}

return pattern {
  parameter = {
    parameter.integer("root_midi",    36, {12, 72},   "Root / a1 Pitch (MIDI)"),
    parameter.enum("scale_mode","dorian",scale_names,  "Scale"),
    parameter.integer("steps_per_move", 4, {1, 16},   "Steps per Move"),
    parameter.number("chaos",         0.30,{0.0, 0.9},"Chaos / Weak Play",
      "0=strong corner-seeking play. 0.9=careless, random placements"),
    parameter.enum("cascade_style", cascade_styles[1], cascade_styles, "Cascade Style"),
    parameter.number("cascade_speed", 0.55,{0.0, 1.0}, "Cascade Speed",
      "0=majestic slow ripple. 1=frantic instant chain reaction"),
    parameter.number("corner_emphasis",0.70,{0.0, 1.0},"Corner Emphasis",
      "How much louder/more resonant corner placements are"),
    parameter.number("vol_black",     0.85,{0.1, 1.0}, "Black Volume"),
    parameter.number("vol_white",     0.68,{0.1, 1.0}, "White Volume"),
    parameter.number("vol_cascade",   0.55,{0.0, 1.0}, "Cascade Ripple Volume"),
    parameter.number("register_span", 24.0,{6.0, 48.0},"Register Span (semitones)",
      "Total pitch range spanned by the 8 rows. Wide=dramatic, narrow=intimate"),
    parameter.number("tension_curve", 0.30,{0.0, 1.0}, "Tension Curve",
      "Dissonant grace notes grow more frequent as the board fills"),
    parameter.number("spread",        0.85,{0.0, 1.0}, "Stereo Spread"),
    parameter.number("game_pause",    0.25,{0.0, 1.0}, "Pause Between Games"),
    parameter.integer("rand_seed",       1,{1, 99999},  "Seed"),
  },

  unit = "1/16",

  event = function(context)
    local p = context.parameter
    if context.playback == "seeking" then return nil end

    local s   = context.step - 1
    local spm = math.max(1, p.steps_per_move)

    local sc    = scale(p.root_midi, p.scale_mode)
    local notes = sc.notes
    local nlen  = #notes

    local function snap(midi)
      local best=midi; local bd=999
      for oct=-3,4 do
        for _,n in ipairs(notes) do
          local nn=n+oct*12
          local d=math.abs(nn-midi)
          if d<bd then bd=d;best=nn end
        end
      end
      return math.max(0,math.min(127,best))
    end

    local function square_midi(col,row)
      local sc_idx = ((col-1) % nlen) + 1
      local reg_off = (row-1) * (p.register_span / 7)
      return snap(notes[sc_idx] + math.floor(reg_off + 0.5))
    end

    local function square_pan(col)
      return math.max(-1,math.min(1, ((col-1)/7 - 0.5) * 2 * p.spread))
    end

    local function is_corner(col,row)
      return (col==1 or col==8) and (row==1 or row==8)
    end

    -- ── FIND FLIP LINES ────────────────────────────────────────────
    local function find_flip_lines(board, col, row, color)
      if board[col][row] ~= 0 then return {} end
      local lines = {}
      for _, d in ipairs(DIRS) do
        local dc, dr = d[1], d[2]
        local c, r = col+dc, row+dr
        local line = {}
        while c>=1 and c<=8 and r>=1 and r<=8 and board[c][r] == -color do
          line[#line+1] = {c,r}
          c=c+dc; r=r+dr
        end
        if #line>0 and c>=1 and c<=8 and r>=1 and r<=8 and board[c][r]==color then
          lines[#lines+1] = line
        end
      end
      return lines
    end

    -- ── SIMULATE ONE FULL GAME ──────────────────────────────────────
    local function simulate_game(seed)
      math.randomseed(seed)
      local board = {}
      for c=1,8 do board[c] = {0,0,0,0,0,0,0,0} end
      board[4][4]=-1; board[5][5]=-1; board[4][5]=1; board[5][4]=1

      local moves = {}
      local color = 1
      local consecutive_passes = 0

      for mv = 1, 60 do
        local empties = {}
        for c=1,8 do for r=1,8 do if board[c][r]==0 then empties[#empties+1]={c,r} end end end
        if #empties == 0 then break end

        for i=#empties,2,-1 do
          local j = math.random(i)
          empties[i], empties[j] = empties[j], empties[i]
        end
        table.sort(empties, function(a,b)
          local ac = is_corner(a[1],a[2])
          local bc = is_corner(b[1],b[2])
          if ac and not bc then return true end
          if bc and not ac then return false end
          return false
        end)

        local chosen, lines
        for _, sq in ipairs(empties) do
          local fl = find_flip_lines(board, sq[1], sq[2], color)
          if #fl > 0 then
            local corner = is_corner(sq[1],sq[2])
            if corner or math.random() > p.chaos*0.5 then
              chosen = sq; lines = fl; break
            end
          end
        end
        if not chosen then
          for _, sq in ipairs(empties) do
            local fl = find_flip_lines(board, sq[1], sq[2], color)
            if #fl > 0 then chosen=sq; lines=fl; break end
          end
        end

        if chosen then
          board[chosen[1]][chosen[2]] = color
          for _, line in ipairs(lines) do
            for _, sq in ipairs(line) do board[sq[1]][sq[2]] = color end
          end
          moves[#moves+1] = {sq=chosen, color=color, lines=lines, pass=false}
          consecutive_passes = 0
        else
          moves[#moves+1] = {sq=nil, color=color, lines={}, pass=true}
          consecutive_passes = consecutive_passes + 1
          if consecutive_passes >= 2 then break end
        end
        color = -color
      end

      local black_c, white_c = 0, 0
      for c=1,8 do for r=1,8 do
        if board[c][r]==1 then black_c=black_c+1
        elseif board[c][r]==-1 then white_c=white_c+1 end
      end end

      return moves, black_c, white_c
    end

    -- ── TIMING ───────────────────────────────────────────────────────
    local move_slot = spm
    local finale_len = spm * 4
    local pause_steps = math.max(1, math.floor(spm * p.game_pause))

    -- Rough upper bound; actual game may be shorter (handled below)
    local max_game_moves = 60
    local game_len = max_game_moves * move_slot + finale_len + pause_steps

    local game_num    = math.floor(s / game_len)
    local pos_in_game = s % game_len

    local moves, black_c, white_c = simulate_game(game_num * 65537 + p.rand_seed * 9973)
    local n_moves = #moves
    local active_len = n_moves * move_slot

    local result = {}

    if pos_in_game < active_len then
      local move_idx = math.floor(pos_in_game / move_slot) + 1
      local step_in  = pos_in_game % move_slot
      if step_in ~= 0 then return nil end

      move_idx = math.max(1, math.min(n_moves, move_idx))
      local mv = moves[move_idx]
      if mv.pass or not mv.sq then return nil end

      local col, row = mv.sq[1], mv.sq[2]
      local midi = square_midi(col,row)
      local pan  = square_pan(col)
      local vol  = (mv.color==1) and p.vol_black or p.vol_white
      local corner = is_corner(col,row)
      if corner then vol = math.min(1.0, vol * (1 + p.corner_emphasis * 0.5)) end

      result[#result+1] = note(midi):volume(math.max(0.04,math.min(1.0,vol))):panning(pan):delay(0.0)

      if corner and p.corner_emphasis > 0 then
        result[#result+1] = note(math.max(0,math.min(127,midi-12)))
          :volume(vol * p.corner_emphasis * 0.4)
          :panning(pan)
          :delay(0.02)
      end

      -- ── CASCADE: each flip line ripples outward ────────────────────
      local base_incr = 0.16 * (1 - p.cascade_speed) + 0.02
      for li, line in ipairs(mv.lines) do
        local incr = base_incr
        local cum  = 0.05 + (li-1)*0.02
        for idx, sq in ipairs(line) do
          cum = math.min(0.85, cum + (idx>1 and incr or 0))
          incr = incr * 0.75

          local fmidi
          if p.cascade_style == cascade_styles[3] then
            -- Chromatic slide from anchor toward the new piece pitch
            local t = idx / #line
            fmidi = math.floor(midi + (square_midi(sq[1],sq[2]) - midi) * t + 0.5)
          else
            fmidi = square_midi(sq[1], sq[2])
          end
          fmidi = math.max(0,math.min(127,fmidi))

          result[#result+1] = note(fmidi)
            :volume(math.max(0.03,math.min(1.0, p.vol_cascade * vol / math.max(0.3,vol))))
            :panning(square_pan(sq[1]))
            :delay(cum)

          if p.cascade_style == cascade_styles[2] then
            local echo = math.max(0,math.min(127, fmidi+12))
            result[#result+1] = note(echo)
              :volume(p.vol_cascade * 0.4)
              :panning(square_pan(sq[1]))
              :delay(math.min(0.9, cum+0.03))
          end
        end
      end

      -- ── TENSION: dissonant grace note as board fills ────────────────
      local fill_frac = move_idx / math.max(1, n_moves)
      math.randomseed(move_idx * 997 + p.rand_seed)
      if math.random() < p.tension_curve * fill_frac then
        local diss = math.max(0,math.min(127, midi + 1))
        result[#result+1] = note(diss)
          :volume(vol * 0.30)
          :panning(-pan*0.4)
          :delay(0.06)
      end

    elseif pos_in_game < active_len + finale_len then
      -- ── FINAL TALLY CHORD ─────────────────────────────────────────
      local fpos  = pos_in_game - active_len
      local fstep = math.floor(fpos)
      if fstep < 4 then
        local majority_black = black_c >= white_c
        local base_row = majority_black and 2 or 6
        local col = fstep * 2 + 1
        col = math.max(1,math.min(8,col))
        local midi = square_midi(col, base_row)
        local margin = math.abs(black_c - white_c) / 64
        local vol = 0.5 + margin * 0.5
        result[#result+1] = note(midi)
          :volume(math.max(0.05,math.min(1.0,vol)))
          :panning(square_pan(col))
          :delay(fstep*0.04)
      end
    end
    -- else: pause between games

    if #result == 0 then return nil end
    return result
  end
}

which generates notes when played, but when I change the input parameters here during playback, the audio glitches. This is with samples and or vst instruments.

Strangely when using the same phrase script in the patterns playground website, there are no glitches when changing input parameter values.

nevermind, this fixes the stutters in renoise;

-- OTHELLO / REVERSI MELODY GENERATOR
local scale_names = {
  "major","minor","dorian","phrygian","lydian","mixolydian",
  "whole tone","harmonic minor","pentatonic major","pentatonic minor",
  "enigmatic","chromatic",
}

local cascade_styles = {
  "Diatonic run (natural scale pitches)",
  "Octave echo (doubled at the octave)",
  "Chromatic slide (smooth glissando)",
}

local DIRS = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}

-- Global simulation cache to prevent re-simulating every step
local game_cache = {
  key = nil,
  moves = nil,
  black_c = 0,
  white_c = 0
}

return pattern {
  parameter = {
    parameter.integer("root_midi",    36, {12, 72},   "Root / a1 Pitch (MIDI)"),
    parameter.enum("scale_mode","dorian",scale_names,  "Scale"),
    parameter.integer("steps_per_move", 4, {1, 16},   "Steps per Move"),
    parameter.number("chaos",         0.30,{0.0, 0.9},"Chaos / Weak Play",
      "0=strong corner-seeking play. 0.9=careless, random placements"),
    parameter.enum("cascade_style", cascade_styles[1], cascade_styles, "Cascade Style"),
    parameter.number("cascade_speed", 0.55,{0.0, 1.0}, "Cascade Speed",
      "0=majestic slow ripple. 1=frantic instant chain reaction"),
    parameter.number("corner_emphasis",0.70,{0.0, 1.0},"Corner Emphasis",
      "How much louder/more resonant corner placements are"),
    parameter.number("vol_black",     0.85,{0.1, 1.0}, "Black Volume"),
    parameter.number("vol_white",     0.68,{0.1, 1.0}, "White Volume"),
    parameter.number("vol_cascade",   0.55,{0.0, 1.0}, "Cascade Ripple Volume"),
    parameter.number("register_span", 24.0,{6.0, 48.0},"Register Span (semitones)",
      "Total pitch range spanned by the 8 rows. Wide=dramatic, narrow=intimate"),
    parameter.number("tension_curve", 0.30,{0.0, 1.0}, "Tension Curve",
      "Dissonant grace notes grow more frequent as the board fills"),
    parameter.number("spread",        0.85,{0.0, 1.0}, "Stereo Spread"),
    parameter.number("game_pause",    0.25,{0.0, 1.0}, "Pause Between Games"),
    parameter.integer("rand_seed",       1,{1, 99999},  "Seed"),
  },

  unit = "1/16",

  event = function(context)
    local p = context.parameter
    if context.playback == "seeking" then return nil end

    local s   = context.step - 1
    local spm = math.max(1, p.steps_per_move)

    local sc    = scale(p.root_midi, p.scale_mode)
    local notes = sc.notes
    local nlen  = #notes

    local function snap(midi)
      local best=midi; local bd=999
      for oct=-3,4 do
        for _,n in ipairs(notes) do
          local nn=n+oct*12
          local d=math.abs(nn-midi)
          if d<bd then bd=d;best=nn end
        end
      end
      return math.max(0,math.min(127,best))
    end

    local function square_midi(col,row)
      local sc_idx = ((col-1) % nlen) + 1
      local reg_off = (row-1) * (p.register_span / 7)
      return snap(notes[sc_idx] + math.floor(reg_off + 0.5))
    end

    local function square_pan(col)
      return math.max(-1,math.min(1, ((col-1)/7 - 0.5) * 2 * p.spread))
    end

    local function is_corner(col,row)
      return (col==1 or col==8) and (row==1 or row==8)
    end

    local function find_flip_lines(board, col, row, color)
      if board[col][row] ~= 0 then return {} end
      local lines = {}
      for _, d in ipairs(DIRS) do
        local dc, dr = d[1], d[2]
        local c, r = col+dc, row+dr
        local line = {}
        while c>=1 and c<=8 and r>=1 and r<=8 and board[c][r] == -color do
          line[#line+1] = {c,r}
          c=c+dc; r=r+dr
        end
        if #line>0 and c>=1 and c<=8 and r>=1 and r<=8 and board[c][r]==color then
          lines[#lines+1] = line
        end
      end
      return lines
    end

    local function simulate_game(seed)
      math.randomseed(seed)
      local board = {}
      for c=1,8 do board[c] = {0,0,0,0,0,0,0,0} end
      board[4][4]=-1; board[5][5]=-1; board[4][5]=1; board[5][4]=1

      local moves = {}
      local color = 1
      local consecutive_passes = 0

      for mv = 1, 60 do
        local empties = {}
        for c=1,8 do for r=1,8 do if board[c][r]==0 then empties[#empties+1]={c,r} end end end
        if #empties == 0 then break end

        for i=#empties,2,-1 do
          local j = math.random(i)
          empties[i], empties[j] = empties[j], empties[i]
        end
        table.sort(empties, function(a,b)
          local ac = is_corner(a[1],a[2])
          local bc = is_corner(b[1],b[2])
          if ac and not bc then return true end
          if bc and not ac then return false end
          return false
        end)

        local chosen, lines
        for _, sq in ipairs(empties) do
          local fl = find_flip_lines(board, sq[1], sq[2], color)
          if #fl > 0 then
            local corner = is_corner(sq[1],sq[2])
            if corner or math.random() > p.chaos*0.5 then
              chosen = sq; lines = fl; break
            end
          end
        end
        if not chosen then
          for _, sq in ipairs(empties) do
            local fl = find_flip_lines(board, sq[1], sq[2], color)
            if #fl > 0 then chosen=sq; lines=fl; break end
          end
        end

        if chosen then
          board[chosen[1]][chosen[2]] = color
          for _, line in ipairs(lines) do
            for _, sq in ipairs(line) do board[sq[1]][sq[2]] = color end
          end
          moves[#moves+1] = {sq=chosen, color=color, lines=lines, pass=false}
          consecutive_passes = 0
        else
          moves[#moves+1] = {sq=nil, color=color, lines={}, pass=true}
          consecutive_passes = consecutive_passes + 1
          if consecutive_passes >= 2 then break end
        end
        color = -color
      end

      local black_c, white_c = 0, 0
      for c=1,8 do for r=1,8 do
        if board[c][r]==1 then black_c=black_c+1
        elseif board[c][r]==-1 then white_c=white_c+1 end
      end end

      return moves, black_c, white_c
    end

    -- ── TIMING & CACHED SIMULATION ──────────────────────────────────
    local move_slot = spm
    local finale_len = spm * 4
    local pause_steps = math.max(1, math.floor(spm * p.game_pause))

    local max_game_moves = 60
    local game_len = max_game_moves * move_slot + finale_len + pause_steps

    local game_num    = math.floor(s / game_len)
    local pos_in_game = s % game_len

    -- Create a unique key for parameters that affect the game simulation
    local sim_seed = game_num * 65537 + p.rand_seed * 9973
    local cache_key = string.format("%d_%f", sim_seed, p.chaos)

    if game_cache.key ~= cache_key then
      local m, b, w = simulate_game(sim_seed)
      game_cache.key = cache_key
      game_cache.moves = m
      game_cache.black_c = b
      game_cache.white_c = w
    end

    local moves, black_c, white_c = game_cache.moves, game_cache.black_c, game_cache.white_c
    local n_moves = #moves
    local active_len = n_moves * move_slot

    local result = {}

    if pos_in_game < active_len then
      local move_idx = math.floor(pos_in_game / move_slot) + 1
      local step_in  = pos_in_game % move_slot
      if step_in ~= 0 then return nil end

      move_idx = math.max(1, math.min(n_moves, move_idx))
      local mv = moves[move_idx]
      if mv.pass or not mv.sq then return nil end

      local col, row = mv.sq[1], mv.sq[2]
      local midi = square_midi(col,row)
      local pan  = square_pan(col)
      local vol  = (mv.color==1) and p.vol_black or p.vol_white
      local corner = is_corner(col,row)
      if corner then vol = math.min(1.0, vol * (1 + p.corner_emphasis * 0.5)) end

      result[#result+1] = note(midi):volume(math.max(0.04,math.min(1.0,vol))):panning(pan):delay(0.0)

      if corner and p.corner_emphasis > 0 then
        result[#result+1] = note(math.max(0,math.min(127,midi-12)))
          :volume(vol * p.corner_emphasis * 0.4)
          :panning(pan)
          :delay(0.02)
      end

      -- ── CASCADE ───────────────────────────────────────────────────
      local base_incr = 0.16 * (1 - p.cascade_speed) + 0.02
      for li, line in ipairs(mv.lines) do
        local incr = base_incr
        local cum  = 0.05 + (li-1)*0.02
        for idx, sq in ipairs(line) do
          cum = math.min(0.85, cum + (idx>1 and incr or 0))
          incr = incr * 0.75

          local fmidi
          if p.cascade_style == cascade_styles[3] then
            local t = idx / #line
            fmidi = math.floor(midi + (square_midi(sq[1],sq[2]) - midi) * t + 0.5)
          else
            fmidi = square_midi(sq[1], sq[2])
          end
          fmidi = math.max(0,math.min(127,fmidi))

          result[#result+1] = note(fmidi)
            :volume(math.max(0.03,math.min(1.0, p.vol_cascade * vol / math.max(0.3,vol))))
            :panning(square_pan(sq[1]))
            :delay(cum)

          if p.cascade_style == cascade_styles[2] then
            local echo = math.max(0,math.min(127, fmidi+12))
            result[#result+1] = note(echo)
              :volume(p.vol_cascade * 0.4)
              :panning(square_pan(sq[1]))
              :delay(math.min(0.9, cum+0.03))
          end
        end
      end

      -- ── TENSION ───────────────────────────────────────────────────
      local fill_frac = move_idx / math.max(1, n_moves)
      math.randomseed(move_idx * 997 + p.rand_seed)
      if math.random() < p.tension_curve * fill_frac then
        local diss = math.max(0,math.min(127, midi + 1))
        result[#result+1] = note(diss)
          :volume(vol * 0.30)
          :panning(-pan*0.4)
          :delay(0.06)
      end

    elseif pos_in_game < active_len + finale_len then
      -- ── FINAL TALLY CHORD ─────────────────────────────────────────
      local fpos  = pos_in_game - active_len
      local fstep = math.floor(fpos)
      if fstep < 4 then
        local majority_black = black_c >= white_c
        local base_row = majority_black and 2 or 6
        local col = fstep * 2 + 1
        col = math.max(1,math.min(8,col))
        local midi = square_midi(col, base_row)
        local margin = math.abs(black_c - white_c) / 64
        local vol = 0.5 + margin * 0.5
        result[#result+1] = note(midi)
          :volume(math.max(0.05,math.min(1.0,vol)))
          :panning(square_pan(col))
          :delay(fstep*0.04)
      end
    end

    if #result == 0 then return nil end
    return result
  end
}