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.