Last active
April 24, 2024 17:59
-
-
Save oatmealine/b61571399529ea9ea897212d4dfe34c2 to your computer and use it in GitHub Desktop.
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
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