├── op1repacker ├── __init__.py ├── assets │ ├── presets │ │ └── iter │ │ │ ├── iter.aif │ │ │ ├── korder.aif │ │ │ ├── ebullition.aif │ │ │ ├── feu_doux.aif │ │ │ ├── pulse_bass.aif │ │ │ ├── rezpiano.aif │ │ │ ├── wonky_bass.aif │ │ │ ├── wood_bell.aif │ │ │ ├── woodblock.aif │ │ │ ├── crystal_pad.aif │ │ │ └── lotremkords.aif │ └── display │ │ ├── tape-invert.patch.json │ │ ├── cwo-moose.patch.json │ │ └── iter-lab.svg ├── op1_patches.py ├── op1_analyze.py ├── op1_db.py ├── op1_gfx.py ├── op1_repack.py └── main.py ├── setup.cfg ├── images ├── iter.png ├── filter.png ├── cwo-moose.png ├── iter-lab.png └── tape-invert.png ├── op1repacker.py ├── setup.py ├── release.sh ├── LICENSE ├── .gitignore ├── INSTALL.md ├── CHANGELOG.md └── README.md /op1repacker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | inline-quotes = ' 4 | -------------------------------------------------------------------------------- /images/iter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/images/iter.png -------------------------------------------------------------------------------- /images/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/images/filter.png -------------------------------------------------------------------------------- /images/cwo-moose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/images/cwo-moose.png -------------------------------------------------------------------------------- /images/iter-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/images/iter-lab.png -------------------------------------------------------------------------------- /images/tape-invert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/images/tape-invert.png -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/iter.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/iter.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/korder.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/korder.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/ebullition.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/ebullition.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/feu_doux.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/feu_doux.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/pulse_bass.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/pulse_bass.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/rezpiano.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/rezpiano.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/wonky_bass.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/wonky_bass.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/wood_bell.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/wood_bell.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/woodblock.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/woodblock.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/crystal_pad.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/crystal_pad.aif -------------------------------------------------------------------------------- /op1repacker/assets/presets/iter/lotremkords.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/op1hacks/op1repacker/HEAD/op1repacker/assets/presets/iter/lotremkords.aif -------------------------------------------------------------------------------- /op1repacker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Convenience wrapper for running op1repacker directly from source tree.""" 5 | 6 | from op1repacker.main import main 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | from setuptools import setup 4 | 5 | 6 | version = re.search( 7 | "^__version__\s*=\s*'([^']*)'", 8 | open("op1repacker/main.py").read(), 9 | re.M 10 | ).group(1) 11 | 12 | files = [ 13 | "assets/display/*.svg", 14 | "assets/display/*.json", 15 | "assets/presets/*/*.aif", 16 | ] 17 | 18 | setup(name="op1repacker", 19 | version=version, 20 | description="Tool for unpacking, modding and repacking OP-1 firmware.", 21 | author="Richard Lewis", 22 | author_email="richrd.lewis@gmail.com", 23 | url="https://github.com/op1hacks/op1repacker/", 24 | packages=["op1repacker"], 25 | package_data={"": files}, 26 | install_requires=[ 27 | "svg.path", 28 | ], 29 | entry_points={ 30 | "console_scripts": ["op1repacker=op1repacker.main:main"] 31 | }, 32 | classifiers=[] 33 | ) 34 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Exit on first error 3 | set -e 4 | 5 | # Parse our CLI arguments 6 | version="$1" 7 | if test "$version" = ""; then 8 | echo "Expected a version to be provided to \`release.sh\` but none was provided." 1>&2 9 | echo "Usage: $0 [version] # (e.g. $0 1.0.0)" 1>&2 10 | exit 1 11 | fi 12 | 13 | # Bump the version via regexp 14 | sed -E "s/^(__version__ = ')[0-9]+\.[0-9]+\.[0-9]+(')$/\1$version\2/" op1repacker/main.py --in-place 15 | 16 | # Verify our version made it into the file 17 | if ! grep "$version" op1repacker/main.py &> /dev/null; then 18 | echo "Expected \`__version__\` to update via \`sed\` but it didn't" 1>&2 19 | exit 1 20 | fi 21 | 22 | # Commit the change 23 | git add op1repacker/main.py 24 | git commit -a -m "Release $version" 25 | 26 | # Tag the release 27 | git tag "$version" 28 | 29 | # Publish the release to GitHub 30 | git push 31 | git push --tags 32 | 33 | # Publish the release to PyPI 34 | python setup.py sdist --formats=gztar upload 35 | 36 | -------------------------------------------------------------------------------- /op1repacker/assets/display/tape-invert.patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "tape.svg", 3 | "changes": [ 4 | { 5 | "type": "substitute", 6 | "find": "\n", 7 | "replace": " " 8 | }, 9 | { 10 | "type": "substitute", 11 | "find": "\t", 12 | "replace": " " 13 | }, 14 | { 15 | "type": "move_all", 16 | "delta": [0, 31] 17 | }, 18 | { 19 | "type": "move_elements", 20 | "elements": [ 21 | ["line", "centerline_23_"], 22 | ["g", "grid"], 23 | ["line", "ghost_x5F_line"], 24 | ["circle", "loopin"], 25 | ["circle", "loopout"], 26 | ["line", "loop_x5F_line"], 27 | ["line", "track_x5F_active"], 28 | ["line", "track_x5F_semiactive_15_"], 29 | ["line", "track_x5F_inactive"], 30 | ["line", "_x3C_Path_x3E__1_"] 31 | ], 32 | "delta": [0, -147] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 op1hacks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /op1repacker/op1_patches.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import struct 4 | 5 | 6 | def load_patch_folder(path): 7 | patch_files = [name for name in os.listdir(path) if name.lower().endswith('aif')] 8 | 9 | patches = [] 10 | 11 | for file_name in patch_files: 12 | patch_name = os.path.splitext(file_name)[0] 13 | patch_data = read_patch(os.path.join(path, file_name)) 14 | # Set the patch data name based on the filename of the patch 15 | # TODO: option for normalizing patch names 16 | patch_data['name'] = patch_name 17 | patches.append(patch_data) 18 | 19 | return patches 20 | 21 | 22 | def read_patch(patch_filename): 23 | f = open(patch_filename, 'rb') 24 | data = f.read() 25 | f.close() 26 | 27 | # Locate start of APPL chunk 28 | appl_pos = data.find(bytes('APPL', 'utf-8')) 29 | if appl_pos == -1: 30 | raise TypeError('Invalid file. No APPL data found.') 31 | appl_pos += 4 32 | 33 | appl_chunk = data[appl_pos:] 34 | appl_data_len = struct.unpack('>l', appl_chunk[:4])[0] 35 | 36 | appl_data_bin = appl_chunk[4:appl_data_len+4] 37 | appl_data = str(appl_data_bin, 'utf-8').strip() 38 | 39 | if appl_data.startswith('op-1'): 40 | appl_data = appl_data[4:] 41 | 42 | return json.loads(appl_data) 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /op1repacker/op1_analyze.py: -------------------------------------------------------------------------------- 1 | """Analyze unpacked OP-1 firmware directories.""" 2 | 3 | import os 4 | import re 5 | import time 6 | 7 | UNKNOWN_VALUE = 'UNKNOWN' 8 | 9 | 10 | def analyze_boot_ldr(target): 11 | path = os.path.join(target, 'te-boot.ldr') 12 | f = open(path, 'rb') 13 | data = f.read() 14 | f.close() 15 | 16 | version_arr = re.findall(br'TE-BOOT .+?(\d*\.?\d+)', data) 17 | bootloader_version = version_arr[0].decode('utf-8').strip() if version_arr else UNKNOWN_VALUE 18 | 19 | return { 20 | 'bootloader_version': bootloader_version, 21 | } 22 | 23 | 24 | def analyze_main_ldr(target): 25 | path = os.path.join(target, 'OP1_vdk.ldr') 26 | f = open(path, 'rb') 27 | data = f.read() 28 | f.close() 29 | 30 | start_pos = data.find(b'Rev.') 31 | chunk = data[start_pos:] 32 | end_pos = chunk.find(b'\n') 33 | chunk = chunk[:end_pos].decode('utf-8') 34 | 35 | build_version_arr = re.findall(r'Rev.+?(.*?);', chunk) 36 | build_version = build_version_arr[0].strip() if build_version_arr else UNKNOWN_VALUE 37 | 38 | date_arr = re.findall(r'\d\d\d\d/\d\d/\d\d', chunk) 39 | time_arr = re.findall(r'\d\d:\d\d:\d\d', chunk) 40 | 41 | fw_version = re.findall(br'R\..\d\d\d\d?\d?', data) 42 | fw_version = UNKNOWN_VALUE if not fw_version else fw_version[0].decode('utf-8') 43 | 44 | return { 45 | 'firmware_version': str(fw_version), 46 | 'build_version': build_version, 47 | 'build_date': date_arr[0] if date_arr else UNKNOWN_VALUE, 48 | 'build_time': time_arr[0] if time_arr else UNKNOWN_VALUE, 49 | } 50 | 51 | 52 | def analyze_fs(target): 53 | oldest = None 54 | newest = None 55 | for root, dirs, files in os.walk(target): 56 | for file in files: 57 | file_path = os.path.join(root, file) 58 | mtime = os.path.getmtime(file_path) 59 | if oldest is None or mtime < oldest: 60 | oldest = mtime 61 | if newest is None or mtime > newest: 62 | newest = mtime 63 | 64 | return { 65 | 'oldest_file': time.strftime('%Y/%m/%d %H:%M', time.gmtime(oldest)), 66 | 'newest_file': time.strftime('%Y/%m/%d %H:%M', time.gmtime(newest)), 67 | } 68 | 69 | 70 | def analyze_unpacked_fw(target): 71 | main_ldr_info = analyze_main_ldr(target) 72 | boot_ldr_info = analyze_boot_ldr(target) 73 | fs_info = analyze_fs(target) 74 | 75 | return { 76 | **main_ldr_info, 77 | **boot_ldr_info, 78 | **fs_info, 79 | } 80 | -------------------------------------------------------------------------------- /op1repacker/op1_db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os.path 3 | 4 | 5 | class OP1DB: 6 | def __init__(self): 7 | self.conn = None 8 | # New default params for FX that affect the sound as little as possible 9 | # This avoids sudden changes to the sound when enabling an effect 10 | # during live performances. 11 | self.subtle_fx_params = { 12 | "cwo": "[0, 0, 0, 0, 0, 0, 0, 0]", 13 | "delay": "[8000, 8000, 8000, 0, 0, 0, 0, 0]", 14 | "filter": "[7548, 0, 8272, 19572, 8000, 8000, 8000, 8000]", 15 | "grid": "[8000, 8000, 18000, 0, 8000, 8000, 8000, 8000]", 16 | "nitro": "[64, 0, 10323, 16448, 0, 0, 0, 0]", 17 | "phone": "[8000, 8000, 8016, 0, 8000, 8000, 8000, 8000]", 18 | "punch": "[6000, 15000, 20000, 0, 8000, 8000, 8000, 8000]", 19 | "spring": "[16448, 8560, 9728, 0, 8000, 8000, 8000, 8000]", 20 | } 21 | 22 | def __del__(self): 23 | if self.conn: 24 | self.conn.close() 25 | 26 | def open(self, path): 27 | path = os.path.abspath(path) 28 | if not os.path.exists(path): 29 | raise FileNotFoundError("Database file doesn't exist.") 30 | self.conn = sqlite3.connect(path) 31 | 32 | def commit(self): 33 | self.conn.commit() 34 | return True 35 | 36 | # TODO: Don't overwrite row if id exists 37 | def enable_filter(self): 38 | # Make sure it's not already enabled 39 | out = self.conn.execute('SELECT * FROM fx_types WHERE type = \'filter\'') 40 | results = out.fetchall() 41 | if results: 42 | return False 43 | new_row = (2, 'filter', '[7548, 0, 8272, 19572, 8000, 8000, 8000, 8000]') 44 | self.conn.execute('INSERT INTO fx_types VALUES (?,?,?)', new_row) 45 | return True 46 | 47 | def enable_iter(self): 48 | # Make sure it's not already enabled 49 | out = self.conn.execute('SELECT * FROM synth_types WHERE type = \'iter\'') 50 | results = out.fetchall() 51 | if results: 52 | return False 53 | new_row = (11, 'iter', '[1516, 16704, 0, 15168, 0, 0, 0, 0]') 54 | self.conn.execute('INSERT INTO synth_types VALUES (?,?,?)', new_row) 55 | return True 56 | 57 | def enable_subtle_fx_defaults(self): 58 | types = self.get_existing_fx_types() 59 | for fx_type in types: 60 | if fx_type in self.subtle_fx_params: 61 | params = self.subtle_fx_params[fx_type] 62 | self.set_fx_default_params(fx_type, params) 63 | return True 64 | 65 | def get_existing_fx_types(self): 66 | out = self.conn.execute('SELECT type FROM fx_types') 67 | results = out.fetchall() 68 | if not results: 69 | return [] 70 | 71 | fx_types = map(lambda row: row[0], results) 72 | return fx_types 73 | 74 | def set_fx_default_params(self, fx_type, params): 75 | self.conn.execute('UPDATE fx_types SET default_params=? WHERE type=?', (params, fx_type)) 76 | 77 | def synth_preset_folder_exists(self, synth_type): 78 | # Check if there are any synth presets under the folder synth_type 79 | out = self.conn.execute('SELECT * FROM synth_presets WHERE folder=?', (synth_type, )) 80 | results = out.fetchall() 81 | if results: 82 | return True 83 | return False 84 | 85 | def insert_synth_preset(self, patch, folder): 86 | self.conn.execute('INSERT INTO synth_presets (patch, folder) VALUES (?, ?)', (patch, folder)) 87 | return True 88 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 4 | ## Mac OS X 5 | 6 | ### Step 1: Get the firmware file that you want to modify 7 | 8 | First you need to have an official firmware update file, here's how to get one. 9 | 10 | - **Recommended:** Get the latest OP-1 firmware update file from the TE website: https://teenage.engineering/downloads 11 | - *Experimental:* If you want you can try older firmware files from the archive: https://github.com/op1hacks/op1-fw-archive 12 | 13 | ### Step 2: Get into the terminal 14 | 15 | In this step we'll need to make sure **python3** is installed since Python 3 is the programming 16 | language that **op1repacker** is written in. It might already be installed on your system but we'll 17 | check and install it if it's not. 18 | 19 | First we'll need to open the terminal: 20 | - Open the Terminal App on your system (more info about Terminal here: https://macpaw.com/how-to/use-terminal-on-mac) 21 | - Next lets see if python3 is installed. 22 | In the terminal type the following command and press enter: 23 | ```python3 --version``` 24 | If the output looks something like `Python 3.X.X` then you have python3 and can continue to step #3. For example: 25 | ```Python 3.6.7``` 26 | 27 | If you get an error from the command above (something like `command not found: python3` you'll need to install Python 3 yourself. I would recommend checking out one of the following guides for installing it: 28 | - https://www.saintlad.com/install-python-3-on-mac/ 29 | - https://docs.python-guide.org/starting/install3/osx/ 30 | 31 | Feel free to send message on the [OP-1 forum](https://op-forums.com/) or create an issue in the op1repacker GitHub repository if you need more info about installing python3 on Mac OS. 32 | 33 | ### Step 3: Install the op1repacker tool 34 | 35 | - In the terminal run the following command: 36 | ```pip3 install op1repacker``` 37 | - Alternatively try `pip install op1repacker` if that doesn't work. You might also have to add `sudo ` to the beginning of the command if it says something like permission denied. 38 | - You should now have the latest version of the tool. 39 | - To make sure the tool works run the following command: 40 | ```op1repacker -v``` 41 | If the installation worked you'll see a version number of the tool. For example: 42 | ```0.2.2``` 43 | 44 | ### Step 4: Create your custom firmware 45 | - In the terminal, go to the directory where your firmware file is. If the firmware file is in your home directory run the following command: 46 | ```cd ~``` 47 | - If the firmware is in some other directory you can navigate to it in the terminal this: 48 | ```cd /path/to/folder``` 49 | 50 | Now comes the fun part: actually modding the firmware. The commands below use the latest firmware `op1_235` as an example. Change the filename If you are using a different firmware version. 51 | 52 | - First unpack the firmware file by running: 53 | ```op1repacker unpack op1_235.op1``` 54 | - Now you can mod the unpacked firmware. The available mods are described here: https://github.com/op1hacks/op1repacker#modify For example to enable all the available modifications run: 55 | > `op1repacker modify op1_235 --options iter presets-iter filter subtle-fx gfx-iter-lab gfx-tape-invert gfx-cwo-moose` 56 | 57 | You can of course leave any of the mods out if you don't want all of them. 58 | - Now that the mods are done you can get your installable custom firmware file. 59 | Repackage the unpacked firmware with this command: 60 | ```op1repacker repack op1_235``` 61 | 62 | Now your folder should have the file ```op1_235-repacked.op1```. Run the normal OP-1 firmware update and use this file and to get the mods installed on your OP-1. 63 | **Enjoy!** 64 | 65 | 66 | ## Windows / Linux 67 | 68 | No instructions yet, sorry. 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | ## [v0.2.5](https://github.com/op1hacks/op1repacker/tree/v0.2.5) (2020-06-03) compared to previous master branch. 5 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.2.4...v0.2.5) 6 | 7 | **Bugs fixed:** 8 | 9 | - Fixed using absolute path when analyzing firmware (bug #33). 10 | 11 | 12 | ## [v0.2.4](https://github.com/op1hacks/op1repacker/tree/v0.2.4) (2019-12-26) compared to previous master branch. 13 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.2.3...v0.2.4) 14 | 15 | **Bugs fixed:** 16 | 17 | - Fixed tar archive format for Python 3.8. 18 | 19 | 20 | ## [v0.2.3](https://github.com/op1hacks/op1repacker/tree/v0.2.3) (2019-08-18) compared to previous master branch. 21 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.2.2...v0.2.3) 22 | 23 | **Bugs fixed:** 24 | 25 | - Fix bug #27 which caused incorrect firmware builds when path had a slash at the end. 26 | 27 | **Implemented enhancements:** 28 | 29 | - Better Mac OS instructions. 30 | 31 | 32 | ## [v0.2.2](https://github.com/op1hacks/op1repacker/tree/v0.2.2) (2019-04-28) compared to previous master branch. 33 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.2.1...v0.2.2) 34 | 35 | **Implemented enhancements:** 36 | 37 | - Added `analyze` action for analyzing version info etc of unpacked firmware directories. 38 | - Add ability to unpack, repack, modify or analyze multiple files/directories at once. 39 | 40 | 41 | ## [v0.2.1](https://github.com/op1hacks/op1repacker/tree/v0.2.1) (2019-04-21) compared to previous master branch. 42 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/0.2.0...0.2.1) 43 | 44 | **Bugs fixed:** 45 | 46 | - Hotfix installation: iter preset files were missing from the bundle. 47 | 48 | 49 | ## [v0.2.0](https://github.com/op1hacks/op1repacker/tree/v0.2.0) (2019-04-21) compared to previous master branch. 50 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/0.1.4...0.2.0) 51 | 52 | **Implemented enhancements:** 53 | 54 | - Added mod to enable community presets for the iter synth. 55 | 56 | 57 | ## [v0.1.4](https://github.com/op1hacks/op1repacker/tree/v0.1.4) (2018-05-26) compared to previous master branch. 58 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/0.1.3...0.1.4) 59 | 60 | **Implemented enhancements:** 61 | 62 | - Added mod to move tape tracks to top of the screen. 63 | - Added mod that changes the cow graphics in CWO to a moose. 64 | 65 | 66 | ## [v0.1.3](https://github.com/op1hacks/op1repacker/tree/v0.1.3) (2018-01-05) compared to previous master branch. 67 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/0.1.2...0.1.3) 68 | 69 | **Bugs fixed:** 70 | 71 | - Fixed incorrect imports that caused program to not run at all. 72 | 73 | 74 | ## [v0.1.2](https://github.com/op1hacks/op1repacker/tree/v0.1.2) (2018-01-04) compared to previous master branch. 75 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/0.1.1...0.1.2) 76 | 77 | **Implemented enhancements:** 78 | 79 | - Add `gfx-iter-lab` mod for enabling custom graphics for iter. 80 | - Add images to readme. 81 | 82 | 83 | ## [v0.1.1](https://github.com/op1hacks/op1repacker/tree/v0.1.1) (2017-12-27) compared to previous master branch. 84 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.1.0...v0.1.1) 85 | 86 | **Implemented enhancements:** 87 | 88 | - Added `subtle-fx` mod for changing default parameters of effects to be more subtle. 89 | - Changed the name to op1repacker (previously `op1-fw-repacker`). 90 | - Made the tool installable. 91 | 92 | 93 | ## [v0.1.0](https://github.com/op1hacks/op1repacker/tree/v0.1.0) (2017-01-17) compared to previous master branch. 94 | [Full Changelog](https://github.com/op1hacks/op1repacker/compare/v0.0.1...v0.1.0) 95 | 96 | **Implemented enhancements:** 97 | 98 | - Added `modify` mode for enabling `filter` and `iter`. 99 | - Added some error checking and handling. 100 | - Restructured the code. 101 | 102 | **Fixed bugs:** 103 | 104 | - Exclude hidden files from repacked firmware. 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OP-1 Firmware Repacker 2 | 3 | *The* tool for unpacking and repacking OP-1 synthesizer firmware. It's based on 4 | the collective research we've done at the [op-forums.com custom firmware thread](https://op-forums.com/t/custom-firmware-on-the-op-1/4283/680). 5 | This allows you to access and modify the files within the firmware as well as 6 | repacking the files into a valid installable firmware file. Ready made mods 7 | are also included in the tool (see [Modify](#modify)). Lastly it is also 8 | possible to analyze unpacked firmware to get information such as build version, 9 | build time and date, bootloader version etc. 10 | 11 | - Requires Python3 12 | - Tested on Linux, OS X and Windows 10 13 | 14 | ![Filter Effect](https://raw.githubusercontent.com/op1hacks/op1repacker/master/images/filter.png) 15 | ![Custom Iter Graphic](https://raw.githubusercontent.com/op1hacks/op1repacker/master/images/iter-lab.png) 16 | ![Tape Invert](https://raw.githubusercontent.com/op1hacks/op1repacker/master/images/tape-invert.png) 17 | ![CWO Moose](https://raw.githubusercontent.com/op1hacks/op1repacker/master/images/cwo-moose.png) 18 | 19 | 20 | ## Disclaimer 21 | 22 | **Don't use this unless you know exactly what you are doing!** 23 | I take no responsibility whatsoever for any damage that might result from using 24 | this software. You will void your OP-1 warranty and in the worst case brick it 25 | using custom firmware. Everything you do with this is at your own risk! 26 | 27 | 28 | ## Installation 29 | 30 | To install `op1repacker` run the following command: 31 | 32 | pip3 install --user op1repacker 33 | 34 | And to upgrade to a new version: 35 | 36 | pip3 install --user --upgrade op1repacker 37 | 38 | 39 | ## Usage 40 | 41 | ### Unpack & Repack 42 | 43 | op1repacker unpack [filename] # Unpack an OP-1 firmware file. 44 | op1repacker repack [directory] # Repack a directory containing unpacked firmware. 45 | 46 | The firmware is unpacked to a new folder in the same location as the firmware 47 | file is. If you unpack the firmware file `op1_218.op1` at `/home/user/op1/` 48 | you'll get a folder `/home/user/op1/op1_218/` containing the unpacked files. 49 | The same logic works for repacking, the new firmware file is saved in the same 50 | location, but the name will be `op1_218-repacked.op1`. 51 | 52 | 53 | ### Analyze 54 | 55 | After unpacking a firmware file you can analyze the firmware directory. 56 | 57 | op1repacker analyze [directory] 58 | 59 | Example output: 60 | 61 | - FIRMWARE VERSION: R. 00235 62 | - BUILD VERSION: 00235 63 | - BUILD DATE: 2019/01/07 64 | - BUILD TIME: 17:45:00 65 | - BOOTLOADER VERSION: 2.18 66 | - OLDEST FILE: 2017/05/02 12:11 67 | - NEWEST FILE: 2019/04/25 12:06 68 | 69 | 70 | ### Modify 71 | 72 | The firmware can be automatically modified with some predefined mods. 73 | These have been tested on the firmware version 235. 74 | Currently available mods are: 75 | 76 | * iter 77 | 78 | > Enable the hidden iter synth 79 | 80 | * presets-iter 81 | 82 | > Add community presets from [op1.fun](http://op1.fun) to the iter synth 83 | 84 | * filter 85 | 86 | > Enable the hidden filter effect 87 | 88 | * subtle-fx 89 | 90 | > Lower the default intensity of effects. This allows you to turn effects on 91 | > without affecting the sound too much. You can then turn them up as you like. 92 | > This helps with live performances and avoids a sudden change to the sound 93 | > when an effect is enabled. 94 | 95 | * gfx-iter-lab 96 | 97 | > Add custom lab themed visuals to the iter synth. 98 | 99 | * gfx-tape-invert 100 | 101 | > Move the tracks to the top of the tape screen to make them much easier to see 102 | > at certain angles. 103 | 104 | * gfx-cwo-moose 105 | 106 | > Swap the cow in the CWO effect with a moose, because why not. 107 | 108 | 109 | 110 | To enable a mod, first unpack the firmware, then run the following command 111 | (replace mod_name with the mod you want and [directory] with the location 112 | of the unpacked firmware) and repack the firmware after that. 113 | 114 | op1repacker modify [directory] --options mod_name 115 | 116 | For example, to enable all mods run this command: 117 | 118 | op1repacker modify [directory] --options iter presets-iter filter subtle-fx gfx-iter-lab gfx-tape-invert gfx-cwo-moose 119 | 120 | More modifications might be added later. 121 | 122 | 123 | ## Contributing 124 | 125 | If you want to participate please submit issues and pull requests to GitHub. 126 | Pull requests should be opened against the `dev` branch. I like to only push 127 | tested new versions to master. You can also let me know of possible mods you 128 | would like to see by openning a new issue and describing the mod. Keep in 129 | mind that new features can't be added - only changes to what's already in the 130 | firmware are possible. 131 | -------------------------------------------------------------------------------- /op1repacker/op1_gfx.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import json 4 | 5 | from svg.path import parse_path 6 | 7 | # String to add to patched SVGs to help detect already patched files 8 | PATCH_IDENTIFIER = '' 9 | 10 | 11 | def get_full_match(pat): 12 | return pat.string[pat.start():pat.end()] 13 | 14 | 15 | def round_coordinate(v): 16 | return round(v, 4) 17 | 18 | 19 | def simple_delta_move(pat, delta): 20 | text = get_full_match(pat) 21 | match = pat.group(1) 22 | new = str(round_coordinate(float(match)+delta)) 23 | text = text.replace(match, new) 24 | return text 25 | 26 | 27 | def polyline_delta_move(pat, delta): 28 | text = get_full_match(pat) 29 | point_str = pat.group(1) 30 | points = ' '.join(point_str.split()).split(' ') 31 | new_point_str = '' 32 | 33 | for point in points: 34 | x, y = point.split(',') 35 | x = round_coordinate(float(x)+delta[0]) 36 | y = round_coordinate(float(y)+delta[1]) 37 | new_point_str += "{},{} ".format(x, y) 38 | 39 | new_point_str = new_point_str.strip() 40 | text = text.replace(point_str, new_point_str) 41 | return text 42 | 43 | 44 | def move_imaginary(ima, delta): 45 | di = complex(round_coordinate(delta[0]), round_coordinate(delta[1])) 46 | return ima+di 47 | 48 | 49 | def path_primitive_move(primitive, delta): 50 | primitive.start = move_imaginary(primitive.start, delta) 51 | primitive.end = move_imaginary(primitive.end, delta) 52 | if 'control' in dir(primitive): 53 | primitive.control = move_imaginary(primitive.control, delta) 54 | if 'control1' in dir(primitive): 55 | primitive.control1 = move_imaginary(primitive.control1, delta) 56 | if 'control2' in dir(primitive): 57 | primitive.control2 = move_imaginary(primitive.control2, delta) 58 | 59 | 60 | def path_delta_move(pat, delta): 61 | text = get_full_match(pat) 62 | match = pat.group(1) 63 | path = parse_path(match) 64 | for primitive in path: 65 | path_primitive_move(primitive, delta) 66 | new_d = path.d() 67 | return text.replace(match, new_d) 68 | 69 | 70 | def create_delta_move(delta, func): 71 | return lambda pat: func(pat, delta) 72 | 73 | 74 | def move_all(data, delta): 75 | x, y = delta 76 | data = re.sub(r'', create_delta_move(x, simple_delta_move), data) 77 | data = re.sub(r'', create_delta_move(y, simple_delta_move), data) 78 | data = re.sub(r'x1="(.*?)"', create_delta_move(x, simple_delta_move), data) 79 | data = re.sub(r'x2="(.*?)"', create_delta_move(x, simple_delta_move), data) 80 | data = re.sub(r'y1="(.*?)"', create_delta_move(y, simple_delta_move), data) 81 | data = re.sub(r'y2="(.*?)"', create_delta_move(y, simple_delta_move), data) 82 | data = re.sub(r'cx="(.*?)"', create_delta_move(x, simple_delta_move), data) 83 | data = re.sub(r'cy="(.*?)"', create_delta_move(y, simple_delta_move), data) 84 | data = re.sub(r' d="(.*?)"', create_delta_move(delta, path_delta_move), data) 85 | data = re.sub(r' points="(.*?)"', create_delta_move(delta, polyline_delta_move), data) 86 | return data 87 | 88 | 89 | def element_delta_move(pat, delta): 90 | text = get_full_match(pat) 91 | text = move_all(text, delta) 92 | return text 93 | 94 | 95 | def move_element(data, tag, svg_id, delta): 96 | search = r'<' + tag + ' id="' + svg_id + '".*?/>' 97 | if tag == 'g': 98 | search = r'<' + tag + ' id="' + svg_id + '".*?/>.*?' 99 | elem_data = re.sub(search, create_delta_move(delta, element_delta_move), data) 100 | return elem_data 101 | 102 | 103 | def move_elements(data, elements, delta): 104 | for element in elements: 105 | data = move_element(data, element[0], element[1], delta) 106 | return data 107 | 108 | 109 | def apply_patch(data, patch): 110 | for change in patch['changes']: 111 | if change['type'] == 'substitute': 112 | data = re.sub(change['find'], change['replace'], data) 113 | if change['type'] == 'move_all': 114 | data = move_all(data, change['delta']) 115 | if change['type'] == 'move_element': 116 | data = move_element(data, change['tag'], change['id'], change['delta']) 117 | if change['type'] == 'move_elements': 118 | data = move_elements(data, change['elements'], change['delta']) 119 | 120 | # Add the patch identifier to avoid double patching 121 | data = data + PATCH_IDENTIFIER 122 | 123 | return data 124 | 125 | 126 | def is_patched(data): 127 | return PATCH_IDENTIFIER in data 128 | 129 | 130 | def patch_image_file(fw_path, patch_file): 131 | # Read the patch file 132 | f = open(patch_file) 133 | patch_data = f.read() 134 | f.close() 135 | patch = json.loads(patch_data) 136 | 137 | # Read the original SVG 138 | target_file = os.path.join(fw_path, 'content', 'display', patch['file']) 139 | f = open(target_file) 140 | svg_data = f.read() 141 | f.close() 142 | 143 | if is_patched(svg_data): 144 | return False 145 | 146 | # Change the SVG data 147 | new_data = apply_patch(svg_data, patch) 148 | 149 | # Make sure the data got patched 150 | if new_data == svg_data: 151 | print('No changes made, patch already applied?') 152 | return False 153 | 154 | # Write the patched data to the SVG file 155 | f = open(target_file, 'w') 156 | f.write(new_data) 157 | f.close() 158 | 159 | return True 160 | -------------------------------------------------------------------------------- /op1repacker/op1_repack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Unpack and repack OP1 firmware in order to create custom firmware.""" 3 | 4 | import os 5 | import stat 6 | import lzma 7 | import shutil 8 | import struct 9 | import tarfile 10 | import logging 11 | import binascii 12 | 13 | 14 | class OP1Repack: 15 | """Unpack and repack OP-1 firmware and other related utilities.""" 16 | # TODO: 17 | # - Do some checks to make sure the fw is ok when unpacking & repacking (how?) 18 | 19 | def __init__(self, debug=0): 20 | logging.basicConfig(level=logging.WARNING, 21 | # format='[%(asctime)s] %(levelname)-8s %(message)s', 22 | format=' %(levelname)-10s %(message)s', 23 | datefmt='%Y-%m-%d %H:%M', 24 | ) 25 | 26 | self.logger = logging.getLogger() 27 | if debug: 28 | self.logger.setLevel(logging.DEBUG) 29 | # Temporary file suffix to use when unpacking 30 | self.temp_file_suffix = '.unpacking' 31 | # Suffix to add when to FW file when it's repacked 32 | self.repack_file_suffix = '-repacked.op1' 33 | 34 | def create_temp_file(self, from_path): 35 | """Create a temporary file for the unpacking procedure and return its path.""" 36 | to_path = from_path + self.temp_file_suffix 37 | shutil.copy(from_path, to_path) 38 | return to_path 39 | 40 | def unpack(self, input_path): 41 | """Unpack OP-1 firmware.""" 42 | # TODO: maybe do all this in memory without the temp file 43 | path = os.path.abspath(input_path) 44 | if not os.path.isfile(input_path): 45 | self.logger.error("Firmware file doesn't exist: {}".format(input_path)) 46 | return False 47 | root_path = os.path.dirname(path) 48 | target_file = os.path.basename(input_path) 49 | full_path = os.path.join(root_path, target_file) 50 | target_path = os.path.join(root_path, os.path.splitext(target_file)[0]) 51 | 52 | self.logger.debug('Unpacking firmware file: {}'.format(full_path)) 53 | temp_file_path = self.create_temp_file(path) 54 | self.remove_crc(temp_file_path) 55 | self.uncompress_lzma(temp_file_path, temp_file_path) 56 | self.uncompress_tar(temp_file_path, target_path) 57 | os.remove(temp_file_path) 58 | # Don't mess with permissions on Windows 59 | if os.name != 'nt': 60 | self.set_permissions(target_path) 61 | self.logger.debug('Unpacking complete!') 62 | return True 63 | 64 | def repack(self, input_path): 65 | """Repack OP-1 firmware.""" 66 | # TODO: maybe do all this in memory without the temp file 67 | path = os.path.abspath(input_path) 68 | if not os.path.isdir(path): 69 | self.logger.error("Given path isn't a directory: {}".format(input_path)) 70 | return False 71 | root_path = os.path.dirname(path) 72 | target_file = os.path.basename(os.path.normpath(input_path)) 73 | compress_from = os.path.join(root_path, target_file) 74 | compress_to = os.path.join(root_path, target_file + self.repack_file_suffix) 75 | self.logger.debug('Repacking firmware from: {}'.format(compress_from)) 76 | self.compress_tar(compress_from, compress_to) 77 | self.compress_lzma(compress_to) 78 | self.add_crc(compress_to) 79 | self.logger.debug('Repacking complete!') 80 | return True 81 | 82 | def remove_crc(self, path): 83 | """Remove the first 4 bytes of the firmware which contain the CRC-32 checksum.""" 84 | with open(path, 'rb') as f: 85 | data = f.read() 86 | 87 | checksum_data = data[:4] 88 | checksum = struct.unpack('