Skip to content

Instantly share code, notes, and snippets.

@rennfly
Last active February 4, 2026 16:36
Show Gist options
  • Select an option

  • Save rennfly/24727073cbfc6cf32a9ba4c2cd81e86c to your computer and use it in GitHub Desktop.

Select an option

Save rennfly/24727073cbfc6cf32a9ba4c2cd81e86c to your computer and use it in GitHub Desktop.
TINTINNABULI GENERATOR WITH INTEGER SERIES (De Paiva)
--[[
Script: Spiegel Structure Generator (Cumulative Version)
Description: Generates the AUTHENTIC stepwise expanding structure.
Now includes:
- Stepwise filling (0, 1, 2, 3... instead of just 0, 3)
- Velocity Humanization
- Arpeggio Variations
Requirements: ReaImGui
Author: Gemini
]]
-- --- 1. DEPENDENCIES ---
if not reaper.ImGui_GetBuiltinPath then
reaper.ShowMessageBox("ReaImGui is required.", "Error", 0)
return
end
local ctx = reaper.ImGui_CreateContext("Spiegel Generator")
-- --- 2. MUSICAL DATA ---
local NOTE_NAMES = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}
local NOTE_NAMES_STR = "C\0C#\0D\0D#\0E\0F\0F#\0G\0G#\0A\0A#\0B\0"
local SCALES = {
{name = "Major", intervals = {0, 2, 4, 5, 7, 9, 11}, triad = {0, 4, 7}},
{name = "Minor", intervals = {0, 2, 3, 5, 7, 8, 10}, triad = {0, 3, 7}}
}
local SCALES_STR = "Major\0Minor\0"
-- --- 3. PARAMETERS ---
local UI_SEL_NOTE = 5 -- F
local UI_SEL_SCALE = 0 -- Major
local UI_OCTAVE = 4
local UI_STEPS = 6 -- Number of iterations (Phrases get longer!)
local UI_STEP_FILL = true -- True = Walk the scale (Spiegel), False = Jump
local UI_VEL_VAR = 15 -- Humanize velocity amount
local M_NOTE_LEN = 3 -- Melody length (3/4)
local GAP_TICKS = 10
-- --- 4. LOGIC ---
local function get_root_midi()
return (UI_OCTAVE + 1) * 12 + UI_SEL_NOTE
end
local function get_scale_note(root, degree, scale_intervals)
local scale_len = #scale_intervals
local octave_shift = math.floor(degree / scale_len)
if degree < 0 and (degree % scale_len ~= 0) then octave_shift = octave_shift end
local idx = (degree % scale_len) + 1
return root + (octave_shift * 12) + scale_intervals[idx]
end
local function get_triad_notes(melody_note, root, triad_intervals)
local pool = {}
local base_root = root - 24
for oct = 0, 4 do
for _, interval in ipairs(triad_intervals) do
table.insert(pool, base_root + (oct * 12) + interval)
end
end
table.sort(pool)
local closest_idx = 1
local min_dist = 1000
for i, pitch in ipairs(pool) do
local dist = math.abs(pitch - melody_note)
if dist < min_dist then
min_dist = dist
closest_idx = i
end
end
-- Standard Spiegel Pattern: Low - High - Low
local t1 = pool[closest_idx - 1] or pool[closest_idx]
local t2 = pool[closest_idx + 1] or pool[closest_idx]
return {t1, t2, t1}
end
-- --- 5. GENERATOR ---
local function generate_spiegel()
reaper.Undo_BeginBlock()
reaper.PreventUIRefresh(1)
local root_midi = get_root_midi()
local scale_data = SCALES[UI_SEL_SCALE + 1]
local scale_ints = scale_data.intervals
local triad_ints = scale_data.triad
local start_pos = reaper.GetCursorPosition()
-- Create Tracks
local track_idx = reaper.CountTracks(0)
reaper.InsertTrackAtIndex(track_idx, true)
local trM = reaper.GetTrack(0, track_idx)
reaper.GetSetMediaTrackInfo_String(trM, "P_NAME", "M-Voice (Violin)", true)
reaper.InsertTrackAtIndex(track_idx+1, true)
local trT = reaper.GetTrack(0, track_idx+1)
reaper.GetSetMediaTrackInfo_String(trT, "P_NAME", "T-Voice (Piano RH)", true)
reaper.InsertTrackAtIndex(track_idx+2, true)
local trB = reaper.GetTrack(0, track_idx+2)
reaper.GetSetMediaTrackInfo_String(trB, "P_NAME", "Bass (Piano LH)", true)
local itemM = reaper.CreateNewMIDIItemInProj(trM, start_pos, start_pos + 60, false)
local itemT = reaper.CreateNewMIDIItemInProj(trT, start_pos, start_pos + 60, false)
local itemB = reaper.CreateNewMIDIItemInProj(trB, start_pos, start_pos + 60, false)
local takeM = reaper.GetActiveTake(itemM)
local takeT = reaper.GetActiveTake(itemT)
local takeB = reaper.GetActiveTake(itemB)
local current_qn = reaper.TimeMap2_timeToQN(0, start_pos)
-- --- CONSTRUCT SEQUENCE (THE FIX) ---
local sequence_degrees = {}
-- Always start with center
table.insert(sequence_degrees, 0)
for i = 1, UI_STEPS do
-- 1. ASCENDING PHRASE (0 -> 1 -> ... -> i -> ... -> 1 -> 0)
if UI_STEP_FILL then
-- Go Up
for step = 1, i do table.insert(sequence_degrees, step) end
-- Go Down (Mirror)
for step = i-1, 0, -1 do table.insert(sequence_degrees, step) end
else
-- Jump Mode (Old way)
table.insert(sequence_degrees, i)
table.insert(sequence_degrees, 0)
end
-- 2. DESCENDING PHRASE (0 -> -1 -> ... -> -i -> ... -> -1 -> 0)
if UI_STEP_FILL then
-- Go Down
for step = 1, i do table.insert(sequence_degrees, -step) end
-- Go Up (Mirror)
for step = i-1, 0, -1 do table.insert(sequence_degrees, -step) end
else
table.insert(sequence_degrees, -i)
table.insert(sequence_degrees, 0)
end
end
-- --- WRITE NOTES ---
for idx, degree in ipairs(sequence_degrees) do
-- M-Voice
local m_pitch = get_scale_note(root_midi, degree, scale_ints)
local start_sec = reaper.TimeMap2_QNToTime(0, current_qn)
local end_sec = reaper.TimeMap2_QNToTime(0, current_qn + M_NOTE_LEN)
local ppq_start = reaper.MIDI_GetPPQPosFromProjTime(takeM, start_sec)
local ppq_end = reaper.MIDI_GetPPQPosFromProjTime(takeM, end_sec) - GAP_TICKS
-- Velocity: Base 90 + random variation
local vel = 90 + math.random(-UI_VEL_VAR, UI_VEL_VAR)
reaper.MIDI_InsertNote(takeM, false, false, ppq_start, ppq_end, 0, m_pitch, vel, false)
-- T-Voice (Arpeggio)
local t_notes = get_triad_notes(m_pitch, root_midi, triad_ints)
local arp_len = M_NOTE_LEN / 3
for k, t_pitch in ipairs(t_notes) do
local t_qn_start = current_qn + (k-1)*arp_len
local t_sec_s = reaper.TimeMap2_QNToTime(0, t_qn_start)
local t_sec_e = reaper.TimeMap2_QNToTime(0, t_qn_start + arp_len)
local t_ppq_s = reaper.MIDI_GetPPQPosFromProjTime(takeT, t_sec_s)
local t_ppq_e = reaper.MIDI_GetPPQPosFromProjTime(takeT, t_sec_e) - GAP_TICKS
-- T-Voice Velocity slightly softer
reaper.MIDI_InsertNote(takeT, false, false, t_ppq_s, t_ppq_e, 0, t_pitch, vel - 15, false)
end
-- Bass (Only on center or long pauses)
if degree == 0 then
local b_pitch = root_midi - 24
-- 20% chance to play the 5th for subtle variation
if math.random() > 0.8 then b_pitch = b_pitch + 7 end
reaper.MIDI_InsertNote(takeB, false, false, ppq_start, ppq_end, 0, b_pitch, vel - 10, false)
end
current_qn = current_qn + M_NOTE_LEN
end
-- Finish
local final_time = reaper.TimeMap2_QNToTime(0, current_qn)
reaper.SetMediaItemInfo_Value(itemM, "D_LENGTH", final_time - start_pos)
reaper.SetMediaItemInfo_Value(itemT, "D_LENGTH", final_time - start_pos)
reaper.SetMediaItemInfo_Value(itemB, "D_LENGTH", final_time - start_pos)
reaper.MIDI_Sort(takeM)
reaper.MIDI_Sort(takeT)
reaper.MIDI_Sort(takeB)
reaper.UpdateArrange()
reaper.PreventUIRefresh(-1)
reaper.Undo_EndBlock("Spiegel Gen", -1)
end
-- --- 6. UI LOOP ---
local function main_loop()
local visible, open = reaper.ImGui_Begin(ctx, "Spiegel Gen (Cumulative)", true, reaper.ImGui_WindowFlags_AlwaysAutoResize())
if visible then
reaper.ImGui_Text(ctx, "Musical Settings")
local _, new_note = reaper.ImGui_Combo(ctx, "Root", UI_SEL_NOTE, NOTE_NAMES_STR)
if _ then UI_SEL_NOTE = new_note end
local _, new_scale = reaper.ImGui_Combo(ctx, "Mode", UI_SEL_SCALE, SCALES_STR)
if _ then UI_SEL_SCALE = new_scale end
local _, new_oct = reaper.ImGui_SliderInt(ctx, "Octave", UI_OCTAVE, 1, 6)
if _ then UI_OCTAVE = new_oct end
reaper.ImGui_Separator(ctx)
reaper.ImGui_Text(ctx, "Structure")
-- FILL STEPS: The most important fix
local _, new_fill = reaper.ImGui_Checkbox(ctx, "Stepwise Fill (Authentic)", UI_STEP_FILL)
if _ then UI_STEP_FILL = new_fill end
local _, new_steps = reaper.ImGui_SliderInt(ctx, "Expansion Cycles", UI_STEPS, 2, 16)
if _ then UI_STEPS = new_steps end
local _, new_vel = reaper.ImGui_SliderInt(ctx, "Humanize Velocity", UI_VEL_VAR, 0, 40)
if _ then UI_VEL_VAR = new_vel end
reaper.ImGui_Separator(ctx)
local w, _ = reaper.ImGui_GetContentRegionAvail(ctx)
if reaper.ImGui_Button(ctx, "GENERATE", w, 40) then
generate_spiegel()
end
reaper.ImGui_End(ctx)
end
if open then reaper.defer(main_loop) end
end
reaper.defer(main_loop)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment