Site 1.1.0: beat jump in the builder, version and changelog, key colours

- Beat jump (experimental) on the mods page, in the docs and in the builder
  (needs hot cue keys; dependencies are ticked and unticked as a chain).
- Key notation 1.2.0: keys in their Camelot wheel colour, also in BROWSE.
- Version badge at the top with a changelog; every mod's version on its
  card and in the builder. make_builder_data.py writes assets/versions.js
  from tools/changelog.json and the mods' versions and stamps the pages'
  CSS and script links with ?v=<site version>.
- The generator keeps a changed copy of the UTILITY table as the user's
  own table plus our bytes (beat jump renames an item).
- Upcoming: quick search, USB auto load and phase meter reverse (in testing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Thiemo TizianiandClaude Opus 5.5 committed 2026-10-20 02:00:48 +02:00
1 parent e4728b16ea
commit d2d1518fba
16 files changed
+440 -72

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
{
"_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.1.0",
"date": "2026-10-20",
"changes": [
"Beat jump is in the builder (experimental): TRACK SEARCH jumps 1/2/4/8 beats, gapless, in phase.",
"Key notation 1.2.0: the key in its Camelot wheel colour, also in BROWSE › KEY and INFO; lists of keys converted too.",
"Hot cue keys 1.2.0: hot cues load in the background, the deck's keys are locked and HOT KEYS blinks LOADING meanwhile; search and slip no longer upset after a load.",
"Version shown at the top, with this changelog; every mod has its own version.",
"Upcoming: quick search, USB auto load and a reversed phase meter."
]
},
{
"version": "1.0.0",
"date": "2026-10-19",
"changes": [
"First release of the site: the mods, the in-browser builder, docs and upcoming."
]
}
],
"mods": {
"beatjump": [
{"version": "1.0.0", "date": "2026-10-20", "changes": ["Jumps in the DSP's decoded audio, playing, paused or at cue standby; presses the DSP cannot jump yet are ignored, so no EMERGENCY LOOP."]}
],
"hotcuekeys": [
{"version": "1.2.0", "date": "2026-10-20", "changes": ["Background loading invisible; keys locked and LOADING blinking while a track loads; no more interference with search and slip."]},
{"version": "1.1.0", "date": "2026-10-19", "changes": ["Hot cue engine: instant hot cues from the DSP's slots, saved on the USB in rekordbox colours."]},
{"version": "1.0.0", "date": "2026-10-18", "changes": ["Hot cues A-C on the BEAT DIVIDE keys: play, gate, set, delete."]}
],
"keynotation": [
{"version": "1.2.0", "date": "2026-10-20", "changes": ["Keys in their notation and colour everywhere on the screen, BROWSE › KEY included; the SEARCH input is left alone."]},
{"version": "1.1.0", "date": "2026-10-19", "changes": ["The key in its Camelot wheel colour in the title bar."]},
{"version": "1.0.0", "date": "2026-10-19", "changes": ["Camelot, Open Key or classic notation, UTILITY › KEY NOTATION."]}
]
}
}
+72 -16
View File
@@ -4,13 +4,14 @@
usage: make_builder_data.py [--repo ../CDJ900NXSVirtual]
Our mods (cuecolours, hotcuekeys, ledsaver, credit) are made by the emulator
Our mods (beatjump, cuecolours, hotcuekeys, ledsaver, keynotation, credit) are made by the emulator
checkout's mods/patch_cdj900nxs.py for every combination the builder offers;
their bytes do not depend on which of arnoldasr's mods are chosen (checked
below). For each combination the bytes our mods change in the application
go into data.js, except what is the player's own firmware:
copy the UTILITY item table (hotcuekeys and ledsaver extend a copy of it)
copy the UTILITY item table (hotcuekeys, ledsaver and keynotation extend a
copy of it)
is copied from the user's own application in the browser;
box the HOT KEYS box is the QUANTIZE label's image recoloured with
HOT KEYS written on it; the browser makes it from the user's image.
@@ -38,8 +39,8 @@ import sys
HERE = os.path.dirname(os.path.abspath(__file__))
WEB = os.path.dirname(HERE)
OURS = ('cuecolours', 'hotcuekeys', 'ledsaver', 'credit')
NEEDS = {'hotcuekeys': 'cuecolours'}
OURS = ('beatjump', 'cuecolours', 'hotcuekeys', 'ledsaver', 'keynotation', 'credit')
NEEDS = {'hotcuekeys': 'cuecolours', 'beatjump': 'hotcuekeys'}
UT_TABLE_STOCK = 0x08C6C8DC
UT_TABLE_LEN = 17 * 52
BASE = 0x08000000
@@ -71,6 +72,17 @@ def runs(base, final):
return out
def spans(idx):
"""[(lo, hi)] of the runs of consecutive numbers in idx (sorted)."""
out = []
for i in idx:
if out and out[-1][1] == i:
out[-1] = (out[-1][0], i + 1)
else:
out.append((i, i + 1))
return out
def ops_for(stock, final, boxes):
"""The browser's operations for one combination: data, copy, box."""
table = stock[UT_TABLE_STOCK - BASE:UT_TABLE_STOCK - BASE + UT_TABLE_LEN]
@@ -81,18 +93,28 @@ def ops_for(stock, final, boxes):
off = box['addr'] - BASE
masked[off:off + box['len']] = padded[off:off + box['len']]
for addr, data in runs(stock, bytes(masked)):
cuts = [] # (start, end, op) inside this run
k = data.find(table)
# (start, end, ops) inside this run: a copy of the table, a few of its
# items changed (beatjump renames SCREEN SAVER), is the copy and then
# our bytes over it
cuts = []
k = data.find(table[:52])
while k >= 0:
cuts.append((k, k + UT_TABLE_LEN, {'op': 'copy', 'addr': addr + k, 'src': UT_TABLE_STOCK,
'len': UT_TABLE_LEN}))
k = data.find(table, k + UT_TABLE_LEN)
seg = data[k:k + UT_TABLE_LEN]
diff = [i for i in range(len(seg)) if seg[i] != table[i]] if len(seg) == UT_TABLE_LEN else None
if diff is not None and len(diff) <= 2 * 52:
ops = [{'op': 'copy', 'addr': addr + k, 'src': UT_TABLE_STOCK, 'len': UT_TABLE_LEN}]
for lo, hi in spans(diff):
ops.append({'op': 'data', 'addr': addr + k + lo, 'data': seg[lo:hi]})
cuts.append((k, k + UT_TABLE_LEN, ops))
k = data.find(table[:52], k + UT_TABLE_LEN)
else:
k = data.find(table[:52], k + 1)
cuts.sort(key=lambda c: c[0])
pos = 0
for lo, hi, op in cuts:
for lo, hi, ops in cuts:
if lo > pos:
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:lo]})
out.append(op)
out.extend(ops)
pos = hi
if pos < len(data):
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:]})
@@ -112,7 +134,7 @@ def apply_ops(app, stock, ops, hk, size):
elif o['op'] == 'assets':
out[off:off + o['len']] = ASSETS(stock)[0][:o['len']]
else:
_, _, px = hk.box_pixels(stock, o['res'], o['bg'], o['ink'])
_, _, px = hk.box_pixels(stock, o['res'], o['bg'], o['ink'], o['text'])
out[off:off + o['len']] = px
return bytes(out)
@@ -135,8 +157,12 @@ def main():
ASSETS = p.assets
_, labels = hk.routine(hk.DECODER, cuecolours.HOT_TABLE)
w, h, on = hk.box_pixels(stock, hk.RES_QUANTIZE, 0xF800, 0x0000)
boxes = [{'addr': labels['PIX_ON'], 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xF800, 'ink': 0x0000},
{'addr': labels['PIX_OFF'], 'len': len(on), 'res': hk.RES_QUANTIZE_OFF, 'bg': 0x0000, 'ink': 0x4208}]
boxes = [{'addr': labels['PIX_ON'], 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xF800, 'ink': 0x0000,
'text': 'HOT KEYS'},
{'addr': labels['PIX_OFF'], 'len': len(on), 'res': hk.RES_QUANTIZE_OFF, 'bg': 0x0000, 'ink': 0x4208,
'text': 'HOT KEYS'},
{'addr': hk.PIX_LOAD, 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xFC60, 'ink': 0x0000,
'text': 'LOADING'}]
combos = [c for n in range(1, len(OURS) + 1) for c in itertools.combinations(OURS, n) if valid(c)]
variants, chunks = {}, {}
@@ -153,7 +179,7 @@ def main():
elif o['op'] == 'assets':
enc.append(['assets', o['addr'], o['len']])
else:
enc.append(['box', o['addr'], o['res'], o['bg'], o['ink']])
enc.append(['box', o['addr'], o['res'], o['bg'], o['ink'], o['text']])
return enc
# arnoldasr's mods, split up: per combination their bytes, but the asset
@@ -208,7 +234,7 @@ def main():
'ours': variants,
'expect': expect,
'box': {'resTable': hk.RES_TABLE, 'width': w, 'height': h, 'glyphs': glyphs,
'text': 'HOT KEYS', 'gap': 3, 'space': 5},
'gap': 3, 'space': 5},
'chunks': chunks,
}
sys.path.insert(0, os.path.join(os.path.dirname(WEB), 'CDJ900NXSPatcher'))
@@ -228,6 +254,36 @@ def main():
json.dump(data, f, separators=(',', ':'))
f.write(';\n')
print('wrote %s (%d bytes, %d + %d variants, %d chunks)' % (out, os.path.getsize(out), len(upstream), len(variants), len(chunks)))
write_versions(p)
def write_versions(p):
"""assets/versions.js: the site's version and changelog (tools/changelog.json)
and every mod's version as patch_cdj900nxs.VERSIONS has it."""
with open(os.path.join(HERE, 'changelog.json'), encoding='utf-8') as f:
log = json.load(f)
data = {'site': log['site'][0]['version'], 'date': log['site'][0]['date'], 'log': log['site'],
'mods': dict(p.VERSIONS), 'modlog': log.get('mods', {})}
out = os.path.join(WEB, 'assets', 'versions.js')
with open(out, 'w', encoding='utf-8', newline='\n') as f:
f.write("// Generated by tools/make_builder_data.py from tools/changelog.json and the mods' versions.\n")
f.write('window.CDJ_VERSIONS = ' + json.dumps(data, indent=1, ensure_ascii=False) + ';\n')
print('wrote %s (site %s)' % (out, data['site']))
# the pages load the site's own CSS and scripts with ?v=<site version>, so
# a browser takes the new ones after an update
import re
for name in sorted(os.listdir(WEB)):
if not name.endswith('.html'):
continue
path = os.path.join(WEB, name)
with open(path, encoding='utf-8') as f:
html = f.read()
new = re.sub(r'(href|src)="((?:style\.css|script\.js|assets/versions\.js|builder/[a-z]+\.js))(\?v=[^"]*)?"',
lambda m: '%s="%s?v=%s"' % (m.group(1), m.group(2), data['site']), html)
if new != html:
with open(path, 'w', encoding='utf-8', newline='\n') as f:
f.write(new)
print('stamped %s with ?v=%s' % (name, data['site']))
if __name__ == '__main__':