From f251332118527284c18eb87703bec08f8b08ff13 Mon Sep 17 00:00:00 2001 From: DataGreed Date: Thu, 31 Mar 2022 19:27:35 +0100 Subject: [PATCH 01/12] project parser stub --- main.py | 35 +++++++----- parsers/project.py | 130 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 parsers/project.py diff --git a/main.py b/main.py index b2f9a63..d23878b 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,27 @@ if __name__ == '__main__': - from parsers import patterns - - # todo: remove this and implement tests - p = patterns.PatternParser( - # NOTE: this file was created with firmware 1.3.1 or older version - filename="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_06.mtp") - parsed_pattern = p.parse() - # print(parsed_pattern.render_as_table()) - from exporters import midi - - midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern) - print(midi_exporter.generate_midi()) - midi_exporter.write_midi_file("./test_midi_file.mid") + from parsers import patterns, project + + # # todo: remove this and implement tests + # p = patterns.PatternParser( + # # NOTE: this file was created with firmware 1.3.1 or older version + # filename="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_06.mtp") + # parsed_pattern = p.parse() + # # print(parsed_pattern.render_as_table()) + # from exporters import midi + # + # midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern) + # print(midi_exporter.generate_midi()) + # midi_exporter.write_midi_file("./test_midi_file.mid") + + + project = project.ProjectParser( + filename_or_folder="./reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/" + ) + + parsed_project = project.parse() + print("Finished parsing project.") + print(f"BPM: {parsed_project.bpm}") diff --git a/parsers/project.py b/parsers/project.py new file mode 100644 index 0000000..204b34e --- /dev/null +++ b/parsers/project.py @@ -0,0 +1,130 @@ +# polyend tracker project file + +__author__ = "Alexey 'DataGreed' Strelkov" + +import os +from typing import List, Pattern, Dict + + +class Song: + """ + Represents a song, a sequence of patterns + """ + + """Maximum length of a song in patterns.""" + MAXIMUM_SLOTS_PER_SONG = 255 # from docs + + def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int:Pattern]): + """ + + :param pattern_chain: a list of ints representing the order of patterns in the song. + ints a 1-based. So pattern 1 is represented by 1. + :param pattern_mapping: a dict with all of the unique patterns in the song. + Keys are integer number of patterns, values are actual patterns. + """ + if len(pattern_chain)>self.MAXIMUM_SLOTS_PER_SONG: + raise ValueError(f"Maximum song length is {self.MAXIMUM_SLOTS_PER_SONG}, " + f"but {len(pattern_chain)} pattern long song received") + self.pattern_mapping = pattern_mapping + self.pattern_chain = pattern_chain + + def get_song_as_patterns(self) -> List[Pattern]: + """Returns song as a list of patterns ordered. Play them in + the returned order to get the song""" + return [self.pattern_mapping[x] for x in self.pattern_chain] + + +class Project: + """ + Represents a tracker project + """ + + def __init__(self, name: str, song: Song, bpm: int): + """ + :param name: name of the project + :param bpm: project tempo in beats per minutes. Tracker supports fractional tempo + :param song: song (sequence of patterns to play) + """ + self.bpm = bpm + self.name = name + self.song = song + + + OFFSET_END = 0x624 #1572 bytes total files length + OFFSET_START = 0 + + @staticmethod + def from_bytes(data: bytes, patterns_bytes=Dict[int:bytes]) -> "Project": + """ + Constructs a project object from bytes extracted from project file. + :param data: + :param patterns_bytes: a list of bytes for each of projects + patterns extracted from pattern files. + :return: + """ + expected_length = Project.OFFSET_END - Project.OFFSET_START # 6152 or 769*8 just for sanity check + + if len(data) != expected_length: + raise ValueError(f"Expected project data {expected_length} bytes long, got {len(data)} instead") + + raise NotImplementedError() + + return Project() + + +class ProjectParser: + """Parses tracker *.mt files, scans directory for patterns + so a song can be exported to MIDI""" + + PATTERNS_FOLDER_NAME = "patterns" + DEFAULT_PROJECT_FILENAME = "project.mt" + PATTERN_FILE_NAME_TEMPLATE = "pattern_{}.mtp" # todo: format: add leading zero automatically + + MAXIMUM_PATTERNS_PER_PROJECT = 255 # from polyend docs + + def __init__(self, filename_or_folder: str): + """ + :param filename_or_folder: project (*.mt) filename or folder with project file. + Note that pattern files are required to be in a "patterns" subfolder within + the same folder for everything to work properly. + """ + if filename_or_folder.endswith(".mt"): + self.filepath = filename_or_folder + # folder must always end with a folder separator + self.folder = os.sep.join(filename_or_folder.split(os.sep)[:-1]) + os.sep + else: + self.folder = filename_or_folder + if not self.folder.endswith(os.sep): + # folder must always end with a folder separator + self.folder += os.sep + self.filepath = self.folder+self.DEFAULT_PROJECT_FILENAME + + self.patterns_folder = self.folder + self.PATTERNS_FOLDER_NAME + os.sep + + def parse(self) -> Project: + + project_file_bytes = None + pattern_file_bytes_dict: Dict[int:bytes] = {} + + with open(self.filepath, "rb") as f: + project_file_bytes = f.read() # f.read()[Project.OFFSET_START:Project.OFFSET_END] + + # find all project pattern files and add their bytes to the parser too + for i in range(1, self.MAXIMUM_PATTERNS_PER_PROJECT+1): + pattern_file_path = self.patterns_folder+self.PATTERN_FILE_NAME_TEMPLATE + + # FIXME: patterns names goe as pattern_01.mtp, but manual says there can be + # 255 patterns, how pattern 100 file will be named if + # there is only 1 leading zero in one digit projects? + + pattern_number_string = str(i) + if len(pattern_number_string) < 2: + pattern_number_string = "0" + pattern_number_string + + pattern_file_path = pattern_file_path.replace("{}", pattern_number_string) + + with open(pattern_file_path, "rb") as f: + # pattern + pattern_file_bytes_dict[i] = f.read() # reads the whole file + + return Project.from_bytes(project_file_bytes, pattern_file_bytes_dict) \ No newline at end of file From 7d185917a4c0c4f6a7ea6497e5e8d01342943048 Mon Sep 17 00:00:00 2001 From: DataGreed Date: Thu, 31 Mar 2022 20:44:46 +0100 Subject: [PATCH 02/12] reverse engineering session notes and test files --- .../empty 120 bpm.mt | Bin 0 -> 1572 bytes .../empty 120.1 bpm.mt | Bin 0 -> 1572 bytes .../empty 120.5 bpm.mt | Bin 0 -> 1572 bytes .../empty 172 bpm.mt | Bin 0 -> 1572 bytes .../empty130 bpm.mt | Bin 0 -> 1572 bytes .../empty131 bpm.mt | Bin 0 -> 1572 bytes .../not sure project.mt | Bin 0 -> 1572 bytes .../patterns 1-1 130 bpm.mt | Bin 0 -> 1572 bytes .../patterns 1-1-1 130 bpm.mt | Bin 0 -> 1572 bytes .../patterns 1-1-2 130 bpm.mt | Bin 0 -> 1572 bytes .../seems to be rebel path.mt | Bin 0 -> 1572 bytes .../session 2/session2-notes.md | 34 ++++++++++++++++++ 12 files changed, 34 insertions(+) create mode 100755 reverse-engineering/session 2/project files only for analysis/empty 120 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/empty 120.1 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/empty 120.5 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/empty 172 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/empty130 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/empty131 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/not sure project.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/patterns 1-1 130 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/patterns 1-1-1 130 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/patterns 1-1-2 130 bpm.mt create mode 100755 reverse-engineering/session 2/project files only for analysis/seems to be rebel path.mt create mode 100644 reverse-engineering/session 2/session2-notes.md diff --git a/reverse-engineering/session 2/project files only for analysis/empty 120 bpm.mt b/reverse-engineering/session 2/project files only for analysis/empty 120 bpm.mt new file mode 100755 index 0000000000000000000000000000000000000000..920878c8f4daa9114a5e4ff7389b28a6d00ef344 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu>9bpKo4t%yB%SWcpUEDB*B07K(%08Euv>1Qk0mStzgK&kX*vR z@XBwSQ`#rLa)_W2!?knUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eurxOQ;G{r%YlgoKVUTzn{vIXThxR~~U|cPtXCG3On4GO($iR?X z!ocv#Z<|xvC%Txfi5G(Ha+ zpBIhKhsNhe;{*L|sF0dlP*SO2Xk?&ksE|~U%aD^_ma0&mSgeqdT9nFg`BL6X0A*fG ALI3~& literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 2/project files only for analysis/empty 120.5 bpm.mt b/reverse-engineering/session 2/project files only for analysis/empty 120.5 bpm.mt new file mode 100755 index 0000000000000000000000000000000000000000..3e7f73e307056b205dde4b05da5b24022e7deb48 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eu>Al3gOdX7tr`9{ghAqQ_Wkf1*%{e900|` zQ00($%)s2t$WYJ7%FgZ30Fz)u<1?Z0nbG(xXna;QJ{uaJ9gWX{#^*%ibD{CM(fB-Q zd|os@9~z$@jSuvUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu+(u@pocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Huw-&ppocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Huw-^tpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{z#n1hYp#TSyHS zvkxgsOwLv?WMD`xVPJUWx6LW-lV3SR(1_t$I)f@hkt!r4AcDrI!2%I9!6s;mP0$RR zpgA@{3sk`{H~@-=p~@lin1Oklk)fWEm7Uw60VctS#%Ds~Go$fY(De%qYVKKYeH1dSN3r8B596sbZ&0wQRP8Y~b&6KsN}*aXe637TUQ zv_KUMg9D&=7^)mHj~SS^85!ysS=qTA8ekHPXnZC#J~JAh1&z;&#%Dw0v!n4j(DtMa&r4S*N-aw*Do#~M%}Zwh6N$wN8L3673|DUU GI0FE!g-h)K literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 2/project files only for analysis/patterns 1-1-1 130 bpm.mt b/reverse-engineering/session 2/project files only for analysis/patterns 1-1-1 130 bpm.mt new file mode 100755 index 0000000000000000000000000000000000000000..379c5efb9c26cf54d58d3df63eadf56e995d5aae GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|UF8AdWtF<{_PW#E?JU@&6%0t|{2U|{46G28zr6~GcK z>z#n1hYp#TSyHS zvkxgsOwLv?WMD`xVPJUWx6LW-lV3SR(1_t$I)f@hkt!r4AcDrI!2%I9!6s;mP0$RR zpgA@{3sk`{H~@-=p~@lin1Oklk)fWEm7Uw60VctS#%Ds~Go$fY(DBhbJwNIb%Q&PV{j6%Jd5Jr+R!0^g%n^W2+zjBD65yQ1~233Y4RY*uc1dUOH1tMsIP0$pZpcytn zb8LbZsDfc|02B{Hl|$w+1M@Z`Lp>uaJGVmvOo9=O&xFQjM&q-f@mbOMY-oITG(HC! zpA(JGg~sPbBbB|f+4Y3AtSXY KmEqJupT_{e;!HsR literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 2/project files only for analysis/seems to be rebel path.mt b/reverse-engineering/session 2/project files only for analysis/seems to be rebel path.mt new file mode 100755 index 0000000000000000000000000000000000000000..8f2d9a8366c6029acc24f554e0e4183c0700285c GIT binary patch literal 1572 zcmeYbVPIfnX8g|$1S)KKZ=|H8rDbHbw6t|}S^nSh&v;B`!0U!?@tak#29y(-VW?^Mx=iubx=0R6a3;q@-1zH$8(#_=O=g-go z|DPcrJs_Cb`P1{={NTb)_kgN~Bd`a03XH2o^z5@Ule58|OfF$yc;&avDeaSAc~W9= zF-Y)QI)f@hkt$f8AuSg|qmcOph6)*(C_)Sj#o0LpC5GsNd5J}-XkrW*i6vl*P{hI7 zjZlOb7{bDW!XN-B5r8I%;D#{+b2B4DJtHeSw?hL=oDq%BgvMt^#^*-k^Pus0(fE95e10@O(BCPEC5h=psi`Rnx(Y?9NvSys1&JjY3MECE ONu?#J3P$S{jbj0*;8Lmp literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 2/session2-notes.md b/reverse-engineering/session 2/session2-notes.md new file mode 100644 index 0000000..f3f1860 --- /dev/null +++ b/reverse-engineering/session 2/session2-notes.md @@ -0,0 +1,34 @@ +# Session 2 reverse engineering notes + +Object: Polyend Tracker project `*.mt` files + +## BPM reverse engineering + +From docs: +goes from 40 to 800BPM + +From files I have it seems that bpm fraction part and integer part are stored in different offsets. + +integer part: +- 0x1c2 - 2 bytes long (could be more, need to check at 800 bpm) + +examples: +- 120 bpm - F0 42 +- 130 bpm - 02 43 +- 131 bpm - 03 43 +- 172 bpm - 2C 43 (am I mistaken? is it actually 171 bpm? would make more sense probably) +- 90? bpm - B4 42 (not sure if it was actually 90 bpm, gotta check) + +no idea how to convert bpm values yet. I may have incorrectly named files according to bpms. +Have to double check and resave them. + +project files conainns file name at the end and track names - hex viewer see tham as ascii. + +Seems like some of my test files are saved with incorrectly according to their contents. + +It seems that the song pattern sequence starts at 0x10 and each bytes represents a pattern number. + +E.g. 01 01 02 stands for 1,1,2 patterns + +so the offset seems to be 0x10 with length of 256 bytes + From 64555af35eba6bb845d810fe8d587696d85e5efb Mon Sep 17 00:00:00 2001 From: DataGreed Date: Thu, 31 Mar 2022 20:45:17 +0100 Subject: [PATCH 03/12] pattern chain parsing for song construction (not sure if correct yet); no bpm extraction yet --- parsers/project.py | 56 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/parsers/project.py b/parsers/project.py index 204b34e..8d568ff 100644 --- a/parsers/project.py +++ b/parsers/project.py @@ -3,7 +3,9 @@ __author__ = "Alexey 'DataGreed' Strelkov" import os -from typing import List, Pattern, Dict +from typing import List, Dict + +from parsers.patterns import Pattern class Song: @@ -12,7 +14,7 @@ class Song: """ """Maximum length of a song in patterns.""" - MAXIMUM_SLOTS_PER_SONG = 255 # from docs + MAXIMUM_SLOTS_PER_SONG = 255 # from docs def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int:Pattern]): """ @@ -49,17 +51,23 @@ def __init__(self, name: str, song: Song, bpm: int): self.name = name self.song = song - OFFSET_END = 0x624 #1572 bytes total files length OFFSET_START = 0 + PATTERN_CHAIN_OFFSET = 0x10 + PATTERN_CHAIN_END = 0x110 # length is 256 bytes (shouldnt it be 255 according to docs?) + + # BPM_OFFSET_START = + # BPM_OFFSET_END = + @staticmethod def from_bytes(data: bytes, patterns_bytes=Dict[int:bytes]) -> "Project": """ Constructs a project object from bytes extracted from project file. :param data: :param patterns_bytes: a list of bytes for each of projects - patterns extracted from pattern files. + patterns extracted from pattern files. Note: expects full file + byte representation without offsets. :return: """ expected_length = Project.OFFSET_END - Project.OFFSET_START # 6152 or 769*8 just for sanity check @@ -67,9 +75,45 @@ def from_bytes(data: bytes, patterns_bytes=Dict[int:bytes]) -> "Project": if len(data) != expected_length: raise ValueError(f"Expected project data {expected_length} bytes long, got {len(data)} instead") - raise NotImplementedError() + patterns_mapping = {} + + # parse patterns from received bytes for each pattern file + # and save them in a dict tha maps pattern number to Pattern object + # so it can be used later to construct a song + for key, value in patterns_bytes.items(): + patterns_mapping[key] = Pattern.from_bytes(value[Pattern.OFFSET_START:Pattern.OFFSET_END]) + + bpm = 120 # todo: parse bpm from project file! + + pattern_chain = Project.pattern_chain_from_bytes(data[Project.PATTERN_CHAIN_OFFSET:Project.PATTERN_CHAIN_END]) + + name = "MyProject" # todo: extract from project files footer + + return Project(name=name, + song=Song(pattern_chain=pattern_chain, pattern_mapping=patterns_mapping), + bpm=bpm) + + @staticmethod + def pattern_chain_from_bytes(data: bytes) -> List[int]: + """extracts chain of patterns for song from project file bytes""" + + print("EXTRACTING pattern chain from sequence of bytes below:") + print(data) + + result = [] + + for byte in data: + # each byte is just a pattern number + if byte: + result.append(byte) + + # break when we encounter zero. It stand for unoccupied slot. + # there could be no unoccupied slots in a sond (I guess?) + # todo: check that it's correct + break + + return result - return Project() class ProjectParser: From f84071428817429e5b34e63c995446ec58dd9b3b Mon Sep 17 00:00:00 2001 From: DataGreed Date: Thu, 31 Mar 2022 21:59:14 +0100 Subject: [PATCH 04/12] fixed pattern parsing --- main.py | 3 +++ parsers/project.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index d23878b..20f3553 100644 --- a/main.py +++ b/main.py @@ -23,5 +23,8 @@ parsed_project = project.parse() print("Finished parsing project.") print(f"BPM: {parsed_project.bpm}") + print(f"pattern mapping: {parsed_project.song.pattern_mapping}") + print(f"pattern chain: {parsed_project.song.pattern_chain}") + print(f"song as patterns: {parsed_project.song.get_song_as_patterns()}") diff --git a/parsers/project.py b/parsers/project.py index 8d568ff..cdf6375 100644 --- a/parsers/project.py +++ b/parsers/project.py @@ -16,7 +16,7 @@ class Song: """Maximum length of a song in patterns.""" MAXIMUM_SLOTS_PER_SONG = 255 # from docs - def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int:Pattern]): + def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int, Pattern]): """ :param pattern_chain: a list of ints representing the order of patterns in the song. @@ -30,6 +30,14 @@ def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int:Pattern]) self.pattern_mapping = pattern_mapping self.pattern_chain = pattern_chain + # integrity check + for i in set(pattern_chain): + if i not in pattern_mapping.keys(): + raise ValueError(f"Song initialization: pattern_chain has value " + f"{i}, but there is no such pattern in pattern_mapping. " + f"Context: pattern_chain: {pattern_chain}; " + f"pattern_mapping: {pattern_mapping}") + def get_song_as_patterns(self) -> List[Pattern]: """Returns song as a list of patterns ordered. Play them in the returned order to get the song""" @@ -55,13 +63,13 @@ def __init__(self, name: str, song: Song, bpm: int): OFFSET_START = 0 PATTERN_CHAIN_OFFSET = 0x10 - PATTERN_CHAIN_END = 0x110 # length is 256 bytes (shouldnt it be 255 according to docs?) + PATTERN_CHAIN_END = 0x10f # length is 255 bytes # BPM_OFFSET_START = # BPM_OFFSET_END = @staticmethod - def from_bytes(data: bytes, patterns_bytes=Dict[int:bytes]) -> "Project": + def from_bytes(data: bytes, patterns_bytes=Dict[int,bytes]) -> "Project": """ Constructs a project object from bytes extracted from project file. :param data: @@ -99,18 +107,21 @@ def pattern_chain_from_bytes(data: bytes) -> List[int]: print("EXTRACTING pattern chain from sequence of bytes below:") print(data) + print(f"data length {len(data)}") + result = [] for byte in data: # each byte is just a pattern number + print(byte) if byte: result.append(byte) - - # break when we encounter zero. It stand for unoccupied slot. - # there could be no unoccupied slots in a sond (I guess?) - # todo: check that it's correct - break + else: + # break when we encounter zero. It stand for unoccupied slot. + # there could be no unoccupied slots in a sond (I guess?) + # todo: check that it's correct + break return result @@ -148,7 +159,7 @@ def __init__(self, filename_or_folder: str): def parse(self) -> Project: project_file_bytes = None - pattern_file_bytes_dict: Dict[int:bytes] = {} + pattern_file_bytes_dict: Dict[int, bytes] = {} with open(self.filepath, "rb") as f: project_file_bytes = f.read() # f.read()[Project.OFFSET_START:Project.OFFSET_END] @@ -167,8 +178,12 @@ def parse(self) -> Project: pattern_file_path = pattern_file_path.replace("{}", pattern_number_string) - with open(pattern_file_path, "rb") as f: - # pattern - pattern_file_bytes_dict[i] = f.read() # reads the whole file + try: + with open(pattern_file_path, "rb") as f: + # pattern + pattern_file_bytes_dict[i] = f.read() # reads the whole file + except FileNotFoundError as e: + # it's okay. Not all patterns have to exist. + pass return Project.from_bytes(project_file_bytes, pattern_file_bytes_dict) \ No newline at end of file From 3c7b2a4a77512ff3c117b28ed135c3d571e55f5d Mon Sep 17 00:00:00 2001 From: DataGreed Date: Thu, 31 Mar 2022 22:21:55 +0100 Subject: [PATCH 05/12] song midi export stub --- exporters/midi.py | 82 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/exporters/midi.py b/exporters/midi.py index 74aa546..c5dd352 100644 --- a/exporters/midi.py +++ b/exporters/midi.py @@ -3,8 +3,11 @@ from parsers.patterns import Pattern, Note from midiutil import MIDIFile +from parsers.project import Song -class PatternToMidiExporter: + +class BaseMidiExporter: + """Base class for all midi exporters""" # midi utils uses either ticks of beats (quarter notes) as time # beats are expressed in floats @@ -12,6 +15,19 @@ class PatternToMidiExporter: # so this value is a tracker step duration to use with MIDIUtil MIDI_16TH_NOTE_TIME_VALUE = 0.25 + def generate_midi(self) -> MIDIFile: + raise NotImplementedError() + + def write_midi_file(self, path: str): + + midi_file = self.generate_midi() + + with open(path, "wb") as output_file: + midi_file.writeFile(output_file) + + +class PatternToMidiExporter(BaseMidiExporter): + def __init__(self, pattern: Pattern, tempo_bpm=120): self.pattern = pattern @@ -119,10 +135,7 @@ def generate_midi(self) -> MIDIFile: duration = (note_end_position - step_number) * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE - - # TODO: add support for chord fx - # TODO: add support for arp fx - + # add actual notes to midi file data if step.get_arp(): # arpeggio arp = step.get_arp() @@ -206,12 +219,63 @@ def generate_midi(self) -> MIDIFile: return midi_file - def write_midi_file(self, path: str): - midi_file = self.generate_midi() +class SongToMidiExporter(BaseMidiExporter): - with open(path, "wb") as output_file: - midi_file.writeFile(output_file) + def __init__(self, song: Song): + self.song = song + + def get_list_of_instruments(self): + """ + Gets list of all actually used instruments + across all patterns of the song. + Used to create midi file with proper instrument tracks. + :return: + """ + instruments = set() + # iterate over unique patterns only + for pattern in self.song.pattern_mapping.values(): + instruments.update(PatternToMidiExporter(pattern=pattern).get_list_of_instruments()) + + return sorted(instruments) + + def generate_midi(self) -> MIDIFile: + # raise NotImplementedError() + + # tracker tracks are not actual tracks, but voices, + # since every track can use any instrument at even given time and + # every track is monophonic. + # midi tracks typically represent different instruments and are polyphonic + # so we should count number of instruments in pattern and use it as + # number of tracks + instruments = self.get_list_of_instruments() + midi_tracks_count = len(instruments) + + # this maps allows us to quickly find midi track for given instrument + # this should be faster than calling instruments.indexOf() + instrument_to_midi_track_map = {} + + midi_file = MIDIFile(midi_tracks_count) + #FIXME: write bpm to song to get it from there + midi_file.addTempo(track=0, time=0, tempo=self.song.bpm) + + for i in range(len(instruments)): + # todo: get actual track names from project file (or are they stored in instrument files?) + # todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names + midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}") + + instrument_to_midi_track_map[instruments[i]] = i + + previous_pattern:Pattern = None + + for pattern in self.song.get_song_as_patterns(): + + exporter = PatternToMidiExporter(pattern=pattern) + # todo: add arguments and handle them + exporter.generate_midi(midi_file=midi_file, instrument_to_midi_track_map=instrument_to_midi_track_map, + start_time=len(previous_pattern.tracks) * SongToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE) + previous_pattern = pattern + return midi_file \ No newline at end of file From a3a0a42fb1479f3a464ca7cd861c3c2c64b23b98 Mon Sep 17 00:00:00 2001 From: DataGreed Date: Fri, 1 Apr 2022 02:15:22 +0100 Subject: [PATCH 06/12] song export; bpm still not extracted for now --- exporters/midi.py | 107 ++++++++++++++++++++++++++++++-------------- main.py | 2 +- parsers/project.py | 23 +++++----- polytracker2midi.py | 64 ++++++++++++++++++++++---- 4 files changed, 143 insertions(+), 53 deletions(-) diff --git a/exporters/midi.py b/exporters/midi.py index c5dd352..8fbcd24 100644 --- a/exporters/midi.py +++ b/exporters/midi.py @@ -1,3 +1,5 @@ +from typing import Optional + from midiutil.MidiFile import NoteOff from parsers.patterns import Pattern, Note @@ -15,8 +17,8 @@ class BaseMidiExporter: # so this value is a tracker step duration to use with MIDIUtil MIDI_16TH_NOTE_TIME_VALUE = 0.25 - def generate_midi(self) -> MIDIFile: - raise NotImplementedError() + # def generate_midi(self) -> MIDIFile: + # raise NotImplementedError() def write_midi_file(self, path: str): @@ -59,38 +61,62 @@ def get_midi_note_value(note: Note): # tracker C4 is 48 return note.value+12 - def generate_midi(self) -> MIDIFile: + def generate_midi(self, midi_file: MIDIFile = None, + instrument_to_midi_track_map: dict = None, + start_time_offset: float = 0) -> MIDIFile: + degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number - # tracker tracks are not actual tracks, but voices, - # since every track can use any instrument at even given time and - # every track is monophonic. - # midi tracks typically represent different instruments and are polyphonic - # so we should count number of instruments in pattern and use it as - # number of tracks - instruments = self.get_list_of_instruments() - midi_tracks_count = len(instruments) + if not instrument_to_midi_track_map: + # tracker tracks are not actual tracks, but voices, + # since every track can use any instrument at even given time and + # every track is monophonic. + # midi tracks typically represent different instruments and are polyphonic + # so we should count number of instruments in pattern and use it as + # number of tracks + instruments = self.get_list_of_instruments() + + + # this maps allows us to quickly find midi track for given instrument + # this should be faster than calling instruments.indexOf() + instrument_to_midi_track_map = {} + + for i in range(len(instruments)): + # todo: get actual track names from project file (or are they stored in instrument files?) + # todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names + # midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}") + + instrument_to_midi_track_map[instruments[i]] = i + + else: + # instrument_to_midi_track_map is supploed in case we render + # a song. In this case we may have different instruments in different patterns + # and need a mappign for all of them. We also need to create a midi file + # with tracks for all used instruments. + instruments = instrument_to_midi_track_map.keys() - track = 0 channel = 0 - time = 0 # In beats (is it 4:4?) - default_duration = 1 # In beats (is it 4:4?) - tempo = 60 # In BPM default_volume = 127 # 0-127, as per the MIDI standard - # this maps allows us to quickly find midi track for given instrument - # this should be faster than calling instruments.indexOf() - instrument_to_midi_track_map = {} + if not midi_file: + # if we are not supplied with a midi file, + # create a new one (we are supplied with one, e.g. if we render a song + # and we need to append pattern mido to existing file) - midi_file = MIDIFile(midi_tracks_count) - midi_file.addTempo(track=0, time=0, tempo=self.tempo_bpm) + track = 0 - for i in range(len(instruments)): - # todo: get actual track names from project file (or are they stored in instrument files?) - # todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names - midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}") + time = 0 # In beats (is it 4:4?) + default_duration = 1 # In beats (is it 4:4?) + tempo = 60 # In BPM - instrument_to_midi_track_map[instruments[i]] = i + midi_tracks_count = len(instruments) + midi_file = MIDIFile(midi_tracks_count) + midi_file.addTempo(track=0, time=0, tempo=self.tempo_bpm) + + for i in range(len(instruments)): + # todo: get actual track names from project file (or are they stored in instrument files?) + # todo: instrument 48 is midi instrument 1 the next 15 are also midi instruments - set their names + midi_file.addTrackName(track=i, time=0, trackName=f"Instrument {instruments[i]}") for track in self.pattern.tracks: @@ -182,7 +208,7 @@ def generate_midi(self) -> MIDIFile: midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number], channel=channel, pitch=PatternToMidiExporter.get_midi_note_value(note), - time=arp_note_start_time, + time=start_time_offset + arp_note_start_time, duration=arp_note_duration, # TODO: write velocity fx value if set (needs to be converted to 0...127!!!) volume=default_volume, @@ -200,7 +226,7 @@ def generate_midi(self) -> MIDIFile: midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number], channel=channel, pitch=PatternToMidiExporter.get_midi_note_value(note), - time=step_number * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE, + time=start_time_offset + step_number * PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE, duration=duration, # TODO: write velocity fx value if set (needs to be converted to 0...127!!!) volume=default_volume, @@ -211,7 +237,7 @@ def generate_midi(self) -> MIDIFile: midi_file.addNote(track=instrument_to_midi_track_map[step.instrument_number], channel=channel, pitch=PatternToMidiExporter.get_midi_note_value(step.note), - time=step_number*PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE, + time=start_time_offset + step_number*PatternToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE, duration=duration, # TODO: write velocity fx value if set (needs to be converted to 0...127!!!) volume=default_volume, @@ -266,16 +292,31 @@ def generate_midi(self) -> MIDIFile: instrument_to_midi_track_map[instruments[i]] = i - previous_pattern:Pattern = None + print(f"instruments: {instruments}") + print(f"instrument_to_midi_track_map: {instrument_to_midi_track_map}") - for pattern in self.song.get_song_as_patterns(): + previous_pattern: Optional[Pattern] = None + j = 0 + start_time_offset = 0 + print(self.song.pattern_chain) + for pattern in self.song.get_song_as_patterns(): #fixme: temporary slice for debug + j+=1 + print(f"Rendering song slot {j}") exporter = PatternToMidiExporter(pattern=pattern) + if previous_pattern: + # every next pattern should write midi data + # after the previous pattern ended, + # so we have to add time offset for every pattern + start_time_offset += previous_pattern.tracks[0].length * SongToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE + # todo: add arguments and handle them - exporter.generate_midi(midi_file=midi_file, instrument_to_midi_track_map=instrument_to_midi_track_map, - start_time=len(previous_pattern.tracks) * SongToMidiExporter.MIDI_16TH_NOTE_TIME_VALUE) + # todo: no need to do value declaration here really, we already pass it by reference + midi_file = exporter.generate_midi(midi_file=midi_file, + instrument_to_midi_track_map=instrument_to_midi_track_map, + start_time_offset=start_time_offset) previous_pattern = pattern - return midi_file \ No newline at end of file + return midi_file diff --git a/main.py b/main.py index 20f3553..c3dafe3 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,7 @@ parsed_project = project.parse() print("Finished parsing project.") - print(f"BPM: {parsed_project.bpm}") + print(f"BPM: {parsed_project.song.bpm}") print(f"pattern mapping: {parsed_project.song.pattern_mapping}") print(f"pattern chain: {parsed_project.song.pattern_chain}") print(f"song as patterns: {parsed_project.song.get_song_as_patterns()}") diff --git a/parsers/project.py b/parsers/project.py index cdf6375..c580ccf 100644 --- a/parsers/project.py +++ b/parsers/project.py @@ -16,7 +16,8 @@ class Song: """Maximum length of a song in patterns.""" MAXIMUM_SLOTS_PER_SONG = 255 # from docs - def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int, Pattern]): + def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int, Pattern], + bpm:float): """ :param pattern_chain: a list of ints representing the order of patterns in the song. @@ -24,6 +25,7 @@ def __init__(self, pattern_chain: List[int], pattern_mapping: Dict[int, Pattern] :param pattern_mapping: a dict with all of the unique patterns in the song. Keys are integer number of patterns, values are actual patterns. """ + self.bpm = bpm if len(pattern_chain)>self.MAXIMUM_SLOTS_PER_SONG: raise ValueError(f"Maximum song length is {self.MAXIMUM_SLOTS_PER_SONG}, " f"but {len(pattern_chain)} pattern long song received") @@ -49,13 +51,12 @@ class Project: Represents a tracker project """ - def __init__(self, name: str, song: Song, bpm: int): + def __init__(self, name: str, song: Song): """ :param name: name of the project :param bpm: project tempo in beats per minutes. Tracker supports fractional tempo :param song: song (sequence of patterns to play) """ - self.bpm = bpm self.name = name self.song = song @@ -98,23 +99,25 @@ def from_bytes(data: bytes, patterns_bytes=Dict[int,bytes]) -> "Project": name = "MyProject" # todo: extract from project files footer return Project(name=name, - song=Song(pattern_chain=pattern_chain, pattern_mapping=patterns_mapping), - bpm=bpm) + song=Song(pattern_chain=pattern_chain, + pattern_mapping=patterns_mapping, + bpm=bpm), + ) @staticmethod def pattern_chain_from_bytes(data: bytes) -> List[int]: """extracts chain of patterns for song from project file bytes""" - print("EXTRACTING pattern chain from sequence of bytes below:") - print(data) - print(f"data length {len(data)}") + # print("EXTRACTING pattern chain from sequence of bytes below:") + # print(data) + # print(f"data length {len(data)}") result = [] for byte in data: # each byte is just a pattern number - print(byte) + # print(byte) if byte: result.append(byte) else: @@ -186,4 +189,4 @@ def parse(self) -> Project: # it's okay. Not all patterns have to exist. pass - return Project.from_bytes(project_file_bytes, pattern_file_bytes_dict) \ No newline at end of file + return Project.from_bytes(project_file_bytes, pattern_file_bytes_dict) diff --git a/polytracker2midi.py b/polytracker2midi.py index ab06527..ffb6264 100644 --- a/polytracker2midi.py +++ b/polytracker2midi.py @@ -2,7 +2,7 @@ import sys from sys import argv -from parsers import patterns +from parsers import patterns, project from exporters import midi @@ -26,7 +26,16 @@ def main(): # generate output filename from an input one by changing extension # if provided - output_filename = ".".join(input_filename.split(".")[:-1]) + ".mid" + if os.path.isdir(input_filename): + + output_filename = input_filename + if not output_filename.endswith(os.sep): + output_filename += os.sep + + output_filename += "project.mid" + + else: + output_filename = ".".join(input_filename.split(".")[:-1]) + ".mid" try: # try to get output filename from second command line argument @@ -39,22 +48,59 @@ def main(): # not provided - use default one pass - if not os.path.isfile(input_filename): + if not (os.path.isfile(input_filename) or os.path.isdir(input_filename)): print(f"File {input_filename} does not exist") sys.exit(1) if os.path.isfile(output_filename): print(f"File {output_filename} already exists - will overwrite") - p = patterns.PatternParser(filename=input_filename) - parsed_pattern = p.parse() + if input_filename.endswith(".mtp"): + print("Trying to parse a pattern file...") + p = patterns.PatternParser(filename=input_filename) + parsed_pattern = p.parse() + + # print(parsed_pattern.render_as_table()) + + midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern) + midi_exporter.write_midi_file(output_filename) + + print(f"Exported pattern midi to {os.path.abspath(output_filename)}") + + else: + print("Trying to parse a project...") + # try to parse as project + p = project.ProjectParser(filename_or_folder=input_filename) + parsed_project = p.parse() + + # print(parsed_pattern.render_as_table()) + + midi_exporter = midi.SongToMidiExporter(song=parsed_project.song) + midi_exporter.write_midi_file(output_filename) + + print(f"Exported project midi to {os.path.abspath(output_filename)}") + + # todo: export individual patterns, too + + print("Trying to export patterns...") + + for number, pattern in parsed_project.song.pattern_mapping.items(): + midi_exporter = midi.PatternToMidiExporter(pattern=pattern) + + number_string = str(number) + if len(number_string)<2: + number_string = "0" + number_string - # print(parsed_pattern.render_as_table()) + # create directory for patterns + try: + os.mkdir(p.folder + "patterns_midi/") + except FileExistsError: + pass - midi_exporter = midi.PatternToMidiExporter(pattern=parsed_pattern) - midi_exporter.write_midi_file(output_filename) + pattern_output_filename = p.folder + "patterns_midi/" + f"pattern_{number_string}.mid" + midi_exporter.write_midi_file(pattern_output_filename) + print(f"Exported pattern midi to {os.path.abspath(pattern_output_filename)}") - print(f"Exported midi to {os.path.abspath(output_filename)}") if __name__ == '__main__': From 416711322a8f6ae05e98550b9e7002c062683944 Mon Sep 17 00:00:00 2001 From: DataGreed Date: Fri, 1 Apr 2022 02:20:05 +0100 Subject: [PATCH 07/12] test midi files --- .../patterns/pattern_26.mid | Bin 0 -> 243 bytes .../patterns/pattern_42.mid | Bin 0 -> 444 bytes .../patterns/pattern_42.txt | 33 ++++++++++++++++++ .../patterns_midi/pattern_01.mid | Bin 0 -> 618 bytes .../patterns_midi/pattern_02.mid | Bin 0 -> 1918 bytes .../patterns_midi/pattern_03.mid | Bin 0 -> 1954 bytes .../patterns_midi/pattern_04.mid | Bin 0 -> 3166 bytes .../patterns_midi/pattern_05.mid | Bin 0 -> 3166 bytes .../patterns_midi/pattern_06.mid | Bin 0 -> 679 bytes .../patterns_midi/pattern_07.mid | Bin 0 -> 679 bytes .../patterns_midi/pattern_09.mid | Bin 0 -> 3166 bytes .../patterns_midi/pattern_26.mid | Bin 0 -> 243 bytes .../patterns_midi/pattern_27.mid | Bin 0 -> 525 bytes .../patterns_midi/pattern_28.mid | Bin 0 -> 1882 bytes .../patterns_midi/pattern_29.mid | Bin 0 -> 2865 bytes .../patterns_midi/pattern_30.mid | Bin 0 -> 3166 bytes .../patterns_midi/pattern_31.mid | Bin 0 -> 706 bytes .../patterns_midi/pattern_32.mid | Bin 0 -> 2675 bytes .../patterns_midi/pattern_42.mid | Bin 0 -> 444 bytes .../patterns_midi/pattern_43.mid | Bin 0 -> 508 bytes .../patterns_midi/pattern_44.mid | Bin 0 -> 679 bytes .../patterns_midi/pattern_45.mid | Bin 0 -> 953 bytes .../patterns_midi/pattern_51.mid | Bin 0 -> 62 bytes .../project.mid | Bin 0 -> 14111 bytes 24 files changed, 33 insertions(+) create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_26.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.txt create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_01.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_02.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_03.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_04.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_05.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_06.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_07.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_09.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_26.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_27.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_28.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_29.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_30.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_31.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_32.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_42.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_43.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_44.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_45.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_51.mid create mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_26.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_26.mid new file mode 100644 index 0000000000000000000000000000000000000000..6152167edd380a6c867b9951611f72a314b5d4fd GIT binary patch literal 243 zcmeYb$w*;fU|?flWMF1K;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2Mr3{26sc4p%ST+_+u&i$lXt1m&i)91SYXj121!7s%gM0)4VP literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.mid new file mode 100644 index 0000000000000000000000000000000000000000..259b667af5f1a8eaaa6b817ed1a71f5b80ee4463 GIT binary patch literal 444 zcmeYb$w*;fU|?flWMF1K;2Tnu4dih%{10SiU#P(FU!MUY#3;n@pPAP)uehYBG&eP` zM8U|sv0#EnePcm`M?C|njOGM#biwT!SnUvJz(aKsW bRc8Y-#Rg=G6^La8V%gQV1~k~!gQ5liy-uh= literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.txt b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.txt new file mode 100644 index 0000000..a82f1d6 --- /dev/null +++ b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns/pattern_42.txt @@ -0,0 +1,33 @@ + Track 1 | Track 2 | Track 3 | Track 4 | Track 5 | Track 6 | Track 7 | Track 8 +--- -- -- --- -- --- | OFF 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- V 42 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 47 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 43 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 45 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 45 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- V 45 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 48 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 48 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 43 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- V 47 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 47 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 48 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 45 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | C4 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 43 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- V 48 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 47 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | A#3 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 46 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 48 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | OFF 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | D4 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 43 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- +--- -- -- --- -- --- | --- 49 -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | --- -- -- --- -- --- | C5 27 V 44 -- --- | --- -- -- --- -- --- \ No newline at end of file diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_01.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_01.mid new file mode 100644 index 0000000000000000000000000000000000000000..9f057e76615289d522199bc359df94f9124a034a GIT binary patch literal 618 zcmeYb$w*;fU|?flWME@H;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2Mr3{26qG*;F8zQ+N6;0B_vaw)-WqoTvgJnHgEE|ws8<1Wr5X-6_ z$+E9#mYEuWt!OL&TE+mh6_qi_NLIs@!gpd8SvKtH1~5YZ5d?s>!JgbA+o q%?S;zK#dNdSa$$2oIxVaK!zQNVFzS5f*6j-QV#XA6hMlBh64bmd*loN literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_02.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_02.mid new file mode 100644 index 0000000000000000000000000000000000000000..c32a9aeb0b5249610621dd135158a84c2d67016b GIT binary patch literal 1918 zcmeYb$w*;fU|?flWME}J;2Tnu4dih%{10SiU#P(FU!MUY#AwR!pPAP)uehYBG&eP` zM8VLKVS;CUyF-I#{U`%_ARq!S6fN*fEE@|ZSk|`&G+5S?#j*kEwE^k10#- zA!i;UWin4i3tUqJaF8|@00Wl+7|N)OK_&-^LtsQQOh9LVqZTM<1(X9tFEV40`2jtM z;eifxC%U)b9v$`H@J*kH%*iN+o;eMh6D9=KHzza%0u#0mr~>f;GWHoh~Wif z_<|U|$WlJ_a|D2jTLVbd56nCuqkTcS2xKBYgF-;oh5!`>ffzwx)j$S{8v{WO2_(ZI SK!ZRI@u{Ds05TC+KL7y7n({jU literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_03.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_03.mid new file mode 100644 index 0000000000000000000000000000000000000000..5a929974df1586540d0dbe409fe67a0884d2cac3 GIT binary patch literal 1954 zcmeYb$w*;fU|?flWME}J;2Tnu4dih%{10SiU#P(FU!MUY#AwR!pPAP)uehYBG&eP` zM8VLKVS;CUyF-I#{U`%_ARq!S6fN*fEE@|ZSk|`&G+5S?#j*kEwE^k10#- zA!i;UWin4i3tUqJaF8|@00Wl+7|N)OK_&-^LtsQQOh9LVqZTM<1(X9tFEV40`2jtM z;eifxC%U)b9v$`H@J*kH%*p74o;eMh6D9=KHzza%0u#0mr~>f;GWHoh~Wif z_<|U|$WlJ_a|D2jTLVbd56nCuqkTcS2xOv9eYXOttv>a=IE@YgIV1$AEeOO20_z7d iP+T1dG7;nSfJ4$$NJRI0=p4dhX4T3762Lm literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_04.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_04.mid new file mode 100644 index 0000000000000000000000000000000000000000..1466fcfa6944d2a616ed00da67f23a5b838d0c18 GIT binary patch literal 3166 zcmeHJ!AiqG5S?!;a7NKqHh|22SjprP6B)l9ah zizzH#OjC=u1j})mEd8!3y;7bROLgnMEy52+W;1$v8(8PYHfXxo2CiBSBNr>71Zs0` zn{z!zUb!k6gMU{qTebF+A#Uq9l@uPh@S6qBgB^>phcJ?26QtH6<{`%R=E08jU`nh- z2T!vt-DuTc;acW3`%2H>j+OE+8&2!Y9L~ma97~1$Dsiy||K}9|13no5^cnO40tP`o zB-G0x9$pQ%&ByR?>42w*D++b*6yGY;!>6S~CK4iT#9)->0~A<{9 literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_05.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_05.mid new file mode 100644 index 0000000000000000000000000000000000000000..13b92da70b308520629df6bbb245c3ae6f9d018a GIT binary patch literal 3166 zcmeHJ!AiqG5SNcg-x=c*a%u7x7VG_5fsT%?<&(v<$1A`x9-~_{BUG8qo=lkaiML4x{YmM%VjULu@VZP zCg(Oe*J0!otCBMKclFX$Yd&k@HjWcX;eidmS>P<#u^4*@BPr5BVl840Vr*{~>{t)F z#7cDV)Z0>xmi^_fWmdDV^!)8uA^+0hG|u$ltSy4bm)Ng9F1FzRyaHgzCqsY%g8_iY zz{`e&d_Ba&%igy67#=R|@f2}IAs?*#TZMf5v~<8k0;G)?jFNnS9BZE|azm;QpDX$W P$f7ip74jwFh;PUTHp DvpD$X literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_07.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_07.mid new file mode 100644 index 0000000000000000000000000000000000000000..c99cffc76688bad8b519d8c6bee882a5c677129a GIT binary patch literal 679 zcmeYb$w*;fU|?flWME@H;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2M`HaQ_kamP6c4(FunY24haIf!YXmAJGf{g(*0V0Jkg;5CI6!XS{ z2_E%8r5-?2NMtl8kfRH3*T8CrI0F&Bm1rSjVhIcx%X*+AErG#99IH72t2!HyDK;Qe ztUxR)5X-K DvpD$X literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_09.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_09.mid new file mode 100644 index 0000000000000000000000000000000000000000..2b6541b678078f2d33fdd3509d42d4c167c02a9f GIT binary patch literal 3166 zcmeHJO-lk%6urZQE+XhB%s(hYi4l=!B7WeYBh%hRK@f?Ut)C*|BBI%}i8gNf9sM&q zcZ@gB7er1_2D3Zf`|ig(=iNJc!PNy3Wyqqe{pJOuYy8@$m2YR44y}%8ZL$iqvJaYr z+wth`=5jE0PI44BQIa0TL|+QczS$WK090k-;j6kZqji8 literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_26.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_26.mid new file mode 100644 index 0000000000000000000000000000000000000000..6152167edd380a6c867b9951611f72a314b5d4fd GIT binary patch literal 243 zcmeYb$w*;fU|?flWMF1K;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2Mr3{26sc4p%ST+_+u&i$lXt1m&i)91SYXj121!7s%gM0)4VP literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_27.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_27.mid new file mode 100644 index 0000000000000000000000000000000000000000..2d2f9767b20035baddd56a4fff0a62b4e1332a6a GIT binary patch literal 525 zcmeYb$w*;fU|?flWME}J;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2Mr3{26qG*;F8zQ+N6;0B_vaw)-WqoTvgJnHgEE|ws8<1Wr5X-6_ v$+E9#mYEuWt!OL&TE+mh6_qi_NLIs@!gpd8SvKtH1~fYAT|0v5t^ literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_28.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_28.mid new file mode 100644 index 0000000000000000000000000000000000000000..949e8885cc3b24eb011cbdb221ad8d9f7ba46fc2 GIT binary patch literal 1882 zcmeH|ze>YU6vj^o>L7xTkT=kR4hf~)SURL6v?+1#pdg5))H>gkQXE9g;vz1-i7(_c z)b9%+Nw1T2iQ@L_Irn_`{JE!zlB)|5*%eE6?Dr^{UE|ljtYdrc&5`wy6s8YZ+Xunq zc0Rkixtz?MmMdAH?^PP;2EmgQ`TPXRqOEhI=o5D_&0M`2rmo(K+Kg>8)+2e_sZ<&K zI=$*e+0RCEPmlAKE~aR%pv#-kFdt`#K}u#qK3e!Z_&DA=G^_@*#bwixr&(56=zo2G z_kC8&X`PwN*;0Y3Twwgbu(b_;r`kuZK9i8h$q(%R_0OTSP^r zADqowm3}OwV_qBMP{`CmKtf9b7!gLf3qaR^mH{B9BBN>+D8@|ot73Vm&-gyVB0^M; M5A}<~JMazp1Y=y{G5`Po literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_29.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_29.mid new file mode 100644 index 0000000000000000000000000000000000000000..0204029ed3e0b5b7e465b7804932623b46c8da43 GIT binary patch literal 2865 zcmeYb$w*;fU|?flWMF4L;2Tnu4dih%{10SiU#P(FU!MUY#AwR!pPAP)uehYBG&eP` zM8U|AVS;CUyF-I#J;MYK5W@q=7!^SZ4S3)&EJF`GlXiy*?)Ci)4emho1I+-&9ncwY z*RrsoyVkt102sxM1>h(qk|z#@SRmJLX+4M?vQh-F2dDF_z~Ir9)jE%Q{gz%?}h2PrTPEP?S23SeZ$ zAd>^dAuu8tCZIFGQ45r_0?L7+7nw20{D2IkB<5eC0Qbe8NTqOw;WmV Phsc}^qUf2^5F`lz{`6e- literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_30.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_30.mid new file mode 100644 index 0000000000000000000000000000000000000000..2b6541b678078f2d33fdd3509d42d4c167c02a9f GIT binary patch literal 3166 zcmeHJO-lk%6urZQE+XhB%s(hYi4l=!B7WeYBh%hRK@f?Ut)C*|BBI%}i8gNf9sM&q zcZ@gB7er1_2D3Zf`|ig(=iNJc!PNy3Wyqqe{pJOuYy8@$m2YR44y}%8ZL$iqvJaYr z+wth`=5jE0PI44BQIa0TL|+QczS$WK090k-;j6kZqji8 literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_31.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_31.mid new file mode 100644 index 0000000000000000000000000000000000000000..451bf02d4055ce86a70c2c88f1d0b7a31e05a6b6 GIT binary patch literal 706 zcmeYb$w*;fU|?flWME}J;2Tnu4dih%{10SiU#P(FU!MUY#3;n@pPAP)uehYBG&eP` zM8U|sv0#EnePcm`M?C|njOGM#biwT!SnUvJz(a;16)j{;EP)|oS>GDaU0DBcE2lOh?&u9!p MNQbMDMN_mLpFyAeV(gduj#RS-?@@wG3~(A@j9M0zCFrk#dJ&4a7) z<81o$a66t^rx)`{(v|Z`+?Ap%Sd6N$^1Et!S;1@Rpjh2DmD^_BKlwH=uC;A&W@8)J zayf`?tb_`vNp6!|hsdcGrFg-=i&v~#^Gy@?{5X>oHL$U2DXG0puL1Vk}*^OO* zHZ5&{7C{TZBk)w0D?bi#csVG`$M7(;$0=e&SH4)Ax4QE6J9NlvLmcw?G#?KsW bRc8Y-#Rg=G6^La8V%gQV1~k~!gQ5liy-uh= literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_43.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_43.mid new file mode 100644 index 0000000000000000000000000000000000000000..d81f8caf904ccdbf32d72d50a3bfe2f9b5cd21c1 GIT binary patch literal 508 zcmeYb$w*;fU|?flWME-F;2Tnu4dih%{10SiU#P(FU!MUY#3;n@pPAP)uehYBG&eP` zM8U|sv0#EnePcm`M?C|njOGM#biwT!SnUvJz(aKsW zRc8Y-#Rg=G6^La8V%gQV1~k~!Bcg`E0?h%YMhrmH7$&&Z&tsTiTHntw!KA)d0O%Tq N23H_91!5DhegIpyyu<(i literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_44.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_44.mid new file mode 100644 index 0000000000000000000000000000000000000000..c99cffc76688bad8b519d8c6bee882a5c677129a GIT binary patch literal 679 zcmeYb$w*;fU|?flWME@H;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2M`HaQ_kamP6c4(FunY24haIf!YXmAJGf{g(*0V0Jkg;5CI6!XS{ z2_E%8r5-?2NMtl8kfRH3*T8CrI0F&Bm1rSjVhIcx%X*+AErG#99IH72t2!HyDK;Qe ztUxR)5X-K DvpD$X literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_45.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/patterns_midi/pattern_45.mid new file mode 100644 index 0000000000000000000000000000000000000000..c4be7e50f16e775755118001a324ad186d762642 GIT binary patch literal 953 zcmeYb$w*;fU|?flWZ+;v;2Tnu4dih%{10SiU#P(FU!MUg6wC0Rnb$L~xTL5wH#M(B z!O)Unf@giZLxX2M`HaQ_kamP6c4(FunY24haIf!YXmAJGf{g(*0V0Jkg;5CI6!XS{ z2_E%8r5-?2NMtl8kfRH3*T8CrI0F&BqG%ywY={UphDtO^6H8#QS=Ix6U8pOtAr(Vg+JZfmn9+tpN>o$g%$w%>kwcU~7S%umr{%$PvhlK_&`PJ51#-4 literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid new file mode 100644 index 0000000000000000000000000000000000000000..c8f80b589221f3f9eae23733a001f4e1a8491bc4 GIT binary patch literal 14111 zcmeHOL2DC16rLJTu?X$Kd;Wo@O^^*CY$zp3NvK(l z9{mSiJo*p3>Zxb(iZSFkS-+Orf&dz?mU?{g~ADF$SEnsev@Jjii zo&q?Z&S1g&y9ngzt_G}TeqR8I(Ri}0D9N${+AOXf@+4rzFe9RH6}CNzzIIit1FcJ@^^hjZaP1A-3DW) z(qjUro5U)3mYBy)xTFpl6WS!EjmMvUMg=dEmz56+Xe=<-6-Q{FRoc%rZ6)}`yL^^Y zm+5Mk<%O5IXR}^pTJXbDY;fVGu4&ghtA+6H=kgn^vuNO8WZ-7GXW;%P0jkipLfbM? zIhmI*0`Zq*6(oPz<{vgbU%yizrwg5rkE^iFS2B>N(h;XVtze=uKjEiNoEh+1_$l>eM2Iuspe_TcpT|EcMz_6SBTU&Eo&Ea#k^@Xy z4sWtS$Oa)BgkR!jPqwE#M7=>TjI zY Date: Fri, 1 Apr 2022 04:23:08 +0100 Subject: [PATCH 08/12] added test bpm projects --- reverse-engineering/session 3/bpm/131.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.0.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.1.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.2.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.3.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.4.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.5.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.6.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.7.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.8.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/40.9.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/41.0.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/41.1.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/41.2.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/41.3.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/42.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/50.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/51.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/52.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/799.mt | Bin 0 -> 1572 bytes reverse-engineering/session 3/bpm/800.mt | Bin 0 -> 1572 bytes 21 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 reverse-engineering/session 3/bpm/131.mt create mode 100755 reverse-engineering/session 3/bpm/40.0.mt create mode 100755 reverse-engineering/session 3/bpm/40.1.mt create mode 100755 reverse-engineering/session 3/bpm/40.2.mt create mode 100755 reverse-engineering/session 3/bpm/40.3.mt create mode 100755 reverse-engineering/session 3/bpm/40.4.mt create mode 100755 reverse-engineering/session 3/bpm/40.5.mt create mode 100755 reverse-engineering/session 3/bpm/40.6.mt create mode 100755 reverse-engineering/session 3/bpm/40.7.mt create mode 100755 reverse-engineering/session 3/bpm/40.8.mt create mode 100755 reverse-engineering/session 3/bpm/40.9.mt create mode 100755 reverse-engineering/session 3/bpm/41.0.mt create mode 100755 reverse-engineering/session 3/bpm/41.1.mt create mode 100755 reverse-engineering/session 3/bpm/41.2.mt create mode 100755 reverse-engineering/session 3/bpm/41.3.mt create mode 100755 reverse-engineering/session 3/bpm/42.mt create mode 100755 reverse-engineering/session 3/bpm/50.mt create mode 100755 reverse-engineering/session 3/bpm/51.mt create mode 100755 reverse-engineering/session 3/bpm/52.mt create mode 100755 reverse-engineering/session 3/bpm/799.mt create mode 100755 reverse-engineering/session 3/bpm/800.mt diff --git a/reverse-engineering/session 3/bpm/131.mt b/reverse-engineering/session 3/bpm/131.mt new file mode 100755 index 0000000000000000000000000000000000000000..5fa2577e5e6ebb556616b61cbd7eba80b64b3a1e GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Huw-^tpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&HuvBnTpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(euuMx+a8jVXHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGGHWXR7-1;%ShYH^7|N@7VOLpnoZu|h^_Q7XgAZ-H(Ao25%K literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/40.2.mt b/reverse-engineering/session 3/bpm/40.2.mt new file mode 100755 index 0000000000000000000000000000000000000000..185cbfbdd11f1290797a1ddc88ba1fb221e93ef4 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eusm}{!AXJk)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eurxAObW)(bHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGGHX2{P<1;%ShYH^7|N@7VOLpnoZu|h^_Q7VJpu}drf+GI*% literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/40.4.mt b/reverse-engineering/session 3/bpm/40.4.mt new file mode 100755 index 0000000000000000000000000000000000000000..3b4bc9460781fc58435b37c04c0302f4dab32de9 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eu$(bd(Mf^!)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eu>AL5(Mf^!)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(euuMr)a#EnZHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGGHW5~}+1;%ShYH^7|N@7VOLpnoZu|h^_Q7Xf~vZFizoWn~- literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/40.7.mt b/reverse-engineering/session 3/bpm/40.7.mt new file mode 100755 index 0000000000000000000000000000000000000000..81f373988fe49c46eee7afc9d6832a9fb51f9daa GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eusn4}$w`6s)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eurx4Mc2c0dHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGHyV93u(1;%ShYH^7|N@7VOLpnoZu|h^_Q7XgJC_7gG+p$Vt literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/40.9.mt b/reverse-engineering/session 3/bpm/40.9.mt new file mode 100755 index 0000000000000000000000000000000000000000..360c99122853e3ebd58e0cc7726a590823513166 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eu$(?q*-3%+)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYwgD literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/41.0.mt b/reverse-engineering/session 3/bpm/41.0.mt new file mode 100755 index 0000000000000000000000000000000000000000..9e5e8cf97f9f450cd9a56f300582ccb9c3c44d6c GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&HuvBqUpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{vf6Q~)oeN&o-= literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/41.1.mt b/reverse-engineering/session 3/bpm/41.1.mt new file mode 100755 index 0000000000000000000000000000000000000000..c0775c161d4430101eb128fa6d2a4221f25b5148 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(euuMx+aZ;eYHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGSLWXR7-1;%ShYH^7|N@7VOLpnoZu|h^_Q7Xe>{WE0%tcXiB literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/41.2.mt b/reverse-engineering/session 3/bpm/41.2.mt new file mode 100755 index 0000000000000000000000000000000000000000..c03a46149a361cfac630dad35f4509d93bf77594 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eusm}{#Yuto)(n3e!XWWD{5?vv5AA^}!MIvP&pxCmF*#eokbxn& zgn{9e-!`YTPk!YPK_iB1=?tn2MXC^!A%e!J)<6VJunC%C6EwpnXpT+L0#z^!4uIle zsB*|WW?*h+WThLe<8z|%xzPCBXnYUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz(eurxAObyA?cHN)SAFi1QOe~%LFLwle~Fs>HSvkxgsOwLv?WMD`x zVPJUWx6LW-lV3SR(1_t$I)f@hkt)Pwh@dg5H4s4)Y=WlP1kJDsnqw2RKotyw1E6>q zsvI(p8JL?H8R{8X*|{AWU=oaId?qwLGa8=-jn9h4XG7z&qwzV=_?&2bE;K$j8lMM^ z&x^+AL*w(K@qzwMGBGSLX2{P<1;%ShYH^7|N@7VOLpnoZu|h^_Q7VIryuS$m>6=M| literal 0 HcmV?d00001 diff --git a/reverse-engineering/session 3/bpm/42.mt b/reverse-engineering/session 3/bpm/42.mt new file mode 100755 index 0000000000000000000000000000000000000000..db0ea23448b403d85657723202032dc9bfabb632 GIT binary patch literal 1572 zcmeYbVPIfnW%$nv1S)KKZx|Uy0V4yq1P6l=!xvy+qyU2>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu+(r;pocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu=H?JpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{?buK>Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu=H_KpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&HuncfgpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{Ux?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&9usq=IqCf|0hP@SGka!&S{v_Um_CTdzTrHw!A5xTr+R z!0^g%n^W2+zjBD65yQ1~233Y4Rfy3LL1R>VAc7{?1WmCCnqd<($0lfjDi{U_K=CkC zIbUx?ZMN2vgopjqz(3_Wzn z#LU9V#?HaX#mz&Hu=H?IpocZX-HtFwJPvnnlHfmkpjt4l7SXd0DN0PvRxo5>NG@Su zc;&avDeaSAIYiKi;aWO_DnpSf#B7M5F{(unK@)6(rq~3{unC%D6SP1T41)upco?c2 zGLIRUn;9AE8Clu69U5Q~jA(o&G(IyLp9PK2ipFO{ Date: Fri, 1 Apr 2022 04:23:14 +0100 Subject: [PATCH 09/12] Update session2-notes.md --- reverse-engineering/session 2/session2-notes.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/reverse-engineering/session 2/session2-notes.md b/reverse-engineering/session 2/session2-notes.md index f3f1860..1d37243 100644 --- a/reverse-engineering/session 2/session2-notes.md +++ b/reverse-engineering/session 2/session2-notes.md @@ -32,3 +32,14 @@ E.g. 01 01 02 stands for 1,1,2 patterns so the offset seems to be 0x10 with length of 256 bytes +# upd +Okay, I've re-saved the files again properly with names corresponding to bpms. +I see that bpms with fractions take 4 bytes. Seems like they just use a float. + +Checked 40.1 bpm: + +> round(struct.unpack('f', bytes([0x66,0x66,0x20,0x42]))[0], 1) +> 40.1 +> +Yup, that is correct. offset 0x1c and length is 4 bytes. + From 1c87baa915e242cd29d31ad55516844bd80010d3 Mon Sep 17 00:00:00 2001 From: DataGreed Date: Fri, 1 Apr 2022 04:23:32 +0100 Subject: [PATCH 10/12] working bpm extraction from project --- out_midi.mid | Bin 0 -> 14111 bytes parsers/project.py | 24 ++++++++++++++---- .../project.mid | Bin 14111 -> 0 bytes 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 out_midi.mid delete mode 100644 reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid diff --git a/out_midi.mid b/out_midi.mid new file mode 100644 index 0000000000000000000000000000000000000000..f069df920bbd236e1c31e72160ca80ccd53e934e GIT binary patch literal 14111 zcmeHOL2DC16rLIosR-@Cd(I+gYJzMCVM8fNN(|uah8i|qc&`SzE6uIPyE{)F-QU{H7YyY#?R~S?v<1vf5)PCf z>M4Np@eCHczl}g%+&MjkJVz0p-qm=zQgFUU;%$(!jVNWM#eVilGUVyv|6B+P;lfLu zU`6Mr>DFy1st#Vw8i3w)yZ`xwh@hHbu&$-gFfwqn+%s_hlK@p{TcK^4 zsGQ787=idpvI>&FZ1WErpReC3kkf_E$H!IJ<|`St`0RlZt*WKew#ew+}A^D zc3AZ$BiU0@zNdr-l;uP1fKs~ohr#2JW8iO7I`EDx>FHzjL6 z%|TCbkaWbUPb-+H%uo2K6K4jz7Jf>784=gVx~iqUN^*a%a$MrXf%zvKXu zmcyHD5VAqY2H}^u85Ca8pXg8YH)r|_Z)b8x`t3}i|GVq1@#c69%r^P9fC;e5mQ{c% zK^34zP>Xh%_PZqv#!C$`?QxEz>l*J-=+U%a "Project": @@ -92,11 +93,17 @@ def from_bytes(data: bytes, patterns_bytes=Dict[int,bytes]) -> "Project": for key, value in patterns_bytes.items(): patterns_mapping[key] = Pattern.from_bytes(value[Pattern.OFFSET_START:Pattern.OFFSET_END]) - bpm = 120 # todo: parse bpm from project file! + bpm = Project.bpm_from_bytes(data[Project.BPM_OFFSET_START:Project.BPM_OFFSET_START+Project.BPM_BYTES_LENGTH]) pattern_chain = Project.pattern_chain_from_bytes(data[Project.PATTERN_CHAIN_OFFSET:Project.PATTERN_CHAIN_END]) - name = "MyProject" # todo: extract from project files footer + # todo: extract from project files footer + # tracker project files actually store file names inside of them, but + # the problem is that it seems to be buggy, since a lot of times i see + # part of the previous project names in new projects with shorter names. + # not sure if it's actually possible to unpack a real, not mangled name + # from a project file. + name = "MyProject" return Project(name=name, song=Song(pattern_chain=pattern_chain, @@ -112,7 +119,6 @@ def pattern_chain_from_bytes(data: bytes) -> List[int]: # print(data) # print(f"data length {len(data)}") - result = [] for byte in data: @@ -128,6 +134,14 @@ def pattern_chain_from_bytes(data: bytes) -> List[int]: return result + @staticmethod + def bpm_from_bytes(data: bytes) -> List[int]: + + if len(data) != 4: + raise ValueError(f"Length of data for BPM should be 4 bytes, but received {len(bytes)}: {bytes}") + # unpacks 4 bytes to a float + return round(struct.unpack('f', data)[0], 1) + class ProjectParser: diff --git a/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid b/reverse-engineering/session 1/project files/datagreed - rebel path tribute 2/project.mid deleted file mode 100644 index c8f80b589221f3f9eae23733a001f4e1a8491bc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14111 zcmeHOL2DC16rLJTu?X$Kd;Wo@O^^*CY$zp3NvK(l z9{mSiJo*p3>Zxb(iZSFkS-+Orf&dz?mU?{g~ADF$SEnsev@Jjii zo&q?Z&S1g&y9ngzt_G}TeqR8I(Ri}0D9N${+AOXf@+4rzFe9RH6}CNzzIIit1FcJ@^^hjZaP1A-3DW) z(qjUro5U)3mYBy)xTFpl6WS!EjmMvUMg=dEmz56+Xe=<-6-Q{FRoc%rZ6)}`yL^^Y zm+5Mk<%O5IXR}^pTJXbDY;fVGu4&ghtA+6H=kgn^vuNO8WZ-7GXW;%P0jkipLfbM? zIhmI*0`Zq*6(oPz<{vgbU%yizrwg5rkE^iFS2B>N(h;XVtze=uKjEiNoEh+1_$l>eM2Iuspe_TcpT|EcMz_6SBTU&Eo&Ea#k^@Xy z4sWtS$Oa)BgkR!jPqwE#M7=>TjI zY Date: Fri, 1 Apr 2022 04:23:39 +0100 Subject: [PATCH 11/12] Delete out_midi.mid --- out_midi.mid | Bin 14111 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 out_midi.mid diff --git a/out_midi.mid b/out_midi.mid deleted file mode 100644 index f069df920bbd236e1c31e72160ca80ccd53e934e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14111 zcmeHOL2DC16rLIosR-@Cd(I+gYJzMCVM8fNN(|uah8i|qc&`SzE6uIPyE{)F-QU{H7YyY#?R~S?v<1vf5)PCf z>M4Np@eCHczl}g%+&MjkJVz0p-qm=zQgFUU;%$(!jVNWM#eVilGUVyv|6B+P;lfLu zU`6Mr>DFy1st#Vw8i3w)yZ`xwh@hHbu&$-gFfwqn+%s_hlK@p{TcK^4 zsGQ787=idpvI>&FZ1WErpReC3kkf_E$H!IJ<|`St`0RlZt*WKew#ew+}A^D zc3AZ$BiU0@zNdr-l;uP1fKs~ohr#2JW8iO7I`EDx>FHzjL6 z%|TCbkaWbUPb-+H%uo2K6K4jz7Jf>784=gVx~iqUN^*a%a$MrXf%zvKXu zmcyHD5VAqY2H}^u85Ca8pXg8YH)r|_Z)b8x`t3}i|GVq1@#c69%r^P9fC;e5mQ{c% zK^34zP>Xh%_PZqv#!C$`?QxEz>l*J-=+U%a Date: Fri, 1 Apr 2022 04:32:59 +0100 Subject: [PATCH 12/12] fixed bpm for individual exported patterns; fixed midi pattern folder --- polytracker2midi.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/polytracker2midi.py b/polytracker2midi.py index ffb6264..cac7502 100644 --- a/polytracker2midi.py +++ b/polytracker2midi.py @@ -85,19 +85,22 @@ def main(): print("Trying to export patterns...") for number, pattern in parsed_project.song.pattern_mapping.items(): - midi_exporter = midi.PatternToMidiExporter(pattern=pattern) + midi_exporter = midi.PatternToMidiExporter(pattern=pattern, tempo_bpm=int(parsed_project.song.bpm)) number_string = str(number) - if len(number_string)<2: + if len(number_string) < 2: number_string = "0" + number_string # create directory for patterns + # in the same folder we export project to + out_folder = os.sep.join(output_filename.split(os.sep)[:-1]) + os.sep + try: - os.mkdir(p.folder + "patterns_midi/") + os.mkdir(out_folder + "patterns_midi/") except FileExistsError: pass - pattern_output_filename = p.folder + "patterns_midi/" + f"pattern_{number_string}.mid" + pattern_output_filename = out_folder + "patterns_midi/" + f"pattern_{number_string}.mid" midi_exporter.write_midi_file(pattern_output_filename) print(f"Exported pattern midi to {os.path.abspath(pattern_output_filename)}")