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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+96
View File
@@ -0,0 +1,96 @@
// Generated by tools/make_builder_data.py from tools/changelog.json and the mods' versions.
window.CDJ_VERSIONS = {
"site": "1.1.0",
"date": "2026-10-20",
"log": [
{
"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": {
"extended_bpm": "1.0.0",
"phasemeter": "1.0.0",
"rgb_waveform": "1.0.0",
"beatjump": "1.0.0",
"cuecolours": "1.0.0",
"hotcuekeys": "1.2.0",
"ledsaver": "1.0.0",
"keynotation": "1.2.0",
"usbautoload": "0.1.0",
"quicksearch": "0.1.0",
"phasereverse": "0.1.0",
"credit": "1.0.0"
},
"modlog": {
"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."
]
}
]
}
};
+6 -5
View File
@@ -8,11 +8,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
<script src="builder/data.js"></script>
<script src="builder/builder.js"></script>
<script src="builder/ui.js"></script>
<link rel="stylesheet" href="style.css?v=1.1.0">
<script src="assets/versions.js?v=1.1.0"></script>
<script src="script.js?v=1.1.0"></script>
<script src="builder/data.js?v=1.1.0"></script>
<script src="builder/builder.js?v=1.1.0"></script>
<script src="builder/ui.js?v=1.1.0"></script>
</head>
<body>
+5 -5
View File
@@ -14,7 +14,7 @@
var ADDRESS_BYTES = { 0: 2, 1: 2, 2: 3, 3: 4, 5: 2, 6: 3, 7: 4, 8: 3, 9: 2 };
var LABEL = '1.32';
var UPSTREAM = ['extended_bpm', 'phasemeter', 'rgb_waveform'];
var OURS = ['cuecolours', 'hotcuekeys', 'ledsaver', 'credit'];
var OURS = ['beatjump', 'cuecolours', 'hotcuekeys', 'ledsaver', 'keynotation', 'credit'];
function fail(msg) { throw new Error(msg); }
@@ -415,13 +415,13 @@
}
// ---------- the mods' bytes (builder/data.js) ----------
function boxPixels(stock, res, bg, ink) {
function boxPixels(stock, res, bg, ink, text) {
var B = D.box, rec = B.resTable + res * 44, w = u16(stock, rec + 4), h = u16(stock, rec + 6);
var off = B.resTable + u32(stock, rec + 0x20), px = new Uint16Array(w * h);
for (var i = 0; i < w * h; i++) px[i] = u16(stock, off + i * 2);
for (var y = 3; y < 13; y++) for (var x = 3; x < w - 3; x++) px[y * w + x] = bg;
// as box_pixels: width = sum(space for ' ', else glyph width + gap) - gap
var text = B.text, width = -B.gap, c;
var width = -B.gap, c;
for (c = 0; c < text.length; c++) width += text[c] === ' ' ? B.space : B.glyphs[text[c]][0].length + B.gap;
var x0 = Math.floor((w - width) / 2), y0 = 4;
for (c = 0; c < text.length; c++) {
@@ -436,14 +436,14 @@
}
// One variant of data.js: the application grown to its size, then its
// operations: our bytes, a copy from the user's own application, the HOT
// KEYS box or arnoldasr's asset blob, both made from the user's application.
// KEYS / LOADING boxes or arnoldasr's asset blob, both made from the user's application.
function applyVariant(app, stock, v) {
var out = grow(app.slice(), v.size), blob = null;
v.ops.forEach(function (o) {
var off = o[1] - BASE;
if (o[0] === 'data') out.set(fromB64(D.chunks[o[2]]), off);
else if (o[0] === 'copy') out.set(stock.subarray(o[2] - BASE, o[2] - BASE + o[3]), off);
else if (o[0] === 'box') out.set(boxPixels(stock, o[2], o[3], o[4]), off);
else if (o[0] === 'box') out.set(boxPixels(stock, o[2], o[3], o[4], o[5]), off);
else if (o[0] === 'assets') { blob = blob || assets(stock).blob; out.set(blob.subarray(0, o[2]), off); }
else fail('unknown operation ' + o[0]);
});
+1 -1
View File
@@ -1,2 +1,2 @@
// Generated by tools/make_builder_data.py -- do not edit.
window.BUILDER_DATA = {"stockUpdSha256":"e96bca7d1ac983985992a8e5fa7813e1ebf04bef514e0c6cfd0a8375f667f7f9","stockAppSha256":"1ac3e30cde292865413950fe159663b3958e85939ed0a5d0122f796809a5f8ef","appSize":16519056,"dataBase":150744320,"upstream":{"extended_bpm":{"size":16576352,"ops":[["data",145914824,"45b88bcd5afea839"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"2853d86b697a9dfa"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"acf759afff056d54"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"rgb_waveform":{"size":16577120,"ops":[["data",144311836,"ca71ec9290d6ed48"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",147243844,"d77596e8499df246"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"9bed08a38f1020b6"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"extended_bpm,phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"8Line truncated
window.BUILDER_DATA = {"stockUpdSha256":"e96bca7d1ac983985992a8e5fa7813e1ebf04bef514e0c6cfd0a8375f667f7f9","stockAppSha256":"1ac3e30cde292865413950fe159663b3958e85939ed0a5d0122f796809a5f8ef","appSize":16519056,"dataBase":150744320,"upstream":{"extended_bpm":{"size":16576352,"ops":[["data",145914824,"45b88bcd5afea839"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"2853d86b697a9dfa"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"845677fae2e67916"],["data",150538228,"845677fae2e67916"],["data",150556534,"845677fae2e67916"],["data",150572646,"845677fae2e67916"],["data",150587026,"845677fae2e67916"],["data",150601380,"845677fae2e67916"],["data",150619274,"845677fae2e67916"],["data",150637628,"845677fae2e67916"],["data",150655862,"845677fae2e67916"],["data",150673666,"845677fae2e67916"],["data",150692276,"845677fae2e67916"],["data",150709976,"845677fae2e67916"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150739456,"786195378d06baff"],["data",150739521,"503c09b6b0baefb3"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"7010683be462baa0"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"172e933902cb3738"],["data",150742016,"acf759afff056d54"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"rgb_waveform":{"size":16577120,"ops":[["data",144311836,"ca71ec9290d6ed48"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",147243844,"d77596e8499df246"],["data",150736896,"6d11cfe939a0630a"],["data",150737408,"a4d77ab57ab896dd"],["data",150737920,"906ea135b6ba2cef"],["data",150738432,"d6db78bde1edbcae"],["data",150738944,"91aaac306bd43135"],["data",150739456,"786195378d06baff"],["data",150739521,"d5326732238d3e24"],["data",150739968,"160adecaa849e284"],["data",150740480,"d82da843aae59533"],["data",150740992,"f9699e90e078917d"],["data",150741248,"ebd1070ec574dec1"],["data",150741504,"bb010f4579136412"],["data",150742016,"9bed08a38f1020b6"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"extended_bpm,phasemeter":{"size":16576352,"ops":[["data",135069542,"845677fae2e67916"],["data",145914824,"45b88bcd5afea839"],["data",145941230,"6922e93e3827642c"],["data",145986008,"84014a74358fe2ac"],["data",145987228,"84014a74358fe2ac"],["data",145988312,"84014a74358fe2ac"],["data",146168552,"84014a74358fe2ac"],["data",146172320,"84014a74358fe2ac"],["data",146173504,"84014a74358fe2ac"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147246288,"060f2c4b6cd4c5e4"],["data",150411792,"845677fae2e67916"],["data",150427952,"845677fae2e67916"],["data",150446592,"845677fae2e67916"],["data",150464904,"845677fae2e67916"],["data",150483144,"845677fae2e67916"],["data",150501528,"845677fae2e67916"],["data",150519852,"8Line truncated
+23 -7
View File
@@ -9,12 +9,16 @@
what: 'Two-line phase meter instead of the four boxes; UTILITY › PHASE METER (was SLIP FLASHING) switches it.' },
{ id: 'rgb_waveform', name: 'RGB waveform', by: 'arnoldasr', state: 'stable',
what: 'The big waveform in RGB colours, the overview as a red-to-white gradient.' },
{ id: 'beatjump', name: 'Beat jump', by: 'ours', state: 'experimental', needs: 'hotcuekeys',
what: 'TRACK SEARCH ◀◀ ▶▶ jumps 1/2/4/8 beats along the beat grid, at once and in phase. UTILITY › BEAT JUMP (was SCREEN SAVER).' },
{ id: 'cuecolours', name: 'Cue colours', by: 'ours', state: 'stable',
what: 'Memory cues in their rekordbox colours; rekordbox hot cues shown as lettered boxes.' },
{ id: 'hotcuekeys', name: 'Hot cue keys', by: 'ours', state: 'experimental', needs: 'cuecolours',
what: 'Hot cues A–C on the BEAT DIVIDE keys: play, gate, set and delete, saved on the USB, loaded in the background.' },
{ id: 'ledsaver', name: 'LED animation', by: 'ours', state: 'stable',
what: 'While the screen saver dims the display, the button LEDs and the jog rings play an animation. UTILITY › LED ANIMATION.' },
{ id: 'keynotation', name: 'Key notation', by: 'ours', state: 'experimental',
what: 'Every key in one notation and in its Camelot wheel colour, whatever rekordbox exported: Camelot, Open Key or classic, also in BROWSE › KEY. UTILITY › KEY NOTATION.' },
{ id: 'credit', name: 'Boot text', by: 'ours', state: 'stable',
what: 'The boot screen says “Pionaarr Mods 1.0” instead of “Ver 1.31”.' }
];
@@ -25,14 +29,25 @@
function byId(id) { return document.getElementById(id); }
function needsOf(id) { var m = MODS.filter(function (x) { return x.id === id; })[0]; return m && m.needs; }
// A mod ticked ticks what it needs (and what that needs); one unticked
// unticks every mod that needs it, directly or not.
function toggle(id, on) {
chosen[id] = on;
var note = '';
if (on && needsOf(id) && !chosen[needsOf(id)]) { chosen[needsOf(id)] = true; note = 'Ticked ' + needsOf(id) + ' too: ' + id + ' needs it.'; }
if (!on) MODS.forEach(function (m) {
if (m.needs === id && chosen[m.id]) { chosen[m.id] = false; note = 'Unticked ' + m.id + ' too: it needs ' + id + '.'; }
});
byId('pick-note').textContent = note;
var notes = [];
if (on) {
for (var n = id; needsOf(n); n = needsOf(n)) {
if (!chosen[needsOf(n)]) { chosen[needsOf(n)] = true; notes.push('Ticked ' + needsOf(n) + ' too: ' + n + ' needs it.'); }
}
} else {
var changed = true;
while (changed) {
changed = false;
MODS.forEach(function (m) {
if (m.needs && !chosen[m.needs] && chosen[m.id]) { chosen[m.id] = false; changed = true; notes.push('Unticked ' + m.id + ' too: it needs ' + m.needs + '.'); }
});
}
}
byId('pick-note').textContent = notes.join(' ');
render();
}
@@ -44,7 +59,8 @@
row.className = 'pick' + (chosen[m.id] ? ' on' : '');
row.innerHTML =
'<input type="checkbox"' + (chosen[m.id] ? ' checked' : '') + '>' +
'<span class="pick-main"><span class="pick-name">' + m.name + ' <code>' + m.id + '</code></span>' +
'<span class="pick-main"><span class="pick-name">' + m.name + ' <code>' + m.id + '</code>' +
(window.CDJ_VERSIONS && window.CDJ_VERSIONS.mods[m.id] ? ' <span class="ver">v' + window.CDJ_VERSIONS.mods[m.id] + '</span>' : '') + '</span>' +
'<span class="pick-what">' + m.what + '</span>' +
'<span class="pick-by">' + (m.by === 'ours' ? 'ours · made with Claude Code' : 'by arnoldasr') +
(m.needs ? ' · needs <code>' + m.needs + '</code>' : '') + '</span></span>' +
+36 -6
View File
@@ -8,8 +8,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
<link rel="stylesheet" href="style.css?v=1.1.0">
<script src="assets/versions.js?v=1.1.0"></script>
<script src="script.js?v=1.1.0"></script>
</head>
<body>
@@ -38,7 +39,9 @@
<a href="#phasemeter">phasemeter</a>
<a href="#cuecolours">cuecolours</a>
<a href="#hotcuekeys">hotcuekeys</a>
<a href="#beatjump">beatjump</a>
<a href="#ledsaver">ledsaver</a>
<a href="#keynotation">keynotation</a>
<a href="#credit">credit</a>
<div class="h">Hot cue keys</div>
<a href="#hk-mode">The hot key mode</a>
@@ -74,7 +77,7 @@
<p>The Python patcher builds exactly the same file, byte for byte. Tick the mods with the arrow keys and <kbd>Space</kbd>, build with <kbd>Enter</kbd>, accept the disclaimer by typing <code>yes</code>:</p>
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD</div>
<p>Or name the mods and accept with <code>--accept</code>:</p>
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,credit --accept</div>
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,keynotation,credit --accept</div>
<h2 id="stock">Back to stock</h2>
<p>Build with no mod ticked in the builder (or <code>--stock</code> with the patcher): the original application, relabelled so the player accepts it. Update as above. Keep this file on a stick before you try any mod.</p>
@@ -126,7 +129,8 @@
<ul>
<li>the big waveform stays black and appears once all are ready;</li>
<li><kbd>PLAY</kbd>, <kbd>CUE</kbd> and the hot cue keys are locked, so nothing interrupts the loading;</li>
<li>the key of the hot cue being loaded blinks.</li>
<li>the key of the hot cue being loaded blinks;</li>
<li>nothing is heard: the output stays silent until the deck stands at its CUE point again.</li>
</ul>
<p>Then the deck stands at its CUE point again, as before, and each of the three starts at once. This happens whether the hot key mode is on or not, as long as HOT CUE KEYS is ON.</p>
@@ -139,6 +143,7 @@
<li>held <strong>less than 0.5 s</strong>: it simply plays on (a normal hot cue);</li>
<li>held <strong>0.5 s or longer</strong>: it jumps back to the hot cue and stands there, ready to go again.</li>
</ul>
<p>From 0.5 s on the key blinks slowly while you hold it, so you can see the gate is armed.</p>
<h3 id="hk-set">Set a hot cue</h3>
<p>On a key <strong>without</strong> a hot cue (LED off):</p>
@@ -163,6 +168,7 @@
<span class="led"><i></i> off — no hot cue</span>
<span class="led"><i class="dim"></i> dim — hot cue there</span>
<span class="led"><i class="blink"></i> blinking — pressed, set, deleted or being loaded</span>
<span class="led"><i class="blink"></i> blinking slowly — a gate held (0.5 s or longer)</span>
</div>
<h3 id="hk-limits">Good to know</h3>
@@ -173,6 +179,15 @@
<li>No slip hot cues yet: with SLIP on, a hot cue plays as without SLIP.</li>
</ul>
<h2 id="beatjump">beatjump <span class="badge author">ours</span> <span class="state experimental">experimental</span></h2>
<p>The TRACK SEARCH keys ◀◀ / ▶▶ jump along the track's beat grid instead of changing the track. How far is chosen in UTILITY › <strong>BEAT JUMP</strong> (where SCREEN SAVER was): OFF, 1, 2, 4 or 8 beats; 4 until you choose, and your choice stays when the player is switched off.</p>
<ul>
<li>The jump is exactly that many grid beats, so with 4 or 8 the phase meter shows the same place in the bar after the jump; with 1 or 2 the beats still line up, the bar moves on by one or two beats.</li>
<li>Playing, it is made at once in the audio the player has already decoded: no gap, no pre-roll. Paused or at the cue it moves the deck there; PLAY plays on from it.</li>
<li>The player always keeps 10 s of decoded audio ahead; a press that would leave less (very fast clicks with 8 beats) is ignored and the next one counts again. Nothing is put off, so the player never runs dry (no EMERGENCY LOOP).</li>
<li>It needs <code>hotcuekeys</code> (its engine makes the jumps) and <code>cuecolours</code>; the builder ticks them for you.</li>
</ul>
<h2 id="ledsaver">ledsaver <span class="badge author">ours</span></h2>
<p>UTILITY › <strong>LED ANIMATION</strong> (the last item) is <strong>ON</strong> from the first start; set it to OFF to switch it off (the player keeps the choice). When the player's own screen saver dims the display (5 minutes without a track loaded, or 100 minutes paused or at the cue), the LEDs play a loop of about four and a half seconds:</p>
<ul>
@@ -183,11 +198,24 @@
</ul>
<p>Any key ends it, as it ends the screen saver.</p>
<h2 id="keynotation">keynotation <span class="badge author">ours</span> <span class="state experimental">experimental</span></h2>
<p>rekordbox writes each track's key as text, in whatever notation was set in its preferences when the stick was exported: <em>4A</em>, <em>9m</em> or <em>Fm</em> for the same key. This mod shows every key in one notation of your choice, UTILITY › <strong>KEY NOTATION</strong> (the last item):</p>
<ul>
<li><strong>CAMELOT</strong> (default) — 1A–12A minor, 1B–12B major, as on the Camelot wheel;</li>
<li><strong>OPEN KEY</strong> — 1m–12m minor, 1d–12d major (8A = 1m = Am);</li>
<li><strong>CLASSIC</strong> — Am, F#m, Db … as rekordbox writes them;</li>
<li><strong>OFF</strong> — the key as exported.</li>
</ul>
<p>The player converts with a table of all spellings, sharps and flats included (G#m, D#m, A#, Gbm …). It works on the main screen, in INFO and in BROWSE › KEY (the key list and its compatible-key groups such as “1A, 1B, 12A, 2A”); a new choice shows from the next track loaded.</p>
<p>Each key is drawn in the colour of its place on the Camelot wheel (8A pink, 5A orange, 4A yellow …), in all three notations; with OFF it stays white. A highlighted row keeps its black text, and the letters you type in SEARCH are left as they are. The player's text colour is set per text box, so the mod sets it for a key's box just before the player draws it.</p>
<figure class="shot"><img src="assets/key-colour.png" alt="Key 5A in orange"><figcaption>5A in its wheel colour.</figcaption></figure>
<figure class="shot"><img src="assets/utility-key.png" alt="UTILITY with KEY NOTATION"><figcaption>UTILITY › KEY NOTATION.</figcaption></figure>
<h2 id="credit">credit <span class="badge author">ours</span></h2>
<p>The boot screen says <em>Pionaarr Mods 1.0</em> where it said <em>Ver 1.31</em>. Ticked at the start; untick it to keep the stock text. UTILITY and the LINK screen still show the real version, 1.31.</p>
<figure class="shot"><img src="assets/boot-mods.png" alt="Boot screen with Pionaarr Mods 1.0"><figcaption>The boot screen.</figcaption></figure>
<p>Beat jump, stems, a uniform key notation and a 3-band waveform are in the works: see <a href="upcoming.html">Upcoming</a>.</p>
<p>Quick search, USB auto load, a reversed phase meter, stems and a 3-band waveform are in the works: see <a href="upcoming.html">Upcoming</a>.</p>
<!-- ===================== status ===================== -->
<h2 id="status">Status</h2>
@@ -200,7 +228,9 @@
<tr><td><code>phasemeter</code></td><td>arnoldasr</td><td><span class="state stable">stable</span></td><td><span class="player">ran on a player</span></td></tr>
<tr><td><code>cuecolours</code></td><td>ours</td><td><span class="state stable">stable</span></td><td><span class="player">ran on a player</span></td></tr>
<tr><td><code>hotcuekeys</code></td><td>ours</td><td><span class="state experimental">experimental</span></td><td><span class="player">ran on a player</span></td></tr>
<tr><td><code>beatjump</code></td><td>ours</td><td><span class="state experimental">experimental</span></td><td><span class="player">ran on a player</span></td></tr>
<tr><td><code>ledsaver</code></td><td>ours</td><td><span class="state stable">stable</span></td><td><span class="player">ran on a player</span></td></tr>
<tr><td><code>keynotation</code></td><td>ours</td><td><span class="state experimental">experimental</span></td><td><span class="player no">emulator only</span></td></tr>
<tr><td><code>credit</code></td><td>ours</td><td><span class="state stable">stable</span></td><td><span class="player">ran on a player</span></td></tr>
</tbody>
</table>
@@ -224,7 +254,7 @@
<div class="wrap cols">
<div>
<div class="ai"><span class="dot"></span>Made with Claude Code · Claude Opus 5.5</div>
<p>This website, the builder, the mods <code>cuecolours</code>, <code>hotcuekeys</code>, <code>ledsaver</code> and <code>credit</code>, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods <code>rgb_waveform</code>, <code>extended_bpm</code> and <code>phasemeter</code> are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>.</p>
<p>This website, the builder, the mods <code>cuecolours</code>, <code>hotcuekeys</code>, <code>ledsaver</code>, <code>keynotation</code> and <code>credit</code>, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods <code>rgb_waveform</code>, <code>extended_bpm</code> and <code>phasemeter</code> are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>.</p>
</div>
<div>
<h4>Disclaimer</h4>
+49 -6
View File
@@ -8,8 +8,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
<link rel="stylesheet" href="style.css?v=1.1.0">
<script src="assets/versions.js?v=1.1.0"></script>
<script src="script.js?v=1.1.0"></script>
</head>
<body>
@@ -33,8 +34,8 @@
<h1>New tricks for an <span class="grad">old favourite</span>.</h1>
<p class="lead">
A set of firmware mods that bring features of newer players to the CDJ-900NXS:
an RGB waveform, a BPM to two decimals, a phase meter, rekordbox cue colours and
<strong>hot cues you can play from the deck itself</strong>. Build your own update file
an RGB waveform, a BPM to two decimals, a phase meter, rekordbox cue colours,
beat jump and <strong>hot cues you can play from the deck itself</strong>. Build your own update file
right here in the browser — the boot loader stays untouched.
</p>
<div class="cta">
@@ -195,6 +196,24 @@
<a class="more" href="docs.html#hotcuekeys">Hot cue guide →</a>
</article>
<!-- beatjump -->
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">⏭️</div>
<div>
<h3>Beat jump <code>beatjump</code></h3>
<div class="by">ours · made with Claude Code · needs <code>hotcuekeys</code></div>
</div>
<div class="meta"><span class="player">ran on a player</span><span class="state experimental">experimental</span></div>
</div>
<p>TRACK SEARCH ◀◀ / ▶▶ jumps 1, 2, 4 or 8 beats along the rekordbox beat grid instead of changing the track — at once, without a gap and in phase with the other deck, as on the newest players. Chosen in UTILITY › BEAT JUMP (default 4 beats, kept when the player is off). Works playing, paused and at the cue. The jump is made in the audio the player has already decoded; a press it cannot jump yet (clicked very fast with 8 beats) is simply ignored instead of running the player dry.</p>
<ul class="diff">
<li><span class="k">Stock</span><span>TRACK SEARCH goes to the previous / next track.</span></li>
<li><span class="k new">Modded</span><span>Whole bars (4, 8) or beats (1, 2) forward and back, the phase meter stays where it was.</span></li>
</ul>
<a class="more" href="docs.html#beatjump">Read more →</a>
</article>
<!-- ledsaver -->
<article class="card">
<div class="card-head">
@@ -213,6 +232,30 @@
<a class="more" href="docs.html#ledsaver">Read more →</a>
</article>
<!-- keynotation -->
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">🎼</div>
<div>
<h3>Key notation <code>keynotation</code></h3>
<div class="by">ours · made with Claude Code</div>
</div>
<div class="meta"><span class="state experimental">experimental</span></div>
</div>
<p>Every track's key in one notation, whatever rekordbox exported: Camelot (8A), Open Key (1m) or classic (Am), chosen in UTILITY › KEY NOTATION. The player converts on its own with a table of all spellings, sharps and flats included — on the main screen, in INFO and in BROWSE › KEY. The key shows in the colour of its place on the Camelot wheel.</p>
<div class="vs stack">
<figure><img src="assets/key-stock.png" alt="Key as exported: 4A"><figcaption>As exported (Camelot): 4A</figcaption></figure>
<figure class="mod"><img src="assets/key-mods.png" alt="Key in Open Key: 9m"><figcaption>KEY NOTATION › OPEN KEY: 9m</figcaption></figure>
<figure class="mod"><img src="assets/key-colour.png" alt="Key 5A in its Camelot wheel colour, orange"><figcaption>Wheel colour: 5A in orange</figcaption></figure>
<figure class="mod"><img src="assets/utility-key.png" alt="UTILITY menu with the KEY NOTATION item"><figcaption>UTILITY › KEY NOTATION</figcaption></figure>
</div>
<ul class="diff">
<li><span class="k">Stock</span><span>The key as rekordbox's export setting wrote it.</span></li>
<li><span class="k new">Modded</span><span>OFF, CAMELOT (default), OPEN KEY or CLASSIC — the same notation on every stick, in its wheel colour.</span></li>
</ul>
<a class="more" href="docs.html#keynotation">Read more →</a>
</article>
<!-- credit -->
<article class="card">
<div class="card-head">
@@ -231,7 +274,7 @@
</article>
</div>
<p class="caption">More in the works — beat jump, stems, a uniform key notation and a 3-band waveform: see <a href="upcoming.html">Upcoming</a>.</p>
<p class="caption">More in the works — beat jump, coloured Camelot keys, stems and a 3-band waveform: see <a href="upcoming.html">Upcoming</a>.</p>
</div>
</section>
@@ -258,7 +301,7 @@
<div class="wrap cols">
<div>
<div class="ai"><span class="dot"></span>Made with Claude Code · Claude Opus 5.5</div>
<p>This website, the builder, the mods <code>cuecolours</code>, <code>hotcuekeys</code>, <code>ledsaver</code> and <code>credit</code>, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods <code>rgb_waveform</code>, <code>extended_bpm</code> and <code>phasemeter</code> are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>.</p>
<p>This website, the builder, the mods <code>cuecolours</code>, <code>hotcuekeys</code>, <code>ledsaver</code>, <code>keynotation</code> and <code>credit</code>, and the patcher were developed with Claude Code (Anthropic's AI coding assistant, model Claude Opus 5.5), by reverse engineering the firmware in an emulator. The mods <code>rgb_waveform</code>, <code>extended_bpm</code> and <code>phasemeter</code> are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>.</p>
</div>
<div>
<h4>Disclaimer</h4>
+35
View File
@@ -21,6 +21,41 @@
label();
});
// version badge and changelog (assets/versions.js, made by tools/make_builder_data.py)
var V = window.CDJ_VERSIONS;
var head = document.querySelector('.top .wrap');
if (V && head) {
var esc = function (t) { return String(t).replace(/[&<>]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]; }); };
var vb = document.createElement('button');
vb.type = 'button'; vb.className = 'ver-btn'; vb.textContent = 'v' + V.site;
vb.setAttribute('aria-haspopup', 'dialog'); vb.title = 'Changelog';
var brand = head.querySelector('.brand');
if (brand && brand.nextSibling) head.insertBefore(vb, brand.nextSibling); else head.appendChild(vb);
var pop = document.createElement('div');
pop.className = 'ver-pop'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-label', 'Changelog');
var html = '<div class="ver-box"><button class="close" type="button" aria-label="Close">✕</button>' +
'<h2>Changelog</h2><p class="sub">Site ' + esc(V.site) + ' · ' + esc(V.date) + ' · Semantic Versioning, dates ISO 8601</p>';
V.log.forEach(function (e) {
html += '<h3>' + esc(e.version) + '<span class="d">' + esc(e.date) + '</span></h3><ul>' +
e.changes.map(function (c) { return '<li>' + esc(c) + '</li>'; }).join('') + '</ul>';
});
html += '<h3>Mods</h3><div class="ver-mods">' + Object.keys(V.mods).map(function (m) {
return '<code>' + esc(m) + '</code><span>' + esc(V.mods[m]) + '</span>';
}).join('') + '</div></div>';
pop.innerHTML = html;
document.body.appendChild(pop);
var close = function () { pop.classList.remove('open'); vb.focus(); };
vb.addEventListener('click', function () { pop.classList.add('open'); pop.querySelector('.close').focus(); });
pop.querySelector('.close').addEventListener('click', close);
pop.addEventListener('click', function (e) { if (e.target === pop) close(); });
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && pop.classList.contains('open')) close(); });
// every mod card's version beside its name
document.querySelectorAll('.card h3 code').forEach(function (c) {
var v = V.mods[c.textContent.trim()];
if (v) { var s = document.createElement('span'); s.className = 'ver'; s.textContent = 'v' + v; c.parentNode.appendChild(s); }
});
}
// before / after
document.querySelectorAll('.compare').forEach(function (box) {
var range = box.querySelector('input[type="range"]');
+36
View File
@@ -376,3 +376,39 @@ button.btn { font: inherit; font-weight: 700; cursor: pointer; }
@media (max-width: 360px) {
.tabs a { padding: 5px 6px; font-size: .74rem; }
}
/* ---- version badge and changelog ---- */
.ver-btn {
border: 1px solid var(--line); background: var(--card); color: var(--muted);
font: 600 .74rem/1 var(--mono); padding: 7px 10px; border-radius: 999px; cursor: pointer; white-space: nowrap;
}
.ver-btn:hover { color: var(--text); border-color: var(--accent); }
.ver-pop {
position: fixed; inset: 0; z-index: 50; display: none; align-items: flex-start; justify-content: center;
background: color-mix(in srgb, var(--bg) 55%, transparent); padding: 80px 16px 16px;
}
.ver-pop.open { display: flex; }
.ver-box {
width: min(560px, 100%); max-height: calc(100vh - 100px); overflow: auto;
background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow);
padding: 20px 22px;
}
.ver-box h2 { margin: 0 0 4px; font-size: 1.15rem; }
.ver-box .sub { color: var(--muted); font-size: .85rem; margin: 0 0 14px; }
.ver-box h3 { margin: 16px 0 6px; font-size: .95rem; }
.ver-box h3 .d { color: var(--faint); font: 500 .78rem var(--mono); margin-left: 8px; }
.ver-box ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: .9rem; }
.ver-box .close {
float: right; border: 1px solid var(--line); background: var(--card-2); color: var(--muted);
width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 1rem;
}
.ver-box .close:hover { color: var(--text); }
.ver-mods { display: grid; grid-template-columns: 1fr auto; gap: 2px 16px; font-size: .88rem; margin-top: 6px; }
.ver-mods code { background: none; border: 0; padding: 0; }
.ver-mods span { color: var(--muted); font-family: var(--mono); }
.ver { font: 600 .7rem/1 var(--mono); color: var(--faint); margin-left: 6px; }
@media (max-width: 560px) {
.top .brand { display: none; }
.ver-btn { padding: 6px 7px; font-size: .68rem; }
.tabs a { padding: 6px 10px; }
}
+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__':
+43 -26
View File
@@ -4,12 +4,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CDJ-900NXS Mods Upcoming</title>
<meta name="description" content="What comes next for the CDJ-900NXS mods: beat jump, stems, a uniform Camelot / Open Key notation and a 3-band waveform.">
<meta name="description" content="What comes next for the CDJ-900NXS mods: quick search, USB auto load, a reversed phase meter, stems and a 3-band waveform.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
<link rel="stylesheet" href="style.css?v=1.1.0">
<script src="assets/versions.js?v=1.1.0"></script>
<script src="script.js?v=1.1.0"></script>
</head>
<body>
@@ -29,7 +30,7 @@
<main class="wrap upcoming">
<div class="eyebrow">Upcoming</div>
<h1>What comes next</h1>
<p class="lead">Ideas we are working on or looking into. None of them is in the builder yet. <strong>Planned</strong> means we know how to do it; <strong>feasibility</strong> means we are still finding out whether the player can do it at all.</p>
<p class="lead">Ideas we are working on or looking into. None of them is in the builder yet. <strong>In testing</strong> means built and working in the emulator, waiting for a player; <strong>planned</strong> means we know how to do it; <strong>feasibility</strong> means we are still finding out whether the player can do it at all.</p>
<div class="legend">
<span class="state planned">planned</span>
<span class="state feasibility">feasibility</span>
@@ -39,17 +40,49 @@
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">⏭️</div>
<div class="icon" aria-hidden="true">🔎</div>
<div>
<h3>Beat jump <code>beatjump</code></h3>
<h3>Quick search <code>quicksearch</code></h3>
<div class="by">ours · made with Claude Code</div>
</div>
<div class="meta"><span class="player no">emulator only</span><span class="state planned">planned</span></div>
<div class="meta"><span class="player no">emulator only</span><span class="state planned">in testing</span></div>
</div>
<p>TRACK SEARCH jumps 1, 2, 4 or 8 beats along the beat grid instead of changing the track, chosen in UTILITY › BEAT JUMP. While playing, the jump goes through a buffer of its own and comes back in phase; the CUE point and the hot cues stay.</p>
<p>BROWSE held opens SEARCH straight away, from wherever you are; the next plain BROWSE takes you back to the folder you were in. BROWSE pressed twice goes to the list the playing track was loaded from, with the track picked.</p>
<ul class="diff">
<li><span class="k">Now</span><span>Built and working in the emulator, on top of the hot cue engine.</span></li>
<li><span class="k new">Next</span><span>Testing on a player before it goes into the builder.</span></li>
<li><span class="k">Now</span><span>Built and working in the emulator; it walks the player's own menus for you, so it takes a moment to get to SEARCH.</span></li>
<li><span class="k new">Next</span><span>Testing on a player, then the builder.</span></li>
</ul>
</article>
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">💾</div>
<div>
<h3>USB auto load <code>usbautoload</code></h3>
<div class="by">ours · made with Claude Code</div>
</div>
<div class="meta"><span class="player no">emulator only</span><span class="state planned">in testing</span></div>
</div>
<p>The MY SETTINGS on your stick are loaded as soon as the player finds them — no more “Push the MENU button to LOAD”. UTILITY › USB AUTO LOAD switches it on and off.</p>
<ul class="diff">
<li><span class="k">Now</span><span>Built and working in the emulator (“Your settings were loaded to PLAYER.” right after the stick is read).</span></li>
<li><span class="k new">Next</span><span>Testing on a player, then the builder.</span></li>
</ul>
</article>
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">↔️</div>
<div>
<h3>Phase meter reverse <code>phasereverse</code></h3>
<div class="by">ours · made with Claude Code · needs <code>phasemeter</code></div>
</div>
<div class="meta"><span class="player no">emulator only</span><span class="state planned">in testing</span></div>
</div>
<p>A third value for UTILITY › PHASE METER: OFF, ON or REVERSE — the line meter with its markers running from right to left.</p>
<ul class="diff">
<li><span class="k">Now</span><span>Built and working in the emulator, on top of arnoldasr's phase meter.</span></li>
<li><span class="k new">Next</span><span>Testing on a player, then the builder.</span></li>
</ul>
</article>
@@ -69,22 +102,6 @@
</ul>
</article>
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">🎼</div>
<div>
<h3>Uniform Camelot / Open Key</h3>
<div class="by">ours · idea</div>
</div>
<div class="meta"><span class="state planned">planned</span></div>
</div>
<p>Every track's key in one notation of your choice — Camelot (8A) or Open Key (1m) — chosen in UTILITY, on the main screen and in the browser, whatever notation the USB was exported with.</p>
<ul class="diff">
<li><span class="k">Now</span><span>The player shows the key as it was exported.</span></li>
<li><span class="k new">Planned</span><span>One notation everywhere, set on the player.</span></li>
</ul>
</article>
<article class="card">
<div class="card-head">
<div class="icon" aria-hidden="true">🌊</div>