Skip to content

Instantly share code, notes, and snippets.

@oatmealine
Last active April 24, 2024 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oatmealine/b61571399529ea9ea897212d4dfe34c2 to your computer and use it in GitHub Desktop.
Save oatmealine/b61571399529ea9ea897212d4dfe34c2 to your computer and use it in GitHub Desktop.
local sm = {}
local function filterComments(text)
local lines = {}
for line in string.gmatch(text, '([^\n\r]*)[\n\r]?') do
if not string.match(line, '^%s*//.+') and string.len(line) > 0 then
table.insert(lines, line)
end
end
return table.concat(lines, '\n')
end
local function chartToNotedata(text)
local measures = {}
for measure in string.gmatch(text, '%s*([^,]*)%s*,?') do
local lines = {}
for line in string.gmatch(measure, '%s*([^\n\r]*)%s*[\n\r]?') do
if line ~= '' then
table.insert(lines, line)
end
end
table.insert(measures, lines)
end
local notedata = {}
for i, measure in ipairs(measures) do
local precision = 1/#measure
local measureBeat = (i - 1) * 4
for row, notes in ipairs(measure) do
local beat = measureBeat + (row - 1) * precision * 4
local column = 0
for note in string.gmatch(notes, '%S') do
if note ~= '0' then
table.insert(notedata, {beat, column, note})
end
column = column + 1
end
end
end
return notedata
end
local parsers = {}
local function numParser(n)
return tonumber(n)
end
local function boolParser(n)
return n == 'YES'
end
function parsers.NOTES(value)
local chunks = {}
for chunk in string.gmatch(value, '%s*([^:]*)%s*:?') do
table.insert(chunks, chunk)
end
return {
type = chunks[1],
credit = chunks[2],
difficulty = chunks[3],
rating = chunks[4],
grooveRadar = chunks[5],
notes = chartToNotedata(chunks[6]),
}
end
local function listParser(value)
local values = {}
local segments = {}
for v in string.gmatch(value .. ',', '(.-),') do
--print(v)
local key, value = string.match(v, '([%d.]+)=(.+)')
--print(key, value)
if key and value then
if #segments > 1 then
local mergedValue = table.concat(segments, ',')
--print(mergedValue)
local keyNew, valueNew = string.match(mergedValue, '([%d.]+)=(.+)')
if keyNew and valueNew then
table.remove(values, #values)
table.insert(values, {tonumber(keyNew), valueNew})
--print('/ ', keyNew, valueNew)
end
end
segments = { v }
--print('+ ', key, value)
table.insert(values, {tonumber(key), value})
else
table.insert(segments, v)
end
end
if #segments > 1 then
local mergedValue = table.concat(segments, ',')
--print(mergedValue)
local keyNew, valueNew = string.match(mergedValue, '([%d.]+)=(.+)')
if keyNew and valueNew then
table.remove(values, #values)
table.insert(values, {tonumber(keyNew), valueNew})
--print('/ ', keyNew, valueNew)
end
end
for _, v in ipairs(values) do
print(v[1], v[2])
end
return values
end
local function numListParser(value)
local values = {}
for _, n in ipairs(listParser(value)) do
table.insert(values, {n[1], tonumber(n[2])})
end
return values
end
parsers.BPMS = numListParser
function parsers.TIMESIGNATURES(value)
local sigs = {}
for _, n in ipairs(listParser(value)) do
local _, _, a, b = string.find(n[2], '([%d.]+)=([%d.]+)')
table.insert(sigs, {n[1], a, b})
end
return sigs
end
parsers.LABELS = listParser
parsers.WARPS = numListParser
parsers.DELAYS = numListParser
parsers.STOPS = numListParser
parsers.FAKES = numListParser
parsers.OFFSET = numParser
parsers.SELECTABLE = boolParser
parsers.SAMPLELENGTH = numParser
-- cathy-specific
--parsers.ANNOUNCE = boolParser
--parsers.LOOP = boolParser
local function idxm(k, v)
if type(k) ~= 'table' then
return k
else
return k[v]
end
end
function sm.parse(text, isSSC)
-- initial parse pass
local res = {}
for key, value in string.gmatch(text, '#([A-Z]-):(.-);') do
value = filterComments(value)
if res[key] and type(res[key]) ~= 'table' then
res[key] = {res[key], value}
elseif res[key] and type(res[key]) == 'table' then
table.insert(res[key], value)
else
res[key] = value
end
end
-- specialized parsers
for key, value in pairs(res) do
if type(value) == 'table' then
for i, v in ipairs(value) do
local parser = parsers[key]
if parser and not (key == 'NOTES' and isSSC) then
res[key][i] = parser(v)
end
end
else
local parser = parsers[key]
if parser and not (key == 'NOTES' and isSSC) then
res[key] = parser(value)
end
end
end
if res.NOTES then
if res.NOTES.notes then
res.NOTES = {res.NOTES}
end
end
if isSSC then
local compatNotes = {}
if type(res.NOTES) == 'string' then
res.NOTES = { res.NOTES }
end
for i, c in ipairs(res.NOTES) do
table.insert(compatNotes, {
type = idxm(res.STEPSTYPE, i),
credit = idxm(res.DESCRIPTION, i),
difficulty = idxm(res.DIFFICULTY, i),
rating = idxm(res.METER, i),
grooveRadar = idxm(res.RADARVALUES, i),
notes = chartToNotedata(c),
})
end
res.NOTES = compatNotes
end
if res.NOTES and #res.NOTES > 0 then
print('loaded track ' .. (res.MUSIC or '???') .. ' w/ ' .. (res.NOTES and #res.NOTES or 0) .. ' charts:')
for _, v in ipairs(res.NOTES) do
print(' ' .. v.credit .. ': ' .. #v.notes .. ' notes [' .. v.type .. ']')
end
else
print('loaded track ' .. (res.MUSIC or '???'))
end
return res
end
function sm.notedataToColumns(data)
local columns = {}
for _, note in ipairs(data) do
columns[note[2]] = columns[note[2]] or {}
table.insert(columns[note[2]], note[1])
end
return columns
end
-- sm lib end
assert(arg[1], 'pass in an SM filepath!')
local file, err = io.open(arg[1], 'r')
assert(file, 'failed to open ' .. arg[1] .. ': ' .. tostring(err))
local data = file:read('a')
file:close()
local parse = sm.parse(data, string.sub(arg[1], -3, -1) == 'ssc')
local chart
for _, c in ipairs(parse.NOTES) do
if c.type == 'dance-double' or c.type == 'pump-double' then
chart = c
break
end
end
assert(chart, 'no chart of type dance-double or pump-double found!')
print('using chart ' .. chart.credit .. ' (' .. chart.difficulty .. ', ' .. #chart.notes .. ' notes)')
print('chart type: ' .. chart.type)
if chart.type == 'dance-double' then
print()
print('!! THIS MEANS THE FOLLOWING MAPPING !!')
print()
print('col 1 2 3 4 5 6 7 8')
print('key l A S D L ; " r')
print()
print('where l is left gear shift, r is right gear shift')
print('only holds are valid for gear shifts !! other types will be ignored')
elseif chart.type == 'pump-double' then
print()
print('!! THIS MEANS THE FOLLOWING MAPPING !!')
print()
print('col 1 2 3 4 5 6 7 8 9 0')
print('key l A S D L ; " r < >')
print()
print('where l is left gear shift, r is right gear shift, < is left drift, > is right drift')
print('only holds are valid for gear shifts and drifts !! other types will be ignored')
else
print('unknown chart type')
os.exit(1)
end
local SEGMENT_INCR = 1 -- beats
local segments = {}
function gcd(m, n)
while n ~= 0 do
local q = m
m = n
n = q % n
end
return m
end
function lcm(m, n)
return ( m ~= 0 and n ~= 0 ) and m * n / gcd( m, n ) or 0
end
function n(s)
if s == nil then return '0' end
if s == '1' then return '1' end -- tap note
if s == '2' then return '2' end -- hold start
if s == '3' then return '4' end -- hold end
return '0'
end
function g(s)
if s == nil then return '0' end
if s == '2' then return '1' end -- gear head
if s == '3' then return '2' end -- gear end
return '0'
end
function dl(s)
if s == nil then return '0' end
if s == '2' then return '1' end -- drift left
if s == '3' then return '3' end -- drift end
return '0'
end
function dr(s)
if s == nil then return '0' end
if s == '2' then return '2' end -- drift right
if s == '3' then return '3' end -- drift end
return '0'
end
function quantize(b)
if b > 0.5 then
return math.floor((1 / (1 - b)) + 0.5)
end
return math.floor((1 / b) + 0.5)
end
local b = 0
local notesIdx = 1
while true do
if notesIdx > #chart.notes then break end
local segment = {}
for i = notesIdx, #chart.notes do
local note = chart.notes[i]
if note[1] >= (b + SEGMENT_INCR) then
break
end
if note[1] >= b then
--print(note[1], b)
table.insert(segment, { beat = note[1], note = note })
end
end
notesIdx = notesIdx + #segment
for _, v in ipairs(parse.BPMS or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) and v[1] ~= 0 then
table.insert(segment, { beat = v[1], bpm = v[2] })
end
end
for _, v in ipairs(parse.LABELS or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
table.insert(segment, { beat = v[1], label = v[2] })
end
end
for _, v in ipairs(parse.TIMESIGNATURES or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
table.insert(segment, { beat = v[1], timesig = { v[2], v[3] } })
end
end
for _, v in ipairs(parse.WARPS or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
table.insert(segment, { beat = v[1], warp = v[2] })
end
end
for _, v in ipairs(parse.DELAYS or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
-- functionally the same as a stop
table.insert(segment, { beat = v[1], stop = v[2] })
end
end
for _, v in ipairs(parse.STOPS or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
table.insert(segment, { beat = v[1], stop = v[2] })
end
end
for _, v in ipairs(parse.FAKES or {}) do
if v[1] >= b and v[1] < (b + SEGMENT_INCR) then
table.insert(segment, { beat = v[1], fake = v[2] })
end
end
local rowsN = 1
for _, event in ipairs(segment) do
if event.beat > b then
rowsN = lcm(rowsN, quantize(event.beat - b))
end
end
--print('-> ', #segment, rowsN)
local segmentStr = {}
--print(#segment .. ' in segment')
for row = 1, rowsN do
local offset = (row - 1) / rowsN
local cols = {}
for i, event in ipairs(segment) do
if math.abs(event.beat - (b + offset)) < 0.001 then
if event.note then
cols[event.note[2]] = event.note[3]
elseif event.label then
if string.sub(event.label, 1, 1) == '#' then
table.insert(segmentStr, event.label)
else
table.insert(segmentStr, '#LABEL=' .. event.label)
end
elseif event.timesig then
table.insert(segmentStr, '#TIME_SIGNATURE=' .. event.timesig[1] .. ',' .. event.timesig[2])
elseif event.bpm then
table.insert(segmentStr, '#BPM=' .. event.bpm)
elseif event.warp then
table.insert(segmentStr, '#WARP=' .. event.warp)
elseif event.stop then
table.insert(segmentStr, '#STOP_SECONDS=' .. event.stop)
elseif event.fake then
table.insert(segmentStr, '#FAKE=' .. event.fake)
end
end
end
local df = '0'
-- thank you to starundrscre
if cols[8] ~= nil and cols[9] ~= nil then
print('warning: you have two drift markers at the same time in your chart at beat ' .. b .. ', this will not work!')
elseif cols[8] ~= nil then
df = dl(cols[8])
elseif cols[9] ~= nil then
df = dr(cols[9])
end
table.insert(segmentStr, n(cols[1]) .. n(cols[2]) .. n(cols[3]) .. '-' .. n(cols[4]) .. n(cols[5]) .. n(cols[6]) .. '|' .. g(cols[0]) .. g(cols[7]) .. '|' .. df)
end
--print(table.concat(segmentStr, '\n'))
table.insert(segments, table.concat(segmentStr, '\n'))
b = b + SEGMENT_INCR
end
local writePath = arg[1] .. '.xdrv'
local out, err = io.open(writePath, 'w+')
assert(out, 'failed to write to result file: ' .. tostring(err))
out:write('--\n' .. table.concat(segments, '\n--\n') .. '\n--')
out:close()
print()
print('wrote result to ' .. writePath)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment