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:
1 parent
e4728b16ea
commit
d2d1518fba
16 files changed
+440
-72
No files matched your search
@@ -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
@@ -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__':
|
||||
|
||||
Reference in new issue
Block a user