├── 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 | 
15 | 
16 | 
17 | 
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('.*?",
12 | "replace": ""
13 | },
14 | {
15 | "type": "substitute",
16 | "find": "",
17 | "replace": ""
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/op1repacker/assets/display/iter-lab.svg:
--------------------------------------------------------------------------------
1 |
2 |
310 |
--------------------------------------------------------------------------------