From c1fe1bf2dc27f8832e72bb7b251299a9144f54b3 Mon Sep 17 00:00:00 2001 From: Thiemo Tiziani Date: Wed, 21 Oct 2026 08:56:03 +0200 Subject: [PATCH] site 1.10.0: BPM x2 / /2 (bpmhalfdouble) in the builder, docs and Mods page Co-Authored-By: Claude Opus 5.5 --- assets/bpmx2.png | Bin 0 -> 33462 bytes assets/versions.js | 20 ++++++++++++++++++-- builder.html | 12 ++++++------ builder/py/bundle.json | 2 +- builder/ui.js | 4 +++- datenschutz.html | 6 +++--- docs.html | 20 ++++++++++++++------ impressum.html | 6 +++--- index.html | 22 +++++++++++++++++----- issues.html | 6 +++--- tools/changelog.json | 16 ++++++++++++++++ tools/make_builder_data.py | 2 +- upcoming.html | 6 +++--- 13 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 assets/bpmx2.png diff --git a/assets/bpmx2.png b/assets/bpmx2.png new file mode 100644 index 0000000000000000000000000000000000000000..4e54207fdb465fc2d422f602086451e788fea930 GIT binary patch literal 33462 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 literal 0 HcmV?d00001 diff --git a/assets/versions.js b/assets/versions.js index 9ff7751..03a11e2 100644 --- a/assets/versions.js +++ b/assets/versions.js @@ -1,8 +1,15 @@ // Generated by tools/make_builder_data.py from tools/changelog.json and the mods' versions. window.CDJ_VERSIONS = { - "site": "1.9.0", + "site": "1.10.0", "date": "2026-10-21", "log": [ + { + "version": "1.10.0", + "date": "2026-10-21", + "changes": [ + "New mod: BPM ×2 / ÷2 (bpmhalfdouble), the track's BPM and beat grid doubled or halved from the deck, kept on the USB stick." + ] + }, { "version": "1.9.0", "date": "2026-10-21", @@ -198,7 +205,7 @@ window.CDJ_VERSIONS = { "cuenames": "0.1.1", "endwarn": "0.1.0", "played": "0.3.1", - "bpmhalfdouble": "0.1.0", + "bpmhalfdouble": "0.2.0", "wide_waveform": "0.1.0", "credit": "1.1.0" }, @@ -409,6 +416,15 @@ window.CDJ_VERSIONS = { "The big waveform 624 columns wide instead of 536, to the right edge; the extra columns are ahead of the playing position. The ZOOM/GRID wheel replaced by a grey ZOOM / GRID box over the BPM display." ] } + ], + "bpmhalfdouble": [ + { + "version": "0.2.0", + "date": "2026-10-21", + "changes": [ + "TAG TRACK held + CALL ◀ / ▶ halves or doubles the loaded track's BPM and beat grid; the track is loaded again with it, so display, beat ticks, bars, quantize and Pro DJ Link follow. Kept on the USB stick in PIONEER/PIONAARR.BPM. Results under 60 or over 240 BPM refused." + ] + } ] } }; diff --git a/builder.html b/builder.html index aedcc9f..14450c1 100644 --- a/builder.html +++ b/builder.html @@ -6,12 +6,12 @@ Pionaarr Mods Builder - - - - - - + + + + + + diff --git a/builder/py/bundle.json b/builder/py/bundle.json index 339cff2..a9acad2 100644 --- a/builder/py/bundle.json +++ b/builder/py/bundle.json @@ -1 +1 @@ -{"mods/patch_cdj900nxs.py":"#!/usr/bin/env python3\n# SPDX-License-Identifier: GPL-2.0-or-later\n\"\"\"Apply arnoldasr's CDJ-900NXS 1.31 mods to the unpacked MAIN image.\n\n usage: patch_cdj900nxs.py [,...]\n patch_cdj900nxs.py --list\n\nThe mods come from https://github.com/arnoldasr/cdj900nxs, read as data from a\ncheckout of it (CDJ900NXS_MODS_SRC, default mods/cdj900nxs/upstream, which is\nnot committed: that repository carries no licence). Nothing of its Python is\nimported; what its build_firmware.py does is redone here:\n\n extended_bpm the BPM with hundredths: the stock tenths moved into a new\n small digit above, the lower one showing hundredths.\n phasemeter the two-line phase meter (orange master, blue this deck, a\n marker each) in place of the four boxes, chosen in UTILITY >\n PHASE METER (SLIP FLASHING relabelled; mods/phase-meter-\n utility.cdjmod.json).\n advanced_waveforms (was rgb_waveform; based on arnoldasr's, updated and\n maintained by us) the big waveform in rekordbox's RGB colours\n (1.0.1: also when its colour detail is a little short,\n cdj900nxs/wavefix.py), and the overview's colour table as a red-to-white\n gradient (mods/waveform-colour.cdjmod.json); with our\n UTILITY > WAVEFORM: BLUE / RGB / 3BAND (cdj900nxs/wavemodes.py,\n left out with public=True).\n\n The three are arnoldasr's code mod (patches/mods.json): hooks\n in the application redirected to SH-4 code appended after it\n (0x08FC1000), plus an asset blob at 0x08FC2D00 generated from\n the stock image (the resource table extended by four phase-meter\n images, then the RGB waveform palette and LUT). Upstream tested\n it as one build, and as one without the RGB waveform\n (patches/bpm-phase-meter.json); one of those two is built and\n checked against its hash first, then the hooks of a feature not\n chosen get their original bytes back. Their shared startup hook\n (0x08C071FC) installs the BPM digits and the phase markers: the\n call of the one not chosen is taken out there.\n beatjump ours, not arnoldasr's (cdj900nxs/beatjump.py): TRACK SEARCH\n jumps 1/2/4/8 beats along the grid, chosen in UTILITY > BEAT JUMP,\n made at once in the DSP's decoded audio (cdj900nxs/dspjump.py, a\n patch of the audio DSP's code),\n which replaces SCREEN SAVER. hotcuekeys' engine makes the jumps,\n so it needs hotcuekeys and cuecolours. Needs no upstream checkout.\n cuecolours ours (cdj900nxs/cuecolours.py): memory cues drawn in their\n rekordbox colour on the overview and the detail waveform, the\n rekordbox hot cues shown as lettered boxes.\n Needs no upstream checkout.\n hotcuekeys ours (cdj900nxs/hotcuekeys.py): UTILITY > HOT CUE KEYS (a new,\n 18th item); with it ON, QUANTIZE briefly pressed switches a hot\n cue key mode shown in a HOT KEYS box, held it is QUANTIZE. In\n the mode the top three BEAT DIVIDE keys play hot cues A-C (held: a\n gate), set one (empty key held while paused) or delete one\n (DELETE held). The hot cues come from cuecolours.\n ledsaver ours (cdj900nxs/ledsaver.py): UTILITY > LED ANIMATION (one more\n item, after the others); with it ON, while the screen saver dims\n the display the button LEDs and the jog's rings play a loop.\n keynotation ours (cdj900nxs/keynotation.py): UTILITY > KEY NOTATION (one\n more item): every track's key in Camelot, Open Key or classic\n notation, whatever rekordbox exported.\n usbautoload ours (cdj900nxs/usbautoload.py): UTILITY > USB AUTO LOAD (one\n more item): a stick's MY SETTINGS are loaded when the player finds\n them, without waiting for MENU.\n quicksearch ours (cdj900nxs/quicksearch.py): BROWSE held opens SEARCH,\n BROWSE twice the list the playing track came from.\n (phasemeter, in unreleased builds, also gets ours from cdj900nxs/phasereverse.py:\n UTILITY > PHASE METER OFF / ON / REVERSE, and the beat countdown\n moved clear of the line meter; `phasereverse` is an old name for it.)\n wide_waveform ours (cdj900nxs/widewave.py): the big waveform runs on to the\n right edge (624 columns, the extra ones ahead of the position); the\n ZOOM/GRID wheel gives way to a small grey ZOOM / GRID box.\n credit ours (cdj900nxs/credit.py): the boot screen says \"Pionaarr Mods\n 1.0\" where it said \"Ver 1.31\".\n\nEvery patch is applied only where the image holds the expected original\nbytes, and arnoldasr's code is first built as one of the builds he tested\nand checked against its hash.\n\nOur lzss_decode.py runs 54 bytes past the end of the packed stream, so\nmain_unpacked.bin is the 1.31 application plus a tail the firmware never\nreads; the application is cut back to its own length first.\n\"\"\"\nimport hashlib\nimport json\nimport os\nimport struct\nimport sys\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, os.path.join(HERE, 'cdj900nxs'))\nimport beatjump # noqa: E402\nimport credit # noqa: E402\nimport dspjump # noqa: E402\nimport cuecolours # noqa: E402\nimport hotcuekeys # noqa: E402\nimport keynotation # noqa: E402\nimport ledsaver # noqa: E402\nimport usbautoload # noqa: E402\nimport quicksearch # noqa: E402\nimport phasereverse # noqa: E402\nimport wavemodes # noqa: E402\nimport wavefix # noqa: E402\nimport cuenames # noqa: E402\nimport endwarn # noqa: E402\nimport played # noqa: E402\nimport bpmhalfdouble # noqa: E402\nimport widewave # noqa: E402\n\nBASE = 0x08000000\nAPP_SIZE = 16519056\nAPP_SHA256 = '1ac3e30cde292865413950fe159663b3958e85939ed0a5d0122f796809a5f8ef'\n\nMODS = {\n 'extended_bpm': 'BPM with hundredths: tenths as a small digit above (arnoldasr)',\n 'phasemeter': 'two-line phase meter, UTILITY > PHASE METER (SLIP FLASHING) chooses it (arnoldasr); '\n 'OFF / ON / REVERSE and the beat countdown moved clear of it (ours: cdj900nxs/phasereverse.py)',\n 'advanced_waveforms': 'big waveform in RGB colours, overview as a red-to-white gradient (arnoldasr); '\n 'UTILITY > WAVEFORM: BLUE / RGB / 3BAND (ours: cdj900nxs/wavemodes.py)',\n 'beatjump': 'TRACK SEARCH jumps 1/2/4/8 beats, UTILITY > BEAT JUMP (ours: cdj900nxs/beatjump.py)',\n 'cuecolours': 'memory cues in their rekordbox colours, hot cues shown (ours: cdj900nxs/cuecolours.py)',\n 'hotcuekeys': 'hot cues A-C on the BEAT DIVIDE keys: play, gate, set, delete; UTILITY > HOT CUE KEYS, '\n 'QUANTIZE short = hot key mode (ours: cdj900nxs/hotcuekeys.py)',\n 'ledsaver': 'the button and jog LEDs play an animation while the screen saver dims the display; '\n 'UTILITY > LED ANIMATION (ours: cdj900nxs/ledsaver.py)',\n 'keynotation': 'every key in one notation (Camelot / Open Key / classic), UTILITY > KEY NOTATION '\n '(ours: cdj900nxs/keynotation.py)',\n 'usbautoload': 'MY SETTINGS on a USB stick loaded at once, UTILITY > USB AUTO LOAD '\n '(ours: cdj900nxs/usbautoload.py)',\n 'quicksearch': 'BROWSE held: SEARCH; BROWSE twice: the playing track in its list '\n '(ours: cdj900nxs/quicksearch.py)',\n 'cuenames': 'the rekordbox comment of the next cue (\"Drop\") in the title bar, in its colour '\n '(ours: cdj900nxs/cuenames.py, needs cuecolours)',\n 'endwarn': 'the playing-address bar blinks red in the last 30 seconds (ours: cdj900nxs/endwarn.py)',\n 'played': 'BROWSE ticks the tracks loaded on this player since it was switched on (ours: cdj900nxs/played.py)',\n 'bpmhalfdouble': 'IN PROGRESS, not for players yet: BPM x2 / /2, TAG TRACK held + CALL < / > '\n '(ours: cdj900nxs/bpmhalfdouble.py)',\n 'wide_waveform': 'the big waveform to the right edge, the ZOOM/GRID wheel replaced by a small ZOOM / GRID box '\n '(ours: cdj900nxs/widewave.py)',\n 'credit': 'boot screen says \"Pionaarr Mods 1.0\" instead of \"Ver 1.31\" (ours: cdj900nxs/credit.py)',\n}\n# every mod's version (Semantic Versioning); ours from their modules, arnoldasr's\n# split-up mods as packaged here\nVERSIONS = {\n 'extended_bpm': '1.0.0', 'phasemeter': '1.0.0', 'advanced_waveforms': wavemodes.VERSION, # with its WAVEFORM modes (wavefix: wavefix.VERSION)\n 'beatjump': beatjump.VERSION, 'cuecolours': cuecolours.VERSION, 'hotcuekeys': hotcuekeys.VERSION,\n 'ledsaver': ledsaver.VERSION, 'keynotation': keynotation.VERSION, 'usbautoload': usbautoload.VERSION,\n 'quicksearch': quicksearch.VERSION,\n 'cuenames': cuenames.VERSION, 'endwarn': endwarn.VERSION, 'played': played.VERSION, 'bpmhalfdouble': bpmhalfdouble.VERSION,\n 'wide_waveform': widewave.VERSION, 'credit': credit.VERSION,\n}\nMANIFESTS = {\n 'phasemeter': 'mods/phase-meter-utility.cdjmod.json',\n 'advanced_waveforms': 'mods/waveform-colour.cdjmod.json',\n}\n# arnoldasr's code mod split up: each feature's hooks in the application\nUPSTREAM = ('extended_bpm', 'phasemeter', 'advanced_waveforms')\nHOOKS = {\n 'extended_bpm': (0x08BD3E50, 0x08BD3ED8, 0x08BD3A48, 0x08BCBA60, 0x08BCBCFC),\n 'phasemeter': (0x08B2E2EE, 0x08C6A6CC, 0x08C6CCD0),\n 'advanced_waveforms': (0x089A061C, 0x08BC6830, 0x08BC6A7C, 0x08BCFD72, 0x08BCFF50),\n}\nSHARED_HOOKS = (0x08C071FC, 0x08B27BC8) # startup binding, arena reset: BPM and phase\nBPM_INSTALL_CALL = 0x08FC2444 # startup_bind's jsr of the BPM digits' installer\nPHASE_MARKERS_CALL = 0x08FC2450 # ... and of the phase markers' (startup_markers)\n\n\ndef source_dir():\n return os.environ.get('CDJ900NXS_MODS_SRC') or os.path.join(HERE, 'cdj900nxs', 'upstream')\n\n\ndef read_json(rel):\n path = os.path.join(source_dir(), rel)\n if not os.path.isfile(path):\n raise SystemExit('patch_cdj900nxs: %s missing -- check out '\n 'https://github.com/arnoldasr/cdj900nxs into %s'\n % (path, source_dir()))\n with open(path, encoding='utf-8') as f:\n return json.load(f)\n\n\ndef sha(data):\n return hashlib.sha256(data).hexdigest()\n\n\n# The asset blob, as tools/phase_assets.py builds it (same constants).\nTABLE, DESCRIPTOR, ORIGINAL_COUNT, TEMPLATE = 0x08100000, 0x2C, 0x2A1, 0x1C8\nDATA_BASE = 0x08FC2D00\nWIDTH, HEIGHT, MARKER_WIDTH, BAR = 200, 24, 8, 5\nKEY = 0xF81F\nORANGE_LINE, ORANGE_MARK = (120, 65, 20), (255, 140, 0)\nBLUE_LINE, BLUE_MARK = (40, 90, 120), (60, 170, 255)\nWAVE_COLOURS = [\n 448, 457, 47, 458, 449, 111, 495, 466, 175, 467, 487, 239, 303, 479, 367, 459, 456, 431,\n 468, 465, 475, 476, 474, 39, 477, 31, 471, 46, 469, 45, 23, 460, 109, 484, 110, 485, 103,\n 478, 483, 493, 494, 486, 95, 15, 450, 167, 492, 173, 174, 231, 115, 470, 238, 44, 237, 159,\n 295, 87, 108, 302, 51, 301, 491, 482, 359, 223, 366, 365, 236, 300, 179, 423, 461, 287, 430,\n 429, 172, 364, 473, 428, 351, 114, 151, 463, 299, 363, 415, 215, 427, 178, 490, 50, 279, 235,\n 79, 462, 451, 343, 511, 298, 362, 400, 52, 116, 426, 234, 407, 7, 143, 180, 207, 113, 233,\n 243, 481, 57, 271, 452, 417, 177]\nWHITENESS_COLOURS = [(150, 20, 20), (220, 40, 30), (255, 110, 20), (255, 190, 30), (170, 230, 60),\n (40, 210, 170), (60, 160, 255), (225, 240, 255)]\n\n\ndef rgb565(rgb):\n r, g, b = rgb\n return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)\n\n\ndef line_image(colour):\n px = [KEY] * (WIDTH * HEIGHT)\n for x in range(2, WIDTH - 2):\n px[12 * WIDTH + x] = rgb565(colour)\n for i in range(5):\n x = 2 + i * (WIDTH - 5) // 4\n for y in range(7, 18):\n px[y * WIDTH + x] = rgb565(colour)\n return px\n\n\ndef marker_image(colour):\n return [rgb565(colour) if 3 <= y <= 21 and x < BAR else KEY\n for y in range(HEIGHT) for x in range(MARKER_WIDTH)]\n\n\ndef rgb9(code):\n return tuple(((code >> shift) & 7) * 255 // 7 for shift in (6, 3, 0))\n\n\ndef wave_palette():\n colours = WHITENESS_COLOURS + [rgb9(c) for c in WAVE_COLOURS]\n return struct.pack('<128H', *[rgb565(c) for c in colours])\n\n\ndef wave_lut():\n def nearest(code):\n r, g, b = (code >> 6) & 7, (code >> 3) & 7, code & 7\n return min(range(len(WAVE_COLOURS)), key=lambda i: (\n (((WAVE_COLOURS[i] >> 6) & 7) - r) ** 2 + (((WAVE_COLOURS[i] >> 3) & 7) - g) ** 2\n + ((WAVE_COLOURS[i] & 7) - b) ** 2, i))\n return bytes(8 + nearest(code) for code in range(512))\n\n\ndef assets(app):\n images = [(WIDTH, line_image(ORANGE_LINE)), (WIDTH, line_image(BLUE_LINE)),\n (MARKER_WIDTH, marker_image(ORANGE_MARK)), (MARKER_WIDTH, marker_image(BLUE_MARK))]\n original = app[TABLE - BASE:TABLE - BASE + ORIGINAL_COUNT * DESCRIPTOR]\n template = original[TEMPLATE * DESCRIPTOR:(TEMPLATE + 1) * DESCRIPTOR]\n count = ORIGINAL_COUNT + len(images)\n table_bytes = count * DESCRIPTOR\n pixels_base = DATA_BASE + ((table_bytes + 31) & ~31)\n table, pixels = bytearray(original), bytearray()\n for i, (width, data) in enumerate(images):\n entry = bytearray(template)\n struct.pack_into(' mov #0,r0 (not installed)\n if 'phasemeter' not in names:\n call(PHASE_MARKERS_CALL, 0x410B, 0x0009) # jsr @r1 -> nop\n return bytes(out)\n\n\ndef apply_manifest(app, stock, name):\n doc = read_json(MANIFESTS[name])\n if doc['target']['application_sha256'] != APP_SHA256:\n raise SystemExit('patch_cdj900nxs: %s targets another application' % name)\n out = bytearray(app)\n for p in doc['patches']:\n off = int(p['offset'], 16) if isinstance(p['offset'], str) else p['offset']\n before, after = bytes.fromhex(p['expect']), bytes.fromhex(p['replace'])\n if len(before) != len(after) or off + len(before) > APP_SIZE:\n raise SystemExit('patch_cdj900nxs: %s: bad patch at 0x%x' % (name, off))\n if stock[off:off + len(before)] != before or app[off:off + len(before)] != before:\n raise SystemExit('patch_cdj900nxs: %s: bytes at 0x%x are not the expected original'\n % (name, off))\n out[off:off + len(before)] = after\n return bytes(out)\n\n\n# old names still taken\nALIASES = {'rgb_waveform': 'advanced_waveforms', 'waveform3band': 'advanced_waveforms',\n 'phasereverse': 'phasemeter'}\n\n\ndef check_names(names):\n \"\"\"The names with old ones taken, every mod known and what each needs.\"\"\"\n names = list(dict.fromkeys(ALIASES.get(n, n) for n in names))\n unknown = [n for n in names if n not in MODS]\n if unknown:\n raise SystemExit('patch_cdj900nxs: unknown mod(s) %s; known: %s'\n % (', '.join(unknown), ', '.join(MODS)))\n if 'cuenames' in names and 'cuecolours' not in names:\n raise SystemExit('patch_cdj900nxs: cuenames needs cuecolours (its cue reader brings the comments)')\n if 'beatjump' in names and not {'cuecolours', 'hotcuekeys'} <= set(names):\n raise SystemExit('patch_cdj900nxs: beatjump needs cuecolours and hotcuekeys '\n '(the jumps are made by hotcuekeys)')\n return names\n\n\ndef patch_upstream(stock, names):\n \"\"\"Stage 1: arnoldasr's mods (his code and assets, his manifests, and\n advanced_waveforms' fix that sits on his code). The website's builder\n has this stage as data (tools/make_builder_data.py).\"\"\"\n upstream = [n for n in UPSTREAM if n in names]\n app = apply_code(stock, upstream) if upstream else stock\n for name in ('phasemeter', 'advanced_waveforms'):\n if name in names:\n app = apply_manifest(app, stock, name)\n if 'advanced_waveforms' in names:\n app = wavefix.patch(app, stock) # 1.0.1: a slightly short PWV5 is taken too\n return app\n\n\ndef patch_ours(app, stock, names):\n \"\"\"Stage 2: our mods, on the application stage 1 made (the website's\n builder runs this function itself, in the browser).\"\"\"\n if 'beatjump' in names:\n app = beatjump.patch(app, stock)\n app = dspjump.patch(app, stock) # the jumps in the DSP's decoded audio\n if 'cuecolours' in names:\n app = cuecolours.patch(app, stock)\n if 'hotcuekeys' in names:\n app = hotcuekeys.patch(app, stock)\n if 'ledsaver' in names:\n app = ledsaver.patch(app, stock)\n if 'keynotation' in names:\n app = keynotation.patch(app, stock)\n if 'cuenames' in names:\n app = cuenames.patch(app, stock) # after keynotation: chains its text draw\n if 'played' in names:\n app = played.patch(app, stock) # after keynotation and cuenames: chains their text draw\n if 'endwarn' in names:\n app = endwarn.patch(app, stock)\n if 'usbautoload' in names:\n app = usbautoload.patch(app, stock)\n if 'quicksearch' in names:\n app = quicksearch.patch(app, stock)\n if 'bpmhalfdouble' in names:\n app = bpmhalfdouble.patch(app, stock) # after the text draw chain (cuenames, played): first in it\n if 'phasemeter' in names:\n app = phasereverse.patch(app, stock) # PHASE METER: OFF / ON / REVERSE\n app = phasereverse.shift_bars(app, stock) # the beat countdown clear of the line meter\n if 'advanced_waveforms' in names:\n app = wavemodes.patch(app, stock) # UTILITY > WAVEFORM: BLUE / RGB / 3BAND\n if 'wide_waveform' in names:\n app = widewave.patch(app, stock) # after advanced_waveforms (its centre-row literal) and hotcuekeys (the draw method)\n if 'credit' in names:\n app = credit.patch(app, stock)\n return app\n\n\ndef patch(data, names, public=False):\n \"\"\"The patched application for the mods named, in a fixed order: stage 1\n (arnoldasr's) and stage 2 (ours). Everything is released now; `public`\n is still taken and changes nothing.\"\"\"\n names = check_names(names)\n stock = data[:APP_SIZE]\n if sha(stock) != APP_SHA256:\n raise SystemExit('patch_cdj900nxs: not the CDJ-900NXS 1.31 application')\n return patch_ours(patch_upstream(stock, names), stock, names)\n\n\ndef main():\n args = sys.argv[1:]\n if args == ['--list']:\n for name, what in MODS.items():\n print('%-13s %-6s %s' % (name, VERSIONS[name], what))\n return\n if len(args) != 3:\n raise SystemExit(__doc__)\n names = [n for n in args[2].replace(' ', ',').split(',') if n]\n with open(args[0], 'rb') as f:\n out = patch(f.read(), names)\n with open(args[1], 'wb') as f:\n f.write(out)\n print('patch_cdj900nxs: %s (%s), %d bytes, sha256 %s'\n % (args[1], ','.join(names), len(out), sha(out)))\n\n\nif __name__ == '__main__':\n main()\n","mods/cdj900nxs/beatjump.py":"# SPDX-License-Identifier: GPL-2.0-or-later\n\"\"\"BEAT JUMP for the CDJ-900NXS 1.31: TRACK SEARCH jumps N beats along the\ntrack's beat grid, N set in UTILITY > BEAT JUMP (OFF/1/2/4/8, default 4).\n\nOur own mod, not arnoldasr's; mods/patch_cdj900nxs.py applies it as `beatjump`.\nEverything here was found by tracing the firmware in the emulator:\n\n * The UI's poller calls the panel decoder 0x08B63278 (through its literal\n at 0x08B622A8), which turns report byte 0x12 bits 0x04 / 0x08 of the\n frame at 0x0B2E8280 into the key bits TRACK - (0x0B2E82C6 bit 0x01) and\n TRACK + (0x0B2E82C5 bit 0x80); later readers (0x08B630D0) turn those\n into the track search. The literal now points at bj_poll, which runs the\n decoder and then, with BEAT JUMP on and a beat grid loaded, takes both\n key bits out again: the track search never starts, so neither the UI nor\n the player changes track, and a press is a jump instead. (Clearing the\n report byte before the decoder instead lost a race with the panel task\n refilling the frame.)\n * The jump is made by hotcuekeys' engine (hk_engine's he_bj, which is\n why beatjump needs hotcuekeys and cuecolours): bj_poll writes the\n target in 1/150 s with bit 31 set to hotcuekeys.BJ_REQ, the tick of\n the press to BJ_TICK and the jump's length in ms to BJ_DELTA, unless a\n jump is still waiting. The engine has the DSP move its playing head by\n that many 1/75 s slots inside the audio it has already decoded\n (dspjump.py, a patch of the audio DSP): at once, gapless, the CUE point\n stays, playing, paused or at cue standby (standing, the UI is left\n paused, so PLAY plays on from there). Only where the DSP has not that\n much audio decoded does it refuse: playing, the engine then loads the\n target into the beat jumps' own DSP cue slot (hotcuekeys.BJ_SLOT) the\n old way, moved on by the time such a load takes so the deck comes back\n in phase; standing, the press is dropped (the old way there, a memory\n CALL, left the UI waiting for good after the third or fourth).\n [Since: no fallback at all; the slot load left the deck standing. A\n press the DSP cannot jump now (it keeps dspjump.MARGIN decoded ahead, so\n the time needed grows with the setting) or made while a jump still\n waits is ignored: nothing is put off, the next press counts again.]\n * The setting is kept in the player's settings journal (hotcuekeys'\n hk_save2/hk_load2 through HK_API, id SAVE_ID): the first poll takes the\n last saved value (else DEFAULT, 4 beats) into the setting's word, a\n change is written back. The firmware itself kept SCREEN SAVER's ON/OFF\n only, so BEAT JUMP came back as 1 after a restart.\n * The grid is read as arnoldasr's phase meter reads it, from the player\n context 0x099445DC: track *(ctx+0x20), X = *(track+0xC4), grid\n *(X+0x104), timestamp adjustment *(X+0x10C); 0x08A27D4C(grid, adj,\n pos_ms) gives 1 + the index of the beat at or before pos_ms, 0x08A0EFB6\n (record, adj) a beat's time in ms. The position in ms is ctx+0x2CC.\n * A jump keeps the position's offset within its beat: target = pos +\n t[i +/- N] - t[i]. Before the first beat (a track just loaded stands at\n its CUE, often a hair before beat 1) i is beat 0. A press whose target\n would leave the grid does nothing. With the setting OFF, or no grid, TRACK SEARCH is untouched.\n\nUTILITY > SCREEN SAVER (item 14, a word at 0x0A4DD270 + 0x1760, values\nOFF/ON, factory default ON = 1) becomes BEAT JUMP: its label in all 19\nlanguages, five values OFF/1/2/4/8 (the existing \"OFF\" and MIDI CHANNEL's digit\nstrings), so the old ON is now \"1\". The screen saver itself no longer reads\nthe setting: its getter literal (0x08BB7E60) points at a firmware tail that\nreturns 1, so it stays on as it was by default.\n\"\"\"\nimport os\nimport struct\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nimport hotcuekeys # noqa: E402\n\nVERSION = '1.0.1' # Semantic Versioning: MAJOR.MINOR.PATCH\n\nBASE = 0x08000000\nBJ_BASE = 0x08FCF400 # after arnoldasr's code mod (ends 0x08FCF260)\n\nPLAYER_CTX = 0x099445DC\nDECODER_LIT = 0x08B622A8 # the UI poller's literal of the panel decoder\nDECODER = 0x08B63278\nKEYS_TRACK = 0x0B2E82C5 # decoded keys: TRACK + is bit 0x80 here, TRACK - 0x01 of the next byte\nSEND = 0x08B3B666 # the UI's player-message builder\nGET_SETTING = 0x08AFD9E0 # SCREEN SAVER getter: *(0x0A4DD270 + 0x1760)\nGRID_LOOKUP = 0x08A27D4C\nGRID_TIME = 0x08A0EFB6\nFPSCR_MASK = 0xFFE7FFFF # what the firmware does to fpscr after every call\n\nSAVER_ITEM = 0x08C6CBB4 # UTILITY item 14 (copied to RAM at start-up)\nSAVER_CONSUMER_LIT = 0x08BB7E60 # the screen saver's literal of the getter\nRETURN_ONE = 0x08BDA1B0 # 'rts; mov #1,r0' at the end of a firmware function\nVALUE_STRINGS = [0xA80D013C, 0xA80D0144, 0xA80D0148, 0xA80D0150, 0xA80D0160] # OFF 1 2 4 8\nLABEL_OLD = 'SCREEN SAVER\\0'.encode('utf-16-le')\nLABEL_NEW = 'BEAT JUMP'.encode('utf-16-le').ljust(len(LABEL_OLD), b'\\0')\nLABEL_COPIES = 19\n\nVALUE_LIST = BJ_BASE + 0x400 # the five value-string pointers, after the code\nPREV_BITS = BJ_BASE + 0x3FC # last frame's TRACK bits, for press edges\nLAST_SET = BJ_BASE + 0x3F8 # word: the setting last seen (-1 at start: taken from the journal then)\nSETTING = 0x0A4DD270 + 0x1760 # the setting's word (the SCREEN SAVER's)\nSAVE_ID = 0x83E2 # ours in the settings journal (hotcuekeys' is 0x83E1)\nDEFAULT = 3 # 4 beats\nBJ_DELTA = 0x08FCF818 # word: the last jump's length in ms (signed), for hotcuekeys' DSP jump\nBJ_POSV = hotcuekeys.BJ_POSV # word: the position (ms) to reckon from, if not 0\nBJ_PEND = hotcuekeys.BJ_PEND # word: presses counted while hotcuekeys' CALL had no grid\n\n\nclass Asm:\n \"\"\"Just enough SH-4 to write bj_poll: two passes over a list of\n instructions, labels, and a literal pool of .long/.word constants.\"\"\"\n\n def __init__(self, base):\n self.base, self.items, self.labels = base, [], {}\n\n def label(self, name):\n self.items.append(('label', name))\n\n def op(self, fn):\n self.items.append(('op', fn))\n\n def long(self, name, value):\n self.items.append(('align4', None))\n self.label(name)\n self.items.append(('long', value))\n\n def word(self, name, value):\n self.label(name)\n self.items.append(('word', value))\n\n def assemble(self):\n for final in (False, True):\n pc, out = self.base, bytearray()\n for kind, arg in self.items:\n if kind == 'label':\n self.labels[arg] = pc\n continue\n if kind == 'align4':\n while pc % 4:\n out += struct.pack('> 36 = q / 20\n a.long('L_req', hotcuekeys.BJ_REQ)\n a.long('L_tick', hotcuekeys.TICK)\n a.long('L_bit31', 0x80000000)\n a.long('L_delta', BJ_DELTA)\n a.long('L_posv', BJ_POSV)\n a.long('L_pend', BJ_PEND)\n a.long('L_third', 0xAAAAAAAB) # q * this >> 33 = q / 3\n a.long('L_last', LAST_SET)\n a.long('L_setting', SETTING)\n a.long('L_bjid', SAVE_ID)\n a.long('L_bjid16', SAVE_ID << 16)\n a.long('L_api', hotcuekeys.HK_API)\n code = a.assemble()\n return code, None\n\n\ndef patch(app, stock):\n \"\"\"The application with BEAT JUMP added; `stock` is the unmodified 1.31\n application, which every replaced byte is checked against.\"\"\"\n code, _ = routine()\n assert len(code) <= LAST_SET - BJ_BASE\n out = bytearray(app)\n end = VALUE_LIST + 4 * len(VALUE_STRINGS) - BASE\n if len(out) < end:\n out += bytes(end - len(out))\n if any(out[BJ_BASE - BASE:end]):\n raise SystemExit('patch_cdj900nxs: beatjump: its area 0x%08x.. is not free' % BJ_BASE)\n\n def put(addr, before, after):\n off = addr - BASE\n if stock[off:off + len(before)] != before or out[off:off + len(before)] != before:\n raise SystemExit('patch_cdj900nxs: beatjump: bytes at 0x%08x are not the expected original'\n % addr)\n out[off:off + len(after)] = after\n\n out[BJ_BASE - BASE:BJ_BASE - BASE + len(code)] = code\n out[LAST_SET - BASE:LAST_SET - BASE + 4] = b'\\xff\\xff\\xff\\xff'\n out[VALUE_LIST - BASE:end] = b''.join(struct.pack('= 0:\n put(BASE + at, LABEL_OLD, LABEL_NEW)\n found += 1\n at = stock.find(LABEL_OLD, at + 2)\n if found != LABEL_COPIES:\n raise SystemExit('patch_cdj900nxs: beatjump: %d SCREEN SAVER labels, expected %d'\n % (found, LABEL_COPIES))\n return bytes(out)\n","mods/cdj900nxs/bpmhalfdouble.py":"# SPDX-License-Identifier: GPL-2.0-or-later\n\"\"\"BPM x2 / /2 (half / double) for the CDJ-900NXS 1.31: the loaded track's\nbeat grid halved or doubled, as rekordbox's grid editor does it, from the\ndeck. Our own mod; mods/patch_cdj900nxs.py applies it as `bpmhalfdouble`\n(with quicksearch, which loads the track again for it).\n\n * TAG TRACK/REMOVE (panel 0x14:0x20, decoded 0x0B2E82D6 bit 0x40) is the\n shift while a track is loaded and BROWSE is closed: held, CALL < (decoded\n 0x0B2E82C4 bit 0x10) halves the BPM, CALL > (bit 0x08) doubles it; the\n CALL keys are hidden from the firmware meanwhile. TAG TRACK pressed\n briefly alone (under SHORT_TICKS) is handed to the firmware as a press of\n its own once let go, so tagging works as before.\n * Where the grid comes from: loading a track, the DB task asks for it\n with dbcl_GetQuantizeData (0x089771FA: rmif, rekordbox id, &size,\n &object, a fifth argument on the stack; 1 when it came; the caller's\n frame +4 the device, player << 16 | slot << 8) and\n hands the object on (0x089874BE); the player, the UI (its copy at\n 0x0BAA04E4), the waveform's beat ticks and the bar counters all take\n their copies from it. bd_qtz sits in that call (its literal QTZ_LIT): for\n a track with a factor 2^k kept (k -2..2) it builds the object anew --\n header (+4 beats, +8 their bytes, +0x10 the entries) and entries of 16\n bytes (+0 the beat in its bar 1-4, +2 the tempo in BPM x 100, +4 the time\n in ms, then 8 bytes 0xFF): doubled, every interval split (the tempo\n doubled); halved, every other beat kept (the tempo halved); the bar on\n rekordbox's first downbeat, which stays a 1 -- in a block of its own\n (malloc, as the DB's replies are) and frees the one it got. Everything\n after it sees one consistent grid.\n * A change on the deck keeps the new factor for the track and, the deck\n not playing, has quicksearch load the track again by its browse path\n (quicksearch.RELOAD): the grid comes in through bd_qtz as above. Playing,\n nothing is changed (\"Pause first\"). The track's BPM (TRACK_INFO +0x18,\n from the DB's track info, not from the grid) is set to the grid's first\n tempo a moment after the load (PENDING_TICKS), if it is still the\n unscaled one.\n * As rekordbox: a result under MIN_BPM or over MAX_BPM is not done; the\n title bar says so (\"/2 makes no sense: 42.50 BPM\") in red. A change shows\n there in yellow for MSG_TICKS (\"BPM x2 -> 170.00 BPM\").\n * The factor is kept per track (device, rekordbox id) in a table of\n TABLE_N while the player is on.\n\nSTATE 2026-10-21 (0.1.0, not released): keys, the range check, the title\nbar's messages, the table and bd_qtz's grid (verified: the object built for\n2^k, 408 beats at 72.50 for /2) work. Missing: (1) loading the same track\nagain does not take a new grid -- the player keeps its own track info for a\ntrack it has (the new object arrives, the old one stays, the UI's copy is\ndropped: no beat ticks); a track loaded fresh (or after the info cache lost\nit) would take it. Needed: the player's own re-init of a loaded track, or\nevicting its cached info. (2) The BPM shown comes from the track info (0x38A4\nat TRACK_INFO +0x18 and the player's ctx +0x2E8), not from the grid. (3) Not\nsaved on the USB yet (own file through the DB task's file API 0x089A0A04 ff.\nwas looked at, not built).\n\"\"\"\nimport os\nimport struct\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nimport sh4asm # noqa: E402\nimport hotcuekeys # noqa: E402\nimport quicksearch # noqa: E402\n\nVERSION = '0.1.0' # Semantic Versioning: MAJOR.MINOR.PATCH\n\nBASE = 0x08000000\nBD_BASE = 0x08FE4000 # after played's 0x08FE0800..0x08FE4000\nBD_END = 0x08FE5000\n# state at BD_BASE: bytes +0 TAG down, +1 a CALL used in this hold, +2 the CALL keys last, +3 k;\n# words +4 TAG down at, +8 TAG handed back till, +12 the grid seen, +40 the message shown till,\n# +44 its colour, +52 1: the BPM still to be set, +56 the track came at, +60 the table's next slot\nBD_MSG = BD_BASE + 0x80 # the message (UTF-16, up to 63 characters)\nBD_CLONE = BD_BASE + 0x100 # the copy of the title widget (96 bytes)\nBD_TABLE = BD_BASE + 0x200 # TABLE_N x 12: +0 device, +4 rekordbox id, +8 k (byte)\nTABLE_N = 64\nBD_STR = BD_TABLE + TABLE_N * 12\nBD_CODE = BD_BASE + 0x700\n\nPLAYER_CTX = 0x099445DC\nPLAY_STATE = PLAYER_CTX + 0x10C # word: 1 playing\nTRACK_INFO = 0x09944900 # +0 device (player << 16 | slot << 8), +8 rekordbox id, +0x18 BPM x 100\nDEPTH = 0x0B7CC618 # the browse screen's level (0 closed)\nKEY_TAG = 0x0B2E82D6 # decoded TAG TRACK/REMOVE: bit 0x40\nTAG_BIT = 0x40\nKEY_CALL = 0x0B2E82C4 # decoded CALL <: 0x10, CALL >: 0x08\nDRAW_LIT = 0x080B27F4 # the text widgets' draw method in its class table\nTITLE_ID = 0x00210009\nQTZ_LIT = 0x08987974 # the DB task's track loader's literal of dbcl_GetQuantizeData\nQTZ = 0x089771FA\nMALLOC, FREE = 0x08A5C6EC, 0x08A5EBC2\n\nSHORT_TICKS = 60 # TAG TRACK let go before 0.6 s, no CALL: the firmware's press\nPULSE = 8\nMSG_TICKS = 250 # a message shown 2.5 s\nPENDING_TICKS = 100 # the BPM set 1 s after the track came\nMIN_BPM = 6000 # x 100: rekordbox's sensible range\nMAX_BPM = 24000\nOK_ABGR = 0xFF00E6FF # yellow\nNO_ABGR = 0xFF3030FF # red\n\nSTRINGS = [('S_X2', 'BPM \u00d72 \u2192 '), ('S_D2', 'BPM /2 \u2192 '), ('S_ORIG', 'BPM as analysed: '),\n ('S_FAST', '\u00d72 makes no sense: '), ('S_SLOW', '/2 makes no sense: '),\n ('S_PAUSE', 'Pause first: '), ('S_UNIT', ' BPM'), ('S_LATER', ' BPM, load it again')]\n\nSOURCE = r'''\n; ---- bd_poll: the panel decoder (or whatever ran there), then ours\nbd_poll:\n sts.l pr,@-r15\n mov.l BA_prev,r0\n jsr @r0\n nop\n mov.l r0,@-r15 ; the decoder's result, returned as it is\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n mov.l r12,@-r15\n mov.l r13,@-r15\n mov.l r14,@-r15\n mov.l BA_base,r14\n mov.l BA_tick,r13\n mov.l @r13,r13 ; now\n bsr bd_track\n nop\n bsr bd_keys\n nop\n mov.l @r15+,r14\n mov.l @r15+,r13\n mov.l @r15+,r12\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n mov.l @r15+,r8\n mov.l @r15+,r0\n lds.l @r15+,pr\n rts\n nop\n\n; ---- bd_grid -> r0 the loaded track's grid (0 none); r1, r2\nbd_grid:\n mov.l BA_ctx,r1\n mov.l @(32,r1),r1 ; the track\n tst r1,r1\n bt bg_none\n mov #-60,r0\n extu.b r0,r0 ; +0xC4: X\n mov.l @(r0,r1),r1\n tst r1,r1\n bt bg_none\n mov #65,r0\n shll2 r0 ; +0x104: the grid\n mov.l @(r0,r1),r0\n tst r0,r0\n bt bg_ret\n mov.l @(4,r0),r1 ; with beats\n mov #2,r2\n cmp/hs r2,r1\n bt bg_ret\nbg_none:\n mov #0,r0\nbg_ret:\n rts\n nop\n\n; ---- bd_track: a new track (another grid): its kept factor; its BPM set a moment later\nbd_track:\n sts.l pr,@-r15\n bsr bd_grid\n nop\n mov.l @(12,r14),r1\n cmp/eq r0,r1\n bt bt_same\n mov.l r0,@(12,r14)\n mov r0,r8\n mov #0,r0\n mov.b r0,@(3,r14)\n mov.l r0,@(52,r14)\n tst r8,r8\n bt bt_ret\n mov.l r13,@(56,r14)\n mov #1,r0\n mov.l r0,@(52,r14)\n bsr bd_find_info\n nop\n tst r0,r0\n bt bt_ret\n mov r0,r1\n mov.b @(8,r1),r0\n bra bt_ret\n mov.b r0,@(3,r14)\nbt_same:\n mov.l @(52,r14),r0 ; the BPM, once the track has settled\n tst r0,r0\n bt bt_ret\n mov.l @(56,r14),r1\n mov r13,r2\n sub r1,r2\n mov #PENDING_TICKS,r1\n cmp/hs r1,r2\n bf bt_ret\n mov #0,r0\n mov.l r0,@(52,r14)\n mov.b @(3,r14),r0 ; k: the info's BPM scaled, if it is still the track's own\n tst r0,r0\n bt bt_ret\n mov r0,r2\n mov.l @(12,r14),r1 ; the grid's first tempo\n mov.l @(16,r1),r1\n mov.w @(2,r1),r0\n extu.w r0,r3\n mov.l BA_info,r1\n mov.l @(24,r1),r4\n mov r4,r5\n shad r2,r5 ; the info's BPM x 2^k\n mov r3,r0\n sub r5,r0 ; about the grid's (a hundredth off from rounding)?\n add #2,r0\n mov #4,r5\n cmp/hs r5,r0\n bt bt_ret\n mov.l r3,@(24,r1)\nbt_ret:\n lds.l @r15+,pr\n rts\n nop\n\n; ---- bd_find_info -> r0 the table's entry for the loaded track (TRACK_INFO), or 0; r1-r5\nbd_find_info:\n mov.l BA_info,r1\n mov.l @r1,r2 ; device\n mov.l @(8,r1),r3 ; rekordbox id\n; ---- bd_find: r2 device, r3 id -> r0 the table's entry, or 0; r4, r5\nbd_find:\n mov.l BA_table,r4\n mov #TABLE_N,r5\nbf_loop:\n mov.l @r4,r0\n cmp/eq r2,r0\n bf bf_next\n mov.l @(4,r4),r0\n cmp/eq r3,r0\n bf bf_next\n rts\n mov r4,r0\nbf_next:\n dt r5\n bf/s bf_loop\n add #12,r4\n rts\n mov #0,r0\n\n; ---- bd_keep: r0 k, kept for the loaded track (a new slot round robin); r1-r6\nbd_keep:\n sts.l pr,@-r15\n mov r0,r6\n bsr bd_find_info\n nop\n tst r0,r0\n bf bk_put\n mov.l @(60,r14),r0 ; a new slot\n mov r0,r1\n add #1,r1\n mov #TABLE_N,r2\n cmp/hs r2,r1\n bf bk_n\n mov #0,r1\nbk_n:\n mov.l r1,@(60,r14)\n mov r0,r1\n shll2 r0\n shll r0 ; 8 i\n shll2 r1 ; 4 i\n add r1,r0 ; 12 i\n mov.l BA_table,r1\n add r1,r0\n mov.l BA_info,r1\n mov.l @r1,r2\n mov.l r2,@r0\n mov.l @(8,r1),r2\n mov.l r2,@(4,r0)\nbk_put:\n mov r0,r1\n mov r6,r0\n mov.b r0,@(8,r1)\n lds.l @r15+,pr\n rts\n nop\n\n .align 4\nBA_prev: .long PREV_DECODER\nBA_base: .long BD_BASE\nBA_tick: .long TICK\nBA_ctx: .long PLAYER_CTX\nBA_info: .long TRACK_INFO\nBA_table: .long BD_TABLE\n\n; ---- bd_keys: TAG TRACK the shift, CALL < /2, CALL > x2\nbd_keys:\n sts.l pr,@-r15\n mov.l @(12,r14),r0 ; a track loaded and BROWSE closed: ours\n tst r0,r0\n bt bk_free\n mov.l BK_depth,r1\n mov.l @r1,r0\n tst r0,r0\n bf bk_free\n mov.l BK_tag,r9\n mov.b @r9,r0\n mov r0,r10\n and #TAG_BIT,r0\n mov r0,r11 ; r11: TAG down\n mov r10,r0\n and #0xBF,r0 ; hidden from the firmware\n mov.b r0,@r9\n mov.b @r14,r0\n tst r0,r0\n bf bk_was\n tst r11,r11\n bt bk_out\n mov #1,r0 ; down now\n mov.b r0,@r14\n mov #0,r0\n mov.b r0,@(1,r14)\n mov.l r13,@(4,r14)\n mov.l BK_call,r1\n mov.b @r1,r0\n and #0x18,r0\n bra bk_hide\n mov.b r0,@(2,r14)\nbk_was:\n tst r11,r11\n bt bk_up\n mov.l BK_call,r1 ; held: a CALL key pressed now?\n mov.b @r1,r0\n and #0x18,r0\n mov r0,r2\n mov.b @(2,r14),r0\n not r0,r0\n and r2,r0\n mov r0,r3 ; r3: CALL keys down now\n mov r2,r0\n mov.b r0,@(2,r14)\n mov r3,r0\n tst #0x10,r0\n bt bk_k1\n mov #1,r0 ; CALL <: /2\n mov.b r0,@(1,r14)\n mov.b @(3,r14),r0\n add #-1,r0\n bsr bd_apply\n mov r0,r4\n bra bk_hide\n nop\nbk_k1:\n mov r3,r0\n tst #0x08,r0\n bt bk_hide\n mov #1,r0 ; CALL >: x2\n mov.b r0,@(1,r14)\n mov.b @(3,r14),r0\n add #1,r0\n bsr bd_apply\n mov r0,r4\nbk_hide:\n mov.l BK_call,r1 ; the CALL keys hidden while TAG is held\n mov.b @r1,r0\n and #0xE7,r0\n bra bk_out\n mov.b r0,@r1\nbk_up:\n mov #0,r0 ; let go: a short press alone is the firmware's\n mov.b r0,@r14\n mov.b @(1,r14),r0\n tst r0,r0\n bf bk_out\n mov.l @(4,r14),r1\n mov r13,r2\n sub r1,r2\n mov #SHORT_TICKS,r1\n cmp/hs r1,r2\n bt bk_out\n mov #PULSE,r0\n add r13,r0\n bra bk_out\n mov.l r0,@(8,r14)\nbk_free:\n mov #0,r0\n mov.b r0,@r14\nbk_out:\n mov.l @(8,r14),r0 ; TAG handed back\n sub r13,r0\n cmp/pl r0\n bf bk_ret\n mov.l BK_tag,r1\n mov.b @r1,r0\n or #TAG_BIT,r0\n mov.b r0,@r1\nbk_ret:\n lds.l @r15+,pr\n rts\n nop\n\n; ---- bd_apply: r4 the new k. Checked against the range and the deck, kept, the track\n; loaded again (through quicksearch), the title bar told\nbd_apply:\n sts.l pr,@-r15\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov r4,r8 ; r8: the new k\n mov.l @(12,r14),r1 ; r9: the BPM it makes: the grid's first tempo x 2^(new k - k)\n mov.l @(16,r1),r1\n mov.w @(2,r1),r0\n extu.w r0,r9\n mov.b @(3,r14),r0\n mov r8,r1\n sub r0,r1\n shad r1,r9\n mov r1,r0\n cmp/pz r0\n bt ba_up\n mov.l BK_min,r1 ; /2: not under MIN_BPM\n cmp/hs r1,r9\n bt ba_ok\n bra ba_no\n mov.l BK_sslow,r4\nba_up:\n mov.l BK_max,r1 ; x2: not over MAX_BPM\n cmp/hi r1,r9\n bf ba_ok\n mov.l BK_sfast,r4\nba_no:\n mov r9,r5\n mov.l BK_red,r6\n bsr bd_msg\n mov.l BK_sunit,r7\n bra ba_ret\n nop\nba_ok:\n mov.l BK_state,r1 ; playing: nothing changed\n mov.l @r1,r0\n cmp/eq #1,r0\n bf ba_still\n mov.l BK_spause,r4\n bra ba_no\n nop\nba_still:\n mov r8,r0\n mov.b r0,@(3,r14)\n bsr bd_keep\n mov r8,r0\n mov.l BK_sorig,r4 ; what the title bar says\n mov r8,r0\n tst r0,r0\n bt ba_say\n mov.l BK_sx2,r4\n cmp/pz r0\n bt ba_say\n mov.l BK_sd2,r4\nba_say:\n mov.l BK_sunit,r7\n mov.l BK_qsre,r1 ; the track loaded again by its path, if quicksearch knows it\n tst r1,r1\n bt ba_later\n mov.l BK_qslev,r2\n mov.b @r2,r0\n tst r0,r0\n bt ba_later\n mov #1,r0\n bra ba_tell\n mov.l r0,@r1\nba_later:\n mov.l BK_slater,r7 ; else: at the next load\nba_tell:\n mov r9,r5\n mov.l BK_yellow,r6\n bsr bd_msg\n nop\nba_ret:\n mov.l @r15+,r9\n mov.l @r15+,r8\n lds.l @r15+,pr\n rts\n nop\n\n .align 4\nBK_depth: .long DEPTH\nBK_tag: .long KEY_TAG\nBK_call: .long KEY_CALL\nBK_state: .long PLAY_STATE\nBK_min: .long MIN_BPM\nBK_max: .long MAX_BPM\nBK_red: .long NO_ABGR\nBK_yellow: .long OK_ABGR\nBK_sslow: .long S_SLOW\nBK_sfast: .long S_FAST\nBK_spause: .long S_PAUSE\nBK_sorig: .long S_ORIG\nBK_sx2: .long S_X2\nBK_sd2: .long S_D2\nBK_sunit: .long S_UNIT\nBK_slater: .long S_LATER\nBK_qsre: .long QS_RELOAD\nBK_qslev: .long QS_LEVELS\n\n; ---- bd_msg: r4 the prefix, r5 a BPM x 100, r6 the colour, r7 the ending (UTF-16):\n; the title bar shows \"\" for MSG_TICKS\nbd_msg:\n sts.l pr,@-r15\n mov.l r7,@-r15\n mov.l r6,@(44,r14)\n mov.l BM_msg,r7 ; the message\nbm_cp:\n mov.w @r4+,r0\n tst r0,r0\n bt bm_num\n mov.w r0,@r7\n bra bm_cp\n add #2,r7\nbm_num:\n mov.l BM_r100,r1 ; integer part and hundredths\n dmulu.l r1,r5\n sts mach,r2\n shlr2 r2\n shlr2 r2\n shlr r2 ; / 100\n mov #100,r1\n mulu.w r1,r2\n sts macl,r3\n mov r5,r6\n sub r3,r6 ; r6: hundredths\n mov r2,r5 ; r5: integer part (< 1000)\n mov #100,r1 ; hundreds, if any\n mov #0,r3\nbm_h:\n cmp/hs r1,r5\n bf bm_hd\n sub r1,r5\n bra bm_h\n add #1,r3\nbm_hd:\n mov r3,r0\n tst r0,r0\n bt bm_t\n add #48,r0\n mov.w r0,@r7\n add #2,r7\nbm_t:\n mov #10,r1 ; tens (always once there are hundreds)\n mov #0,r2\nbm_t2:\n cmp/hs r1,r5\n bf bm_td\n sub r1,r5\n bra bm_t2\n add #1,r2\nbm_td:\n mov r2,r0\n or r3,r0\n tst r0,r0\n bt bm_u\n mov r2,r0\n add #48,r0\n mov.w r0,@r7\n add #2,r7\nbm_u:\n mov r5,r0\n add #48,r0\n mov.w r0,@r7\n add #2,r7\n mov #46,r0 ; \".\"\n mov.w r0,@r7\n add #2,r7\n mov #10,r1\n mov #0,r2\nbm_f:\n cmp/hs r1,r6\n bf bm_fd\n sub r1,r6\n bra bm_f\n add #1,r2\nbm_fd:\n mov r2,r0\n add #48,r0\n mov.w r0,@r7\n add #2,r7\n mov r6,r0\n add #48,r0\n mov.w r0,@r7\n add #2,r7\n mov.l @r15+,r4 ; the ending\nbm_cp2:\n mov.w @r4+,r0\n mov.w r0,@r7\n tst r0,r0\n bf/s bm_cp2\n add #2,r7\n mov.l BM_ticks,r0\n add r13,r0\n mov.l r0,@(40,r14)\n lds.l @r15+,pr\n rts\n nop\n\n; ---- bd_draw: the text widgets' draw method (r4 the renderer, r5 the widget):\n; while a message is shown, the title bar's title is drawn as a copy with it\nbd_draw:\n mov.l BM_base,r1\n mov.l @(40,r1),r0\n mov.l BM_tick,r2\n mov.l @r2,r2\n sub r2,r0\n cmp/pl r0\n bf bd_go\n mov.l @(4,r5),r0\n mov.l BM_title,r2\n cmp/eq r2,r0\n bf bd_go\n mov.l BM_clone,r6 ; the copy\n mov r5,r2\n mov r6,r3\n mov #24,r7\nbd_cp:\n mov.l @r2+,r0\n mov.l r0,@r3\n dt r7\n bf/s bd_cp\n add #4,r3\n mov #0,r0\n mov.l r0,@(4,r6) ; (not the title any more for the hooks after us)\n mov.l @(44,r1),r0\n mov.l r0,@(40,r6) ; the colour\n mov.l BM_msg,r0\n mov.l r0,@(52,r6) ; the message\n mov r6,r5\nbd_go:\n mov.l BM_prev,r0\n jmp @r0\n nop\n\n .align 4\nBM_r100: .long 0x51EB851F\nBM_ticks: .long MSG_TICKS\nBM_base: .long BD_BASE\nBM_tick: .long TICK\nBM_title: .long TITLE_ID\nBM_prev: .long PREV_DRAW\nBM_msg: .long BD_MSG\nBM_clone: .long BD_CLONE\n\n; ---- bd_qtz: dbcl_GetQuantizeData (r4 rmif, r5 rekordbox id, r6 &the object's size, r7\n; &the object, a fifth argument on the stack; 1 returned when it came; the device at +4 of the caller's frame), in the DB task. A track with a factor kept gets its\n; grid built for it, in a block of its own; the one the DB gave freed\nbd_qtz:\n mov.l @(4,r15),r1 ; the device (the caller's frame, before ours)\n sts.l pr,@-r15\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n mov.l r12,@-r15\n mov r1,r8\n mov r5,r9\n mov r6,r10\n mov r7,r11\n mov.l @(24,r15),r0 ; its fifth argument (on the stack) on top again\n mov.l r0,@-r15\n mov.l BQ_prev,r0\n jsr @r0\n nop\n add #4,r15\n mov r0,r12 ; its result (1: the grid came), returned as it is\n tst r0,r0\n bt bq_ret\n mov r8,r2 ; a factor kept for it?\n bsr bd_find\n mov r9,r3\n tst r0,r0\n bt bq_ret\n mov.b @(8,r0),r0\n tst r0,r0\n bt bq_ret\n mov r0,r5\n mov.l @r11,r4 ; (r7: &object, r6: &its size)\n tst r4,r4\n bt bq_ret\n bsr bd_scale\n nop\n tst r0,r0\n bt bq_ret ; no memory: the track's own grid\n mov.l @r11,r4 ; the DB's freed, ours in its place\n mov.l r0,@r11\n mov.l @(8,r0),r1\n add #20,r1\n mov.l r1,@r10\n mov.l BQ_free,r0\n jsr @r0\n nop\nbq_ret:\n mov r12,r0\n mov.l @r15+,r12\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n mov.l @r15+,r8\n lds.l @r15+,pr\n rts\n nop\n\n; ---- bd_scale: r4 a grid object (entries at +20), r5 k -> r0 a new one for 2^k (0: no\n; memory). r1-r7, r8-r13 used (saved)\nbd_scale:\n sts.l pr,@-r15\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n mov.l r12,@-r15\n mov.l r13,@-r15\n mov r5,r8 ; r8: k\n mov r4,r9 ; r9: the object\n mov r4,r10\n add #20,r10 ; r10: its entries\n mov.l @(4,r4),r11 ; r11: their count\n mov r10,r1 ; the first downbeat: its index in r12\n mov #0,r12\nbs_anchor:\n mov.w @r1,r0\n cmp/eq #1,r0\n bt bs_anc\n add #16,r1\n add #1,r12\n cmp/hs r11,r12\n bf bs_anchor\n mov #0,r12\nbs_anc:\n mov #1,r7 ; r7: f = 2^|k|\n mov r8,r0\n cmp/pz r0\n bt bs_f\n neg r0,r0\nbs_f:\n shld r0,r7\n mov r8,r0\n cmp/pz r0\n bf bs_half\n mov r11,r5 ; x2: (n - 1) f + 1 entries\n add #-1,r5\n mulu.w r7,r5\n sts macl,r5\n add #1,r5\n bra bs_alloc\n nop\nbs_half:\n mov r7,r0 ; /2: i0 = anchor mod f, (n - 1 - i0) / f + 1 entries\n add #-1,r0\n and r12,r0\n mov r11,r5\n add #-1,r5\n sub r0,r5\n shad r8,r5\n add #1,r5\nbs_alloc:\n mov r5,r13 ; r13: the new count\n mov r5,r4\n shll2 r4\n shll2 r4\n add #20,r4 ; header + 16 bytes an entry\n mov.l r7,@-r15\n mov.l BS_malloc,r0\n jsr @r0\n nop\n mov.l @r15+,r7\n tst r0,r0\n bt bs_ret\n mov r0,r6 ; r6: the new object\n mov.l @r9,r1 ; its header: the old one's, the count, bytes and entries its own\n mov.l r1,@r6\n mov.l @(12,r9),r1\n mov.l r1,@(12,r6)\n mov.l r13,@(4,r6)\n mov r13,r1\n shll2 r1\n shll2 r1\n mov.l r1,@(8,r6)\n mov r6,r1\n add #20,r1\n mov.l r1,@(16,r6)\n mov.l r6,@-r15\n mov r6,r0\n add #20,r0\n mov r0,r6 ; r6: the new entries (bs_put)\n mov r8,r0\n cmp/pz r0\n bf bs_fillh\n mov r12,r3 ; x2: the new downbeat's index anchor x f\n mulu.w r7,r3\n sts macl,r3\n mov #0,r2 ; r2: j\n mov #0,r1 ; r1: i\nbs_iloop:\n mov r1,r0\n shll2 r0\n shll2 r0\n add r10,r0\n mov.l @(4,r0),r4 ; t0\n mov.w @(2,r0),r0\n extu.w r0,r5\n shld r8,r5 ; the tempo x f\n mov r1,r0\n add #1,r0\n cmp/hs r11,r0 ; the last: no interval after it\n bt bs_last\n shll2 r0\n shll2 r0\n add r10,r0\n mov.l @(4,r0),r0\n sub r4,r0 ; d\n mov.l r0,@-r15\n mov #0,r0 ; s\nbs_sloop:\n mov.l r0,@-r15\n mov.l @(4,r15),r12 ; d (the anchor is in r3 now)\n mulu.w r0,r12\n sts macl,r12\n neg r8,r0\n shld r0,r12 ; d s / f\n add r4,r12 ; the time\n bsr bs_put\n nop\n mov.l @r15+,r0\n add #1,r0\n cmp/hs r7,r0\n bf bs_sloop\n add #4,r15\n bra bs_inext\n nop\nbs_last:\n mov r4,r12\n bsr bs_put\n nop\nbs_inext:\n add #1,r1\n cmp/hs r11,r1\n bf bs_iloop\n bra bs_done\n nop\nbs_fillh:\n mov r7,r0 ; /2: every f-th from i0; the new downbeat at (anchor - i0) / f\n add #-1,r0\n and r12,r0\n mov r0,r1 ; r1: i = i0\n mov r12,r3\n sub r0,r3\n shad r8,r3\n mov #0,r2\nbs_hloop:\n mov r1,r0\n shll2 r0\n shll2 r0\n add r10,r0\n mov.l @(4,r0),r12 ; the time\n mov.w @(2,r0),r0\n extu.w r0,r5\n shad r8,r5 ; the tempo / f\n bsr bs_put\n nop\n add r7,r1\n cmp/hs r11,r1\n bf bs_hloop\nbs_done:\n mov.l @r15+,r0 ; the new object\nbs_ret:\n mov.l @r15+,r13\n mov.l @r15+,r12\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n mov.l @r15+,r8\n lds.l @r15+,pr\n rts\n nop\n\n; bs_put: entry r2 at r6: r5 tempo, r12 time, r3 the downbeat's index; r2 += 1 (r0, r12)\nbs_put:\n mov r2,r0\n shll2 r0\n shll2 r0\n add r6,r0\n mov.l r12,@(4,r0)\n mov r0,r12\n mov r5,r0\n mov.w r0,@(2,r12)\n mov r2,r0 ; the beat in its bar: ((j - downbeat) & 3) + 1\n sub r3,r0\n and #3,r0\n add #1,r0\n mov.w r0,@r12\n mov #-1,r0\n mov.l r0,@(8,r12)\n mov.l r0,@(12,r12)\n rts\n add #1,r2\n\n .align 4\nBQ_prev: .long PREV_QTZ\nBQ_free: .long FREE\nBS_malloc: .long MALLOC\n'''\n\n\ndef strings():\n \"\"\"{name: address}, bytes of the UTF-16 strings at BD_STR.\"\"\"\n at, out, addr = BD_STR, b'', {}\n for name, text in STRINGS:\n addr[name] = at\n b = text.encode('utf-16-le') + b'\\0\\0'\n out += b\n at += len(b)\n assert at <= BD_CODE, 'bpmhalfdouble: strings past their room'\n return addr, out\n\n\ndef routine(prev_decoder, prev_draw, prev_qtz=QTZ, have_qs=True):\n addr, _ = strings()\n syms = {\n 'PREV_DECODER': prev_decoder, 'PREV_DRAW': prev_draw, 'PREV_QTZ': prev_qtz,\n 'BD_BASE': BD_BASE, 'BD_TABLE': BD_TABLE, 'BD_MSG': BD_MSG, 'BD_CLONE': BD_CLONE,\n 'TABLE_N': TABLE_N, 'TICK': hotcuekeys.TICK, 'PLAYER_CTX': PLAYER_CTX, 'PLAY_STATE': PLAY_STATE,\n 'TRACK_INFO': TRACK_INFO, 'DEPTH': DEPTH, 'KEY_TAG': KEY_TAG, 'TAG_BIT': TAG_BIT,\n 'KEY_CALL': KEY_CALL, 'TITLE_ID': TITLE_ID, 'MALLOC': MALLOC, 'FREE': FREE,\n 'SHORT_TICKS': SHORT_TICKS, 'PULSE': PULSE, 'MSG_TICKS': MSG_TICKS, 'PENDING_TICKS': PENDING_TICKS,\n 'MIN_BPM': MIN_BPM, 'MAX_BPM': MAX_BPM, 'OK_ABGR': OK_ABGR, 'NO_ABGR': NO_ABGR,\n 'QS_RELOAD': quicksearch.RELOAD if have_qs else 0,\n 'QS_LEVELS': quicksearch.QS_BASE + 11,\n }\n syms.update(addr)\n return sh4asm.assemble(SOURCE, BD_CODE, syms)\n\n\ndef patch(app, stock):\n \"\"\"The application with BPM x2 / /2: chained after the decoder literal, the\n text draw entry and the DB task's grid request as it finds them.\"\"\"\n h = hotcuekeys\n out = bytearray(app)\n word = lambda a: struct.unpack_from('= quicksearch.QS_END - BASE and \\\n any(out[quicksearch.QS_CODE - BASE:quicksearch.QS_CODE - BASE + 16])\n code, labels = routine(word(h.DECODER_LIT), word(DRAW_LIT), word(QTZ_LIT), have_qs)\n assert BD_CODE + len(code) <= BD_END, 'bpmhalfdouble: past its area'\n if len(out) < BD_END - BASE:\n out += bytes(BD_END - BASE - len(out))\n if any(out[BD_BASE - BASE:BD_END - BASE]):\n raise SystemExit('patch_cdj900nxs: bpmhalfdouble: its area 0x%08x.. is not free' % BD_BASE)\n _, sdata = strings()\n out[BD_STR - BASE:BD_STR - BASE + len(sdata)] = sdata\n out[BD_CODE - BASE:BD_CODE - BASE + len(code)] = code\n struct.pack_into('> 16 if hi else val) & 0xFFFF\n return pred(p) | dn << 23 | cst << 7 | (0b11010 if hi else 0b01010) << 2 | ds << 1\n\n\ndef enc_d(op, data, base, off, p=''):\n \"\"\"ld/st *+base(off) (byte offset, scaled by the access size).\"\"\"\n ss, sn = reg(data)\n bs, bn = reg(base)\n sc = D_SCALE[op]\n assert off % sc == 0 and 0 <= off // sc < 32, (op, off)\n return pred(p) | sn << 23 | bn << 18 | (off // sc) << 13 | 0b0001 << 9 | bs << 7 | D_OPS[op] << 4 | 0b01 << 2 | ss << 1\n\n\ndef enc_b_imm(at, target, side=0, p=''):\n disp = (target - (at & ~31)) // 4\n assert -(1 << 20) <= disp < (1 << 20)\n return pred(p) | (disp & 0x1FFFFF) << 7 | 0b00100 << 2 | side << 1\n\n\ndef enc_b_reg(r, p=''):\n rs, rn = reg(r)\n return pred(p) | rn << 18 | (1 if rs == 0 else 0) << 12 | 0x362 # .S2 (X for an A register)\n\n\ndef enc_nop(n):\n assert 1 <= n <= 9\n return (n - 1) << 13\n\n\nDINT, RINT = 0x10004000, 0x10006000\n\n\ndef enc_shl(src, cst, dst, p=''):\n \"\"\"shl .S src,ucst5,dst\"\"\"\n ds, dn = reg(dst)\n ss, sn = reg(src)\n assert ss == ds and 0 <= cst < 32\n return pred(p) | dn << 23 | sn << 18 | cst << 13 | 0x32 << 6 | 0b1000 << 2 | ds << 1\n\n\ndef enc_dp(op, data, off, p=''):\n \"\"\"ld/st *+b14(ucst15) (byte offset, scaled), .D2\"\"\"\n ds, dn = reg(data)\n sc = D_SCALE[op]\n assert off % sc == 0 and 0 <= off // sc < 32768\n return pred(p) | dn << 23 | (off // sc) << 8 | 0 << 7 | D_OPS[op] << 4 | 0b11 << 2 | ds << 1\n\n\ndef enc_extu(src, csta, cstb, dst, p=''):\n \"\"\"extu .S src,csta,cstb,dst (field form, bits 7-6 = 00).\"\"\"\n ds, dn = reg(dst)\n ss, sn = reg(src)\n assert ss == ds\n return pred(p) | dn << 23 | sn << 18 | csta << 13 | cstb << 8 | 0b00 << 6 | 0b0010 << 2 | ds << 1\n\n\ndef enc_addaw(src2, src1, dst, p=''):\n \"\"\"addaw .D dst = src2 + src1*4 (src1 register or ucst5).\"\"\"\n ds, dn = reg(dst)\n s2s, s2n = reg(src2)\n assert s2s == ds\n if isinstance(src1, int):\n return pred(p) | dn << 23 | s2n << 18 | (src1 & 31) << 13 | 0x3A << 7 | 0b10000 << 2 | ds << 1\n s1s, s1n = reg(src1)\n return pred(p) | dn << 23 | s2n << 18 | s1n << 13 | 0x38 << 7 | 0b10000 << 2 | ds << 1\n\n\nclass Asm:\n def __init__(self, base):\n self.base, self.items, self.labels = base, [], {}\n\n def label(self, name):\n self.items.append(('label', name))\n\n def org(self, addr):\n \"\"\"continue at addr (a new block; the caller branches there)\"\"\"\n self.items.append(('org', addr))\n\n def op(self, mn, *a, p=''):\n self.items.append(('op', (mn, a, p)))\n\n def word(self, w):\n self.items.append(('word', w))\n\n def _enc(self, at, mn, a, p):\n if mn == 'mvk':\n return enc_mvk(0, a[0], a[1], p)\n if mn == 'mvkh':\n return enc_mvk(1, a[0], a[1], p)\n if mn == 'mvkl32': # helper: emitted as two words by assemble()\n raise AssertionError\n if mn in L_OPS:\n return enc_l(mn, a[0], a[1], a[2], p)\n if mn in D_OPS:\n return enc_d(mn, a[0], a[1], a[2], p)\n if mn == 'b':\n t = a[0]\n if isinstance(t, str) and t[0] in 'ab' and t[1:].isdigit():\n return enc_b_reg(t, p)\n t = self.labels[t] if isinstance(t, str) else t\n return enc_b_imm(at, t, a[1] if len(a) > 1 else 0, p)\n if mn == 'nop':\n return enc_nop(a[0] if a else 1)\n if mn == 'dint':\n return DINT\n if mn == 'rint':\n return RINT\n if mn == 'extu':\n return enc_extu(a[0], a[1], a[2], a[3], p)\n if mn == 'addaw':\n return enc_addaw(a[0], a[1], a[2], p)\n if mn == 'shl':\n return enc_shl(a[0], a[1], a[2], p)\n if mn == 'lddp': # ldbu/ldw/... *+b14(off)\n return enc_dp(a[0], a[1], a[2], p)\n if mn == 'mv':\n return enc_l('add', 0, a[0], a[1], p)\n raise KeyError(mn)\n\n def assemble(self):\n \"\"\"-> [(addr, word)]\"\"\"\n out = []\n for final in (False, True):\n pc, out = self.base, []\n for kind, arg in self.items:\n if kind == 'label':\n self.labels[arg] = pc\n continue\n if kind == 'org':\n pc = arg\n continue\n if kind == 'word':\n out.append((pc, arg))\n pc += 4\n continue\n mn, a, p = arg\n if mn == 'const': # mvk + mvkh of a 32-bit value\n v = self.labels.get(a[0], a[0]) if isinstance(a[0], str) else a[0]\n out.append((pc, enc_mvk(0, v if final or not isinstance(a[0], str) else 0, a[1], p)))\n out.append((pc + 4, enc_mvk(1, v if final or not isinstance(a[0], str) else 0, a[1], p)))\n pc += 8\n continue\n try:\n w = self._enc(pc, mn, a, p)\n except KeyError:\n if final:\n raise\n w = 0\n out.append((pc, w))\n pc += 4\n return out\n\n\ndef selftest():\n ref = [\n (enc_d('ldw', 'a4', 'a3', 0), 0x020c0264),\n (enc_d('stw', 'a3', 'a5', 0), 0x01940274),\n (enc_d('sth', 'a4', 'a3', 2), 0x020c2254),\n (enc_d('ldh', 'a4', 'a3', 6), 0x020c6244),\n (enc_d('ldhu', 'a4', 'a3', 0), 0x020c0204),\n (enc_l('cmpeq', 1, 'a4', 'a0'), 0x00102a58),\n (enc_l('or', 0, 'a4', 'a0'), 0x00100fd8),\n (enc_l('add', 'b10', 'b4', 'b6'), 0x0311407b & ~1),\n (enc_l('cmplt', 0, 'b4', 'b0'), 0x00100adb & ~1),\n (enc_l('cmpgt', 'a3', 'a10', 'a0'), 0x002868f8),\n (enc_l('sub', 'a3', 'a4', 'a10'), 0x051060f8),\n (enc_l('add', 'a4', 'a3', 'a3'), 0x018c8078),\n (enc_mvk(0, -7576, 'b4'), 0x0271342a),\n (enc_mvk(1, 0x1182e268, 'b4'), 0x0208c16a),\n (enc_b_reg('b4'), 0x00100362),\n (enc_addaw('a4', 'a6', 'a3'), 0x0190dc41 & ~1),\n (enc_nop(5), 0x00008000),\n (enc_shl('a14', 3, 'a8'), 0x04386ca1 & ~1),\n (enc_dp('ldbu', 'b6', 969), 0x0303c91e),\n ]\n bad = [(hex(a), hex(b)) for a, b in ref if a != b]\n assert not bad, bad\n return True\n\n\nif __name__ == '__main__':\n print('selftest', selftest())\n","mods/cdj900nxs/credit.py":"# SPDX-License-Identifier: GPL-2.0-or-later\n\"\"\"The mods' credit on the CDJ-900NXS 1.31 boot screen: \"Pionaarr Mods\"\nand the release (RELEASE, e.g. \"Pionaarr Mods 1.8.1\") where the splash\nshows \"Ver 1.31\". Our own; mods/patch_cdj900nxs.py\napplies it as `credit` (the patcher ticks it at the start).\n\nThe splash's text is made by 0x08BDCC14: a 50-byte buffer gets strcpy\n(0x08C28460) of the string the .data word at RAM 0x1110766C points to\n(image 0x08C6D22C, \"Ver \" at 0xA80D12BC), then strcat (the same function)\nof the version the getter 0x08BE9A8C returns (\"1.31\" at 0xA8000740), and is\nshown. The .data word now points at our text, and the function's own\nliteral of the getter (0x08BDCD8C, read only at 0x08BDCC34) at a stub that\nreturns an empty string, so nothing is added to it. The text's box (.data\n0x08C841E0: 139,363 - 619,385, right-aligned) is 8 lines taller, so the\ndescenders of p and y show. The getter itself and\neverything else that shows the version (the LINK screen's \"Ver %s\",\nUTILITY, the updater, the boot loader) are untouched.\"\"\"\nimport struct\n\nVERSION = '1.1.0' # Semantic Versioning: MAJOR.MINOR.PATCH\nRELEASE = '1.9.0' # the Pionaarr Mods release, ALWAYS the latest: the website's generator\n # (tools/make_builder_data.py) writes its version here\n\nBASE = 0x08000000\nDATA_WORD = 0x08C6D22C # .data: the splash's \"Ver \" pointer (RAM 0x1110766C)\nDATA_VER = 0xA80D12BC # \"Ver \"\nGETTER_LIT = 0x08BDCD8C # the splash function's literal of the version getter\nGETTER = 0x08BE9A8C\nTEXT = 'Pionaarr Mods ' + RELEASE\nCREDIT = 0x08FCF900 # between beatjump's 0x08FCF400..0x08FCF814 and cuecolours' 0x08FCFA00\nCREDIT_LEN = 0x40\nSTUB = CREDIT + 0x20 # mov.l @(4,pc),r0; rts; nop; nop; .long the text's final NUL\nBOX = 0x08C841E0 # .data: the splash text's box, x1 y1 x2 y2 (16 bits each)\nBOX_STOCK = (139, 363, 619, 385)\nBOX_NEW = (139, 363, 619, 393) # \"Ver 1.31\" has no descender; p and y need 8 more lines\n\n\ndef area():\n \"\"\"The credit area's bytes: the text, then the stub returning \"\".\"\"\"\n text = TEXT.encode('ascii') + b'\\0' # the splash takes bytes as Latin-1: no UTF-8\n # the splash's buffer is 50 bytes, its UTF-16 copy 100\n assert len(text) <= STUB - CREDIT and len(text) <= 50\n empty = CREDIT + len(text) - 1\n stub = struct.pack('<4HI', 0xD001, 0x000B, 0x0009, 0x0009, empty | 0xA0000000)\n return text.ljust(STUB - CREDIT, b'\\0') + stub\n\n\ndef patch(app, stock):\n \"\"\"The application with the credit; `stock` is the unmodified 1.31\n application, which every replaced byte is checked against.\"\"\"\n out = bytearray(app)\n end = CREDIT + CREDIT_LEN - BASE\n if len(out) < end:\n out += bytes(end - len(out))\n if any(out[CREDIT - BASE:end]):\n raise SystemExit('patch_cdj900nxs: credit: its area 0x%08x.. is not free' % CREDIT)\n\n def put(addr, before, after):\n off = addr - BASE\n if stock[off:off + 4] != struct.pack(' the 44-byte resource records\nTRI_ID = 0xC5 # the red memory-cue triangle, 16 x 5, RGB565\nTRI_PIXELS = 0x0815A1B0 # its pixels in the image\nTRI_RED = 0xF800\nMEMLIST = 0x0B7BE8D4 # the UI's memory list, 10 x 0x24\nOV_RECS = 0x0B800D9C # the overview's marker records, 12 bytes\nMK_RECS = 0x0B801258 # the marker records they come from, 16 bytes\nMK_RECORDS = 101\nMKCOPY_LIT = 0x08BC6BF8 # the waveform preparer's literal for memcpy, which fills MK_RECS\nMEMCPY = 0x08C279C4\nHOT_DEFAULT = 0x07E0 # a hot cue without a colour: green, as rekordbox's default\n\n# rekordbox's eight memory-cue colours (ids 1-8), as the player itself draws\n# them: the track-colour dots of the browser, resources 0x14, 0x16, 0x13,\n# 0x18, 0x12, 0x0F, 0x11 and 0x15 (RGB565 there).\nMEMORY_RGB565 = [\n 0xFA55, # 1 pink\n 0xF800, # 2 red\n 0xFC20, # 3 orange\n 0xFFE0, # 4 yellow\n 0x07E0, # 5 green\n 0x079F, # 6 aqua\n 0x22FF, # 7 blue\n 0x881F, # 8 purple\n]\n\n# hot cue letters A-H, 5 x 7, for the overview's bigger marker (bit 7 = left)\nLETTERS5 = [\n ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],\n ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],\n ['01110', '10001', '10000', '10000', '10000', '10001', '01110'],\n ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],\n ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],\n ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],\n ['01110', '10001', '10000', '10111', '10001', '10001', '01111'],\n ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],\n]\n\n# hot cue letters A-H, 3 x 5, rows top down: drawn black into a 9 x 5 marker\nLETTERS = ['010101111101101', '110101110101110', '011100100100011', '110101101101110',\n '111100110100111', '111100110100100', '011100101101011', '101101111101101']\n\n# data after the code (zero in the image)\nDATA = 0x08FD0000 # after the code (checked in patch())\nHOT_TABLE = DATA # 8 x 8: +0 time 1/150 s (-1 none), +4 RGB565, +6 hot cue number;\n # entry 0's +7 counts the loads (cue_get)\nSLOTS = 18 # triangles: memory list index 0-9, hot cues 10-17\nTRI_BUF = DATA + 0x40 # SLOTS x 160 bytes\nHOTBOX_W, HOTBOX_H = 16, 9 # the overview's hot cue marker: 11 x 9, then transparent; the\nHOTBOX_SIZE = 288 # blitter wants lines of a multiple of 16 bytes, as all the firmware's\nHOTBOX = (TRI_BUF + SLOTS * 160 + 31) & ~31 # 8 x HOTBOX_SIZE\nFAKE_RECS = HOTBOX + 8 * HOTBOX_SIZE # one 44-byte record, the detail waveform's\nDATA_END = FAKE_RECS + 44\nassert DATA_END <= CC_END\n\n\ndef rgb565(r, g, b):\n return (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3\n\n\nSOURCE = r'''\n; ---- cue_get: dbcl_GetUsbSdCueInfo(r4 connection, r5 track, r6 reply) plus colours\ncue_get:\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n mov.l r12,@-r15\n mov.l r13,@-r15\n mov.l r14,@-r15\n sts.l pr,@-r15\n add #-20,r15 ; +0 wait's slot, +4/+8 message, +12 block, +16 size\n mov r4,r8\n mov r5,r9\n mov r6,r10\n mov.l K_cueinfo,r1\n jsr @r1\n nop\n mov r0,r12 ; the stock result, returned as it is\n mov.l K_hot,r1 ; no hot cues known\n mov #8,r2\n mov #-1,r3\ncg_clear:\n mov.l r3,@r1\n dt r2\n bf/s cg_clear\n add #8,r1\n mov.l K_hot,r1 ; one more load: entry 0's spare byte counts\n mov.b @(7,r1),r0 ; them (hotcuekeys reloads its slots on it)\n add #1,r0\n mov.b r0,@(7,r1)\n cmp/pl r12\n bf cg_early\n mov.l @r8,r11 ; the connection's socket side\n tst r11,r11\n bt cg_early\n mov.l K_check,r1\n jsr @r1\n mov r11,r4\n tst r0,r0\n bt cg_early\n mov.l K_alloc,r1\n jsr @r1\n mov #72,r4\n tst r0,r0\n bt cg_early\n mov.l r0,@(4,r15)\n mov.l r0,@(8,r15)\n mov #0,r1\n mov.l r1,@r15\n mov.l r1,@(12,r15)\n mov.l r1,@(16,r15)\n mov r8,r4\n mov r0,r5\n mov.l K_type,r6\n mov.l K_init,r1\n jsr @r1\n mov #8,r7 ; as dbcl_GetUsbSdCueInfo\n mov.l @(4,r15),r2\n mov.l r9,@(24,r2) ; the track\n mov #0,r1\n mov.l r1,@(28,r2)\n mov r11,r4\n mov.l @(8,r15),r5\n mov.l K_send,r1\n jsr @r1\n nop\n tst r0,r0\n bf cg_early\n mov r15,r1\n add #12,r1\n mov.l r1,@-r15 ; &block\n mov.l @(12,r15),r4 ; the message\n mov.l K_reply,r5\n mov r15,r6\n add #4,r6\n mov r15,r7\n mov.l K_wait,r1\n jsr @r1\n add #20,r7 ; &size\n add #4,r15\n mov.l @(12,r15),r13 ; the reply's entries\n tst r13,r13\n bt cg_early\n cmp/pz r0\n bf cg_free\n mov.l @(16,r15),r14\n add r13,r14 ; their end\n bra cg_entry\n nop\ncg_early:\n bra cg_out\n nop\ncg_entry:\n mov r13,r1\n add #16,r1\n cmp/hi r14,r1\n bt cg_free ; not even a header left\n mov.l @r13,r11 ; length\n mov #64,r1\n cmp/hs r1,r11\n bf cg_free\n mov r13,r1\n add r11,r1\n cmp/hi r14,r1\n bt cg_free ; runs past the end\n mov.l @(12,r13),r1 ; ms -> 1/150 s: ms * 3 / 20\n mov r1,r2\n add r1,r2\n add r1,r2\n mov.l K_div20,r1\n dmulu.l r1,r2\n sts mach,r9\n shlr2 r9\n shlr2 r9 ; r9 = time\n mov.b @(4,r13),r0\n extu.b r0,r6 ; r6 = hot cue number, 0 = memory cue\n tst r6,r6\n bf cg_hot\n mov #58,r0\n mov.b @(r0,r13),r0 ; colour id\n extu.b r0,r0\n tst r0,r0\n bt cg_next\n mov #8,r1\n cmp/hi r1,r0\n bt cg_next\n add r0,r0\n mov.l K_pal,r1\n add r1,r0\n mov.w @r0,r7\n bra cg_match\n extu.w r7,r7\ncg_hot:\n mov.l K_green,r7 ; no colour: rekordbox's default\n mov #82,r1 ; colour code, R, G, B, 2 bytes: 0x52 at least\n cmp/hs r1,r11\n bf cg_hstore\n mov #72,r0 ; the comment: its length in bytes at +0x48, text from +0x4A,\n mov.w @(r0,r13),r2 ; then 4 bytes, then colour code, R, G, B\n extu.w r2,r2 ; (the server's own entries: not checked against the length)\n add r13,r2\n add #78,r2\n mov.b @r2,r0 ; colour code; 0 = none\n tst r0,r0\n bt cg_hstore\n mov.b @(1,r2),r0 ; R\n extu.b r0,r7\n mov #-8,r0\n and r0,r7\n shll8 r7\n mov.b @(2,r2),r0 ; G\n extu.b r0,r0\n shlr2 r0\n shll2 r0\n shll r0\n shll2 r0\n or r0,r7\n mov.b @(3,r2),r0 ; B\n extu.b r0,r0\n shlr2 r0\n shlr r0\n or r0,r7\ncg_hstore:\n mov #9,r1 ; hot cues 1-8 into our table\n cmp/hs r1,r6\n bt cg_match\n mov r6,r1\n add #-1,r1\n shll2 r1\n shll r1\n mov.l K_hot,r2\n add r1,r2\n mov.l r9,@r2\n mov r7,r0\n mov.w r0,@(4,r2)\n mov r6,r0\n mov.b r0,@(6,r2)\ncg_match:\n ; mark the stock reply's entry with the same hot cue number and time\n mov.l @(24,r10),r1 ; entries\n tst r1,r1\n bt cg_next\n mov.l @(16,r10),r2\n mov.l @(20,r10),r3\n add r3,r2 ; memory + hot cue entries\n mov #16,r3\n cmp/hi r3,r2\n bf cg_m_ok\n mov r3,r2\ncg_m_ok:\n tst r2,r2\n bt cg_next\ncg_m:\n mov.b @(2,r1),r0\n extu.b r0,r0\n cmp/eq r6,r0\n bf cg_m_next\n mov.l @(12,r1),r0\n sub r9,r0 ; time difference, within one unit\n add #1,r0\n mov #2,r3\n cmp/hi r3,r0\n bt cg_m_next\n mov r7,r0\n mov.w r0,@(16,r1)\n mov r1,r3\n add #18,r3\n mov #1,r0\n mov.b r0,@r3\ncg_m_next:\n dt r2\n bf/s cg_m\n add #36,r1\ncg_next:\n bra cg_entry\n add r11,r13\ncg_free:\n mov.l @(12,r15),r4\n tst r4,r4\n bt cg_out\n mov.l K_free,r1\n jsr @r1\n nop\ncg_out:\n mov r12,r0\n add #20,r15\n lds.l @r15+,pr\n mov.l @r15+,r14\n mov.l @r15+,r13\n mov.l @r15+,r12\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n rts\n mov.l @r15+,r8\n\n .align 4\nK_alloc: .long 0x089A791E\nK_check: .long 0x089736F2\nK_cueinfo: .long CUEINFO\nK_div20: .long 0xCCCCCCCD\nK_free: .long 0x08A5EBC2\nK_green: .long HOT_DEFAULT\nK_hot: .long HOT_TABLE\nK_init: .long 0x08973C50\nK_pal: .long PALETTE\nK_reply: .long 0x00004E02\nK_send: .long 0x089738F0\nK_type: .long 0x00002B04\nK_wait: .long 0x08974198\n\n; ---- colour_for: r4 time (1/150 s), r5 tolerance -> r0 RGB565 or -1, r1 slot\ncolour_for:\n mov.l L_memlist,r1\n mov #0,r2 ; slot\ncf_loop:\n mov.l @r1,r0\n cmp/eq #-1,r0\n bt cf_hot\n mov #18,r0\n mov.b @(r0,r1),r0\n cmp/eq #1,r0\n bf cf_skip\n mov.l @(12,r1),r0\n sub r4,r0\n add r5,r0\n mov r5,r3\n add r5,r3\n cmp/hi r3,r0\n bt cf_skip\n mov.w @(16,r1),r0\n extu.w r0,r0\n rts\n mov r2,r1\ncf_skip:\n add #1,r2\n mov #10,r0\n cmp/hs r0,r2\n bf/s cf_loop\n add #36,r1\ncf_hot:\n mov.l L_hot,r1 ; then the hot cues: slots 10-17\n mov #10,r2\ncf_hloop:\n mov.l @r1,r0\n cmp/eq #-1,r0\n bt cf_hskip\n sub r4,r0\n add r5,r0\n mov r5,r3\n add r5,r3\n cmp/hi r3,r0\n bt cf_hskip\n mov.w @(4,r1),r0\n extu.w r0,r0\n rts\n mov r2,r1\ncf_hskip:\n add #1,r2\n mov #18,r0\n cmp/hs r0,r2\n bf/s cf_hloop\n add #8,r1\n rts\n mov #-1,r0\n\n; ---- make_tri: r4 slot, r5 RGB565 -> r0 its triangle (uncached address)\nmake_tri:\n mov r4,r0\n shll2 r0\n add r4,r0\n shll2 r0\n shll2 r0\n shll r0 ; slot * 160\n mov.l L_tribuf,r1\n add r0,r1 ; destination, through P2\n mov r1,r6\n mov #10,r0\n cmp/hs r0,r4\n bt mt_box\n mov.l L_tripix,r2\n mov.l L_red,r3\n mov #80,r7\nmt_loop:\n mov.w @r2+,r0\n extu.w r0,r0\n cmp/eq r3,r0\n bf mt_keep\n mov r5,r0\nmt_keep:\n mov.w r0,@r1\n dt r7\n bf/s mt_loop\n add #2,r1\n rts\n mov r6,r0\nmt_box:\n mov r4,r0 ; the letter, left-aligned in a word\n add #-10,r0\n shll2 r0\n mov.l L_font,r2\n mov.l @(r0,r2),r3\n mov.l L_key,r2 ; transparent\n mov #5,r7\nmb_row:\n mov #0,r4\nmb_col:\n mov #9,r0\n cmp/hs r0,r4\n bt mb_t ; past the marker\n mov #3,r0\n cmp/hs r0,r4\n bf mb_c\n mov #6,r0\n cmp/hs r0,r4\n bt mb_c\n shll r3 ; the letter's next pixel into T\n bf mb_c\n bra mb_st\n mov #0,r0 ; black\nmb_c:\n bra mb_st\n mov r5,r0\nmb_t:\n mov r2,r0\nmb_st:\n mov.w r0,@r1\n add #2,r1\n add #1,r4\n mov #16,r0\n cmp/hs r0,r4\n bf mb_col\n dt r7\n bf mb_row\n rts\n mov r6,r0\n\n; ---- mk_copy: memcpy(r4, r5, r6) for the waveform preparer; filling the\n; marker records, it adds the hot cues to them as memory-cue markers.\nmk_copy:\n mov.l r8,@-r15\n sts.l pr,@-r15\n mov r4,r8\n mov.l L_memcpy,r1\n jsr @r1\n nop\n mov.l L_mkrecs,r1\n cmp/eq r1,r8\n bf mk_out\n mov.l r0,@-r15 ; memcpy's result\n mov r8,r1\n mov #101,r2\n mov #-1,r3\n extu.b r3,r3 ; 0xff ends the list\nmk_find:\n mov.l @r1,r0\n cmp/eq r3,r0\n bt mk_add\n dt r2\n bf/s mk_find\n add #16,r1\n bra mk_done\n nop\nmk_add:\n mov.l L_hot,r3\n mov #8,r7\nmk_h:\n mov.l @r3,r0 ; time\n cmp/eq #-1,r0\n bt mk_hn\n tst r2,r2\n bt mk_done ; the list is full\n mov.l r0,@(8,r1)\n mov #0,r0\n mov.l r0,@r1 ; a memory cue\n mov.l r0,@(4,r1)\n mov #-1,r0\n mov.l r0,@(12,r1)\n add #16,r1\n add #-1,r2\nmk_hn:\n dt r7\n bf/s mk_h\n add #8,r3\nmk_done:\n mov.l @r15+,r0\nmk_out:\n lds.l @r15+,pr\n rts\n mov.l @r15+,r8\n\n; ---- det_res: the resource resolver, for the detail waveform (r9 = its column).\n; Every 0xC5 it is asked for becomes a 16 x 8 marker of ours, as the renderer\n; (patched) copies 11 x 8: a memory cue's triangle in rows 3-7 (coloured, or\n; the stock red one), a hot cue's box with its letter in rows 0-7.\ndet_res:\n mov.l L_resolve,r1\n mov.l L_triid,r0\n cmp/eq r0,r4\n bt dr_mine\n jmp @r1\n nop\ndr_mine:\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n sts.l pr,@-r15\n mov r9,r8\n jsr @r1\n nop\n mov r0,r10 ; the record\n tst r10,r10\n bt dr_out\n mov r8,r4\n bsr colour_for\n mov #16,r5 ; zoomed out, a column stands for up to 16\n mov r1,r9 ; slot\n cmp/eq #-1,r0\n bt dr_plain\n mov r0,r5\n mov r9,r4\n mov #10,r2\n cmp/hs r2,r4\n bt dr_hot\n bsr make_tri\n nop\n mov #3,r11 ; triangle: rows 3-7\n bra dr_copy\n mov #5,r6\ndr_hot:\n bsr make_hotbox\n nop\n mov #0,r11 ; box: rows 0-7\n bra dr_copy\n mov #8,r6\ndr_plain:\n mov #18,r9 ; no colour: the stock triangle\n mov.l L_tripix,r0\n mov #3,r11\n mov #5,r6\ndr_copy:\n mov r0,r3 ; source, 16 pixels a line\n mov.l L_detbuf,r2\n mov r9,r0\n shll8 r0\n add r0,r2\n mov r2,r8 ; this slot's 16 x 8\n mov.l L_key,r0\n mov #64,r7\n shll r7\n mov r2,r1\ndr_fill:\n mov.w r0,@r1\n dt r7\n bf/s dr_fill\n add #2,r1\n mov r11,r0\n shll2 r0\n shll2 r0\n shll r0\n add r0,r2\ndr_row:\n mov #16,r7\ndr_px:\n mov.w @r3+,r0\n mov.w r0,@r2\n dt r7\n bf/s dr_px\n add #2,r2\n dt r6\n bf dr_row\n mov.l L_fake,r2 ; the record, with our size and pixels\n mov r10,r3\n mov #11,r4\ndr_rec:\n mov.l @r3+,r1\n mov.l r1,@r2\n dt r4\n bf/s dr_rec\n add #4,r2\n add #-44,r2\n mov.l L_detsize,r1\n mov.l r1,@(4,r2)\n mov.l r8,@(12,r2)\n mov r2,r10\ndr_out:\n mov r10,r0\n lds.l @r15+,pr\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n rts\n mov.l @r15+,r8\n\n; ---- ov_draw: draw_bitmap(r4, r5 x, r6 y, r7 resource) for the overview (r13 = its record)\nov_draw:\n mov.l L_draw,r1\n mov.l L_triid,r0\n cmp/eq r0,r7\n bt od_mine\n jmp @r1\n nop\nod_mine:\n mov.l r13,@-r15\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n mov.l r12,@-r15\n mov.l r14,@-r15\n sts.l pr,@-r15\n mov r4,r8\n mov r5,r9\n mov r6,r10\n mov r7,r11\n mov.l L_ovrecs,r1 ; which record: 12 bytes each, 16 in the other list\n mov r13,r2\n sub r1,r2\n mov #0,r3\n mov #101,r4\nod_idx:\n cmp/pl r2\n bf od_found\n add #-12,r2\n dt r4\n bf/s od_idx\n add #16,r3\n bra od_plain\n nop\nod_found:\n tst r2,r2\n bf od_plain ; not on a record\n mov.l L_mkrecs,r1\n add r3,r1\n mov.l @(8,r1),r4\n bsr colour_for\n mov #0,r5\n cmp/eq #-1,r0\n bt od_plain\n mov r1,r4\n mov #10,r2\n cmp/hs r2,r4\n bt od_hot\n bsr make_tri\n mov r0,r5\n mov.l L_trisize,r13 ; 16 x 5, the triangle's own size\n bra od_pix\n nop\nod_hot:\n bsr make_hotbox ; a hot cue: the bigger box\n mov r0,r5\n mov.l L_boxsize,r13\nod_pix:\n mov.l L_restab,r1 ; the triangle's record\n mov.l @r1,r12\n mov.l L_trirec,r1\n add r1,r12\n mov.l @(32,r12),r14 ; its pixel offset and size, put back below\n mov.l @(4,r12),r1\n mov.l r13,@(4,r12)\n mov r1,r13\n mov.l L_p0mask,r1 ; the blitter gets the plain address\n and r1,r0\n mov.l L_imgbase,r1\n sub r1,r0\n mov.l r0,@(32,r12)\n mov r8,r4\n mov r9,r5\n mov r10,r6\n mov.l L_draw,r1\n jsr @r1\n mov r11,r7\n mov.l r13,@(4,r12)\n bra od_out\n mov.l r14,@(32,r12)\nod_plain:\n mov r8,r4\n mov r9,r5\n mov r10,r6\n mov.l L_draw,r1\n jsr @r1\n mov r11,r7\nod_out:\n lds.l @r15+,pr\n mov.l @r15+,r14\n mov.l @r15+,r12\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n mov.l @r15+,r8\n rts\n mov.l @r15+,r13\n\n .align 4\nL_cueinfo: .long CUEINFO\nL_check: .long 0x089736F2\nL_alloc: .long 0x089A791E\nL_init: .long 0x08973C50\nL_send: .long 0x089738F0\nL_wait: .long 0x08974198\nL_free: .long 0x08A5EBC2\nL_type: .long 0x00002B04\nL_reply: .long 0x00004E02\nL_div20: .long 0xCCCCCCCD\nL_hot: .long HOT_TABLE\nL_memlist: .long MEMLIST\nL_tribuf: .long TRI_BUF_P2\nL_tripix: .long TRI_PIXELS\nL_red: .long TRI_RED\nL_resolve: .long RESOLVE\nL_draw: .long DRAW\nL_triid: .long TRI_ID\nL_fake: .long FAKE_RECS\nL_detbuf: .long DET_BUF\nL_detsize: .long 0x00080010\nL_ovrecs: .long OV_RECS\nL_mkrecs: .long MK_RECS\nL_restab: .long RES_TABLE_PTR\nL_trirec: .long TRI_REC_OFF\nL_imgbase: .long 0x08100000\nL_p0mask: .long 0x1FFFFFFF\nL_pal: .long PALETTE\nL_green: .long HOT_DEFAULT\nL_font: .long FONT\nL_key: .long 0xF81F\nL_memcpy: .long MEMCPY\nL_trisize: .long 0x00050010\nL_boxsize: .long BOXSIZE\n\n; ---- make_hotbox: r4 slot (10-17), r5 RGB565 -> r0 the overview's hot cue\n; marker, 11 x 9 in its colour with the letter in black (uncached address)\nmake_hotbox:\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov r4,r6\n add #-10,r6 ; 0 = A\n mov r6,r1 ; HOTBOX_SIZE (288) * index\n shll8 r1\n mov r6,r2\n shll2 r2\n shll2 r2\n shll r2\n add r2,r1\n mov.l M_hotbox,r2\n add r2,r1\n mov r1,r9 ; returned\n mov r6,r2 ; its letter: 7 bytes from FONT5 + 7 * index\n shll2 r2\n shll r2\n sub r6,r2\n mov.l M_font5,r3\n add r2,r3\n mov.l M_key,r2\n mov #0,r6 ; row\nhb_row:\n mov #0,r4 ; the letter's bits in this row\n mov r6,r0\n cmp/eq #0,r0\n bt hb_cols\n cmp/eq #8,r0\n bt hb_cols\n add #-1,r0\n mov.b @(r0,r3),r4\n extu.b r4,r4\nhb_cols:\n mov #0,r8 ; column\nhb_col:\n mov #11,r0\n cmp/hs r0,r8\n bt hb_t\n mov #3,r0\n cmp/hs r0,r8\n bf hb_c\n mov #8,r0\n cmp/hs r0,r8\n bt hb_c\n mov r4,r0\n tst #128,r0\n bt/s hb_c\n shll r4\n bra hb_st\n mov #0,r0 ; black\nhb_c:\n bra hb_st\n mov r5,r0\nhb_t:\n mov r2,r0\nhb_st:\n mov.w r0,@r1\n add #2,r1\n add #1,r8\n mov #16,r0\n cmp/hs r0,r8\n bf hb_col\n add #1,r6\n mov #9,r0\n cmp/hs r0,r6\n bf hb_row\n mov r9,r0\n mov.l @r15+,r9\n rts\n mov.l @r15+,r8\n\n .align 4\nM_hotbox: .long HOTBOX_P2\nM_font5: .long FONT5\nM_key: .long 0xF81F\nFONT:\n'''\n\n\ndef routine():\n \"\"\"The code at CC_BASE, the memory-cue palette after it, and the labels.\"\"\"\n syms = {\n 'CUEINFO': CUEINFO, 'RESOLVE': RESOLVE, 'DRAW': DRAW, 'HOT_TABLE': HOT_TABLE,\n 'MEMLIST': MEMLIST, 'TRI_BUF_P2': TRI_BUF | 0xA0000000, 'TRI_PIXELS': TRI_PIXELS,\n 'TRI_RED': TRI_RED, 'TRI_ID': TRI_ID, 'FAKE_RECS': FAKE_RECS, 'OV_RECS': OV_RECS,\n 'MK_RECS': MK_RECS, 'RES_TABLE_PTR': RES_TABLE_PTR, 'TRI_REC_OFF': TRI_ID * 44,\n 'HOT_DEFAULT': HOT_DEFAULT, 'MEMCPY': MEMCPY, 'HOTBOX_P2': HOTBOX | 0xA0000000,\n 'BOXSIZE': HOTBOX_W | HOTBOX_H << 16, 'DET_BUF': DET_BUF,\n }\n font = b''.join(struct.pack(' r0 the named cue to show (CN_TABLE entry) or 0; r1-r7 used\ncn_pick:\n mov.l P_hot,r6\n mov.b @(7,r6),r0 ; the table is this load's?\n mov.l P_load,r1\n mov.b @r1,r1\n cmp/eq r1,r0\n bf pk_none\n mov.l P_count,r1\n mov.l @r1,r3\n tst r3,r3\n bt pk_none\n mov.l P_pos,r1\n mov.l @r1,r4\n mov.l P_behind,r1\n sub r1,r4 ; from a second behind\n mov.l P_table,r2\n mov #0,r0 ; best so far\npk_loop:\n mov.l @r2,r1 ; ms\n cmp/ge r4,r1\n bf pk_next ; behind\n mov.l @(8,r2),r5 ; a hot cue still where it was?\n tst r5,r5\n bt pk_ok\n mov #9,r7\n cmp/hs r7,r5\n bt pk_next\n add #-1,r5\n shll2 r5\n shll r5\n add r6,r5\n mov.l @r5,r5 ; HOT_TABLE's time, 1/150 s\n mov.l @(4,r2),r7\n sub r7,r5\n add #1,r5\n mov #2,r7\n cmp/hi r7,r5\n bt pk_next\npk_ok:\n tst r0,r0\n bt pk_take\n mov.l @r0,r7\n cmp/ge r7,r1\n bt pk_next ; not nearer\npk_take:\n mov r2,r0\npk_next:\n dt r3\n bf/s pk_loop\n add #64,r2\n rts\n nop\npk_none:\n rts\n mov #0,r0\n\n; ---- cn_draw: the text widget's draw method (r4 renderer, r5 widget)\ncn_draw:\n mov.l r8,@-r15\n mov.l r9,@-r15\n mov.l r10,@-r15\n mov.l r11,@-r15\n sts.l pr,@-r15\n mov r4,r8\n mov r5,r9\n mov.l @(4,r5),r1 ; the title?\n mov.l D_title,r2\n cmp/eq r2,r1\n bf dr_plain\n mov.l D_depth,r1 ; BROWSE closed?\n mov.b @r1,r1\n tst r1,r1\n bf dr_plain\n mov.l @(40,r9),r1 ; the title white? (dimmed behind a popup, as MENU's: no name)\n mov #-1,r2\n cmp/eq r2,r1\n bf dr_plain\n bsr cn_pick\n nop\n tst r0,r0\n bt dr_plain\n mov r0,r10 ; the cue\n mov.l D_label,r2 ; the label: \"A \" for a hot cue, then the name\n mov.l @(8,r10),r1\n tst r1,r1\n bt dr_name\n add #64,r1 ; 'A' - 1 + number\n mov r1,r0\n mov.w r0,@r2\n mov #32,r0\n mov.w r0,@(2,r2)\n add #4,r2\ndr_name:\n mov r10,r1\n add #16,r1\ndr_ncopy:\n mov.w @r1+,r0\n mov.w r0,@r2\n tst r0,r0\n bf/s dr_ncopy\n add #2,r2\n mov.l @(28,r9),r11 ; the title, narrowed\n mov r11,r1\n mov.l D_hmask,r2\n and r2,r1\n mov.l D_tw,r2\n or r2,r1\n mov.l r1,@(28,r9)\n mov r8,r4\n mov r9,r5\n mov.l D_next,r1\n jsr @r1\n nop\n mov.l r11,@(28,r9)\n mov.l D_clone,r2 ; the copy: our place, text, colour\n mov r9,r1\n mov #24,r3\ndr_clone:\n mov.l @r1+,r0\n mov.l r0,@r2\n dt r3\n bf/s dr_clone\n add #4,r2\n mov.l D_clone,r5\n mov.l @(24,r9),r1 ; y stays, x ours\n mov.l D_hmask,r2\n and r2,r1\n mov.l D_lx,r2\n or r2,r1\n mov.l r1,@(24,r5)\n mov r11,r1\n mov.l D_hmask,r2\n and r2,r1\n mov.l D_lw,r2\n or r2,r1\n mov.l r1,@(28,r5)\n mov.l @(12,r10),r1\n mov.l r1,@(40,r5)\n mov.l D_label,r1\n mov.l r1,@(52,r5)\n mov r8,r4\n mov.l D_next,r1\n jsr @r1\n nop\n bra dr_out\n nop\ndr_plain:\n mov r8,r4\n mov r9,r5\n mov.l D_next,r1\n jsr @r1\n nop\ndr_out:\n lds.l @r15+,pr\n mov.l @r15+,r11\n mov.l @r15+,r10\n mov.l @r15+,r9\n rts\n mov.l @r15+,r8\n\n .align 4\nP_behind: .long BEHIND_MS\nP_count: .long CN_COUNT\nP_hot: .long HOT_TABLE\nP_load: .long CN_LOAD\nP_pos: .long POS_MS\nP_table: .long CN_TABLE\nD_clone: .long CN_CLONE\nD_depth: .long BROWSE_DEPTH\nD_hmask: .long 0xFFFF0000\nD_label: .long CN_LABEL\nD_lw: .long LABEL_W\nD_lx: .long LABEL_X\nD_next: .long NEXT_DRAW\nD_title: .long TITLE_ID\nD_tw: .long TITLE_W\nMEMORY_TAB:\n'''\n\n\ndef routine(next_draw):\n src = SOURCE + ''.join(' .long 0x%08X\\n' % c for c in MEMORY_ABGR)\n return sh4asm.assemble(src, CN_CODE, {\n 'CN_COUNT': CN_COUNT, 'CN_LOAD': CN_LOAD, 'CN_TABLE': CN_TABLE, 'CN_LABEL': CN_LABEL,\n 'CN_CLONE': CN_CLONE, 'CN_MAX': CN_MAX, 'CN_NAME': CN_NAME, 'FREE': FREE,\n 'HOT_TABLE': HOT_TABLE, 'POS_MS': POS_MS, 'BROWSE_DEPTH': BROWSE_DEPTH,\n 'TITLE_ID': TITLE_ID, 'TITLE_W': TITLE_W, 'LABEL_X': LABEL_X, 'LABEL_W': LABEL_W,\n 'BEHIND_MS': BEHIND_MS, 'NO_COLOUR': NO_COLOUR, 'NEXT_DRAW': next_draw})\n\n\ndef patch(app, stock):\n \"\"\"The application with CUE NAMES added (after cuecolours and, if chosen,\n keynotation); `stock` is the unmodified 1.31 application.\"\"\"\n out = bytearray(app)\n word = lambda a: struct.unpack_from(' 0xC00487A4)\nTRAMP_TARGET = 0xC00487A4\nMB = 0x1183E000\nDP = 0x11806900\nHEADS = 0x1182E0E0\nLINKS = 0xC1F38B90\nPLAY_HEAD_DP = 969 # byte: the playing head's index\nMARGIN = 750 # slots (10 s) a forward jump leaves decoded ahead (2 s: EMERGENCY LOOP\n # after rapid 8-beat jumps, emulator too; 5 s was fine there)\nBLOCKS = ((0xC0000040, 0xC0000080), (0xC00000A0, 0xC0000100), (0xC0000120, 0xC00001E0))\nFREE_PACKETS = (struct.pack('<8I', *([0x0c6e0c6e] * 7 + [0xefe00000])), # compact nops only\n struct.pack('<8I', *([0x0c6e0c6e] * 6 + [0, 0xe7e00000])))\n\n\ndef code():\n \"\"\"[(addr, word)] of dj_poll.\"\"\"\n a = c6asm.Asm(BLOCKS[0][0])\n o = a.op\n # block 1: the mailbox; e = heads + 30 * h\n o('const', MB, 'a3')\n o('ldw', 'a4', 'a3', 0)\n o('nop', 4)\n o('cmpeq', 1, 'a4', 'a1')\n o('b', 'tail', p='!a1')\n o('nop', 5)\n o('ldw', 'b4', 'a3', 4) # n\n o('lddp', 'ldbu', 'b5', PLAY_HEAD_DP) # h\n o('nop', 4)\n o('shl', 'b5', 5, 'b6')\n o('shl', 'b5', 1, 'b7')\n o('sub', 'b6', 'b7', 'b6') # 30 h\n o('b', 'blk2')\n o('nop', 5)\n # block 2: the head's fields, the direction\n a.org(BLOCKS[1][0])\n a.label('blk2')\n o('const', HEADS, 'b8')\n o('add', 'b8', 'b6', 'b6') # e\n o('ldhu', 'a5', 'b6', 2) # cur\n o('ldh', 'a6', 'b6', 6) # behind\n o('ldh', 'a7', 'b6', 8) # ahead\n o('nop', 4)\n o('stw', 'a5', 'a3', 16)\n o('stw', 'a6', 'a3', 24)\n o('stw', 'a7', 'a3', 28)\n o('mvk', 2, 'a9') # refused unless done\n o('const', LINKS, 'a4')\n o('cmplt', 0, 'b4', 'b0') # n > 0\n o('b', 'fwd', p='b0')\n o('nop', 5)\n o('cmpgt', 0, 'b4', 'b1') # n < 0\n o('b', 'ok', p='!b1') # n = 0\n o('nop', 5)\n o('sub', 0, 'b4', 'b2') # m = -n\n o('mv', 'b2', 'a8')\n o('sub', 'a6', 'a8', 'a8') # behind - m\n o('b', 'blk3')\n o('nop', 5)\n # block 3: the walk\n a.org(BLOCKS[2][0])\n a.label('blk3')\n o('cmplt', 0, 'a8', 'a1') # at least one slot left behind\n o('b', 'done', p='!a1')\n o('nop', 5)\n a.label('loopb')\n o('addaw', 'a4', 'a5', 'a2')\n o('ldhu', 'a5', 'a2', 0) # prev\n o('nop', 4)\n o('add', -1, 'b2', 'b2')\n o('b', 'loopb', p='b2')\n o('nop', 5)\n o('sub', 0, 'b4', 'b7')\n o('mv', 'b7', 'a8') # m\n o('sub', 'a6', 'a8', 'a6') # behind - m\n o('add', 'a8', 'a7', 'a7') # ahead + m\n o('b', 'store')\n o('nop', 5)\n a.label('fwd')\n o('mv', 'b4', 'a8')\n o('sub', 'a7', 'a8', 'a8') # ahead - n\n o('mvk', MARGIN, 'b7')\n o('cmplt', 'b7', 'a8', 'b1') # more than MARGIN left ahead\n o('b', 'done', p='!b1')\n o('nop', 5)\n o('mv', 'b4', 'b2')\n a.label('loopf')\n o('addaw', 'a4', 'a5', 'a2')\n o('ldhu', 'a5', 'a2', 2) # next\n o('nop', 4)\n o('add', -1, 'b2', 'b2')\n o('b', 'loopf', p='b2')\n o('nop', 5)\n o('mv', 'b4', 'a8')\n o('add', 'a8', 'a6', 'a6') # behind + n\n o('sub', 'a7', 'a8', 'a7') # ahead - n\n a.label('store')\n o('sth', 'a5', 'b6', 2)\n o('sth', 'a6', 'b6', 6)\n o('sth', 'a7', 'b6', 8)\n o('sth', 'a5', 'b6', 14) # the point the decoder fills from (set on a\n o('sth', 'a6', 'b6', 16) # pause): moved along, so standing it decodes\n o('sth', 'a7', 'b6', 18) # on from the new place\n o('stw', 'a5', 'a3', 20)\n a.label('ok')\n o('mvk', 1, 'a9')\n a.label('done')\n o('stw', 'a9', 'a3', 8)\n o('ldw', 'a8', 'a3', 12)\n o('nop', 4)\n o('add', 1, 'a8', 'a8')\n o('stw', 'a8', 'a3', 12)\n o('mvk', 0, 'a8')\n o('stw', 'a8', 'a3', 0)\n a.label('tail')\n o('b', TRAMP_TARGET)\n o('nop', 5)\n words = a.assemble()\n for at, _ in words:\n assert any(lo <= at < hi for lo, hi in BLOCKS), 'dspjump: 0x%08x is past its blocks' % at\n return words\n\n\ndef packets(words):\n \"\"\"{packet addr: 8 words}: our words, the rest of each packet nop.\"\"\"\n out = {}\n for at, w in words:\n fp = at & ~31\n out.setdefault(fp, [0] * 8)[(at - fp) // 4] = w\n return out\n\n\ndef set_cst16(word, value):\n return (word & ~(0xFFFF << 7)) | (value & 0xFFFF) << 7\n\n\ndef patch(app, stock):\n out = bytearray(app)\n for fp, ws in packets(code()).items():\n off = B_IMG - BASE + (fp - 0xC0000000)\n if stock[off:off + 32] not in FREE_PACKETS or out[off:off + 32] != stock[off:off + 32]:\n raise SystemExit('patch_cdj900nxs: dspjump: the vector packet 0x%08x is not the expected one' % fp)\n out[off:off + 32] = struct.pack('<8I', *ws)\n entry = BLOCKS[0][0]\n for at, want, value in ((TRAMP_MVK, 0x0543d22a, entry), (TRAMP_MVKH, 0x0560026a, entry >> 16)):\n off = A_IMG - BASE + (at - A_AT)\n w = struct.unpack_from(' red, grey -> dark red\n\nSOURCE = r'''\n; ---- ew_draw: draw_bitmap(r4, r5 x, r6 y, r7 resource) for the playing-address bar\new_draw:\n mov.l E_draw,r0\n mov.l E_res,r1\n cmp/eq r1,r7\n bt ew_bar\n jmp @r0\n nop\new_bar:\n mov.l E_len,r1 ; length in ms: 1/150 s * 20 / 3\n mov.l @r1,r1\n tst r1,r1\n bt ew_plain\n mov r1,r2\n shll2 r2\n shll2 r2\n shll2 r1\n add r2,r1 ; * 20\n mov.l E_div3,r2\n dmulu.l r2,r1\n sts mach,r1\n shlr r1 ; / 3\n mov.l E_pos,r2\n mov.l @r2,r2\n sub r2,r1 ; left\n cmp/pl r1\n bf ew_plain\n mov.l E_warn,r2\n cmp/hs r2,r1\n bt ew_plain\n mov.l r8,@-r15 ; red: the record pointed at RED_BAR for this call\n mov.l r9,@-r15\n sts.l pr,@-r15\n mov.l E_tab,r8\n mov.l @r8,r8\n mov.l E_recoff,r1\n add r1,r8\n mov.l @(32,r8),r9\n mov.l E_redoff,r1\n mov.l r1,@(32,r8)\n jsr @r0\n nop\n mov.l r9,@(32,r8)\n lds.l @r15+,pr\n mov.l @r15+,r9\n rts\n mov.l @r15+,r8\new_plain:\n jmp @r0\n nop\n\n .align 4\nE_div3: .long 0xAAAAAAAB\nE_draw: .long DRAW\nE_len: .long TRACK_LEN\nE_pos: .long POS_MS\nE_recoff: .long REC_OFF\nE_redoff: .long RED_OFF\nE_res: .long RES_BAR\nE_tab: .long RES_TABLE_PTR\nE_warn: .long WARN_MS\n'''\n\n\ndef routine():\n return sh4asm.assemble(SOURCE, EW_CODE, {\n 'DRAW': DRAW, 'TRACK_LEN': TRACK_LEN, 'POS_MS': POS_MS, 'REC_OFF': RES_BAR * 44,\n 'RED_OFF': RED_BAR - RES_BASE, 'RES_BAR': RES_BAR, 'RES_TABLE_PTR': RES_TABLE_PTR,\n 'WARN_MS': WARN_MS})\n\n\ndef patch(app, stock):\n \"\"\"The application with END WARNING added; `stock` is the unmodified 1.31\n application, which every replaced byte is checked against.\"\"\"\n out = bytearray(app)\n word = lambda buf, a: struct.unpack_from('quicksearch usbautoload wide_waveform + bpmhalfdouble
Hot cue keys
The hot key mode Loading @@ -219,8 +220,8 @@
UTILITY with KEY NOTATION
UTILITY › KEY NOTATION.

credit Pionaarr mod

-

The boot screen says Pionaarr Mods 1.9.0 where it said Ver 1.31. Ticked at the start; untick it to keep the stock text. UTILITY and the LINK screen still show the real version, 1.31.

-
Boot screen with Pionaarr Mods 1.9.0
The boot screen.
+

The boot screen says Pionaarr Mods 1.10.0 where it said Ver 1.31. Ticked at the start; untick it to keep the stock text. UTILITY and the LINK screen still show the real version, 1.31.

+
Boot screen with Pionaarr Mods 1.10.0
The boot screen.

cuenames Pionaarr mod experimental

The comment a memory cue or hot cue has in rekordbox (the cue list's comment field, exported with the stick) appears in the title bar, right of the title, in the cue's colour: always the next cue ahead that has one, a hot cue with its letter ("A DROP"). A cue just passed shows for a second more, so a cue you CALL shows its name while you stand on it. Hidden while BROWSE is open; a long title is cut short before the name. Needs cuecolours.

@@ -246,6 +247,12 @@
The waveform to the right edge, a ZOOM box above it
ZOOM: the waveform to the edge, the drop ahead already in sight.
Grid lines over the whole width, a GRID box above
GRID: beat grid adjust, the box says so.
+

bpmhalfdouble Pionaarr mod experimental

+

Doubles or halves the loaded track's BPM and beat grid from the deck, as rekordbox's grid editor does: a track analysed at 85 BPM becomes 170 BPM to mix with one at 175. Hold TAG TRACK/REMOVE and press CALL ◀ for ÷2 or CALL ▶ for ×2. TAG TRACK pressed alone tags as before.

+

The deck must stand (paused or at cue): the track is loaded again with its new grid, so BPM display, beat ticks, bar counters, quantize, beat jump and the BPM sent over Pro DJ Link all follow. It stands at its cue point afterwards. The title bar says what was done (“BPM ×2 → 170.00 BPM”); as in rekordbox, a result under 60 or over 240 BPM is refused (“×2 makes no sense”). ×2 after ÷2 gives the original grid back exactly.

+

The change is kept on the USB stick, in PIONEER/PIONAARR.BPM (rekordbox's own files are not touched), and is there again the next time the stick is used in a modded player. Tracks from another player's stick over Pro DJ Link keep it until this player is switched off. Needs quicksearch, which loads the track again.

+
145 BPM before, 72.50 BPM after
÷2: 145 BPM becomes 72.50 BPM.
+

Still to come: see Upcoming.

@@ -267,6 +274,7 @@ playedPionaarr modexperimentalran on a player endwarnPionaarr modexperimentalran on a player wide_waveformPionaarr modexperimentalemulator only + bpmhalfdoublePionaarr modexperimentalemulator only quicksearchPionaarr modexperimentalran on a player usbautoloadPionaarr modexperimentalran on a player @@ -283,7 +291,7 @@

Do I need rekordbox?

Cue colours and hot cues come from a USB stick exported with rekordbox. Without them the mods show what the stock player shows.

Why is the version still 1.31?

-

The update file says 1.32 so the player installs it; the application inside still reports 1.31 (in UTILITY and on the LINK screen). Only the boot screen says Pionaarr Mods 1.9.0 instead of Ver 1.31.

+

The update file says 1.32 so the player installs it; the application inside still reports 1.31 (in UTILITY and on the LINK screen). Only the boot screen says Pionaarr Mods 1.10.0 instead of Ver 1.31.

diff --git a/impressum.html b/impressum.html index 5c293c0..2948c2a 100644 --- a/impressum.html +++ b/impressum.html @@ -6,9 +6,9 @@ Impressum · Pionaarr Mods - - - + + + diff --git a/index.html b/index.html index d67dd5a..a57e6c7 100644 --- a/index.html +++ b/index.html @@ -6,9 +6,9 @@ Pionaarr Mods - - - + + + @@ -169,6 +169,18 @@ More → + +
+
+

BPM ×2 / ÷2

+
experimental
+
+
Pionaarr mod · bpmhalfdouble
+
145 BPM halved to 72.50 BPM
+

TAG TRACK held + CALL ◀ / ▶ halves or doubles the track's BPM and grid, kept on the USB.

+ More → +
+
@@ -244,8 +256,8 @@
stable
Pionaarr mod · credit
-
Boot screen: Pionaarr Mods 1.9.0
-

The boot screen says Pionaarr Mods 1.9.0.

+
Boot screen: Pionaarr Mods 1.10.0
+

The boot screen says Pionaarr Mods 1.10.0.

diff --git a/issues.html b/issues.html index 9dc0442..cc5b3d3 100644 --- a/issues.html +++ b/issues.html @@ -6,9 +6,9 @@ Pionaarr Mods Known Issues - - - + + + diff --git a/tools/changelog.json b/tools/changelog.json index d75c452..2306fe1 100644 --- a/tools/changelog.json +++ b/tools/changelog.json @@ -1,6 +1,13 @@ { "_about": "The site's changelog, newest first. Versions follow Semantic Versioning (MAJOR.MINOR.PATCH), dates ISO 8601 (DIN EN 28601). make_builder_data.py adds every mod's current version from its module and writes assets/versions.js.", "site": [ + { + "version": "1.10.0", + "date": "2026-10-21", + "changes": [ + "New mod: BPM ×2 / ÷2 (bpmhalfdouble), the track's BPM and beat grid doubled or halved from the deck, kept on the USB stick." + ] + }, { "version": "1.9.0", "date": "2026-10-21", @@ -389,6 +396,15 @@ "The big waveform 624 columns wide instead of 536, to the right edge; the extra columns are ahead of the playing position. The ZOOM/GRID wheel replaced by a grey ZOOM / GRID box over the BPM display." ] } + ], + "bpmhalfdouble": [ + { + "version": "0.2.0", + "date": "2026-10-21", + "changes": [ + "TAG TRACK held + CALL ◀ / ▶ halves or doubles the loaded track's BPM and beat grid; the track is loaded again with it, so display, beat ticks, bars, quantize and Pro DJ Link follow. Kept on the USB stick in PIONEER/PIONAARR.BPM. Results under 60 or over 240 BPM refused." + ] + } ] } } diff --git a/tools/make_builder_data.py b/tools/make_builder_data.py index a51c563..252c8c5 100644 --- a/tools/make_builder_data.py +++ b/tools/make_builder_data.py @@ -140,7 +140,7 @@ def apply_ops(app, stock, ops, hk, size): MODS_ALL = ('extended_bpm', 'phasemeter', 'advanced_waveforms', 'beatjump', 'cuecolours', 'hotcuekeys', - 'ledsaver', 'keynotation', 'usbautoload', 'quicksearch', 'cuenames', 'played', 'endwarn', 'wide_waveform', 'credit') + 'ledsaver', 'keynotation', 'usbautoload', 'quicksearch', 'cuenames', 'played', 'endwarn', 'wide_waveform', 'bpmhalfdouble', 'credit') def python_bundle(repo): diff --git a/upcoming.html b/upcoming.html index bdeb6c9..a1d1231 100644 --- a/upcoming.html +++ b/upcoming.html @@ -6,9 +6,9 @@ Pionaarr Mods Upcoming - - - + + +