- tools/musicator:\n - markov model generator and pattern generator operational\n- DDR sound generation in-progress
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
mr.one
2026-03-20 20:18:10 +01:00
parent d899a74411
commit 46d3ff2ebc
8 changed files with 1274 additions and 94 deletions

View File

@@ -1,130 +1,127 @@
unpack = unpack or table.unpack
-- key separator: |
-- empty note: "..."
function build_markov_model(sequence, order)
local function make_key(tbl)
return table.concat(tbl, "|")
math.randomseed(os.time())
local unpack = unpack or table.unpack
local function make_key(tbl)
return table.concat(tbl, "|")
end
local function unmake_key(k)
local result = {}
for t in string.gmatch(k, "[^|]+") do
result[#result + 1] = t
end
local function unmake_key(k)
local result = {}
for t in string.gmatch(k, "[^|]+") do
result[#result + 1] = t
return result
end
local function add_key(str, value)
return str .. "|" .. value
end
local function split_last(full)
local i = full:match(".*()|")
return full:sub(1, i-1), full:sub(i+1)
end
local function has_value (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return result
end
return false
end
local function add_key(str, value)
return str .. "|" .. value
-- helper: split key into parts
local function split(k)
local t = {}
for part in string.gmatch(k, "[^|]+") do
t[#t+1] = part
end
return t
end
local function split_last(full)
local i = full:match(".*()|")
return full:sub(1, i-1), full:sub(i+1)
end
function build_markov_model(sequence, order)
-- TODO: add {"..." x order} to beginning?
local counts = {}
local totals = {}
local model = { }
-- count
for i = 1, #sequence - order do
local notes = make_key({unpack(sequence, i, i + order - 1)})
totals[notes] = (totals[notes] or 0) + 1
local key = make_key({unpack(sequence, i, i + order - 1)})
local next_note = sequence[i + order]
local notes_full = add_key(notes, sequence[i + order])
counts[notes_full] = (counts[notes_full] or 0) + 1
local data = model[key] or { next={}, total=0 }
data.next[next_note] = (data.next[next_note] or 0) + 1
data.total = data.total + 1
model[key] = data
end
-- build model
local model = {}
for notes_full,count in pairs(counts) do
local notes, _ = split_last(notes_full)
model[notes_full] = count[notes_full] / total[notes]
-- normalize
for temp_key,temp_data in pairs(model) do
for temp_note, temp_count in pairs(temp_data.next) do
model[temp_key].next[temp_note] = temp_count / temp_data.total
end
end
--[[
for k,v in pairs(model) do
print("-----" .. k)
for k2,v2 in pairs(v.next) do
print(k2, v2)
end
end
--]]
return {
order = order,
model = model,
counts = counts -- keep raw counts (useful!)
model = model
}
end
function generate_sequence(model_data, length)
local model = model_data.model
local order = model_data.order
local order = model.order
local model_data = model_data.model
-- helper: split key into parts
local function split(k)
local t = {}
for part in string.gmatch(k, "[^|]+") do
t[#t+1] = part
end
return t
end
-- pick random starting state
local start_key
-- random start key
local model_keys = {}
for k,_ in pairs(model) do
start_key = k
break
model_keys[#model_keys + 1] = k
end
local start_key = model_keys[math.ceil(math.random() * #model_keys)]
-- (optional: better random start)
for k,_ in pairs(model) do
if math.random() < 0.1 then
start_key = k
end
end
local parts = split(start_key)
-- initial sequence = first `order` items
local seq = {}
for i = 1, order do
seq[i] = parts[i]
end
-- sequence starts with the start key
local seq = unmake_key(start_key)
-- generation loop
while #seq < length do
-- build current state key
local state = table.concat({unpack(seq, #seq - order + 1, #seq)}, "|")
local current_key = table.concat({unpack(seq, #seq - order + 1, #seq)}, "|")
-- collect matching transitions
local matches = {}
for full,prob in pairs(model) do
if full:sub(1, #state) == state and full:sub(#state+1, #state+1) == "|" then
matches[#matches+1] = {key=full, prob=prob}
local chosen = "..."
local key_data = model[current_key]
if key_data then
local r = math.random()
local prob_sum = 0.0
for new_note, new_prob in pairs(key_data.next) do
prob_sum = prob_sum + new_prob
if prob_sum < r then
chosen = new_note
end
end
end
if #matches == 0 then break end
-- print(current_key .. " --> " .. chosen)
-- weighted pick
local r = math.random()
local sum = 0
local chosen
for _,m in ipairs(matches) do
sum = sum + m.prob
if r <= sum then
chosen = m.key
break
end
end
if not chosen then
chosen = matches[#matches].key
end
-- extract next symbol (after last '|')
local next_symbol = chosen:match("|([^|]+)$")
seq[#seq+1] = next_symbol
seq[#seq+1] = chosen
end
return seq
end
-- todo: feed samples