Last active
February 4, 2026 16:36
-
-
Save rennfly/24727073cbfc6cf32a9ba4c2cd81e86c to your computer and use it in GitHub Desktop.
TINTINNABULI GENERATOR WITH INTEGER SERIES (De Paiva)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --[[ | |
| 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