Skip to content

Instantly share code, notes, and snippets.

@chayleaf
Created December 16, 2018 09:42
Show Gist options
  • Select an option

  • Save chayleaf/f8b845de6ff936b676d56dcbd167d16c to your computer and use it in GitHub Desktop.

Select an option

Save chayleaf/f8b845de6ff936b676d56dcbd167d16c to your computer and use it in GitHub Desktop.
osu! beatmap speed scaler
#change this
path='C:\\Users\\pavlukivan\\AppData\\Local\\osu!\\Songs\\328117 Kurokotei - Galaxy Collapse\\Kurokotei - Galaxy Collapse (Sayaka-) [Oni].osu'
outPath='C:\\Users\\pavlukivan\\AppData\\Local\\osu!\\Songs\\Custom Collapse\\Kurokotei - Galaxy Collapse (Sayaka-) [Oni].osu'
newSpeed = 0.5
class OsuFile:
SECTION_DICT = 0
SECTION_DICT_NO_SPACE = 1
SECTION_TUPLES = 2
SECTION_OBJECTS = 3
SECTION_TIMINGS = 4
SECTION_RAW = 5
SECTION_TYPES = {
'General': SECTION_DICT,
'Editor': SECTION_DICT,
'Metadata': SECTION_DICT_NO_SPACE,
'Difficulty': SECTION_DICT_NO_SPACE,
'Events': SECTION_TUPLES,
'TimingPoints': SECTION_TIMINGS,
'HitObjects': SECTION_OBJECTS
}
def __init__(self, path):
self.path = path
with open(path, 'rb') as f:
d = f.read().decode('utf-8')
self.lines = [l.strip() for l in d.split('\n') if not l.startswith('//')]
self.data = {}
n = -1
i = 0
while n != 0:
n = self.loadSectionAtPos(i)
i += n
def loadSectionAtPos(self, i):
if i >= len(self.lines):
return 0
if len(self.lines[i]) == 0:
return 1
if not self.lines[i].startswith('['):
if self.lines[i].startswith('osu file format v'):
self.data['Version'] = self.lines[i][17:]
return 2
else:
raise ValueError(f'Invalid section at index {i}')
name = self.lines[i][1:-1]
self.data[name] = {}
values = []
i += 1
while len(self.lines[i]) > 0:
values.append(self.lines[i])
i += 1
type = self.SECTION_TYPES[name] if name in self.SECTION_TYPES.keys() else self.SECTION_RAW
if type in [self.SECTION_DICT, self.SECTION_DICT_NO_SPACE]:
sep = (': ' if type is self.SECTION_DICT else ':')
values = [v.split(sep, 1) for v in values]
for k,v in values:
self.data[name][k] = v
return len(values)+2
elif type in [self.SECTION_TUPLES, self.SECTION_OBJECTS, self.SECTION_TIMINGS]:
values = [v.split(',') for v in values]
if type is self.SECTION_OBJECTS:
for i in range(len(values)):
values[i][-1] = values[i][-1].split(':')
self.data[name] = [tuple(v) for v in values]
return len(values)+2
elif type == self.SECTION_RAW:
self.data[name] = values
return len(values)
def save(self):
d = ['osu file format v', self.data['Version'], '\n']
d.append('\n[General]\n')
for k,v in self.data['General'].items():
d.append(k)
d.append(': ')
d.append(v)
d.append('\n')
d.append('\n[Editor]\n')
for k,v in self.data['Editor'].items():
d.append(k)
d.append(': ')
d.append(v)
d.append('\n')
d.append('\n[Metadata]\n')
for k,v in self.data['Metadata'].items():
d.append(k)
d.append(':')
d.append(v)
d.append('\n')
d.append('\n[Difficulty]\n')
for k,v in self.data['Difficulty'].items():
d.append(k)
d.append(':')
d.append(v)
d.append('\n')
d.append('\n[Events]\n')
for v in self.data['Events']:
d.append(','.join(v))
d.append('\n')
d.append('\n[TimingPoints]\n')
for v in self.data['TimingPoints']:
d.append(','.join(v))
d.append('\n')
if 'Colours' in self.data.keys():
d.append('\n[Colours]\n')
for v in self.data['Colours']:
d.append(v)
d.append('\n')
d.append('\n[HitObjects]\n')
for v in self.data['HitObjects']:
v = (*v[:-1], ':'.join(v[-1]))
d.append(','.join(v))
d.append('\n')
d.append('\n')
d = ''.join(d).encode('utf-8')
with open(self.path, 'wb') as f:
f.write(d)
#increase song speed by n times
def scaleFile(o, n):
m = 1 / n
def scaleByKey(d, k, n, t=int):
d[k] = str(t(t(d[k]) * n))
def scaleIfExists(d, k, n, t=int):
if k in d.keys():
d[k] = str(t(t(d[k]) * n))
scaleIfExists(o.data['General'], 'AudioLeadIn', m)
scaleIfExists(o.data['General'], 'PreviewTime', m)
#todo editor stuff. Not really useful tho
#OD cant be scaled perfectly, so 300 notes are use to determine hit windows
scaleIfExists(o.data['Difficulty'], 'SliderMultiplier', n, float)
OD = float(o.data['Difficulty']['OverallDifficulty'])
if OD >= 13.33333333333333333333:
pass #300s cant even be done so it would only get worse, do nothing
else:
OD = max((-40+6*OD)/3, 0)
o.data['Difficulty']['OverallDifficulty'] = str(OD)
for i in range(len(o.data['Events'])):
e=list(o.data['Events'][i])
if e[0] == 2: #break
scaleByKey(e, 1, m)
scaleByKey(e, 2, m)
o.data['Events'][i] = tuple(e)
for i in range(len(o.data['TimingPoints'])):
t = list(o.data['TimingPoints'][i])
scaleByKey(t, 0, m)
#if float(t[1]) >= 0.0: # SV
# scaleByKey(t, 1, m, float)
o.data['TimingPoints'][i] = tuple(t)
for i in range(len(o.data['HitObjects'])):
h = list(o.data['HitObjects'][i])
scaleByKey(h, 2, m) #time
if int(h[3]) & 8: #spinner
scaleByKey(h, 5, m) #end time
o.data['HitObjects'][i] = tuple(h)
o.data['Metadata']['Title'] += f' {n}x speed'
o.data['Metadata']['TitleUnicode'] += f' {n}x speed'
del o.data['Metadata']['BeatmapID']
del o.data['Metadata']['BeatmapSetID']
pathEnd = '['+o.path.split('[')[-1]
pathBeginning = o.path[:-len(pathEnd)]
o.path = f'{pathBeginning} {n}x speed {pathEnd}'
afn = o.data['General']['AudioFilename']
aExt = afn[::-1].split('.', 1)[0][::-1]
aBeginning = afn[:-len(aExt)-1]
o.data['General']['AudioFilename'] = f'{aBeginning} {n}.{aExt}'
o=OsuFile(path)
o.path = outPath
scaleFile(o, newSpeed)
o.save()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment