Created
December 16, 2018 09:42
-
-
Save chayleaf/f8b845de6ff936b676d56dcbd167d16c to your computer and use it in GitHub Desktop.
osu! beatmap speed scaler
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
| #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