How to turn your own copy of the stock firmware into a modded one, install it, and get back to stock. Nothing here contains or redistributes Polyend's firmware: you download it from Polyend and the patcher modifies your copy locally.
Use at your own risk. The mods here were tested on one Tracker+, and switching back to stock always worked. That is not a guarantee for your unit.
-
Download Tracker+ firmware 1.2.0 from Polyend's website (the Tracker+ firmware downloads). The file is
TrackerPlus_1.2.0.ptf. -
Check it:
Size 4,586,889 bytes SHA-256 d4616304f33d6d0b56516c658f34fa22517c1dd1630021c1ca89afca7b618ccdsha256sum TrackerPlus_1.2.0.ptf # Linux / Git Bash Get-FileHash TrackerPlus_1.2.0.ptf # Windows PowerShell shasum -a 256 TrackerPlus_1.2.0.ptf # macOS
The patcher also checks the decoded image (SHA-256
0d924ba9…1f3b) and refuses anything else, so a different firmware version can't be patched by mistake.
The web patcher does what the Python command below does, without installing anything:
- Choose your stock
TrackerPlus_1.2.0.ptf. The page checks the file's SHA-256 (the one in step 1) and the image's, and refuses anything else, including a file that's already patched. - Tick the mods you want. One mod gives its own build, "Select all" gives the
patch/allbundle (all fourteen,1.2.0-MNP), and any other mix gets a version text that says which mods are in it:Mplus two hex digits for colorsong 01, humanize 02, interpolate 04, masterfx 08, swing 10, visualizer 20, condtrig 40 and synths 80 (colorsong + swing is1.2.0-M11);Ninstead ofMwhen legato is in it (colorsong + legato is1.2.0-N01),Owhen copypattern is (colorsong + copypattern is1.2.0-O01),Pwhen both are, and four letters further on (QtoT) when slicethru is in it too (colorsong + slicethru is1.2.0-Q01). With resample the letter is eight further on,UtoZand thenAandB(colorsong + resample is1.2.0-U01). With tracklen it is sixteen further on,CtoLand then the digits0to5(colorsong + tracklen is1.2.0-C01). With songloop the last hex digit becomes a letter,Gfor 0 toVfor F (colorsong + songloop is1.2.0-M0H). - Patch and download. The page only offers the file when its SHA-256 is the one
tools/patch_ptf.pymakes for that selection (web/expected.json), and says when it's also one of the hardware-tested builds in the table below. A mix that isn't in the table hasn't been tested on hardware as one firmware.
Everything runs in the browser: the firmware is not uploaded, and the site serves only this repository's page and prebuilt patch files. For a mix, the page shows the bundle spec it used, so the Python command gives the same file. How it's built and tested: WEB_PATCHER_PLAN.md.
Then install it as in section 3.
You only need Python 3 (standard library only, no compiler; tested with 3.15). The repository ships
the compiled patch code in patch/<name>/prebuilt/, so this works out of the box:
python tools/patch_ptf.py path/to/TrackerPlus_1.2.0.ptf patch/colorsong/patch.json build/TrackerPlus_c11.ptfUse patch/humanize/patch.json for Humanize alone, patch/interpolate/patch.json for
interpolate alone, or patch/all/patch.json for all fourteen in one firmware (see "Choosing patches" below for your own
mix).
The patcher checks the stock image, checks the original bytes at every patched
address, writes the new .ptf, reads it back, and prints its SHA-256. The output
must be byte-identical to these builds:
| Patch | Output file | Version text in Config | On hardware | Output SHA-256 |
|---|---|---|---|---|
| colorsong | TrackerPlus_c11.ptf |
1.2.0-c11 |
tested | cc9119abe627d994ba6010bfe0db59129e3f494611dbc8c1e5c68fcf0653ebc6 |
| humanize | TrackerPlus_h10.ptf |
1.2.0-h10 |
tested | c85f5fa05d83dc6570e54fa38a8485659db1951fe12113fbfa72304cb78439c5 |
| the previous humanize (New Project then an immediate Save As kept the previous project's settings) | TrackerPlus_hz9.ptf |
1.2.0-hz9 |
tested | 1b4585815fa0c66b5689135375f2da8e7bfec57cce6f5cf2e62592f4c4765626 |
| the previous humanize (Save As dropped the settings) | TrackerPlus_hz8.ptf |
1.2.0-hz8 |
tested (with colorsong and interpolate) | ec3c30fda0d186299e39cce2be48031303f3bc0583fd1e8803600b500bf4b46f |
| interpolate | TrackerPlus_ip8.ptf |
1.2.0-ip8 |
tested | bb08491d9c5b2949bacd2c6433e668bc5e33d6a22e2850bfa5b377a763984bb0 |
| the previous interpolate (New Project then an immediate Save As kept the previous project's settings) | TrackerPlus_ip7.ptf |
1.2.0-ip7 |
tested | 8200a81f3de0c395e4dab97c273def7125f6890d2e1147d33cd66bb9f6841f33 |
| the previous interpolate (slower look-up) | TrackerPlus_ip6.ptf |
1.2.0-ip6 |
tested | 598dc01fb55ec0549c9809d2a14702e591354f29a07c0ad7d7401d405171391e |
| the previous interpolate (Save As dropped the settings) | TrackerPlus_ip5.ptf |
1.2.0-ip5 |
tested (with colorsong and humanize) | f3d98eb18f5852dc718d41ba9311e813d056d1ab5b37b1252e87cf0341c0942a |
| masterfx | TrackerPlus_mf8.ptf |
1.2.0-mf8 |
tested | fa24cdc2ae1e96f73a46d3a3e7e510280b03108ad0feb0796b2b2672900798b7 |
| the previous masterfx (New Project then an immediate Save As kept the previous project's settings) | TrackerPlus_mf7.ptf |
1.2.0-mf7 |
tested | 7149651eda407e0bbbe7d84ea947ae3d1ee0035989cb63d0c59bf6779080fe9f |
| the previous masterfx ("Gated" still a delay, before the lighter effects) | TrackerPlus_mf5.ptf |
1.2.0-mf5 |
tested | f7ec780456867eac7a8cb469d6bb929949ef7a334ccaeb1c0c645863c8657a5a |
| the previous masterfx (Save As dropped the settings) | TrackerPlus_mf4.ptf |
1.2.0-mf4 |
tested | 0f3f9cd0c0c00ceed7651aacece96bd52dd084991e82f9e03d814630b898df1a |
| swing | TrackerPlus_sw3.ptf |
1.2.0-sw3 |
tested | 1244a4e02dd10dd79369f5c30b0139cfb7e11cec8bd495354e4e24a9498491d6 |
| the previous swing (New Project then an immediate Save As kept the previous project's settings) | TrackerPlus_sw2.ptf |
1.2.0-sw2 |
tested | 05a3f65fb21381224f95f0e41f1782909b86fbe0628cbaa9263da18e4ac1a275 |
| the previous swing (Save As dropped the settings) | TrackerPlus_sw1.ptf |
1.2.0-sw1 |
tested | 510fe9ad078ed737f1cf3c63ea706a8a0e50f978c9d115bfbc773f4252a0e8d7 |
| visualizer | TrackerPlus_vz6.ptf |
1.2.0-vz6 |
tested | 38860b39e6ca58102b137dbd14690b3c134d6e09aadb779cb23b887b9ff2ec18 |
| the previous visualizer (animated while hidden) | TrackerPlus_vz5.ptf |
1.2.0-vz5 |
tested | 46970bb1689414fd711cadc6cfd149da458145aafcd9f6d274f10759dce9bdbd |
| the previous visualizer (the same animation, every pad update a full flush) | TrackerPlus_vz2.ptf |
1.2.0-vz2 |
tested | 8b3ddcb09de46829799f542cf9d7423d95d4f39aa76db44881f5c18eb0361736 |
| condtrig | TrackerPlus_ct2.ptf |
1.2.0-ct2 |
tested | 9ddb1fc889f69293cf15e5cd8910e15d97cf742b1684ab6c878c8f82d5a3a440 |
| the previous condtrig (before Variable Track Length) | TrackerPlus_ct1.ptf |
1.2.0-ct1 |
tested | 539328948185c4a02577ce62181fc4143a5e86dc06a33e61b469d52b21da8197 |
| synths (WAVE and PHASE) | TrackerPlus_sy9.ptf |
1.2.0-sy9 |
tested | 1c3dc6dad3f0abbe1fda5cc9837a55a28c63a088cd40f238fba1776e0d1e52fc |
| the previous synths (Save As over an existing project kept that project's choices) | TrackerPlus_sy8.ptf |
1.2.0-sy8 |
tested | 75087646e0902daa7867fe5fdf91de98874348a2140f1becbd5d0ba29bee06e2 |
| the previous synths (WAVE only) | TrackerPlus_sy7.ptf |
1.2.0-sy7 |
tested | dddf733b197e59d8a6dc7f3e1ee253ec450518238ca991d651f03ce4e0dc940f |
| the previous synths (without the effect) | TrackerPlus_sy6.ptf |
1.2.0-sy6 |
tested | 6f56aa00881797f55bec494b359d1248badd0fa11a66e4b3b15cd1adb878dc52 |
| the previous synths (without Drive) | TrackerPlus_sy5.ptf |
1.2.0-sy5 |
tested | 4ccd24c538a814e7f930130d7cf69bfb9733515ae362569e1d41cf1aa6996e36 |
| the previous synths (filter envelope on the Env 2 tile) | TrackerPlus_sy4.ptf |
1.2.0-sy4 |
tested | 2fca2e07bb19ef4e539328152af27c173067ec6088604c45d9f28b96aa876e97 |
| legato (MIDI and synth slots) | TrackerPlus_lg2.ptf |
1.2.0-lg2 |
tested | a7bab99688e6036e58b66bd8e60806170a045b10c57a218d6607ab9261f81a75 |
| the previous legato (MIDI only) | TrackerPlus_lg1.ptf |
1.2.0-lg1 |
tested | f266ed1f131fc15edf12b224e081f16d9109938b004d8410e2cb0cdb380a68e6 |
| copypattern | TrackerPlus_cp4.ptf |
1.2.0-cp4 |
tested (also with humanize) | 7bfbc3eb09a7eb9d9dd4ccf3a8a52b1221743be7061397048f72bbc32908db01 |
| all (colorsong + humanize + interpolate + masterfx + swing + visualizer + condtrig + synths + legato + copypattern + slicethru + resample + tracklen + songloop) | TrackerPlus_MNP.ptf |
1.2.0-MNP |
tested | d4c22592906cf68d57e27f337e4c7dae082388eca148c34d0a67a76e0fe6c1ec |
| the previous all (the same with swing sw2, humanize hz9, interpolate ip7, masterfx mf7, slicethru bt5) | TrackerPlus_MR8.ptf |
1.2.0-MR8 |
tested | ce41aa1e9f8053b56855abe977d11aebca8122ae5d2597f10470ae8d8b34c574 |
| the previous all (the same with resample rs4) | TrackerPlus_SLA.ptf |
1.2.0-SLA |
tested | 643755cc94ba6f904298a4adbfcadd739925db8c50e0c719d34c0b8d3f79ad1b |
| the previous all (without songloop) | TrackerPlus_MVL.ptf |
1.2.0-MVL |
tested | babfded088d61424511b6a3edb0a384937f28458d0721f70c9e9c27670880ed3 |
| the previous all (without tracklen, with condtrig ct1) | TrackerPlus_MTZ.ptf |
1.2.0-MTZ |
tested | 5393e48e8bd23b1504ff0a8b75ee29ed1962fee1f19acb4a30bc705d99d0e5ac |
| the previous all (the same with interpolate ip6 and visualizer vz5) | TrackerPlus_MTY.ptf |
1.2.0-MTY |
tested | 190bdcd2cbb24ad7b71d0e1aee61f828f0a761a60bbd3e8e9a73b55c2bc028f4 |
| the previous all (the same with masterfx mf5) | TrackerPlus_MTW.ptf |
1.2.0-MTW |
tested | b927f87b5584f6576f5d5f09d2481bbeb9203328a690380843d68bca93aef933 |
| the previous all (the same with synths sy8) | TrackerPlus_MTS.ptf |
1.2.0-MTS |
tested | ac0edf2f1f2d3077e3c9f95babd136f065ca40ec18080e23d8cab1f496f595c9 |
| the previous all (the same with swing sw1, humanize hz8, interpolate ip5, masterfx mf4) | TrackerPlus_MT5.ptf |
1.2.0-MT5 |
tested | 61446c44bfd6669dd1044da4415ceb13f0df6b47c6f6a49ad37f2405195ae4f7 |
| the previous all (the same with slicethru bt4, without Loop) | TrackerPlus_MT4.ptf |
1.2.0-MT4 |
tested | eec53343f9fc18ed981863af0808761974ee23024158705df4c362a87442fee8 |
| the previous all (the same with slicethru bt3) | TrackerPlus_MR4.ptf |
1.2.0-MR4 |
tested | b2376c91b9382658c39018a78daf78c9b22a083bfb1b4c739e147bffea84748f |
| the previous all (without resample) | TrackerPlus_MS8.ptf |
1.2.0-MS8 |
tested | ee0c901bd8cc7c7786e68c19d7c9e97fc93847fb10adbaa48a80b138638fbd4d |
| the previous all (the same with synths sy7) | TrackerPlus_MBT.ptf |
1.2.0-MBT |
tested | f610374f5bbf29a100272dc5fa4af545ecd778932d57b9a913d54f1a30a4c33c |
| the previous all (without slicethru) | TrackerPlus_MS7.ptf |
1.2.0-MS7 |
tested | 5ed2e44bed5432a84e4787d10b0222072f1a9d72776a1d3de357582679cafefd |
| the previous all (the same with synths sy6) | TrackerPlus_MS6.ptf |
1.2.0-MS6 |
tested | 764994f1711bddafa691d44567239442cc1729fd5da776ab1510ae1e0b416457 |
| the previous all (the same with synths sy5) | TrackerPlus_MS5.ptf |
1.2.0-MS5 |
tested | 05d5d8153c41a81d2b40a50f9c300befd76a850608f4b8d0cb40f9b151b363d7 |
| the previous all (the same with synths sy4) | TrackerPlus_ML2.ptf |
1.2.0-ML2 |
tested | 8fd81d702351f30b57b1da47c297248e184043a363b10e9a3b75027e6e84aa5e |
| the previous all (the same with legato lg1) | TrackerPlus_MLC.ptf |
1.2.0-MLC |
tested | 4170d1cbf34c3a91073d6f276798844dcdd10faee99acb7fc079af484bc269b0 |
| the all before that (with legato lg1, without copypattern) | TrackerPlus_MLG.ptf |
1.2.0-MLG |
tested | 4afd36206ccbb73b6d2556b4de9228afc060360653152fa492d67139da2ad0a2 |
| the one before that (without legato and copypattern) | TrackerPlus_MSY.ptf |
1.2.0-MSY |
tested (made with the web patcher) | f3fee2dd07e88d3b8810cd1d9b371e0636f7ad4920c777f021b28da2afd2fd99 |
| the one before that (without synths, legato and copypattern) | TrackerPlus_MCT.ptf |
1.2.0-MCT |
tested | 6d818234483106b3b6444c1f180bf3f2b5cb93060d75762f752f252fce6b8437 |
| the one before that (without condtrig, synths, legato and copypattern) | TrackerPlus_MTE.ptf |
1.2.0-MTE |
tested | 72f65355487ea13908a73cc891dacf3568e883ce2fc5388c3c169c888852d982 |
| slicethru (Beat Slice Thru, with Loop) | TrackerPlus_bt6.ptf |
1.2.0-bt6 |
tested | e5c3a47b684a92b8277813444dda7a327ddc6886baa9a09ec7f54cc585dcf20c |
| the previous slicethru (New Project then an immediate Save As kept the previous project's flags) | TrackerPlus_bt5.ptf |
1.2.0-bt5 |
tested | 5f50698d8545833a6c6a7cbba495de4004da2cd9250b36e7ad548f50dc4981e8 |
| the previous slicethru (without Loop) | TrackerPlus_bt4.ptf |
1.2.0-bt4 |
tested | 8aed6732660f5c8e8ab231b20fbfa58bfaf3b875896ce790b5b9ae2bd6552232 |
| the previous slicethru (Save As dropped the setting) | TrackerPlus_bt3.ptf |
1.2.0-bt3 |
tested | 76e0a004302bb9e57d214cb04cf0053e906f1b6de3141a4ad4809f440de2cb2e |
| resample (live resampling, Trk Audio and Trk Record) | TrackerPlus_rs8.ptf |
1.2.0-rs8 |
tested | b7f2056a34bc3b476ce433d7a7b8d91b76fca7c536dd944cffdc408537347653 |
| the previous resample (Play and Shift+Rec always on, no Record Tracker) | TrackerPlus_rs4.ptf |
1.2.0-rs4 |
tested | 5cf21721e8c1f9339304b24adfcc355a07f37dcaff9697a5b97ab41d50352925 |
| tracklen (Variable Track Length) | TrackerPlus_tl7.ptf |
1.2.0-tl7 |
in patch/all (1.2.0-MVL), not on its own |
d4a19b7aad99f388a8448791957116746b9a8f9e0924b8efb86be8224077f2fd |
| songloop (Song Loop) | TrackerPlus_sl4.ptf |
1.2.0-sl4 |
tested (also in patch/all, 1.2.0-SLA) |
403c76593d2e5f096893ddb2771dd7c90536b1dedc7f64352bf5c154c24ca935 |
| ramdiag (diagnostic, on its own) | TrackerPlus_RD2.ptf |
1.2.0-RD2 |
tested | 84a33f1858bdd3c908eeea62227d84f68fe450dc4e3e21fb21077292fef1b82e |
| versiontest | TrackerPlus_MT0.ptf |
1.2.0-MT0 |
tested | f45510a17f8b9b120c0e699f239a737a145c3d57bbaf596046fb7a8bc643ffff |
| itcmtest (ITCM hooks test, on its own) | TrackerPlus_it1.ptf |
1.2.0-it1 |
tested | 701b35dfadb4626ebe19e94ccb3af47aff9e4d604684d59b2e8b9cbf5da18d7c |
Name the output TrackerPlus<anything>.ptf. Config's firmware list only shows
files that start with TrackerPlus and end with .ptf. Keep names short: on the
original Tracker, names longer than 31 characters showed garbage.
Each patch can be installed on its own, or several can go into one firmware:
-
One patch: use its own spec.
patch/colorsong/patch.jsongives colorsong only,patch/humanize/patch.jsonHumanize only,patch/interpolate/patch.jsoninterpolate only,patch/masterfx/patch.jsonthe master reverb and delay chosen per project (see its README for the effects),patch/swing/patch.jsonGlobal Swing,patch/visualizer/patch.jsonthe pad visualizer,patch/condtrig/patch.jsonconditional trigs,patch/synths/patch.jsonthe WAVE and PHASE synths,patch/legato/patch.jsonMIDI legato,patch/copypattern/patch.jsonthe Copy to New button,patch/slicethru/patch.jsonthe Beat Slice Thru play mode,patch/resample/patch.jsonlive resampling,patch/tracklen/patch.jsonVariable Track Length,patch/songloop/patch.jsonSong Loop. -
Several patches: use a bundle spec, which lists the patches to combine.
patch/all/patch.jsonis the bundle that ships (all fourteen: colorsong, Humanize, interpolate, masterfx, swing, the visualizer, condtrig, synths, legato, copypattern, slicethru, resample, tracklen and songloop):"include": ["../colorsong/patch.json", "../humanize/patch.json", "../interpolate/patch.json", "../masterfx/patch.json", "../swing/patch.json", "../visualizer/patch.json", "../condtrig/patch.json", "../synths/patch.json", "../legato/patch.json", "../copypattern/patch.json", "../slicethru/patch.json", "../resample/patch.json", "../tracklen/patch.json", "../songloop/patch.json"], "version": "MNP",
To make your own mix, copy patch/all/ to a new folder (for example patch/mymix/)
and edit its patch.json:
- List the patches you want in
include, as paths relative to the bundle's folder. They are placed in flash in that order. Your own patches can be listed too. - Change
name, and setversionto 1–3 characters that tell your build apart. Config shows1.2.0-<version>; longer text doesn't show. - Leave
stock_image_sha256andversion_wordas they are.
python tools/patch_ptf.py path/to/TrackerPlus_1.2.0.ptf patch/mymix/patch.json build/TrackerPlus_mymix.ptfThe patches in one firmware have to fit together:
-
Flash: handled for you. The patcher puts the patches after the stock image in the order listed, each right after the one before (16-byte aligned), and moves each one's code to where it lands, using the relocation table its build makes (
prebuilt/<name>.rel). Listing["../interpolate/patch.json", "../humanize/patch.json", "../colorsong/patch.json"]puts them the other way round (tools/test_layout.pychecks both orders, and both were tested on hardware). A patch whose spec has a fixedbaseand no relocation table (like versiontest) stays exactly there, and must not overlap what comes before it. An output that would end within the first 256 bytes of a 4 KB flash sector gets0xFFpadding into the sector's second page: the bootloader doesn't erase a sector whose data stop in its first page, and a build that ended there boot-looped (FINDINGS section 26). -
Hook sites: shared ones must be chained. Two patches can hook the same call or data word only if both chain it: the hook names a
nextword in the patch, which the patcher fills with what the site pointed to before (the stock function, or the hook of the patch listed before it), and the patch calls through that word instead of calling the stock function. The last patch listed runs first. Humanize, interpolate, masterfx and swing chain the Project Settings menu and the Pattern and Config update methods this way, so they all appear in Config. Swing and the visualizer both chain the sequencer's audio-block and step-advance calls, and the visualizer the Pattern update method too. Sites only one patch needs can stay plain. -
RAM: handled for you. Each patch's state variables are one block (its
.mt_ramsection). The patcher hands the blocks out from the free RAM regions below, in the order the patches are listed: each block goes into the first region (in the patch's order) that still has room, 16-byte aligned, and the patch's code is moved to match. It prints where each block went and writes it, with the code ranges, to<output>.map.jsonnext to the.ptf.Region Addresses Size Notes ocram_tail0x20205000–0x20207fff12,288 B on-chip dtcm_above_heap0x20051c70–0x20052fff5,008 B on-chip, below the stack sdram_top0x83ff9bf0–0x83ffffff25,616 B external SDRAM; not before main. Fine for state used once the unit runs (colorsong); not for state written from the audio interrupt during boot (that hung the unit at boot)dtcm_after_audio0x200094c0–0x200097ff832 B on-chip itcm_tail0x0001fb40–0x0001ffff1,216 B only when a spec names it; aligned accesses only The default order is the first four, top to bottom. colorsong (12,208 B) and Humanize (32 B) both fit in
ocram_tail. If a patch's block fits nowhere, the patcher refuses to build. -
Firmware: every patch must be for the same stock firmware (
stock_image_sha256).
The patcher refuses to build when code ranges overlap, two patches hook the same site
without both chaining it, a patch is for another firmware, an original byte doesn't match, the RAM runs out, or
a bundle includes a patch marked standalone (the ramdiag diagnostic). It can't
detect two patches that each work alone but disturb each other.
A new combination, or the same patches in another order, is a firmware nobody has
tested, and its SHA-256 isn't in the table above. Before installing it, run the
emulator tests of every patch it contains against the combined .ptf (below), and
keep the stock .ptf in /Firmware. tools/test_layout.py checks the placement
itself (below).
With the Arm GNU Toolchain installed (arm-none-eabi-gcc on PATH, or ARM_GCC_BIN
set to its bin folder):
patch/colorsong/build.sh # writes patch/colorsong/build/
python tools/patch_ptf.py path/to/TrackerPlus_1.2.0.ptf patch/colorsong/patch.json build/TrackerPlus_c11.ptfWhen patch/<name>/build/ exists, the patcher uses it instead of prebuilt/. The
tested build used Arm GNU Toolchain 15.3.rel2. A different compiler may produce
different (but equivalent) code and therefore a different hash.
The quickest way is tools/run_tests.py, which runs the checks below in parallel and
skips anything that already passed on the same files:
python tools/run_tests.py path/to/TrackerPlus_1.2.0.ptf --tier quick # while working on a patch (~2 min)
python tools/run_tests.py path/to/TrackerPlus_1.2.0.ptf --tier release # before a commit users get (~40 min with an empty cache)--mixes-of swing,humanize,... (release or deep) also runs every patch's test on every
mix of two or more of the named mods, for a change to a hook they all share.
- quick:
test_layout.py, then every patch's emulator test on each patch alone and onpatch/all, andtest_bundle_cost.py(what the firmware adds to the audio block's work over stock, sequencer side) on each of them. - release: quick, plus
test_web_patcher.py(every web patcher selection byte-identical topatch_ptf.py's), plus every patch's test on a cover set of web patcher mixes and on the bundlestest_layout.pybuilds in other orders.- The cover set is the fewest mixes that between them contain every case that differs
from mix to mix: each patch's RAM at the start, middle and last 64 bytes of each
region it can land in, each pair of patches next to each other on a shared hook, and
an image the sector padding pads and one it doesn't. For the fourteen mods that's 20
of the 16369 mixes, besides
patch/all. - masterfx and synths run in
--quickmode, which leaves out their sound, cost and long stress checks (these depend only on their own code), except onpatch/all, where they run in full when their own files (patch.json,prebuilt/,build/) changed since their last full pass.test_bundle_cost.pyruns in full (the whole block, with the real render) onpatch/all.
- The cover set is the fewest mixes that between them contain every case that differs
from mix to mix: each patch's RAM at the start, middle and last 64 bytes of each
region it can land in, each pair of patches next to each other on a shared hook, and
an image the sector padding pads and one it doesn't. For the fourteen mods that's 20
of the 16369 mixes, besides
- deep: release with masterfx and synths in full everywhere; only when their audio code changes.
It prints an estimate before it starts and the time left as it goes, keeps a log per
test in build/testrun/logs, and writes build/testrun/summary.txt. Its cache
(build/test_cache.json) remembers what passed: a test isn't run again on a firmware
file it already passed with the same test and emulator code. The web patcher check
keeps its own cache per selection, so after a change to one mod it redoes only the
selections that contain it (all 16383 took about 68 minutes on six cores, none about
30 seconds).
--no-cache runs everything; --jobs N sets how many run at once (the web patcher
check, too, uses N processes).
Before anything long it runs checks that take seconds, and stops if one fails: every
patch.json and web/*.json parses, the web patcher's JavaScript passes node --check,
and each patch's build/ (which patch_ptf.py uses in place of prebuilt/) matches its
prebuilt/ (the quick tier only names those that don't). The layout test then runs
alone, before the emulator tests. The web patcher check first checks that no two mixes'
names differ only in case (Windows and FAT ignore case); if that or anything else in it
fails, the tests not yet started are dropped.
Each check can also be run on its own:
python -m pip install unicorn capstone numpy
python tools/emu/check_version.py build/TrackerPlus_c11.ptf # -> '1.2.0-c11'
python tools/emu/test_colorsong.py build/TrackerPlus_c11.ptf
python tools/emu/test_rescan.py build/TrackerPlus_c11.ptf "path/to/<project A>" "path/to/<project B>"
python tools/emu/test_humanize.py build/TrackerPlus_h10.ptf
python tools/emu/test_interpolate.py build/TrackerPlus_ip8.ptf
python tools/emu/test_masterfx.py build/TrackerPlus_mf8.ptf path/to/TrackerPlus_1.2.0.ptf out/fx_wav
python tools/emu/test_swing.py build/TrackerPlus_sw3.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_visualizer.py build/TrackerPlus_vz6.ptf
python tools/emu/test_condtrig.py build/TrackerPlus_ct1.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_itcmtest.py build/TrackerPlus_it1.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_synths.py build/TrackerPlus_sy9.ptf path/to/TrackerPlus_1.2.0.ptf [wav folder]
python tools/emu/test_phase.py build/TrackerPlus_sy9.ptf path/to/TrackerPlus_1.2.0.ptf [--wav folder]
python tools/emu/test_legato.py build/TrackerPlus_lg2.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_copypattern.py build/TrackerPlus_cp4.ptf
python tools/emu/test_slicethru.py build/TrackerPlus_bt6.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_resample.py build/TrackerPlus_rs8.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_songloop.py build/TrackerPlus_sl4.ptf path/to/TrackerPlus_1.2.0.ptf
python tools/emu/test_bundle_cost.py build/TrackerPlus_all.ptf path/to/TrackerPlus_1.2.0.ptf [--quick]tools/run_tests.py runs the tests of the mods the web patcher offers.
Each test works on any firmware that contains its patch, so for a bundle run every
contained patch's tests on the bundle's .ptf. The tests read where the patch's RAM
went from the .map.json the patcher wrote next to the .ptf, so keep the two
together.
tools/test_layout.py checks the patcher's placement without the emulator. It builds
each patch alone, patch/all (all fourteen) and its reverse, colorsong, humanize and interpolate in the
other order, those three with masterfx in both orders, the six before condtrig in the other order, the six with synths in both orders,
copypattern alone, with humanize in both orders and with all nine in both orders,
slicethru alone and first with the others after it in reverse order,
resample alone and first with the others after it in reverse order,
songloop alone, after the others and first with them after it in reverse order, and two
patches on the same chained sites, and
checks that each patch's code and RAM sit where they
should (RAM in a region the patch may use, no two blocks overlapping), that exactly the
words in its relocation table moved, that each chain word holds what its site pointed
to before, and that every hooked site points at the last patch that hooks it. With
itcmtest (alone, after and before all the others, and chained with a copy of itself)
it also checks the ITCM stubs: each hooked ITCM call goes to its stub, the stub jumps
to the last hook, the copy table copies the stubs to ITCM, and no RAM overlaps them.
It also checks that a shared site that isn't chained by both patches is refused, and
two patches passing different registers at one ITCM site:
python tools/test_layout.py path/to/TrackerPlus_1.2.0.ptf build/layouttools/test_web_patcher.py (needs Node) runs the web patcher's code on every selection
the page offers and on test_layout.py's other bundle orders, checks each output is
byte-identical to patch_ptf.py's, and runs test_layout.py's checks on them. After
changing a patch, or the mods in web/manifest.json, run it with --update to refresh
web/expected.json; the Pages workflow refuses to publish until then. The release tier
does that in the same run when given --update, so the harness doesn't run twice:
python tools/run_tests.py path/to/TrackerPlus_1.2.0.ptf --tier release --update
python tools/test_web_patcher.py path/to/TrackerPlus_1.2.0.ptf build/webtest --update # the harness aloneEach published site is a build named by a digest of its files (web/stage.py). The page
loads its code by that name and checks build.json, so a browser still holding an earlier
build's page or code (GitHub Pages lets it keep them for 10 minutes) reloads once onto the
new build, instead of running old code on new patch files.
The test projects are copies of project folders from the SD card (Projects/...).
- Back up your SD card (the whole card, including the
Workspacefolder). - In the card's
/Firmwarefolder, keep the stockTrackerPlus_1.2.0.ptfand add the patched file. Remove other old.ptfversions, so an emergency reflash (below) can only pick one of these two. - On the Tracker+: Config → Firmware, choose the patched file, confirm. The unit updates and reboots.
- Config should now show the patch's version text, e.g.
1.2.0-c11 Build 1504.
Config → Firmware → TrackerPlus_1.2.0.ptf → confirm. Config shows 1.2.0 Build 1504
again.
Some mods keep per-project settings in a small file inside the project folder:
colorsong its colors in colorsong.bin, Humanize its ranges and velocities in
humanize.bin, interpolate its switches in interpolate.bin, masterfx its effect
choices in masterfx.bin, swing its settings in swing.bin. Stock firmware ignores these files, and you can delete them
(deleting one resets that mod's settings for the project: colorsong's colors go back to
song order, the others to their defaults).
The bootloader is separate from the firmware and isn't touched by patches.
- On a computer, remove the patched
.ptffrom/Firmware, so only the stockTrackerPlus_1.2.0.ptfis left. - Put the card back, power on, and press the pinhole button next to the USB-C
port with a pin. The bootloader shows "Updating..." and installs the
.ptffrom/Firmware.
This worked on the test unit (with the unit on and the card in). If even that fails, contact Polyend support.
If the unit only freezes in one place (e.g. a mod's screen) but boots, pull the power, boot, and switch back to stock through Config.
See patch/colorsong/ for a complete example and docs/FINDINGS.md for the firmware
details. A patch is:
- C/assembly code, linked at any address after the stock image (
link.ld). - State variables in the
.mt_ramsection:static struct my_state state __attribute__((section(".mt_ram")));. It isn't initialised (no zeroing, no initial values): the patch has to set it up itself, for example with a magic word checked on first use. The patcher picks the address. - A relocation table, so the patcher can move the code and the RAM:
build.shlinks the patch three times (as built, code moved 64 KB, RAM moved 64 KB), andtools/mkrel.pylists the words that differ by exactly that much (pointers to the patch's own code and data, and to its RAM). It refuses code it can't move this way. The colorsong and Humanize build scripts show how. patch.json: the stock image hash, the build outputs (bin,sym,rel, and theirprebuilt_copies), Thumb-2BLsites to retarget (with their original bytes), and data words to replace (literal pools, vtable slots with"thumb": true). Instead of a relocation table, a spec can give a fixedbase. A hook other patches may share gets"next": "<symbol>": a 4-byte word in the patch, linked as 0, that the patch calls through instead of the original (const volatile uint32_tin C, so the compiler reads it; see Humanize'sCHAIN_WORD). A hook can also replace 4 bytes that aren't aBL(two 16-bit instructions, or one 32-bit one): abranchesentry with"replaces": "insns". The patcher writes aBLto the hook there, and the hook does the replaced instructions' work itself and returns to the next one. With no original call to chain to, such a site takes nonext, and no second patch may hook it. The patcher refuses any two sites that overlap. A tail call (ab.wto the original, which never comes back to its caller) is abranchesentry with"tail": true: the patcher writes ab.wto the hook instead of aBL, and it chains like any other site. Ab.wsite without"tail", or"tail"on aBLsite, is refused. Calls inside the sequencer's ITCM code go initcm_branchesinstead:atis the ITCM address of theBL, with its original bytes as inbranches, andargsnames up to two registers (r4–r12, sp) that the call site holds and the hook should get as its third and fourth arguments. ITCM code can't call patch code in flash directly, so the patcher puts a stub per site at the start of the ITCM tail (0x1fb40, which is then not given out as RAM), and sets the startup copy table's unused entry 3 to copy the stubs there at boot. The stub movesargsinto r2 and r3 and jumps to the hook, which returns straight to the ITCM caller. Chaining works as forbranches, with the original arguments plusargspassed on throughnext, so every patch on one ITCM site must name the sameargs. See patch/itcmtest/. Optional RAM settings:"ram_startup": truewhen the state is used beforemain(for example from the Config menu constructor, as Humanize does). The patch then never getssdram_top, which may not be set up that early."ram_regions": [...]to choose the regions and their order instead of the default. Nameitcm_tailhere to allow it (32-bit aligned accesses only)."late_ram": "<symbol>"for a second block, for state used only once the unit has booted (it may land insdram_top). The symbol names a word in the patch that holds the block's size,const volatile uint32_t name = sizeof(...); the patcher gives the block out after every patch's own, from the default region order, and writes its address over the size. See synths' effect state."standalone": truefor a patch that must not be combined with others.
"links"for a patch that works with another one when both are installed: each entry,{"word": "<symbol>", "patch": "<name>", "symbol": "<symbol>"}(with"thumb": truefor a function), names a word in the patch, linked as 0, that the patcher fills with the other patch's symbol address, or leaves 0 when that patch isn't in the build. See condtrig's link to tracklen's track lengths.tools/patch_ptf.pychecks everything, appends the code without gaps, and keeps the stock Intel HEX layout, so an unmodified image re-encodes to the exact stock file.- To combine it with other patches, give it its own hook sites or chain the shared
ones, and mark its version-text word with
"version": trueso a bundle can replace it (see "Choosing patches" in section 2).