Skip to content

Instantly share code, notes, and snippets.

@0gust1
Last active November 24, 2025 22:34
Show Gist options
  • Select an option

  • Save 0gust1/98666bcde73846af69022ff26cc39e2d to your computer and use it in GitHub Desktop.

Select an option

Save 0gust1/98666bcde73846af69022ff26cc39e2d to your computer and use it in GitHub Desktop.
Renoise Pattrns script - ★ Melody Generator - "Exotic Scales + Renoise Scales" edition
-- ★ Melody Generator - "Exotic Scales + Renoise Scales" edition
-- Similar to the `melody_synth_sparse_motif.lua` but with custom scale definitions
--
-- ---
-- - same motif-based composition approach as melody_synth_sparse_motif.lua
-- - but with additional scales (not truly microtonal: 12-TET approximations)
-- - supports all built-in Renoise scales
--
-- SCALE ACCURACY CATEGORIES:
-- (**Do not** trust blindly this indicator - maybe inaccurate)
-- Each custom scale is prefixed with an accuracy indicator:
-- • ✓ Compatible - Maps perfectly to 12-TET with no compromise
-- • ~ Approximated - Minor tuning compromises (5-15¢ errors) but character preserved
-- • ✗ Heavily Distorted - Significant errors (>20¢) that alter the scale's intended character
--
-- AVAILABLE CUSTOM SCALES:
--
-- WENDY CARLOS SCALES (12-TET APPROXIMATIONS):
-- • ✗ Alpha - Carlos's first microtonal scale (~78¢/step → 100¢/step, heavily distorted)
-- • ✗ Beta - More consonant variant (~63.8¢/step → varies, heavily distorted)
--
-- AFRICAN SCALES:
-- • ✓ Pygmy - Central African pentatonic scale used in Pygmy music
-- • ✓ Mbira Dza Vadzimu - Zimbabwean shona mbira tuning (nyamaropa mode)
--
-- ASIAN TRADITIONAL SCALES:
-- • ✓ Hirajoshi - Japanese pentatonic with characteristic minor 2nd intervals
-- • ✓ In Sen - Japanese pentatonic with suspended quality
-- • ✓ Iwato - Japanese dark/mysterious, no P4/P5 (used in koto music)
-- • ✓ Yo Scale - Japanese bright pentatonic (no 4th, like maj penta)
-- • ✓ Kumoi - Japanese ethereal blend of minor/major (Sakura melody)
-- • ✓ Pelog - Balinese gamelan 5-note with semitone clusters
-- • ✓ Pelog Degung - Sundanese 7-note chromatic variant (more melodic freedom)
--
-- MIDDLE EASTERN SCALES:
-- • ~ Persian - Double harmonic with two augmented 2nds (exotic tension)
--
-- INDIAN RAGAS:
-- • ~ Marwa - Raga with augmented 4th (approximated)
--
-- EUROPEAN EXOTIC:
-- • ✓ Hungarian Major - Bright counterpart with augmented 2nds
-- • ✓ Dorian b2 - Phrygian #6, dark but with major VI
--
-- JAZZ/MODERN SCALES:
-- • ✓ Bebop Dominant - Mixolydian + maj7 for chromatic approach (Parker)
-- • ✓ Bebop Major - Major + #5, chromatic passing between 5-6 (I chord)
-- • ✓ Bebop Dorian - Dorian + 3, chromatic pass for ii-V (Coltrane)
-- • ✓ Bebop Minor - Natural minor + maj7 for i-IV progressions
-- • ✓ Bebop Harmonic Minor - Harmonic minor + maj7
-- • ✓ Bebop Melodic Minor - Melodic minor + #5
-- • ✓ Mixolydian b6 - Hindu scale, Indian-influenced rock
--
-- MESSIAEN MODES:
-- • ✓ Messiaen Mode 3 - 9-tone mode (composed for 12-TET piano)
-- • ✓ Messiaen Mode 4 - 8-tone mode with minor 2nd tetrachords
-- • ✓ Messiaen Mode 5 - 6-tone mode, tritone-related hexatonic
-- • ✓ Messiaen Mode 6 - 8-tone mode, whole-tone tetrachords
-- • ✓ Messiaen Mode 7 - 10-tone asymmetric (composed for 12-TET piano)
--
-- BLUES & FOLK:
-- • ✓ Blues Heptatonic - Full 7-note blues with major tones
--
-- MEDIEVAL/RENAISSANCE:
-- • ~ Dorian Hexachord - 6-note medieval mode (approximated, historical tunings varied)
--
-- BUILT-IN RENOISE SCALES:
-- All standard Renoise scales are also available (major, minor, dorian, phrygian, etc.)
-- These appear in the scale selector with the "Renoise:" prefix.
--
-- HOW IT WORKS:
-- Same motif-based algorithm as melody_synth_sparse_motif.lua, but:
-- 1. Select from custom exotic scale definitions OR built-in Renoise scales
-- 2. Each scale has unique interval patterns that create distinctive flavors
-- 3. Some scales have fewer than 7 degrees, which affects melodic possibilities
-- 4. Algorithm adapts to scale length automatically (pentatonic, heptatonic, octatonic, etc.)
--
-- NOTE: Microtonal scales (Carlos, Bohlen-Pierce) are approximated to 12-TET semitones,
-- as Renoise operates in standard MIDI pitch space. True microtonal implementation would
-- require pitch bend messages. The scale() function supports max 11 intervals.
--
-- CONTROLS:
-- Scale Selection:
-- • Scale - Choose from custom exotic or built-in Renoise scales
-- • root_note - Root note (c, c#, d, etc.)
-- • octave - Root octave (3-6)
-- • range - Melodic range in semitones (7-36)
--
-- Rhythm & Density (primary controls):
-- • density - Note frequency (0.15-0.75, higher = more notes)
-- • smooth_density - Smooth density transitions (boolean ; false = gated rhythm with sharp transitions, true = smooth/probabilistic)
-- • rest_prob - Rest probability (0.0-0.6, higher = more silence)
-- • motif_length - Motif length in 1/8 notes (4-16)
--
-- Melodic Character:
-- • step_motion - Stepwise vs leaps (0.2-0.9, higher = more stepwise)
-- • direction - Phrase direction (0=descending, 0.5=balanced, 1=ascending)
-- • variation - Variation amount (0.0-0.8, higher = more variation per repetition)
--
-- Expression:
-- • dynamics - Velocity range (0.1-0.7, higher = more dynamic variation)
-- • accent_strength - Accent strength (0.0-0.6, higher = stronger beat accents)
-- • humanize - Humanize timing (boolean, adds subtle timing variations)
-- • phrase_reset_prob - Phrase reset probability (0.0-0.4, return to root for breathing)
--
-- Seeds:
-- • seed - Random seed (1-99999, for reproducible patterns)
-- Custom scale definitions (intervals in semitones from root)
--
-- SCALE ACCURACY CATEGORIES:
-- ✓ Compatible: Scales that map perfectly to 12-TET with no compromise
-- ~ Approximated: Minor tuning compromises (5-15¢ errors) but character preserved
-- ✗ Heavily Distorted: Significant errors (>20¢) that alter the scale's intended character
--
local EXOTIC_SCALES = {
-- ===== WENDY CARLOS SCALES (12-TET APPROXIMATIONS) =====
-- ✗ Heavily Distorted: These microtonal scales lose significant character in 12-TET
["✗ Alpha"] = {0, 1, 3, 5, 6, 8, 10, 11}, -- ~78¢/step → 100¢/step (distorted)
["✗ Beta"] = {0, 1, 2, 4, 5, 7, 8, 10, 11}, -- ~63.8¢/step → varies (distorted)
-- ===== AFRICAN SCALES =====
-- ✓ Compatible: These scales work well in 12-TET
["✓ Pygmy"] = {0, 1, 3, 6, 8}, -- Central African: pentatonic scale used in Pygmy polyphonic music
["✓ Mbira Dza Vadzimu"] = {0, 2, 3, 7, 9, 10}, -- Zimbabwean Shona: mbira tuning (nyamaropa mode, ancestral spirits music)
-- ===== ASIAN TRADITIONAL SCALES =====
-- ✓ Compatible: These scales work well in 12-TET
["✓ Hirajoshi"] = {0, 2, 3, 7, 8}, -- Japanese: pentatonic with characteristic m2 intervals
["✓ In Sen"] = {0, 1, 5, 7, 10}, -- Japanese: pentatonic with suspended quality
["✓ Iwato"] = {0, 1, 5, 6, 10}, -- Japanese: dark/mysterious, no P4/P5 (used in koto music)
["✓ Yo Scale"] = {0, 2, 5, 7, 9}, -- Japanese: bright pentatonic (no 4th, like maj penta)
["✓ Kumoi"] = {0, 2, 3, 7, 9}, -- Japanese: ethereal blend of minor/major (Sakura melody)
["✓ Pelog"] = {0, 1, 3, 7, 8}, -- Balinese gamelan: 5-note with semitone clusters
["✓ Pelog Degung"] = {0, 1, 3, 6, 7, 8, 10}, -- Sundanese: 7-note chromatic variant (more melodic freedom)
-- ===== MIDDLE EASTERN SCALES =====
-- ~ Approximated: Traditional tunings have microtonal nuances not captured here
["~ Persian"] = {0, 1, 4, 5, 6, 8, 11}, -- Double harmonic: two augmented 2nds (exotic tension)
-- ===== INDIAN RAGAS =====
-- ~ Approximated: Traditional sruti divisions are more granular than 12-TET
["~ Marwa"] = {0, 1, 4, 6, 7, 9, 11}, -- Raga (approximated)
-- ===== EUROPEAN EXOTIC =====
-- ✓ Compatible: These scales are designed for 12-TET
["✓ Hungarian Major"] = {0, 3, 4, 6, 7, 9, 10}, -- Bright counterpart: augmented 2nds with major tonality
["✓ Dorian b2"] = {0, 1, 3, 5, 7, 9, 10}, -- Phrygian #6: dark but with major VI
-- ===== JAZZ/MODERN SCALES =====
-- ✓ Compatible: All designed for 12-TET equal temperament
["✓ Bebop Dominant"] = {0, 2, 4, 5, 7, 9, 10, 11}, -- Mixolydian + maj7: 8 notes for chromatic approach (Parker)
["✓ Bebop Major"] = {0, 2, 4, 5, 7, 8, 9, 11}, -- Major + #5: chromatic passing between 5-6 (I chord)
["✓ Bebop Dorian"] = {0, 2, 3, 4, 5, 7, 9, 10}, -- Dorian + 3: chromatic pass for ii-V (Coltrane)
["✓ Bebop Minor"] = {0, 2, 3, 5, 7, 9, 10, 11}, -- Natural minor + maj7: for i-IV progressions
["✓ Bebop Harmonic Minor"] = {0, 2, 3, 5, 7, 8, 10, 11}, -- Harmonic minor + maj7: chromatic approach
["✓ Bebop Melodic Minor"] = {0, 2, 3, 5, 7, 8, 9, 11}, -- Melodic minor + #5: chromatic passing
["✓ Mixolydian b6"] = {0, 2, 4, 5, 7, 8, 10}, -- Hindu scale: Indian-influenced rock/metal
-- ===== MESSIAEN MODES =====
-- ✓ Compatible: Composed specifically for 12-TET piano
["✓ Messiaen Mode 3"] = {0, 2, 3, 4, 6, 7, 8, 10, 11}, -- 9-tone (compatible)
["✓ Messiaen Mode 4"] = {0, 1, 2, 5, 6, 7, 8, 11}, -- 8-tone: minor 2nd tetrachords
["✓ Messiaen Mode 5"] = {0, 1, 5, 6, 7, 11}, -- 6-tone: tritone-related hexatonic
["✓ Messiaen Mode 6"] = {0, 2, 4, 5, 6, 8, 10, 11}, -- 8-tone: whole-tone tetrachords
["✓ Messiaen Mode 7"] = {0, 1, 3, 4, 6, 7, 8, 10, 11}, -- 10-tone asymmetric (compatible)
-- ===== BLUES & FOLK =====
-- ✓ Compatible: Designed for Western 12-TET instruments
["✓ Blues Heptatonic"] = {0, 2, 3, 4, 5, 7, 9}, -- 7-note blues: with major tones
-- ===== MEDIEVAL/RENAISSANCE =====
-- ~ Approximated: Historical tunings varied; this is a 12-TET compromise
["~ Dorian Hexachord"] = {0, 2, 4, 5, 7, 9}, -- 6-note medieval (approximated)
}
-- Get scale names for parameter enum (custom + built-in)
local function get_all_scale_names()
local names = {}
-- Add custom exotic scales with prefix
for name, _ in pairs(EXOTIC_SCALES) do
table.insert(names, "Custom: " .. name)
end
-- Add all built-in Renoise scales
for _, name in ipairs(scale_names()) do
table.insert(names, "Renoise: " .. name)
end
table.sort(names)
return names
end
return pattern {
unit = "1/8", -- Eighth notes for flexibility
resolution = 1,
parameter = {
-- Seeds for consistency
parameter.integer("seed", 42, {1, 99999}, "Random Seed"),
-- Scale Selection (Custom + Built-in)
parameter.enum("exotic_scale", "Custom: ✓ Hirajoshi", get_all_scale_names(), "Scale"),
parameter.enum("root_note", "c", {"c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"}, "Root Note"),
parameter.integer("octave", 4, {3, 6}, "Root Octave"),
parameter.integer("range", 12, {7, 36}, "Melodic Range (semitones)"),
-- Rhythm & Density (primary controls)
parameter.number("density", 0.40, {0.15, 0.75}, "Density (note frequency)"),
parameter.boolean("smooth_density", false, "Smooth Density (vs gated rhythm)"),
parameter.number("rest_prob", 0.30, {0.0, 0.6}, "Rest Probability"),
parameter.integer("motif_length", 8, {4, 16}, "Motif Length (in 1/8 notes)"),
-- Melodic Character
parameter.number("step_motion", 0.65, {0.2, 0.9}, "Step Motion (low=bigger leaps)"),
parameter.number("direction", 0.5, {0.0, 1.0}, "Phrase Direction (0=down, 0.5=balanced, 1=up)"),
parameter.number("variation", 0.35, {0.0, 0.8}, "Variation Amount"),
-- Expression
parameter.number("dynamics", 0.4, {0.1, 0.7}, "Velocity Range"),
parameter.number("accent_strength", 0.25, {0.0, 0.6}, "Accent Strength"),
parameter.boolean("humanize", true, "Humanize Timing"),
parameter.number("phrase_reset_prob", 0.15, {0.0, 0.4}, "Phrase Reset (return to root)"),
},
pulse = function(init_context)
local rand = math.randomstate(init_context.parameter.seed --[[@as integer]])
return function(context)
-- Read live parameter values
local density = context.parameter.density --[[@as number]]
local rest_prob = context.parameter.rest_prob --[[@as number]]
local smooth_density = context.parameter.smooth_density --[[@as boolean]]
-- Create sparse, irregular pulse with strategic rests
local beat_position = context.pulse_step % 8
-- Emphasize certain beats (downbeats, phrase starts)
local beat_strength = 0.5
if beat_position == 0 then
beat_strength = 0.9 -- Strong downbeat
elseif beat_position == 4 then
beat_strength = 0.7 -- Mid-bar emphasis
elseif beat_position == 2 or beat_position == 6 then
beat_strength = 0.6
end
-- Apply rest probability
if rand() < rest_prob then
return 0
end
-- Combine density with beat strength
local threshold = (1.0 - density) * 0.8
if smooth_density then
-- Smooth probabilistic approach: gradual transition
local play_probability = math.max(0, math.min(1, (beat_strength - threshold) / 0.5 + 0.5))
return rand() < play_probability and beat_strength or 0
else
-- Hard threshold approach: gated rhythm with sharp transitions
return beat_strength > threshold and beat_strength or 0
end
end
end,
event = function(init_context)
local rand = math.randomstate((init_context.parameter.seed --[[@as integer]]) + 1)
local humanize_rand = math.randomstate((init_context.parameter.seed --[[@as integer]]) + 1000)
-- State tracking (for realtime fiability)
local current_degree = 1 -- Start on root
local motif_notes = {} -- Store generated motif
local motif_iteration = 0 -- Track which repetition we're on
local last_scale_name = "" -- Track scale changes
local last_root_note = "" -- Track root changes
local last_motif_length = 0 -- Track motif length changes
local last_octave = 0 -- Track octave changes
return function(context)
-- Early return for rests
if context.pulse_value == 0 then
return nil
end
-- Read live parameter values and create scale
local scale_name = context.parameter.exotic_scale --[[@as string]]
local root_note = context.parameter.root_note --[[@as string]]
local octave = context.parameter.octave --[[@as integer]]
local motif_length = context.parameter.motif_length --[[@as integer]]
-- Detect scale, root, motif length, or octave change and reset motif
if scale_name ~= last_scale_name or root_note ~= last_root_note or
motif_length ~= last_motif_length or octave ~= last_octave then
motif_notes = {} -- Clear motif
motif_iteration = -1 -- Force regeneration
current_degree = 1 -- Reset to root
last_scale_name = scale_name
last_root_note = root_note
last_motif_length = motif_length
last_octave = octave
end
local s
local scale_length
-- Check if it's a custom scale or built-in Renoise scale
if scale_name:sub(1, 8) == "Custom: " then
-- Custom scale: extract name and use interval table
local custom_name = scale_name:sub(9)
local scale_intervals = EXOTIC_SCALES[custom_name]
local root = root_note .. octave
s = scale(root, scale_intervals)
scale_length = #scale_intervals
else
-- Built-in Renoise scale: extract name and use scale() with mode string
local renoise_name = scale_name:sub(10)
local root = root_note .. octave
s = scale(root, renoise_name)
scale_length = #s.notes
end
local step_motion = context.parameter.step_motion --[[@as number]]
local direction_bias = context.parameter.direction --[[@as number]]
local variation_amt = context.parameter.variation --[[@as number]]
local range = context.parameter.range --[[@as integer]]
local dynamics = context.parameter.dynamics --[[@as number]]
local accent_str = context.parameter.accent_strength --[[@as number]]
-- Generate weighted scale degree probabilities
local function get_degree_weights(current)
-- Create weights array for the actual scale length
local weights = {}
for i = 1, scale_length do
-- Root and fifth (if exists) are strongest
if i == 1 then
weights[i] = 1.8 -- Root always strong
elseif scale_length >= 5 and i == math.ceil(scale_length * 0.6) then
weights[i] = 1.5 -- Approximate "fifth" position
else
weights[i] = 1.0 -- Equal weight for others
end
end
-- Boost nearby degrees for stepwise motion
if current > 1 then
weights[current - 1] = weights[current - 1] * (1.0 + step_motion * 2.0)
end
if current < scale_length then
weights[current + 1] = weights[current + 1] * (1.0 + step_motion * 2.0)
end
return weights
end
-- Select next degree based on weights and direction
local function next_degree(current)
local weights = get_degree_weights(current)
-- Apply directional bias
for i = 1, scale_length do
if i > current then
weights[i] = weights[i] * (0.5 + direction_bias)
elseif i < current then
weights[i] = weights[i] * (1.5 - direction_bias)
end
end
-- Normalize and select
local sum = 0
for i = 1, scale_length do
sum = sum + weights[i]
end
local r = rand() * sum
local cumulative = 0
for i = 1, scale_length do
cumulative = cumulative + weights[i]
if r <= cumulative then
return i
end
end
return current
end
-- Apply variation to a motif note
local function vary_note(degree, variation_type)
if rand() > variation_amt then
return degree -- No variation
end
-- Different variation techniques
if variation_type == "transpose" then
-- Shift by a step
local shift = rand() > 0.5 and 1 or -1
return math.max(1, math.min(scale_length, degree + shift))
elseif variation_type == "neighbor" then
-- Neighbor tone ornament
if rand() > 0.5 then
return math.max(1, math.min(scale_length, degree + (rand() > 0.5 and 1 or -1)))
end
end
return degree
end
local step_in_motif = context.pulse_step % motif_length
local current_iteration = math.floor(context.pulse_step / motif_length)
-- Generate new motif if we're starting fresh or iteration changed
if current_iteration ~= motif_iteration then
motif_iteration = current_iteration
-- Decide on variation strategy for this iteration
local var_type = ({"none", "transpose", "neighbor"})[math.floor(rand() * 3) + 1]
-- Apply variations to existing motif or reset
if motif_iteration > 0 and #motif_notes > 0 and rand() < variation_amt then
-- Create variation of existing motif (make a copy)
local new_motif = {}
for i = 1, #motif_notes do
if motif_notes[i] ~= nil then
new_motif[i] = vary_note(motif_notes[i], var_type)
end
end
motif_notes = new_motif
end
end
-- Generate or retrieve motif note
if motif_notes[step_in_motif + 1] == nil then
-- Generate new note for this position
local new_degree
-- Check for phrase reset (return to root for breathing/cadence)
local reset_prob = context.parameter.phrase_reset_prob --[[@as number]]
if rand() < reset_prob then
new_degree = 1 -- Reset to root
-- Resolve to root at phrase end for natural cadence
elseif step_in_motif == motif_length - 1 and rand() > 0.3 then
new_degree = 1 -- Root
else
new_degree = next_degree(current_degree)
end
motif_notes[step_in_motif + 1] = new_degree
current_degree = new_degree
else
-- Use stored motif note
current_degree = motif_notes[step_in_motif + 1]
end
-- Create note from scale degree
-- For custom scales, access notes directly from the scale.notes array
-- since s:degree() only works with traditional 7-note scales
local note_value = s.notes[current_degree]
local n = note(note_value)
-- Octave management - keep within specified range
-- Calculate how many octaves upward we can shift (0 to max)
local max_octave_shift = math.floor((range - 1) / 12)
-- Apply a random octave variation within the allowed range
if max_octave_shift >= 1 then
local octave_shift = math.floor(rand() * (max_octave_shift + 1))
if octave_shift ~= 0 then
n = n:transpose({octave_shift * 12})
end
end
-- Apply dynamics
local base_velocity = 0.5 + (rand() - 0.5) * dynamics
-- Add accents on certain beats
local beat_pos = context.pulse_step % 8
if beat_pos == 0 or beat_pos == 4 then
base_velocity = base_velocity + accent_str
end
base_velocity = math.max(0.3, math.min(1.0, base_velocity))
-- Humanize timing if enabled (applies before volume)
if context.parameter.humanize --[[@as boolean]] then
-- Small random timing variation (0-10% delay for natural feel)
local timing_offset = humanize_rand() * 0.1
n = n:delay(timing_offset)
end
-- Apply volume
n = n:volume(base_velocity)
return n
end
end
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment