Builder, upcoming page, mods page for the current mods
- builder.html: build C900NXS.UPD in the browser (official 1.31 update in, mods ticked with their dependencies, no-liability and Claude Code confirmations, download). Ports of upd.py, the LZSS packer and the mods' application; the result is checked against the SHA-256 the Python patcher gives and is byte for byte the patcher's file. - builder/data.js from tools/make_builder_data.py: the mods' bytes per combination; firmware parts (UTILITY table, HOT KEYS box, the asset blob's resource table) are made from the user's own update. - index.html: rgb_waveform, extended_bpm, phasemeter, cuecolours, hotcuekeys (experimental), ledsaver, credit; stable/experimental and a green "ran on a player" dot. - upcoming.html: beat jump, stems, uniform Camelot / Open Key, 3-band waveform. - docs.html: the builder, the new mod names, hot cue loading and locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f931ca0423
commit
e4728b16ea
12 files changed
+1347
-145
No files matched your search
@@ -0,0 +1 @@
|
||||
_test/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
+110
@@ -0,0 +1,110 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CDJ-900NXS Mods Builder</title>
|
||||
<meta name="description" content="Build your own modded C900NXS.UPD in the browser: upload the official 1.31 update, tick the mods, download the file. Nothing leaves your computer.">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="index.html"><span class="disc" aria-hidden="true"></span><span>CDJ-900NXS Mods</span></a>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<a href="index.html">Mods</a>
|
||||
<a href="builder.html" aria-current="page">Builder</a>
|
||||
<a href="upcoming.html">Upcoming</a>
|
||||
<a href="docs.html">Docs</a>
|
||||
</nav>
|
||||
<button class="theme-btn" type="button" aria-label="Switch light / dark">☀</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap builder">
|
||||
<div class="eyebrow">Builder</div>
|
||||
<h1>Build your update</h1>
|
||||
<p class="lead">Upload the official CDJ-900NXS <strong>1.31</strong> update, tick the mods and download your <code>C900NXS.UPD</code>. Everything runs in your browser: the file never leaves your computer.</p>
|
||||
|
||||
<!-- 1 -->
|
||||
<section class="step" id="step-file">
|
||||
<div class="step-n">1</div>
|
||||
<div class="step-body">
|
||||
<h2>The official update</h2>
|
||||
<p>Pioneer DJ's <code>C900NXS.UPD</code> for firmware 1.31. The builder checks its SHA-256 and refuses any other file.</p>
|
||||
<label class="drop" id="drop">
|
||||
<input type="file" id="file" accept=".UPD,.upd">
|
||||
<span class="drop-t"><strong>Choose C900NXS.UPD</strong> or drop it here</span>
|
||||
<span class="drop-s" id="file-state">No file yet</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2 -->
|
||||
<section class="step" id="step-mods">
|
||||
<div class="step-n">2</div>
|
||||
<div class="step-body">
|
||||
<h2>The mods</h2>
|
||||
<p>Ticking a mod also ticks what it needs; unticking one unticks what needs it. Tick none for the stock firmware (relabelled so the player takes it).</p>
|
||||
<div class="picks" id="picks"></div>
|
||||
<p class="hint" id="pick-note"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3 -->
|
||||
<section class="step" id="step-ok">
|
||||
<div class="step-n">3</div>
|
||||
<div class="step-body">
|
||||
<h2>Before you build</h2>
|
||||
<div class="callout warn">
|
||||
<strong>Unofficial firmware.</strong> Flashing modified firmware can leave your player unusable. The boot loader and the recovery stay untouched, so an official update can still be run, but there is no guarantee.
|
||||
</div>
|
||||
<label class="agree"><input type="checkbox" id="agree-risk"> I flash this at my own risk. The authors accept <strong>no liability</strong> for any damage to my player, my data or anything else.</label>
|
||||
<label class="agree"><input type="checkbox" id="agree-ai"> I understand that our mods, this builder and the website were written with <strong>Claude Code</strong> (Anthropic's AI coding assistant, Claude Opus 5.5) and tested in an emulator and on one player.</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4 -->
|
||||
<section class="step" id="step-build">
|
||||
<div class="step-n">4</div>
|
||||
<div class="step-body">
|
||||
<h2>Build</h2>
|
||||
<button class="btn primary" type="button" id="build" disabled>Build C900NXS.UPD</button>
|
||||
<span class="hint" id="build-why"></span>
|
||||
<div class="log" id="log" hidden></div>
|
||||
<div class="result" id="result" hidden>
|
||||
<a class="btn primary" id="download" download="C900NXS.UPD">Download C900NXS.UPD</a>
|
||||
<div class="sum" id="sum"></div>
|
||||
<details>
|
||||
<summary>What was checked</summary>
|
||||
<ul id="checks"></ul>
|
||||
</details>
|
||||
<p class="hint">Copy <code>C900NXS.UPD</code> to the root of a FAT32 USB stick and run the update on the player as for an official one. The file is labelled 1.32 so the player installs it; the player still reports 1.31.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<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, our mods 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>extended_bpm</code>, <code>phasemeter</code> and <code>rgb_waveform</code> are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Disclaimer</h4>
|
||||
<p>Unofficial. Not affiliated with or endorsed by Pioneer DJ or AlphaTheta; CDJ and rekordbox are their trademarks. Flashing modified firmware can leave your player unusable. Everything here is provided as is, without warranty, and used at your own risk.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,500 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// CDJ-900NXS mods builder, in the browser: the official 1.31 C900NXS.UPD in,
|
||||
// a new one with the chosen mods out. Nothing is uploaded or downloaded.
|
||||
//
|
||||
// Ports of the patcher's upd.py (update file), the emulator checkout's
|
||||
// lzss_encode.py (packer) and patch_cdj900nxs.py's asset blob; the mods'
|
||||
// bytes are in builder/data.js (tools/make_builder_data.py), with the
|
||||
// SHA-256 of the finished application for every combination.
|
||||
(function () {
|
||||
'use strict';
|
||||
var D = window.BUILDER_DATA;
|
||||
var BASE = 0x08000000;
|
||||
var APP_START = 0x40000, APP_END = 0x7E0000, RECORD_DATA = 32;
|
||||
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'];
|
||||
|
||||
function fail(msg) { throw new Error(msg); }
|
||||
|
||||
// ---------- bytes ----------
|
||||
function hex(u8) {
|
||||
var s = '';
|
||||
for (var i = 0; i < u8.length; i++) s += (u8[i] < 16 ? '0' : '') + u8[i].toString(16);
|
||||
return s;
|
||||
}
|
||||
function fromHex(s) {
|
||||
var out = new Uint8Array(s.length >> 1);
|
||||
for (var i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
|
||||
return out;
|
||||
}
|
||||
function fromB64(s) {
|
||||
var bin = atob(s), out = new Uint8Array(bin.length);
|
||||
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
function equal(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
function u16(b, o) { return b[o] | b[o + 1] << 8; }
|
||||
function u32(b, o) { return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0; }
|
||||
function put16(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; }
|
||||
function put32(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; b[o + 2] = v >> 16 & 255; b[o + 3] = v >>> 24 & 255; }
|
||||
function grow(b, size) {
|
||||
if (b.length >= size) return b;
|
||||
var out = new Uint8Array(size);
|
||||
out.set(b);
|
||||
return out;
|
||||
}
|
||||
function latin1(u8) {
|
||||
var s = '';
|
||||
for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
|
||||
return s;
|
||||
}
|
||||
function ascii(s) {
|
||||
var out = new Uint8Array(s.length);
|
||||
for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
function sha256(u8) {
|
||||
return crypto.subtle.digest('SHA-256', u8).then(function (h) { return hex(new Uint8Array(h)); });
|
||||
}
|
||||
|
||||
// CRC-16/XMODEM (Python's binascii.crc_hqx with 0)
|
||||
var CRC_TABLE = (function () {
|
||||
var t = new Uint16Array(256);
|
||||
for (var i = 0; i < 256; i++) {
|
||||
var c = i << 8;
|
||||
for (var k = 0; k < 8; k++) c = c & 0x8000 ? (c << 1 ^ 0x1021) & 0xFFFF : c << 1 & 0xFFFF;
|
||||
t[i] = c;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
function crc16(u8, start, end) {
|
||||
var c = 0;
|
||||
for (var i = start; i < end; i++) c = (c << 8 & 0xFFFF) ^ CRC_TABLE[(c >> 8 ^ u8[i]) & 255];
|
||||
return c;
|
||||
}
|
||||
|
||||
// ---------- the update file (upd.py) ----------
|
||||
var HEADER = /^CDJ-900NXS (DRIV|MAIN|PANL)Ver(\d\.\d\d)/;
|
||||
|
||||
function parseSection(blob) {
|
||||
var n = blob.length;
|
||||
if (n < 36) fail('a section is truncated');
|
||||
if (crc16(blob, 0, n - 2) !== u16(blob, n - 2)) fail('a section fails its CRC');
|
||||
var header = blob.subarray(0, 32), m = HEADER.exec(latin1(header));
|
||||
if (!m) fail('unexpected section header');
|
||||
var sec = { blob: blob, header: header, name: m[1], version: m[2], records: [] };
|
||||
var end = n - 2;
|
||||
if (blob[end - 2] !== 13 || blob[end - 1] !== 10) fail(sec.name + ': the S-records do not end in a line break');
|
||||
var pos = 32, line = 0;
|
||||
while (pos < end) {
|
||||
var e = pos;
|
||||
while (!(blob[e] === 13 && blob[e + 1] === 10)) e++;
|
||||
line++;
|
||||
if (blob[pos] !== 0x53 || e - pos < 4 || (e - pos) % 2) fail(sec.name + ': S-record ' + line + ' is malformed');
|
||||
var kind = blob[pos + 1] - 48;
|
||||
if (!(kind in ADDRESS_BYTES)) fail(sec.name + ': S-record ' + line + ' has type ' + kind);
|
||||
var len = (e - pos - 2) >> 1, raw = new Uint8Array(len), sum = 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var h = blob[pos + 2 + i * 2], l = blob[pos + 3 + i * 2];
|
||||
var v = (h <= 57 ? h - 48 : h - 55) << 4 | (l <= 57 ? l - 48 : l - 55);
|
||||
raw[i] = v; sum += v;
|
||||
}
|
||||
var width = ADDRESS_BYTES[kind];
|
||||
if (len !== raw[0] + 1 || len < width + 2 || (sum & 255) !== 255) fail(sec.name + ': S-record ' + line + ' fails its length or checksum');
|
||||
var addr = 0;
|
||||
for (var k = 0; k < width; k++) addr = addr * 256 + raw[1 + k];
|
||||
sec.records.push({ kind: kind, address: addr, data: raw.subarray(1 + width, len - 1) });
|
||||
pos = e + 2;
|
||||
}
|
||||
if (sec.records[0].kind !== 0 || [7, 8, 9].indexOf(sec.records[sec.records.length - 1].kind) < 0) fail(sec.name + ': no S-record header or end');
|
||||
return sec;
|
||||
}
|
||||
function dataRecords(sec) { return sec.records.filter(function (r) { return r.kind >= 1 && r.kind <= 3; }); }
|
||||
function flashOf(sec, size) {
|
||||
size = size || APP_END;
|
||||
var img = new Uint8Array(size).fill(0xFF);
|
||||
dataRecords(sec).forEach(function (r) {
|
||||
if (r.address + r.data.length > size) fail(sec.name + ': a record reaches past 0x' + size.toString(16));
|
||||
img.set(r.data, r.address);
|
||||
});
|
||||
return img;
|
||||
}
|
||||
function parseUpd(raw) {
|
||||
var pos = 0, sizes = [];
|
||||
for (var s = 0; s < 3; s++) {
|
||||
var e = pos;
|
||||
while (e < raw.length && !(raw[e] === 13 && raw[e + 1] === 10)) e++;
|
||||
var t = latin1(raw.subarray(pos, e));
|
||||
if (!/^\d+$/.test(t)) fail('the file does not start with three section lengths');
|
||||
sizes.push(parseInt(t, 10));
|
||||
pos = e + 2;
|
||||
}
|
||||
if (pos + sizes[0] + sizes[1] + sizes[2] !== raw.length) fail('the section lengths do not add up to the file');
|
||||
var secs = sizes.map(function (n) { var b = raw.subarray(pos, pos + n); pos += n; return parseSection(b); });
|
||||
if (secs.map(function (x) { return x.name; }).join() !== 'DRIV,MAIN,PANL') fail('the sections are not DRIV, MAIN, PANL');
|
||||
return secs;
|
||||
}
|
||||
|
||||
function unpackLzss(stream) {
|
||||
var ring = new Uint8Array(4096).fill(0x20);
|
||||
ring.fill(0, 4078);
|
||||
var r = 4078, out = new Uint8Array(Math.max(1 << 20, stream.length * 3)), o = 0, pos = 0, n = stream.length;
|
||||
function room(k) { if (o + k > out.length) { var b = new Uint8Array(out.length * 2); b.set(out); out = b; } }
|
||||
while (pos < n) {
|
||||
var flags = stream[pos++];
|
||||
for (var bit = 0; bit < 8; bit++) {
|
||||
if (pos >= n) break;
|
||||
if (flags >> bit & 1) {
|
||||
room(1);
|
||||
var b1 = stream[pos++];
|
||||
out[o++] = b1; ring[r] = b1; r = r + 1 & 0xFFF;
|
||||
} else {
|
||||
if (pos + 2 > n) fail('the packed application ends inside a match');
|
||||
var lo = stream[pos], hi = stream[pos + 1];
|
||||
pos += 2;
|
||||
var at = lo | (hi & 0xF0) << 4, count = (hi & 0x0F) + 3;
|
||||
room(count);
|
||||
for (var k = 0; k < count; k++) {
|
||||
var b2 = ring[at + k & 0xFFF];
|
||||
out[o++] = b2; ring[r] = b2; r = r + 1 & 0xFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.slice(0, o);
|
||||
}
|
||||
function appRegion(flash) {
|
||||
var size = u32(flash, APP_START), end = APP_START + 4 + size;
|
||||
if (end + 2 > APP_END) fail('the packed application runs past 0x' + APP_END.toString(16));
|
||||
var sum = 0;
|
||||
for (var i = APP_START; i < end; i++) sum += flash[i];
|
||||
if (u16(flash, end) !== (sum & 0xFFFF)) fail('the packed application fails its additive checksum');
|
||||
return { app: unpackLzss(flash.subarray(APP_START + 4, end)), region: flash.subarray(APP_START, end + 2),
|
||||
stream: flash.subarray(APP_START + 4, end) };
|
||||
}
|
||||
|
||||
// The official update, checked to be exactly the 1.31 file.
|
||||
function readStock(raw) {
|
||||
return sha256(raw).then(function (h) {
|
||||
if (h !== D.stockUpdSha256) fail('This is not the original CDJ-900NXS 1.31 update file (C900NXS.UPD). Its SHA-256 is ' + h + '.');
|
||||
var secs = parseUpd(raw), main = secs[1];
|
||||
if (main.version !== '1.31' || main.header[31] !== 0x30) fail('MAIN is not 1.31 in application-only mode');
|
||||
main.records.forEach(function (r) { if ([0, 2, 7].indexOf(r.kind) < 0) fail('MAIN has unexpected record types'); });
|
||||
var flash = flashOf(main), reg = appRegion(flash);
|
||||
return sha256(reg.app).then(function (ah) {
|
||||
if (reg.app.length !== D.appSize || ah !== D.stockAppSha256) fail('the 1.31 application inside does not match');
|
||||
var boot = dataRecords(main).filter(function (r) { return r.address < APP_START; });
|
||||
boot.forEach(function (r) { if (r.address + r.data.length > APP_START) fail('a stock record crosses into the application'); });
|
||||
return { raw: raw, sections: secs, flash: flash, app: reg.app, stream: reg.stream, boot: boot };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function encodeRecord(r) {
|
||||
var width = ADDRESS_BYTES[r.kind], body = new Uint8Array(width + r.data.length + 2), sum = 0;
|
||||
body[0] = width + r.data.length + 1;
|
||||
for (var k = 0; k < width; k++) body[1 + k] = Math.floor(r.address / Math.pow(256, width - 1 - k)) & 255;
|
||||
body.set(r.data, 1 + width);
|
||||
for (var i = 0; i < body.length - 1; i++) sum += body[i];
|
||||
body[body.length - 1] = ~sum & 255;
|
||||
return 'S' + r.kind + hex(body).toUpperCase() + '\r\n';
|
||||
}
|
||||
function relabel(header, version) {
|
||||
var s = latin1(header), m = HEADER.exec(s);
|
||||
var old = m[0], neu = old.slice(0, -4) + version;
|
||||
var out = s.replace(old, neu);
|
||||
if (out.slice(0, old.length - 4) !== s.slice(0, old.length - 4) || out.slice(old.length) !== s.slice(old.length)) fail('relabelling would change more than the version');
|
||||
return ascii(out);
|
||||
}
|
||||
function encodeSection(header, records) {
|
||||
var parts = [header];
|
||||
var text = '';
|
||||
for (var i = 0; i < records.length; i++) {
|
||||
text += encodeRecord(records[i]);
|
||||
if (text.length > 1 << 20) { parts.push(ascii(text)); text = ''; }
|
||||
}
|
||||
parts.push(ascii(text));
|
||||
var n = parts.reduce(function (a, p) { return a + p.length; }, 0), body = new Uint8Array(n + 2), o = 0;
|
||||
parts.forEach(function (p) { body.set(p, o); o += p.length; });
|
||||
put16(body, n, crc16(body, 0, n));
|
||||
return body;
|
||||
}
|
||||
function join(blobs) {
|
||||
var head = ascii(blobs.map(function (b) { return b.length + '\r\n'; }).join(''));
|
||||
var n = head.length + blobs.reduce(function (a, b) { return a + b.length; }, 0), out = new Uint8Array(n), o = head.length;
|
||||
out.set(head);
|
||||
blobs.forEach(function (b) { out.set(b, o); o += b.length; });
|
||||
return out;
|
||||
}
|
||||
function packRegion(stream) {
|
||||
var n = 4 + stream.length + 2, pad = (RECORD_DATA - n % RECORD_DATA) % RECORD_DATA;
|
||||
var out = new Uint8Array(n + pad).fill(0xFF);
|
||||
put32(out, 0, stream.length);
|
||||
out.set(stream, 4);
|
||||
var sum = 0;
|
||||
for (var i = 0; i < 4 + stream.length; i++) sum += out[i];
|
||||
put16(out, 4 + stream.length, sum & 0xFFFF);
|
||||
return out;
|
||||
}
|
||||
function buildUpd(stock, stream) {
|
||||
var main = stock.sections[1], reg = packRegion(stream);
|
||||
if (APP_START + reg.length > APP_END) fail('the packed application needs ' + reg.length + ' bytes, the flash has ' + (APP_END - APP_START));
|
||||
var records = [main.records[0]].concat(stock.boot);
|
||||
for (var off = 0; off < reg.length; off += RECORD_DATA) records.push({ kind: 2, address: APP_START + off, data: reg.subarray(off, off + RECORD_DATA) });
|
||||
records.push(main.records[main.records.length - 1]);
|
||||
var blob = encodeSection(relabel(main.header, LABEL), records);
|
||||
return join([stock.sections[0].blob, blob, stock.sections[2].blob]);
|
||||
}
|
||||
function sameRecord(a, b) { return a.kind === b.kind && a.address === b.address && equal(a.data, b.data); }
|
||||
// Every check on the finished file, as upd.verify().
|
||||
function verify(raw, stock, app) {
|
||||
var done = [], secs = parseUpd(raw);
|
||||
done.push('section lengths, CRC-16 of every section, every S-record checksum');
|
||||
if (!equal(secs[0].blob, stock.sections[0].blob) || !equal(secs[2].blob, stock.sections[2].blob)) fail('DRIV or PANL differs from stock');
|
||||
done.push('DRIV and PANL byte for byte as stock');
|
||||
var main = secs[1], smain = stock.sections[1];
|
||||
if (!equal(main.header, relabel(smain.header, LABEL))) fail('the MAIN header differs beyond its version label');
|
||||
if (main.header[31] !== 0x30) fail('the MAIN header no longer selects application-only mode');
|
||||
done.push('MAIN header as stock but for the label ' + LABEL + '; application-only mode kept');
|
||||
if (!sameRecord(main.records[0], smain.records[0]) || !sameRecord(main.records[main.records.length - 1], smain.records[smain.records.length - 1])) fail('the S0 header or S7 entry record changed');
|
||||
done.push('S0 header record and S7 entry record as stock');
|
||||
var low = dataRecords(main).filter(function (r) { return r.address < APP_START; });
|
||||
if (low.length !== stock.boot.length || low.some(function (r, i) { return !sameRecord(r, stock.boot[i]); })) fail('the boot loader or recovery records changed');
|
||||
var flash = flashOf(main);
|
||||
if (!equal(flash.subarray(0, APP_START), stock.flash.subarray(0, APP_START))) fail('the boot loader or recovery bytes changed');
|
||||
done.push('boot loader and recovery (flash 0x0-0x3FFFF): every record and byte as stock');
|
||||
if (dataRecords(main).some(function (r) { return r.address < APP_START && APP_START < r.address + r.data.length; })) fail('a record crosses into the boot area');
|
||||
done.push('no record reaches past 0x7E0000 or into the boot area');
|
||||
var reg = appRegion(flash);
|
||||
done.push('packed application: additive checksum');
|
||||
if (!equal(reg.app, app)) fail('the update does not unpack to the patched application');
|
||||
done.push('unpacked with the boot loader\'s LZSS: exactly the patched application');
|
||||
done.push('flash used by the application: ' + reg.region.length + ' of ' + (APP_END - APP_START) + ' bytes');
|
||||
return done;
|
||||
}
|
||||
|
||||
// ---------- LZSS packer (lzss_encode.py), run in a worker ----------
|
||||
function lzssPack(data) {
|
||||
var RING = 4096, START = RING - 18, MIN = 3, MAX = 18, DEPTH = 48;
|
||||
var n = data.length, total = START + n;
|
||||
function cb(i) { return i < START ? 0x20 : data[i - START]; }
|
||||
var head = new Int32Array(1 << 24).fill(-1), prev = new Int32Array(RING);
|
||||
function key(p) { return cb(p) << 16 | cb(p + 1) << 8 | cb(p + 2); }
|
||||
function insert(p) {
|
||||
if (p + 3 <= total) { var k = key(p); prev[p & (RING - 1)] = head[k]; head[k] = p; }
|
||||
}
|
||||
for (var p0 = 0; p0 < START; p0++) insert(p0);
|
||||
var out = new Uint8Array(n + (n >> 3) + 64), o = 0;
|
||||
var flagAt = o++, mask = 1, items = 0;
|
||||
out[flagAt] = 0;
|
||||
var k2 = 0;
|
||||
while (k2 < n) {
|
||||
var a = START + k2, limit = Math.min(MAX, total - a), bestLen = 0, bestPos = 0;
|
||||
if (limit >= MIN) {
|
||||
var lo = a - RING, p = head[key(a)], checked = 0;
|
||||
while (p >= 0 && p >= lo) {
|
||||
var len = 0;
|
||||
while (len < limit && cb(p + len) === cb(a + len)) len++;
|
||||
if (len > bestLen) {
|
||||
bestLen = len; bestPos = p;
|
||||
if (bestLen >= limit) break;
|
||||
}
|
||||
if (++checked >= DEPTH) break;
|
||||
p = prev[p & (RING - 1)];
|
||||
}
|
||||
}
|
||||
var step;
|
||||
if (bestLen >= MIN) {
|
||||
step = bestLen;
|
||||
var off = bestPos & (RING - 1);
|
||||
out[o++] = off & 0xFF;
|
||||
out[o++] = (off >> 4 & 0xF0) | (step - MIN);
|
||||
} else {
|
||||
step = 1;
|
||||
out[flagAt] |= mask;
|
||||
out[o++] = cb(a);
|
||||
}
|
||||
for (var t = 0; t < step; t++) insert(a + t);
|
||||
k2 += step;
|
||||
items++;
|
||||
mask = mask << 1 & 0xFF;
|
||||
if (mask === 0) {
|
||||
mask = 1;
|
||||
if (k2 < n) { flagAt = o++; out[flagAt] = 0; }
|
||||
}
|
||||
}
|
||||
return out.slice(0, o);
|
||||
}
|
||||
function packInWorker(app) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var src = lzssPack.toString() + '\nonmessage = function (e) { var o = lzssPack(new Uint8Array(e.data)); postMessage(o.buffer, [o.buffer]); };';
|
||||
var w;
|
||||
try {
|
||||
w = new Worker(URL.createObjectURL(new Blob([src], { type: 'text/javascript' })));
|
||||
} catch (e) {
|
||||
setTimeout(function () { try { resolve(lzssPack(app)); } catch (x) { reject(x); } }, 30);
|
||||
return;
|
||||
}
|
||||
w.onmessage = function (e) { w.terminate(); resolve(new Uint8Array(e.data)); };
|
||||
w.onerror = function (e) { w.terminate(); reject(new Error('packing failed: ' + e.message)); };
|
||||
var copy = app.slice();
|
||||
w.postMessage(copy.buffer, [copy.buffer]);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- arnoldasr's asset blob (patch_cdj900nxs.assets) ----------
|
||||
var TABLE = 0x08100000, DESCRIPTOR = 0x2C, ORIGINAL_COUNT = 0x2A1, TEMPLATE = 0x1C8, DATA_BASE = 0x08FC2D00;
|
||||
var WIDTH = 200, HEIGHT = 24, MARKER_WIDTH = 8, BAR = 5, KEY = 0xF81F;
|
||||
var ORANGE_LINE = [120, 65, 20], ORANGE_MARK = [255, 140, 0], BLUE_LINE = [40, 90, 120], BLUE_MARK = [60, 170, 255];
|
||||
var WAVE_COLOURS = [
|
||||
448, 457, 47, 458, 449, 111, 495, 466, 175, 467, 487, 239, 303, 479, 367, 459, 456, 431,
|
||||
468, 465, 475, 476, 474, 39, 477, 31, 471, 46, 469, 45, 23, 460, 109, 484, 110, 485, 103,
|
||||
478, 483, 493, 494, 486, 95, 15, 450, 167, 492, 173, 174, 231, 115, 470, 238, 44, 237, 159,
|
||||
295, 87, 108, 302, 51, 301, 491, 482, 359, 223, 366, 365, 236, 300, 179, 423, 461, 287, 430,
|
||||
429, 172, 364, 473, 428, 351, 114, 151, 463, 299, 363, 415, 215, 427, 178, 490, 50, 279, 235,
|
||||
79, 462, 451, 343, 511, 298, 362, 400, 52, 116, 426, 234, 407, 7, 143, 180, 207, 113, 233,
|
||||
243, 481, 57, 271, 452, 417, 177];
|
||||
var WHITENESS = [[150, 20, 20], [220, 40, 30], [255, 110, 20], [255, 190, 30], [170, 230, 60],
|
||||
[40, 210, 170], [60, 160, 255], [225, 240, 255]];
|
||||
function rgb565(c) { return (c[0] >> 3) << 11 | (c[1] >> 2) << 5 | c[2] >> 3; }
|
||||
function lineImage(colour) {
|
||||
var px = new Array(WIDTH * HEIGHT).fill(KEY), c = rgb565(colour);
|
||||
for (var x = 2; x < WIDTH - 2; x++) px[12 * WIDTH + x] = c;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var xx = 2 + Math.floor(i * (WIDTH - 5) / 4);
|
||||
for (var y = 7; y < 18; y++) px[y * WIDTH + xx] = c;
|
||||
}
|
||||
return px;
|
||||
}
|
||||
function markerImage(colour) {
|
||||
var px = [], c = rgb565(colour);
|
||||
for (var y = 0; y < HEIGHT; y++) for (var x = 0; x < MARKER_WIDTH; x++) px.push(y >= 3 && y <= 21 && x < BAR ? c : KEY);
|
||||
return px;
|
||||
}
|
||||
function rgb9(code) { return [6, 3, 0].map(function (s) { return Math.floor(((code >> s) & 7) * 255 / 7); }); }
|
||||
function assets(app) {
|
||||
var images = [[WIDTH, lineImage(ORANGE_LINE)], [WIDTH, lineImage(BLUE_LINE)],
|
||||
[MARKER_WIDTH, markerImage(ORANGE_MARK)], [MARKER_WIDTH, markerImage(BLUE_MARK)]];
|
||||
var t0 = TABLE - BASE, count = ORIGINAL_COUNT + images.length, tableBytes = count * DESCRIPTOR;
|
||||
var pixelsBase = DATA_BASE + ((tableBytes + 31) & ~31);
|
||||
var template = app.subarray(t0 + TEMPLATE * DESCRIPTOR, t0 + (TEMPLATE + 1) * DESCRIPTOR);
|
||||
var pixLen = 0;
|
||||
images.forEach(function (im) { var l = im[1].length * 2; pixLen += l + (32 - l % 32) % 32; });
|
||||
var blobLen = (pixelsBase - DATA_BASE) + pixLen, palette = 128 * 2, lut = 512;
|
||||
var blob = new Uint8Array(blobLen + palette + lut);
|
||||
blob.set(app.subarray(t0, t0 + ORIGINAL_COUNT * DESCRIPTOR));
|
||||
var pix = pixelsBase - DATA_BASE, pl = 0;
|
||||
images.forEach(function (im, i) {
|
||||
var e = ORIGINAL_COUNT * DESCRIPTOR + i * DESCRIPTOR;
|
||||
blob.set(template, e);
|
||||
put16(blob, e + 4, im[0]); put16(blob, e + 6, HEIGHT);
|
||||
put32(blob, e + 0x1C, (ORIGINAL_COUNT + i) * DESCRIPTOR);
|
||||
put32(blob, e + 0x20, pixelsBase + pl - TABLE);
|
||||
im[1].forEach(function (v, j) { put16(blob, pix + pl + j * 2, v); });
|
||||
pl += im[1].length * 2;
|
||||
pl += (32 - pl % 32) % 32;
|
||||
});
|
||||
var paletteAt = DATA_BASE + blobLen;
|
||||
var colours = WHITENESS.concat(WAVE_COLOURS.map(rgb9));
|
||||
colours.forEach(function (c, i) { put16(blob, blobLen + i * 2, rgb565(c)); });
|
||||
for (var code = 0; code < 512; code++) {
|
||||
var r = code >> 6 & 7, g = code >> 3 & 7, b = code & 7, best = 0, bd = Infinity;
|
||||
for (var i = 0; i < WAVE_COLOURS.length; i++) {
|
||||
var w = WAVE_COLOURS[i], d = Math.pow((w >> 6 & 7) - r, 2) + Math.pow((w >> 3 & 7) - g, 2) + Math.pow((w & 7) - b, 2);
|
||||
if (d < bd) { bd = d; best = i; }
|
||||
}
|
||||
blob[blobLen + palette + code] = 8 + best;
|
||||
}
|
||||
return { blob: blob, phaseEnd: blobLen, paletteAt: paletteAt };
|
||||
}
|
||||
|
||||
// ---------- the mods' bytes (builder/data.js) ----------
|
||||
function boxPixels(stock, res, bg, ink) {
|
||||
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;
|
||||
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++) {
|
||||
if (text[c] === ' ') { x0 += B.space; continue; }
|
||||
var g = B.glyphs[text[c]];
|
||||
for (var gy = 0; gy < g.length; gy++) for (var gx = 0; gx < g[gy].length; gx++) if (g[gy][gx] === 'X') px[(y0 + gy) * w + x0 + gx] = ink;
|
||||
x0 += g[0].length + B.gap;
|
||||
}
|
||||
var out = new Uint8Array(w * h * 2);
|
||||
for (i = 0; i < w * h; i++) put16(out, i * 2, px[i]);
|
||||
return out;
|
||||
}
|
||||
// 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.
|
||||
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] === 'assets') { blob = blob || assets(stock).blob; out.set(blob.subarray(0, o[2]), off); }
|
||||
else fail('unknown operation ' + o[0]);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- the whole build ----------
|
||||
function patchApp(stock, names, log) {
|
||||
var up = UPSTREAM.filter(function (n) { return names.indexOf(n) >= 0; });
|
||||
var ours = OURS.filter(function (n) { return names.indexOf(n) >= 0; });
|
||||
var app = stock;
|
||||
if (up.length) {
|
||||
app = applyVariant(app, stock, D.upstream[up.join(',')]);
|
||||
log('arnoldasr\'s mods applied (' + up.join(', ') + ')');
|
||||
}
|
||||
if (ours.length) {
|
||||
app = applyVariant(app, stock, D.ours[ours.join(',')]);
|
||||
log('our mods applied (' + ours.join(', ') + ')');
|
||||
}
|
||||
var key = up.concat(ours).join(',');
|
||||
return sha256(app).then(function (h) {
|
||||
if (h !== D.expect[key]) fail('the patched application is not the one the patcher makes (SHA-256 ' + h + ')');
|
||||
log('the patched application is exactly the patcher\'s (SHA-256 checked)');
|
||||
return app;
|
||||
});
|
||||
}
|
||||
|
||||
function build(raw, names, log) {
|
||||
var stock;
|
||||
log('checking the update file...');
|
||||
return readStock(raw).then(function (s) {
|
||||
stock = s;
|
||||
log('the official CDJ-900NXS 1.31 update: SHA-256 checked, application unpacked and checked');
|
||||
if (!names.length) return { app: stock.app, stream: stock.stream };
|
||||
return patchApp(stock.app, names, log).then(function (app) {
|
||||
log('packing the application (LZSS), this takes a while...');
|
||||
var t = Date.now();
|
||||
return packInWorker(app).then(function (stream) {
|
||||
log('packed ' + app.length + ' -> ' + stream.length + ' bytes in ' + Math.round((Date.now() - t) / 1000) + ' s');
|
||||
return { app: app, stream: stream };
|
||||
});
|
||||
});
|
||||
}).then(function (r) {
|
||||
log('writing the update file...');
|
||||
var out = buildUpd(stock, r.stream);
|
||||
var checks = verify(out, stock, r.app);
|
||||
return Promise.all([sha256(out), sha256(r.app)]).then(function (h) {
|
||||
return { upd: out, checks: checks, updSha: h[0], appSha: h[1] };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, OURS: OURS, lzssPack: lzssPack, unpackLzss: unpackLzss };
|
||||
})();
|
||||
@@ -0,0 +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
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// The builder page: file, mods with their dependencies, the two
|
||||
// confirmations, build and download (the work is in builder.js).
|
||||
(function () {
|
||||
'use strict';
|
||||
var MODS = [
|
||||
{ id: 'extended_bpm', name: 'Extended BPM', by: 'arnoldasr', state: 'stable',
|
||||
what: 'BPM with two decimals: the tenths as a small digit above, the hundredths below.' },
|
||||
{ id: 'phasemeter', name: 'Phase meter', by: 'arnoldasr', state: 'stable',
|
||||
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: '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: 'credit', name: 'Boot text', by: 'ours', state: 'stable',
|
||||
what: 'The boot screen says “Pionaarr Mods 1.0” instead of “Ver 1.31”.' }
|
||||
];
|
||||
var chosen = {};
|
||||
MODS.forEach(function (m) { chosen[m.id] = m.state === 'stable'; });
|
||||
var file = null;
|
||||
|
||||
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; }
|
||||
|
||||
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;
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
var box = byId('picks');
|
||||
box.innerHTML = '';
|
||||
MODS.forEach(function (m) {
|
||||
var row = document.createElement('label');
|
||||
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-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>' +
|
||||
'<span class="state ' + m.state + '">' + m.state + '</span>';
|
||||
row.querySelector('input').addEventListener('change', function (e) { toggle(m.id, e.target.checked); });
|
||||
box.appendChild(row);
|
||||
});
|
||||
ready();
|
||||
}
|
||||
|
||||
function ready() {
|
||||
var why = !file ? 'Choose the update file first.'
|
||||
: !byId('agree-risk').checked || !byId('agree-ai').checked ? 'Confirm both points above to build.' : '';
|
||||
byId('build').disabled = !!why;
|
||||
byId('build-why').textContent = why;
|
||||
}
|
||||
|
||||
function setFile(f) {
|
||||
file = f;
|
||||
byId('file-state').textContent = f ? f.name + ' · ' + (f.size / 1048576).toFixed(1) + ' MB (checked when you build)' : 'No file yet';
|
||||
byId('drop').classList.toggle('has', !!f);
|
||||
byId('result').hidden = true;
|
||||
ready();
|
||||
}
|
||||
|
||||
function log(msg, cls) {
|
||||
var l = byId('log');
|
||||
l.hidden = false;
|
||||
var p = document.createElement('div');
|
||||
if (cls) p.className = cls;
|
||||
p.textContent = msg;
|
||||
l.appendChild(p);
|
||||
l.scrollTop = l.scrollHeight;
|
||||
}
|
||||
|
||||
var url = null;
|
||||
function run() {
|
||||
var names = MODS.map(function (m) { return m.id; }).filter(function (id) { return chosen[id]; });
|
||||
byId('build').disabled = true;
|
||||
byId('result').hidden = true;
|
||||
byId('log').innerHTML = '';
|
||||
log('mods: ' + (names.length ? names.join(', ') : 'none (stock)'));
|
||||
file.arrayBuffer().then(function (buf) {
|
||||
return window.CDJBuilder.build(new Uint8Array(buf), names, log);
|
||||
}).then(function (r) {
|
||||
log('done: every check passed', 'ok');
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
url = URL.createObjectURL(new Blob([r.upd], { type: 'application/octet-stream' }));
|
||||
byId('download').href = url;
|
||||
byId('sum').innerHTML = 'SHA-256 of the file <code>' + r.updSha + '</code><br>of the application <code>' + r.appSha + '</code>';
|
||||
var ul = byId('checks');
|
||||
ul.innerHTML = '';
|
||||
r.checks.forEach(function (c) { var li = document.createElement('li'); li.textContent = c; ul.appendChild(li); });
|
||||
byId('result').hidden = false;
|
||||
}).catch(function (e) {
|
||||
log('stopped: ' + e.message, 'bad');
|
||||
}).then(ready);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
render();
|
||||
byId('file').addEventListener('change', function (e) { setFile(e.target.files[0] || null); });
|
||||
var drop = byId('drop');
|
||||
['dragenter', 'dragover'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.add('over'); }); });
|
||||
['dragleave', 'drop'].forEach(function (t) { drop.addEventListener(t, function (e) { e.preventDefault(); drop.classList.remove('over'); }); });
|
||||
drop.addEventListener('drop', function (e) { if (e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]); });
|
||||
byId('agree-risk').addEventListener('change', ready);
|
||||
byId('agree-ai').addEventListener('change', ready);
|
||||
byId('build').addEventListener('click', run);
|
||||
});
|
||||
})();
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CDJ-900NXS Mods Docs</title>
|
||||
<meta name="description" content="How to install the CDJ-900NXS mods and how each one works, including the hot cue keys: play, gate, set and delete.">
|
||||
<meta name="description" content="How to build and install the CDJ-900NXS mods and how each one works, including the hot cue keys: play, gate, set and delete.">
|
||||
<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">
|
||||
@@ -18,6 +18,8 @@
|
||||
<a class="brand" href="index.html"><span class="disc" aria-hidden="true"></span><span>CDJ-900NXS Mods</span></a>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<a href="index.html">Mods</a>
|
||||
<a href="builder.html">Builder</a>
|
||||
<a href="upcoming.html">Upcoming</a>
|
||||
<a href="docs.html" aria-current="page">Docs</a>
|
||||
</nav>
|
||||
<button class="theme-btn" type="button" aria-label="Switch light / dark">☀</button>
|
||||
@@ -28,16 +30,19 @@
|
||||
<nav class="toc" aria-label="On this page">
|
||||
<div class="h">Start</div>
|
||||
<a href="#install">Install</a>
|
||||
<a href="#cli">Command line</a>
|
||||
<a href="#stock">Back to stock</a>
|
||||
<div class="h">Mods</div>
|
||||
<a href="#code">code</a>
|
||||
<a href="#utility">utility</a>
|
||||
<a href="#overview">overview</a>
|
||||
<a href="#rgb_waveform">rgb_waveform</a>
|
||||
<a href="#extended_bpm">extended_bpm</a>
|
||||
<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="#credit">credit</a>
|
||||
<div class="h">Hot cue keys</div>
|
||||
<a href="#hk-mode">The hot key mode</a>
|
||||
<a href="#hk-load">Loading</a>
|
||||
<a href="#hk-play">Play a hot cue</a>
|
||||
<a href="#hk-gate">Gate</a>
|
||||
<a href="#hk-set">Set a hot cue</a>
|
||||
@@ -45,60 +50,59 @@
|
||||
<a href="#hk-leds">The LEDs</a>
|
||||
<a href="#hk-limits">Good to know</a>
|
||||
<div class="h">More</div>
|
||||
<a href="#status">Test status</a>
|
||||
<a href="#status">Status</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
</nav>
|
||||
|
||||
<article class="prose">
|
||||
<div class="eyebrow">Documentation</div>
|
||||
<h1>Using the mods</h1>
|
||||
<p>Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware <strong>1.31</strong>; the patcher only accepts that update.</p>
|
||||
<p>Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware <strong>1.31</strong>; the builder only accepts that update.</p>
|
||||
|
||||
<!-- ===================== install ===================== -->
|
||||
<h2 id="install">Install</h2>
|
||||
<ol>
|
||||
<li>Get the official <code>C900NXS.UPD</code> for firmware 1.31 from Pioneer DJ. The patcher checks its SHA-256 and refuses any other file.</li>
|
||||
<li>Run the patcher with Python 3. Tick the mods with the arrow keys and <kbd>Space</kbd>, build with <kbd>Enter</kbd>.
|
||||
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD</div>
|
||||
Ticking a mod also ticks what it needs (<code>utility</code> needs <code>code</code>, <code>hotcuekeys</code> needs <code>cuecolours</code>); unticking one unticks what depends on it (<code>beatjump</code> needs <code>hotcuekeys</code>). <code>credit</code> (ticked at the start) makes the boot screen say <em>Pionope Mods by Givou</em> where it said <em>Ver 1.31</em>; untick it to keep the stock text.
|
||||
</li>
|
||||
<li>Read the disclaimer and type <code>yes</code> to accept it. Nothing is built without it.</li>
|
||||
<li>The new file is written to <code>out/<date>-<mods>/C900NXS.UPD</code>, with a <code>report.txt</code> listing every check.</li>
|
||||
<li>Copy <code>C900NXS.UPD</code> to the root of a FAT32 USB stick and run the update from the player as you would for an official update.</li>
|
||||
<li>Get the official <code>C900NXS.UPD</code> for firmware 1.31 from Pioneer DJ.</li>
|
||||
<li>Open the <a href="builder.html">builder</a> and choose the file. It checks its SHA-256 and refuses any other file.</li>
|
||||
<li>Tick the mods. Ticking a mod also ticks what it needs (<code>hotcuekeys</code> needs <code>cuecolours</code>); unticking one unticks what needs it.</li>
|
||||
<li>Confirm the two points (no liability, code written with Claude Code) and build. Everything happens in your browser: nothing is uploaded, nothing is downloaded.</li>
|
||||
<li>Download <code>C900NXS.UPD</code>, copy it to the root of a FAT32 USB stick and run the update from the player as you would for an official update.</li>
|
||||
</ol>
|
||||
<div class="callout"><strong>What the patcher guarantees.</strong> Only the main application is replaced. The boot loader and recovery loader (flash 0x0–0x3FFFF), the S-record header and entry record, and the DRIV and PANL sections are copied from the original byte for byte and checked again on the finished file. The file is labelled 1.32 so that the player does not skip it as the version it already has; the player still reports 1.31.</div>
|
||||
<p>Without a terminal that can read single keys, name the mods instead and accept the disclaimer with <code>--accept</code>:</p>
|
||||
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys --accept</div>
|
||||
<div class="callout"><strong>What the builder guarantees.</strong> Only the main application is replaced. The boot loader and recovery loader (flash 0x0–0x3FFFF), the S-record header and entry record, and the DRIV and PANL sections are copied from the original byte for byte and checked again on the finished file, which is also unpacked again and compared with the patched application. The patched application is checked against the SHA-256 of the one the Python patcher makes for the same mods, so the file is the same byte for byte. The file is labelled 1.32 so that the player does not skip it as the version it already has; the player still reports 1.31.</div>
|
||||
|
||||
<h2 id="cli">Command line</h2>
|
||||
<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>
|
||||
|
||||
<h2 id="stock">Back to stock</h2>
|
||||
<p>Build the original application, relabelled so the player accepts it, and update as above:</p>
|
||||
<div class="term"><span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --stock</div>
|
||||
<p>Keep this file on a stick before you try any mod.</p>
|
||||
<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>
|
||||
|
||||
<!-- ===================== mods ===================== -->
|
||||
<h2 id="code">code <span class="badge author">arnoldasr</span></h2>
|
||||
<p>Three display changes tested together as one build: the detail waveform in RGB colours, the BPM shown with two decimals (stacked beside the tempo) and a two-line phase meter.</p>
|
||||
<h2 id="rgb_waveform">rgb_waveform <span class="badge author">arnoldasr</span></h2>
|
||||
<p>The big waveform in RGB colours from the track's colour waveform data, and the overview's colour table as a red-to-white gradient.</p>
|
||||
<figure class="shot"><img src="assets/detail-mods.png" alt="RGB detail waveform"><figcaption>RGB detail waveform.</figcaption></figure>
|
||||
|
||||
<h2 id="utility">utility <span class="badge author">arnoldasr</span></h2>
|
||||
<p>UTILITY › SLIP FLASHING becomes <strong>PHASE METER</strong>; its value chooses which phase meter is shown. Needs <code>code</code>.</p>
|
||||
<figure class="shot"><img src="assets/utility-phase.png" alt="UTILITY with PHASE METER"><figcaption>UTILITY › PHASE METER.</figcaption></figure>
|
||||
<h2 id="extended_bpm">extended_bpm <span class="badge author">arnoldasr</span></h2>
|
||||
<p>The BPM to two decimals: the tenths as a small digit above, the hundredths in the place of the stock decimal.</p>
|
||||
|
||||
<h2 id="overview">overview <span class="badge author">arnoldasr</span></h2>
|
||||
<p>The overview waveform's colour table becomes a red-to-white gradient. Data only, no code.</p>
|
||||
<h2 id="phasemeter">phasemeter <span class="badge author">arnoldasr</span></h2>
|
||||
<p>A two-line phase meter instead of the four boxes: orange for the master, blue for this deck, each with a marker. UTILITY › SLIP FLASHING becomes <strong>PHASE METER</strong>: ON shows the lines, OFF the stock boxes. The orange marker needs another player as tempo master.</p>
|
||||
<figure class="shot"><img src="assets/utility-phase.png" alt="UTILITY with PHASE METER"><figcaption>UTILITY › PHASE METER.</figcaption></figure>
|
||||
|
||||
<h2 id="cuecolours">cuecolours <span class="badge author">ours</span></h2>
|
||||
<p>When a track is loaded the player asks its database for the cues. This mod also asks for the extended cue list (the one newer players read from rekordbox's export), which carries the colours and the hot cues.</p>
|
||||
<ul>
|
||||
<li><strong>Memory cues</strong> are drawn in the colour set in rekordbox, on the overview and on the detail waveform. A memory cue without a colour, or one stored on the player, stays red.</li>
|
||||
<li><strong>Hot cues A–H</strong> are drawn as small boxes with their letter, in their rekordbox colour (green when none was set).</li>
|
||||
<li><strong>Memory cues</strong> are drawn in the colour set in rekordbox, on the overview and on the big waveform. A memory cue without a colour, or one stored on the player, stays red.</li>
|
||||
<li><strong>Hot cues A–H</strong> are drawn as small boxes with their letter, in their rekordbox colour.</li>
|
||||
</ul>
|
||||
<figure class="shot"><img src="assets/hotcue-overview.png" alt="Overview with coloured memory cues and hot cues A to F"><figcaption>Coloured memory cues and hot cues A–F on the overview.</figcaption></figure>
|
||||
|
||||
<!-- ===================== hot cue keys ===================== -->
|
||||
<h2 id="hotcuekeys">hotcuekeys <span class="badge author">ours</span></h2>
|
||||
<h2 id="hotcuekeys">hotcuekeys <span class="badge author">ours</span> <span class="state experimental">experimental</span></h2>
|
||||
<p>The CDJ-900NXS has no hot cue pads. This mod lends it three: the top three <strong>BEAT DIVIDE</strong> keys left of the jog. It needs <code>cuecolours</code>, which reads the hot cues from your USB.</p>
|
||||
<figure class="shot"><img src="assets/utility-hotcue.png" alt="UTILITY with HOT CUE KEYS"><figcaption>UTILITY › HOT CUE KEYS, the new last item.</figcaption></figure>
|
||||
<figure class="shot"><img src="assets/utility-hotcue.png" alt="UTILITY with HOT CUE KEYS"><figcaption>UTILITY › HOT CUE KEYS.</figcaption></figure>
|
||||
|
||||
<h3 id="hk-mode">The hot key mode</h3>
|
||||
<ol>
|
||||
@@ -115,14 +119,22 @@
|
||||
<div><div class="b">–</div><div class="f">1/4</div></div>
|
||||
<div><div class="b">–</div><div class="f">1/8</div></div>
|
||||
</div>
|
||||
<p>1/4 and 1/8 do nothing in the mode and stay dark: the player has three hot cue buffers, and A–C keep them, so each of the three starts at once.</p>
|
||||
<p>With the mode off, or HOT CUE KEYS set to OFF, the keys do what they always did.</p>
|
||||
<p>1/4 and 1/8 do nothing in the mode and stay dark: the player has three hot cue buffers, and A–C keep them. With the mode off, or HOT CUE KEYS set to OFF, the keys do what they always did.</p>
|
||||
|
||||
<h3 id="hk-load">Loading</h3>
|
||||
<p>After a track is loaded and the deck stands at its CUE point, hot cues A, B and C are buffered one after another, about a second each, like the CDJ-2000NXS does. Meanwhile:</p>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<h3 id="hk-play">Play a hot cue</h3>
|
||||
<p>Press the key <strong>briefly</strong>. The deck jumps to the hot cue and plays — also when it was paused.</p>
|
||||
<p>Press the key <strong>briefly</strong>. The deck jumps to the hot cue and plays — also from a pause, in CDJ and in VINYL mode.</p>
|
||||
|
||||
<h3 id="hk-gate">Gate</h3>
|
||||
<p>Press the key and <strong>keep holding it</strong>. The deck jumps and starts playing at once, exactly as with a short press; nothing waits. When you let go:</p>
|
||||
<p>Press the key and <strong>keep holding it</strong>. The deck jumps and starts playing at once, exactly as with a short press. When you let go:</p>
|
||||
<ul>
|
||||
<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>
|
||||
@@ -132,7 +144,8 @@
|
||||
<p>On a key <strong>without</strong> a hot cue (LED off):</p>
|
||||
<ol>
|
||||
<li>Pause the deck at the place you want.</li>
|
||||
<li>Hold the key for <strong>0.8 s</strong>. The hot cue is set there in green, its LED blinks twice and then stays dim, and the box appears on the waveforms.</li>
|
||||
<li>Hold the key for <strong>0.8 s</strong>. The hot cue is set there — A in green, B in orange, C in blue — its LED blinks twice and then stays dim, and the box appears on the waveforms.</li>
|
||||
<li>It is buffered at once, so it starts instantly from the first press. The deck then stands at the new hot cue; PLAY plays from there.</li>
|
||||
</ol>
|
||||
<p>Keys that already have a hot cue are not overwritten — delete the old one first.</p>
|
||||
|
||||
@@ -149,68 +162,61 @@
|
||||
<div class="leds">
|
||||
<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> two bright blinks — pressed, set or deleted</span>
|
||||
<span class="led"><i class="blink"></i> blinking — pressed, set, deleted or being loaded</span>
|
||||
</div>
|
||||
|
||||
<h3 id="hk-limits">Good to know</h3>
|
||||
<ul>
|
||||
<li><strong>Set and deleted hot cues are saved on the USB stick</strong>, in the track's rekordbox analysis files, the same way MEMORY and DELETE keep memory cues. Like MEMORY, this works once the track has settled, a few seconds after loading.</li>
|
||||
<li><strong>A–C start at once.</strong> After a track is loaded, once the deck stands at its CUE point for a second in the hot key mode, hot cues A, B and C are loaded silently into the player's three hot cue buffers (about three seconds) and the deck goes back to its CUE point. If you press PLAY or CUE meanwhile, it waits for that (PLAY then plays); a hot cue key pressed meanwhile cuts it short and plays.</li>
|
||||
<li>A hot cue you set later loads into its own buffer on its first press (about a second) and starts at once after that.</li>
|
||||
<li>Hot cues D–H from rekordbox are shown on the waveforms but have no key.</li>
|
||||
<li><strong>Set and deleted hot cues are saved on the USB stick</strong>, in the track's rekordbox analysis files with their colours, the same way MEMORY and DELETE keep memory cues; the player shows <em>MEMORY</em> for each. Like MEMORY, this works once the track has settled. Let the last one finish before you load the next track.</li>
|
||||
<li>A hot cue played from a buffer leaves the <strong>CUE point</strong> where it is. A gate's release stands at the hot cue; PLAY then plays from there.</li>
|
||||
<li>A key pressed while a hot cue is still loading waits for it (the last key pressed wins).</li>
|
||||
<li>Hot cues D–H from rekordbox are shown on the waveforms but have no key.</li>
|
||||
<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 emu">emulator only</span></h2>
|
||||
<p>With UTILITY › BEAT JUMP set to 1, 2, 4 or 8 (the item replaces SCREEN SAVER; the screen saver stays on), TRACK SEARCH <kbd>⏮</kbd> <kbd>⏭</kbd> jumps that many beats back or forward along the beat grid, keeping the position within the beat. OFF restores TRACK SEARCH.</p>
|
||||
<ul>
|
||||
<li><strong>While playing</strong>, the jump target is loaded into a buffer of the player that only beat jumps use, so hot cues A–C stay ready. There is a short silence while it loads, but the deck comes back <strong>in phase</strong>, as if it had played on through the jump: the load time is measured on every jump and allowed for, at the deck's tempo. The CUE point stays.</li>
|
||||
<li><strong>Paused</strong>, the jump is a CALL: the deck and its CUE point move by that many beats.</li>
|
||||
<li>A press while a jump or a hot cue is still loading is ignored, so loads never overlap.</li>
|
||||
<li>A jump while a loop plays ends the loop; LOOP IN / OUT work again right after a jump.</li>
|
||||
<li>Needs <code>hotcuekeys</code> (its engine makes the jumps); the HOT CUE KEYS item may stay OFF.</li>
|
||||
</ul>
|
||||
<div class="callout">Rebuilt: the first version jumped off the grid on a player and, after quick presses, reported a corrupted file. The new one has only run in the emulator so far; please report how it behaves on a player.</div>
|
||||
|
||||
<!-- ===================== status ===================== -->
|
||||
<h2 id="ledsaver">ledsaver <span class="badge emu">emulator only</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 three seconds:</p>
|
||||
<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>
|
||||
<li>the BEAT DIVIDE keys fade in a wave from top to bottom and back up,</li>
|
||||
<li>SYNC and MASTER, MASTER TEMPO and DISC EJECT fade in and out against each other,</li>
|
||||
<li>CUE, PLAY, REV, SLIP, the SLIP LOOP bar and RELOOP/EXIT light one after another,</li>
|
||||
<li>the jog's three rings light in turn: inner, middle, outer, middle, inner …</li>
|
||||
</ul>
|
||||
<p>Any key ends it, as it ends the screen saver. The keys without levels (CUE, PLAY …) can only switch on and off; the rings too.</p>
|
||||
<p>Any key ends it, as it ends the screen saver.</p>
|
||||
|
||||
<h2 id="status">Test status</h2>
|
||||
<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>
|
||||
|
||||
<!-- ===================== status ===================== -->
|
||||
<h2 id="status">Status</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Mod</th><th>By</th><th>Status</th></tr></thead>
|
||||
<thead><tr><th>Mod</th><th>By</th><th>Status</th><th>Player</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>code</code></td><td>arnoldasr</td><td><span class="badge ok">ran on a player</span></td></tr>
|
||||
<tr><td><code>utility</code></td><td>arnoldasr</td><td><span class="badge ok">ran on a player</span></td></tr>
|
||||
<tr><td><code>overview</code></td><td>arnoldasr</td><td><span class="badge ok">ran on a player</span></td></tr>
|
||||
<tr><td><code>cuecolours</code></td><td>ours</td><td><span class="badge ok">ran on a player</span></td></tr>
|
||||
<tr><td><code>hotcuekeys</code></td><td>ours</td><td><span class="badge warn">partly tested</span> mode, HOT KEYS box and LEDs on a player; the hot cue buffers (A–C at once), gate, set and delete in the emulator</td></tr>
|
||||
<tr><td><code>beatjump</code></td><td>ours</td><td><span class="badge emu">emulator only</span> rebuilt on the hot cue engine</td></tr>
|
||||
<tr><td><code>ledsaver</code></td><td>ours</td><td><span class="badge emu">emulator only</span> LED map from the LED tests on a player</td></tr>
|
||||
<tr><td><code>credit</code></td><td>ours</td><td><span class="badge emu">emulator only</span> boot screen text (ticked at the start, can be unticked)</td></tr>
|
||||
<tr><td><code>rgb_waveform</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>extended_bpm</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>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>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>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>
|
||||
</div>
|
||||
|
||||
<h2 id="faq">FAQ</h2>
|
||||
<h3>Does this touch the boot loader?</h3>
|
||||
<p>No. The patcher copies the boot loader and recovery loader from the official file byte for byte and checks them again before writing. Only the main application changes.</p>
|
||||
<p>No. The builder copies the boot loader and recovery loader from the official file byte for byte and checks them again before you can download it. Only the main application changes.</p>
|
||||
<h3>Is my firmware uploaded anywhere?</h3>
|
||||
<p>No. The builder runs entirely in your browser; everything it needs comes with the page.</p>
|
||||
<h3>Can I go back?</h3>
|
||||
<p>Yes, build <code>--stock</code> and run the update. Keep that file ready before flashing mods.</p>
|
||||
<p>Yes, build with no mod ticked (or <code>--stock</code>) and run the update. Keep that file ready before flashing mods.</p>
|
||||
<h3>Do I need rekordbox?</h3>
|
||||
<p>Cue colours and hot cues come from a USB stick exported with rekordbox. Without them the mods show what the stock player shows.</p>
|
||||
<h3>Why is the version still 1.31?</h3>
|
||||
<p>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 <em>Pionope Mods by Givou</em> instead of <em>Ver 1.31</em>.</p>
|
||||
<p>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 <em>Pionaarr Mods 1.0</em> instead of <em>Ver 1.31</em>.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
@@ -218,7 +224,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 mods <code>beatjump</code>, <code>cuecolours</code> and <code>hotcuekeys</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>code</code>, <code>utility</code> and <code>overview</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> 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>
|
||||
|
||||
+94
-73
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CDJ-900NXS Mods</title>
|
||||
<meta name="description" content="Firmware mods for the Pioneer CDJ-900NXS (firmware 1.31): RGB waveform, cue colours, hot cues on the beat divide keys and more — compared with stock.">
|
||||
<meta name="description" content="Firmware mods for the Pioneer CDJ-900NXS (firmware 1.31): RGB waveform, extended BPM, phase meter, cue colours, hot cues on the beat divide keys and more — compared with stock, built in your browser.">
|
||||
<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">
|
||||
@@ -18,6 +18,8 @@
|
||||
<a class="brand" href="index.html"><span class="disc" aria-hidden="true"></span><span>CDJ-900NXS Mods</span></a>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<a href="index.html" aria-current="page">Mods</a>
|
||||
<a href="builder.html">Builder</a>
|
||||
<a href="upcoming.html">Upcoming</a>
|
||||
<a href="docs.html">Docs</a>
|
||||
</nav>
|
||||
<button class="theme-btn" type="button" aria-label="Switch light / dark">☀</button>
|
||||
@@ -31,13 +33,13 @@
|
||||
<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, rekordbox cue colours, visible hot cues and
|
||||
<strong>hot cues you can play from the deck itself</strong>. Built into the player's own
|
||||
update file by a patcher that leaves the boot loader untouched.
|
||||
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
|
||||
right here in the browser — the boot loader stays untouched.
|
||||
</p>
|
||||
<div class="cta">
|
||||
<a class="btn primary" href="#mods">See the mods</a>
|
||||
<a class="btn" href="docs.html#install">How to install</a>
|
||||
<a class="btn primary" href="builder.html">Build your update</a>
|
||||
<a class="btn" href="#mods">See the mods</a>
|
||||
<a class="btn" href="docs.html#hotcuekeys">Hot cue guide</a>
|
||||
</div>
|
||||
|
||||
@@ -56,10 +58,10 @@
|
||||
<section>
|
||||
<div class="wrap">
|
||||
<div class="strip">
|
||||
<div class="tile"><div class="n">6</div><div class="t">mods, pick the ones you want</div></div>
|
||||
<div class="tile"><div class="n">A–C</div><div class="t">hot cues on the BEAT DIVIDE keys, at once</div></div>
|
||||
<div class="tile"><div class="n">7</div><div class="t">mods, pick the ones you want</div></div>
|
||||
<div class="tile"><div class="n">A–C</div><div class="t">hot cues on the BEAT DIVIDE keys, loaded in the background</div></div>
|
||||
<div class="tile"><div class="n">0 bytes</div><div class="t">changed in the boot loader and recovery</div></div>
|
||||
<div class="tile"><div class="n">1 file</div><div class="t">a normal <code>C900NXS.UPD</code> for the player's USB update</div></div>
|
||||
<div class="tile"><div class="n">1 file</div><div class="t">a normal <code>C900NXS.UPD</code>, built in your browser</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -67,83 +69,82 @@
|
||||
<section id="mods">
|
||||
<div class="wrap">
|
||||
<h2 class="section-title">The mods</h2>
|
||||
<p class="section-sub">Each one compared with stock. Three are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>, three are ours. Choose any combination in the patcher.</p>
|
||||
<p class="section-sub">Each one compared with stock. Three are by <a href="https://github.com/arnoldasr/cdj900nxs">arnoldasr</a>, four are ours. Choose any combination in the <a href="builder.html">builder</a>.</p>
|
||||
<div class="legend">
|
||||
<span class="badge ok">ran on a player</span>
|
||||
<span class="badge emu">emulator only</span>
|
||||
<span class="badge warn">partly tested</span>
|
||||
<span class="badge bad">known faults</span>
|
||||
<span class="state stable">stable</span>
|
||||
<span class="state experimental">experimental</span>
|
||||
<span class="player">ran on a player</span>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<!-- code -->
|
||||
<!-- rgb_waveform -->
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">🌈</div>
|
||||
<div>
|
||||
<h3>RGB waveform, BPM & phase meter <code>code</code></h3>
|
||||
<h3>RGB waveform <code>rgb_waveform</code></h3>
|
||||
<div class="by">by arnoldasr</div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge ok">ran on a player</span></div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>The detail waveform in RGB colours, as on newer Pioneer players, instead of plain blue. Plus the BPM with two decimals and a two-line phase meter.</p>
|
||||
<p>The big waveform in RGB colours, as on newer Pioneer players, instead of plain blue — and the whole-track overview at the bottom as a warm red-to-white gradient, so drops and breaks stand out.</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/detail-stock.png" alt="Stock blue detail waveform"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/detail-mods.png" alt="RGB detail waveform"><figcaption>Modded</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/detail-mods.png" alt="RGB detail waveform"><figcaption>RGB</figcaption></figure>
|
||||
</div>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/bpm-stock.png" alt="Stock BPM 145.0"><figcaption>BPM · stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/bpm-mods.png" alt="BPM with two decimals"><figcaption>BPM · two decimals</figcaption></figure>
|
||||
<div class="vs stack">
|
||||
<figure><img src="assets/overview-stock.png" alt="Stock blue overview waveform"><figcaption>Overview · stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/overview-mods.png" alt="Gradient overview waveform"><figcaption>Overview · gradient (with cue colours and hot cues)</figcaption></figure>
|
||||
</div>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>Blue waveform, BPM to one decimal, phase meter as four boxes.</span></li>
|
||||
<li><span class="k new">Modded</span><span>RGB waveform, BPM to two decimals (stacked), phase meter on two lines.</span></li>
|
||||
<li><span class="k">Stock</span><span>Blue waveform, flat blue overview.</span></li>
|
||||
<li><span class="k new">Modded</span><span>RGB waveform, red-to-white overview.</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<!-- utility -->
|
||||
<!-- extended_bpm -->
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">🔢</div>
|
||||
<div>
|
||||
<h3>Extended BPM <code>extended_bpm</code></h3>
|
||||
<div class="by">by arnoldasr</div>
|
||||
</div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>The BPM to two decimals: the tenths move into a small digit above, the hundredths take the place below — handy for matching tempos by ear and by eye.</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/bpm-stock.png" alt="Stock BPM 145.0"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/bpm-mods.png" alt="BPM with two decimals"><figcaption>Two decimals</figcaption></figure>
|
||||
</div>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>BPM to one decimal.</span></li>
|
||||
<li><span class="k new">Modded</span><span>BPM to two decimals, stacked.</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<!-- phasemeter -->
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">🎚️</div>
|
||||
<div>
|
||||
<h3>Phase meter switch <code>utility</code></h3>
|
||||
<div class="by">by arnoldasr · needs <code>code</code></div>
|
||||
<h3>Phase meter <code>phasemeter</code></h3>
|
||||
<div class="by">by arnoldasr</div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge ok">ran on a player</span></div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>Choose the phase meter in UTILITY. The rarely used SLIP FLASHING item becomes PHASE METER.</p>
|
||||
<p>A two-line phase meter instead of the four boxes: orange for the master, blue for this deck, each with a marker that sweeps along the bar. Switched in UTILITY, where the rarely used SLIP FLASHING item becomes PHASE METER.</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/phase-stock.png" alt="Stock phase meter: four boxes"><figcaption>Phase meter · stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/phase-mods.png" alt="Two-line phase meter"><figcaption>Phase meter · two lines</figcaption></figure>
|
||||
<figure><img src="assets/phase-stock.png" alt="Stock phase meter: four boxes"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/phase-mods.png" alt="Two-line phase meter"><figcaption>Two lines</figcaption></figure>
|
||||
</div>
|
||||
<div class="vs stack">
|
||||
<figure class="mod"><img src="assets/utility-phase.png" alt="UTILITY menu with PHASE METER item"><figcaption>UTILITY › PHASE METER</figcaption></figure>
|
||||
</div>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>UTILITY › SLIP FLASHING.</span></li>
|
||||
<li><span class="k new">Modded</span><span>UTILITY › PHASE METER picks the meter.</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<!-- overview -->
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">🔥</div>
|
||||
<div>
|
||||
<h3>Overview colour gradient <code>overview</code></h3>
|
||||
<div class="by">by arnoldasr</div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge ok">ran on a player</span></div>
|
||||
</div>
|
||||
<p>The whole-track overview at the bottom of the screen gets a warm red-to-white colour gradient instead of flat blue, so drops and breaks stand out.</p>
|
||||
<div class="vs stack">
|
||||
<figure><img src="assets/overview-stock.png" alt="Stock blue overview waveform"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/overview-mods.png" alt="Gradient overview waveform with coloured cues and hot cues"><figcaption>Modded (with cue colours and hot cues)</figcaption></figure>
|
||||
</div>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>Flat blue overview.</span></li>
|
||||
<li><span class="k new">Modded</span><span>Red-to-white gradient.</span></li>
|
||||
<li><span class="k">Stock</span><span>Four boxes; UTILITY › SLIP FLASHING.</span></li>
|
||||
<li><span class="k new">Modded</span><span>Two lines with markers; UTILITY › PHASE METER picks the meter.</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
@@ -155,9 +156,9 @@
|
||||
<h3>Cue colours & hot cues shown <code>cuecolours</code></h3>
|
||||
<div class="by">ours · made with Claude Code</div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge ok">ran on a player</span></div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>Memory cues take the colour you gave them in rekordbox. The hot cues stored on the USB (A–H) appear as lettered boxes in their rekordbox colour on the overview and the detail waveform — the stock 900NXS ignores them completely.</p>
|
||||
<p>Memory cues take the colour you gave them in rekordbox. The hot cues stored on the USB (A–H) appear as lettered boxes in their rekordbox colour on the overview and the big waveform — the stock 900NXS ignores them completely.</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/detail-stock.png" alt="Stock: red memory triangles only"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/hotcue-detail.png" alt="Hot cue A box on the detail waveform"><figcaption>Hot cue A on the waveform</figcaption></figure>
|
||||
@@ -177,15 +178,15 @@
|
||||
<h3>Hot cue keys <code>hotcuekeys</code></h3>
|
||||
<div class="by">ours · made with Claude Code · needs <code>cuecolours</code></div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge warn">partly tested</span></div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state experimental">experimental</span></div>
|
||||
</div>
|
||||
<p>Play your rekordbox hot cues on the deck. A short press of QUANTIZE switches the hot key mode; the top three BEAT DIVIDE keys become hot cues A–C, ready to start at once. Press to jump and play, hold for a gate, set new ones while paused and delete them with DELETE. The key LEDs show which hot cues exist.</p>
|
||||
<p>Play your rekordbox hot cues on the deck. A short press of QUANTIZE switches the hot key mode; the top three BEAT DIVIDE keys become hot cues A–C. After a track loads they are buffered in the background, so each starts at once — also from a pause, in CDJ and VINYL mode. Hold for a gate, set new ones while paused, delete them with DELETE; changes are saved on the USB with colours (A green, B orange, C blue).</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/display-stock.png" alt="Stock: QUANTIZE label only"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/display-mods.png" alt="HOT KEYS box beside QUANTIZE"><figcaption>HOT KEYS mode on</figcaption></figure>
|
||||
</div>
|
||||
<div class="vs stack">
|
||||
<figure class="mod"><img src="assets/utility-hotcue.png" alt="UTILITY menu with BEAT JUMP and HOT CUE KEYS items"><figcaption>UTILITY › HOT CUE KEYS</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/utility-hotcue.png" alt="UTILITY menu with the HOT CUE KEYS item"><figcaption>UTILITY › HOT CUE KEYS</figcaption></figure>
|
||||
</div>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>No hot cues on the 900NXS; BEAT DIVIDE keys only for loops.</span></li>
|
||||
@@ -194,41 +195,61 @@
|
||||
<a class="more" href="docs.html#hotcuekeys">Hot cue guide →</a>
|
||||
</article>
|
||||
|
||||
<!-- beatjump -->
|
||||
<!-- ledsaver -->
|
||||
<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>LED animation <code>ledsaver</code></h3>
|
||||
<div class="by">ours · made with Claude Code</div>
|
||||
</div>
|
||||
<div class="badges"><span class="badge emu">emulator only</span></div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>TRACK SEARCH jumps 1, 2, 4 or 8 beats along the beat grid instead of changing the track, set in UTILITY › BEAT JUMP (which replaces SCREEN SAVER).</p>
|
||||
<p>When the screen saver dims the display, the deck comes alive: the BEAT DIVIDE keys fade in a wave, SYNC and MASTER pulse against each other, the transport keys light one after another and the jog's three rings run inner, middle, outer and back — a loop of about four and a half seconds. Any key ends it. UTILITY › LED ANIMATION, on from the first start.</p>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Stock</span><span>TRACK SEARCH changes the track; UTILITY › SCREEN SAVER.</span></li>
|
||||
<li><span class="k new">Modded</span><span>TRACK SEARCH jumps beats; UTILITY › BEAT JUMP OFF / 1 / 2 / 4 / 8.</span></li>
|
||||
<li><span class="k">Stock</span><span>The screen saver only dims the display.</span></li>
|
||||
<li><span class="k new">Modded</span><span>Button LEDs and jog rings animate while it is dimmed.</span></li>
|
||||
</ul>
|
||||
<div class="callout" style="margin:0"><strong>Rebuilt.</strong> Jumps while playing go through a buffer of their own and come back in phase; the CUE point and hot cues A–C stay. Needs <code>hotcuekeys</code>. Tested in the emulator only.</div>
|
||||
<a class="more" href="docs.html#ledsaver">Read more →</a>
|
||||
</article>
|
||||
|
||||
<!-- credit -->
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">☠️</div>
|
||||
<div>
|
||||
<h3>Boot text <code>credit</code></h3>
|
||||
<div class="by">ours · made with Claude Code</div>
|
||||
</div>
|
||||
<div class="meta"><span class="player">ran on a player</span><span class="state stable">stable</span></div>
|
||||
</div>
|
||||
<p>The boot screen tells you what you are running: <em>Pionaarr Mods 1.0</em> instead of <em>Ver 1.31</em>. Leave it out to keep the stock text.</p>
|
||||
<div class="vs">
|
||||
<figure><img src="assets/boot-stock.png" alt="Stock boot screen: Ver 1.31"><figcaption>Stock</figcaption></figure>
|
||||
<figure class="mod"><img src="assets/boot-mods.png" alt="Boot screen: Pionaarr Mods 1.0"><figcaption>Pionaarr Mods 1.0</figcaption></figure>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="patcher">
|
||||
<section id="build">
|
||||
<div class="wrap">
|
||||
<h2 class="section-title">One patcher, your choice</h2>
|
||||
<p class="section-sub">The patcher takes the official 1.31 update, adds the mods you tick and writes a new <code>C900NXS.UPD</code>. Only the main application changes; the boot loader, the recovery loader and the other sections are copied byte for byte and checked again before the file is written.</p>
|
||||
<h2 class="section-title">Build it in your browser</h2>
|
||||
<p class="section-sub">The <a href="builder.html">builder</a> takes the official 1.31 update, adds the mods you tick and gives you a new <code>C900NXS.UPD</code> — all on your computer, nothing is uploaded. Only the main application changes; the boot loader, the recovery loader and the other sections are copied byte for byte and checked again before you can download the file.</p>
|
||||
<div class="cta"><a class="btn primary" href="builder.html">Open the builder</a></div>
|
||||
<p class="section-sub" style="margin-top:28px">Prefer the command line? The Python patcher builds the same file, byte for byte:</p>
|
||||
<div class="term"><span class="c"># pick the mods in a list</span>
|
||||
<span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD
|
||||
|
||||
<span class="c"># or name them</span>
|
||||
<span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys
|
||||
<span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys
|
||||
|
||||
<span class="c"># back to stock</span>
|
||||
<span class="p">$</span> python cdj900nxs_patcher.py C900NXS.UPD --stock</div>
|
||||
<p class="caption">Before it builds, the patcher shows a disclaimer that you must accept. <a href="docs.html#install">Full install guide →</a></p>
|
||||
<p class="caption">Both ask you to accept a disclaimer first. <a href="docs.html#install">Full install guide →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -237,7 +258,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 mods <code>beatjump</code>, <code>cuecolours</code> and <code>hotcuekeys</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>code</code>, <code>utility</code> and <code>overview</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> 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>
|
||||
|
||||
@@ -287,3 +287,92 @@ footer .ai .dot { width: 8px; height: 8px; border-radius: 50%; background: var(-
|
||||
.keys .b { font-size: 1.1rem; }
|
||||
.diff li { grid-template-columns: 1fr; gap: 0; }
|
||||
}
|
||||
|
||||
/* ---- status: stable / experimental / planned / feasibility, and the "ran on a player" dot ---- */
|
||||
.state {
|
||||
font: 700 .66rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase;
|
||||
padding: 6px 8px; border-radius: 7px; border: 1px solid currentColor; white-space: nowrap; align-self: flex-start;
|
||||
}
|
||||
.state.stable { color: var(--ok); }
|
||||
.state.experimental { color: var(--warn); }
|
||||
.state.planned { color: var(--accent-2); }
|
||||
.state.feasibility { color: #b48cff; }
|
||||
.player {
|
||||
display: inline-flex; align-items: center; gap: 6px; font-size: .78rem; color: var(--muted); white-space: nowrap;
|
||||
}
|
||||
.player::before {
|
||||
content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--ok);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent);
|
||||
}
|
||||
.player.no::before { background: var(--faint); box-shadow: none; }
|
||||
|
||||
/* ---- builder ---- */
|
||||
.builder { padding: 48px 0 72px; max-width: 900px; }
|
||||
.builder h1 { font-size: clamp(2rem, 4vw, 2.8rem); letter-spacing: -.03em; margin: 10px 0 10px; }
|
||||
.builder .lead { color: var(--muted); font-size: 1.05rem; margin: 0 0 18px; }
|
||||
.step { display: grid; grid-template-columns: 44px 1fr; gap: 16px; padding: 26px 0; border-top: 1px solid var(--line); }
|
||||
.step-n {
|
||||
width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center;
|
||||
background: var(--accent); color: #fff; font-weight: 800;
|
||||
}
|
||||
.step h2 { margin: 2px 0 6px; font-size: 1.25rem; }
|
||||
.step p { color: var(--muted); margin: 0 0 14px; }
|
||||
.drop {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
|
||||
border: 2px dashed var(--line); border-radius: 14px; padding: 26px; cursor: pointer; text-align: center;
|
||||
background: var(--card); transition: border-color .15s ease;
|
||||
}
|
||||
.drop:hover, .drop.over { border-color: var(--accent-2); }
|
||||
.drop.has { border-style: solid; border-color: var(--ok); }
|
||||
.drop input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.drop-s { color: var(--faint); font-size: .88rem; }
|
||||
.picks { display: grid; gap: 10px; }
|
||||
.pick {
|
||||
display: grid; grid-template-columns: 22px 1fr auto; gap: 12px; align-items: start;
|
||||
background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px; cursor: pointer;
|
||||
}
|
||||
.pick.on { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
|
||||
.pick input { margin-top: 4px; width: 17px; height: 17px; accent-color: var(--accent); }
|
||||
.pick-main { display: flex; flex-direction: column; gap: 3px; }
|
||||
.pick-name { font-weight: 700; }
|
||||
.pick-name code { font-size: .78rem; color: var(--muted); margin-left: 4px; }
|
||||
.pick-what { color: var(--muted); font-size: .93rem; }
|
||||
.pick-by { color: var(--faint); font-size: .82rem; }
|
||||
.hint { color: var(--faint); font-size: .88rem; margin-left: 10px; }
|
||||
p.hint { margin: 10px 0 0; }
|
||||
.agree { display: flex; gap: 10px; align-items: flex-start; margin: 10px 0; color: var(--muted); cursor: pointer; }
|
||||
.agree input { margin-top: 5px; width: 17px; height: 17px; accent-color: var(--accent); flex: none; }
|
||||
.agree strong { color: var(--text); }
|
||||
.btn[disabled] { opacity: .45; cursor: not-allowed; filter: none; }
|
||||
button.btn { font: inherit; font-weight: 700; cursor: pointer; }
|
||||
.log {
|
||||
margin-top: 16px; background: #07090d; border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
|
||||
font: 13px/1.7 var(--mono); color: #c9d1de; max-height: 260px; overflow: auto;
|
||||
}
|
||||
.log .ok { color: var(--ok); }
|
||||
.log .bad { color: var(--bad); }
|
||||
.result { margin-top: 18px; display: grid; gap: 12px; }
|
||||
.result .btn { justify-self: start; }
|
||||
.result .sum { color: var(--muted); font-size: .85rem; word-break: break-all; }
|
||||
.result details { color: var(--muted); }
|
||||
.result summary { cursor: pointer; color: var(--text); font-weight: 600; }
|
||||
|
||||
/* ---- upcoming ---- */
|
||||
.upcoming { padding: 48px 0 72px; }
|
||||
.upcoming h1 { font-size: clamp(2rem, 4vw, 2.8rem); letter-spacing: -.03em; margin: 10px 0 10px; }
|
||||
.upcoming .lead { color: var(--muted); font-size: 1.05rem; margin: 0 0 28px; max-width: 760px; }
|
||||
.card .meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-left: auto; justify-content: flex-end; }
|
||||
@media (max-width: 560px) {
|
||||
.step { grid-template-columns: 1fr; gap: 8px; }
|
||||
.pick { grid-template-columns: 22px 1fr; }
|
||||
.pick .state { grid-column: 2; justify-self: start; }
|
||||
.card .meta { margin-left: 0; justify-content: flex-start; width: 100%; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.tabs { gap: 2px; padding: 3px; }
|
||||
.tabs a { padding: 6px 8px; font-size: .8rem; }
|
||||
.theme-btn { width: 32px; height: 32px; flex: none; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.tabs a { padding: 5px 6px; font-size: .74rem; }
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Writes builder/data.js, the data the web builder (builder.html) applies.
|
||||
|
||||
usage: make_builder_data.py [--repo ../CDJ900NXSVirtual]
|
||||
|
||||
Our mods (cuecolours, hotcuekeys, ledsaver, 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)
|
||||
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.
|
||||
|
||||
arnoldasr's mods (credited on the site), as patch_cdj900nxs.py splits them
|
||||
up, are in data.js the same way: per combination of extended_bpm, phasemeter
|
||||
and rgb_waveform the bytes they change, except the asset blob at 0x08FC2D00,
|
||||
which starts with a copy of the firmware's resource table: the browser makes
|
||||
it from the user's application (the port of patch_cdj900nxs.assets()).
|
||||
The SHA-256 of the finished application for every combination is in data.js
|
||||
too; the browser checks its result against it.
|
||||
|
||||
Before data.js is written, the browser's way of applying the data is redone
|
||||
here in Python for every combination of arnoldasr's mods and ours and
|
||||
compared byte for byte with patch_cdj900nxs.py's result.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
WEB = os.path.dirname(HERE)
|
||||
OURS = ('cuecolours', 'hotcuekeys', 'ledsaver', 'credit')
|
||||
NEEDS = {'hotcuekeys': 'cuecolours'}
|
||||
UT_TABLE_STOCK = 0x08C6C8DC
|
||||
UT_TABLE_LEN = 17 * 52
|
||||
BASE = 0x08000000
|
||||
ASSETS = None
|
||||
|
||||
|
||||
def sha(b):
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def valid(combo):
|
||||
return all(NEEDS[m] in combo for m in combo if m in NEEDS)
|
||||
|
||||
|
||||
def runs(base, final):
|
||||
"""[(address, bytes)] where final differs from base (short equal gaps joined)."""
|
||||
n = len(final)
|
||||
base = base + bytes(n - len(base))
|
||||
out, i = [], 0
|
||||
while i < n:
|
||||
if base[i] != final[i]:
|
||||
j = i
|
||||
while j < n and (base[j] != final[j] or (j + 16 < n and base[j:j + 16] != final[j:j + 16])):
|
||||
j += 1
|
||||
out.append((BASE + i, final[i:j]))
|
||||
i = j
|
||||
else:
|
||||
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]
|
||||
out = [dict(box, op='box') for box in boxes] # whole, outside the runs
|
||||
masked = bytearray(final)
|
||||
padded = stock + bytes(len(final) - len(stock))
|
||||
for box in 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)
|
||||
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)
|
||||
cuts.sort(key=lambda c: c[0])
|
||||
pos = 0
|
||||
for lo, hi, op in cuts:
|
||||
if lo > pos:
|
||||
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:lo]})
|
||||
out.append(op)
|
||||
pos = hi
|
||||
if pos < len(data):
|
||||
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:]})
|
||||
return out
|
||||
|
||||
|
||||
def apply_ops(app, stock, ops, hk, size):
|
||||
"""What builder.js does with the operations (redone here to check them):
|
||||
the application grown with zeros to `size` first."""
|
||||
out = bytearray(app) + bytes(max(0, size - len(app)))
|
||||
for o in ops:
|
||||
off = o['addr'] - BASE
|
||||
if o['op'] == 'data':
|
||||
out[off:off + len(o['data'])] = o['data']
|
||||
elif o['op'] == 'copy':
|
||||
out[off:off + o['len']] = stock[o['src'] - BASE:o['src'] - BASE + o['len']]
|
||||
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'])
|
||||
out[off:off + o['len']] = px
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--repo', default=os.path.join(os.path.dirname(WEB), 'CDJ900NXSVirtual'))
|
||||
args = ap.parse_args()
|
||||
sys.path.insert(0, os.path.join(args.repo, 'mods'))
|
||||
sys.path.insert(0, os.path.join(args.repo, 'mods', 'cdj900nxs'))
|
||||
import patch_cdj900nxs as p
|
||||
import hotcuekeys as hk
|
||||
import cuecolours
|
||||
with open(os.path.join(args.repo, 'extract', 'cdj900nxs', 'main_unpacked.bin'), 'rb') as f:
|
||||
stock = f.read()[:p.APP_SIZE]
|
||||
if sha(stock) != p.APP_SHA256:
|
||||
raise SystemExit('not the 1.31 application')
|
||||
|
||||
global ASSETS
|
||||
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}]
|
||||
|
||||
combos = [c for n in range(1, len(OURS) + 1) for c in itertools.combinations(OURS, n) if valid(c)]
|
||||
variants, chunks = {}, {}
|
||||
|
||||
def encode(ops):
|
||||
enc = []
|
||||
for o in ops:
|
||||
if o['op'] == 'data':
|
||||
key = sha(o['data'])[:16]
|
||||
chunks[key] = base64.b64encode(o['data']).decode()
|
||||
enc.append(['data', o['addr'], key])
|
||||
elif o['op'] == 'copy':
|
||||
enc.append(['copy', o['addr'], o['src'], o['len']])
|
||||
elif o['op'] == 'assets':
|
||||
enc.append(['assets', o['addr'], o['len']])
|
||||
else:
|
||||
enc.append(['box', o['addr'], o['res'], o['bg'], o['ink']])
|
||||
return enc
|
||||
|
||||
# arnoldasr's mods, split up: per combination their bytes, but the asset
|
||||
# blob (it starts with a copy of the firmware's resource table) made by
|
||||
# the browser from the user's own application
|
||||
upstream, up_apps = {}, {(): stock}
|
||||
for k in range(1, len(p.UPSTREAM) + 1):
|
||||
for up in itertools.combinations(p.UPSTREAM, k):
|
||||
app = p.patch(stock, list(up))
|
||||
blob_len = len(app) - (p.DATA_BASE - BASE)
|
||||
masked = bytearray(app)
|
||||
masked[p.DATA_BASE - BASE:] = bytes(blob_len)
|
||||
ops = ops_for(stock, bytes(masked), [])
|
||||
ops.append({'op': 'assets', 'addr': p.DATA_BASE, 'len': blob_len})
|
||||
if apply_ops(stock, stock, ops, hk, len(app)) != app:
|
||||
raise SystemExit('%s: the operations do not give the patcher result' % ','.join(up))
|
||||
upstream[','.join(up)] = {'size': len(app), 'ops': encode(ops)}
|
||||
up_apps[up] = app
|
||||
print('%-40s %3d operations' % (','.join(up), len(ops)))
|
||||
for combo in combos:
|
||||
final = p.patch(stock, list(combo))
|
||||
ops = ops_for(stock, final, boxes if 'hotcuekeys' in combo else [])
|
||||
if apply_ops(stock, stock, ops, hk, len(final)) != final:
|
||||
raise SystemExit('%s: the operations do not give the patcher\'s result' % ','.join(combo))
|
||||
variants[','.join(combo)] = {'size': len(final), 'ops': encode(ops)}
|
||||
print('%-40s %3d operations' % (','.join(combo), len(ops)))
|
||||
|
||||
# the browser applies ours on top of whatever of arnoldasr's was applied:
|
||||
# check every pair, and note the result's SHA-256 for the browser to check
|
||||
expect = {}
|
||||
for k in range(0, len(p.UPSTREAM) + 1):
|
||||
for up in itertools.combinations(p.UPSTREAM, k):
|
||||
base = up_apps[up]
|
||||
if up:
|
||||
expect[','.join(up)] = sha(base)
|
||||
for combo in combos:
|
||||
final = p.patch(stock, list(up) + list(combo))
|
||||
alone = p.patch(stock, list(combo))
|
||||
ops = ops_for(stock, alone, boxes if 'hotcuekeys' in combo else [])
|
||||
if apply_ops(base, stock, ops, hk, len(alone)) != final:
|
||||
raise SystemExit('%s + %s: differs from the patcher' % (','.join(up), ','.join(combo)))
|
||||
expect[','.join(list(up) + list(combo))] = sha(final)
|
||||
print('checked: every combination of arnoldasr and ours gives the patcher bytes')
|
||||
|
||||
glyphs = {c: rows for c, rows in hk.GLYPHS.items()}
|
||||
data = {
|
||||
'stockUpdSha256': '', # filled from the patcher's upd.py below
|
||||
'stockAppSha256': p.APP_SHA256,
|
||||
'appSize': p.APP_SIZE,
|
||||
'dataBase': p.DATA_BASE,
|
||||
'upstream': upstream,
|
||||
'ours': variants,
|
||||
'expect': expect,
|
||||
'box': {'resTable': hk.RES_TABLE, 'width': w, 'height': h, 'glyphs': glyphs,
|
||||
'text': 'HOT KEYS', 'gap': 3, 'space': 5},
|
||||
'chunks': chunks,
|
||||
}
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(WEB), 'CDJ900NXSPatcher'))
|
||||
import upd
|
||||
data['stockUpdSha256'] = upd.STOCK_UPD_SHA256
|
||||
for key, b64 in chunks.items(): # no firmware in what we ship
|
||||
blob = base64.b64decode(b64)
|
||||
for i in range(0, max(0, len(blob) - 47), 8):
|
||||
win = blob[i:i + 48]
|
||||
if len(set(win)) > 2 and stock.find(win) >= 0:
|
||||
raise SystemExit('chunk %s holds firmware bytes (at %d)' % (key, i))
|
||||
out = os.path.join(WEB, 'builder', 'data.js')
|
||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||
with open(out, 'w', encoding='utf-8', newline='\n') as f:
|
||||
f.write('// Generated by tools/make_builder_data.py -- do not edit.\n')
|
||||
f.write('window.BUILDER_DATA = ')
|
||||
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)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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.">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="index.html"><span class="disc" aria-hidden="true"></span><span>CDJ-900NXS Mods</span></a>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<a href="index.html">Mods</a>
|
||||
<a href="builder.html">Builder</a>
|
||||
<a href="upcoming.html" aria-current="page">Upcoming</a>
|
||||
<a href="docs.html">Docs</a>
|
||||
</nav>
|
||||
<button class="theme-btn" type="button" aria-label="Switch light / dark">☀</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
<div class="legend">
|
||||
<span class="state planned">planned</span>
|
||||
<span class="state feasibility">feasibility</span>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<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</div>
|
||||
</div>
|
||||
<div class="meta"><span class="player no">emulator only</span><span class="state planned">planned</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>
|
||||
<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>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<div class="icon" aria-hidden="true">🎤</div>
|
||||
<div>
|
||||
<h3>Stems</h3>
|
||||
<div class="by">ours · idea</div>
|
||||
</div>
|
||||
<div class="meta"><span class="state feasibility">feasibility</span></div>
|
||||
</div>
|
||||
<p>Two stem modes on the two BEAT DIVIDE keys the hot cues leave free: one to take the vocals out, one to take the melody out — like the stem buttons on the newest players.</p>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Question</span><span>Whether the player's audio DSP has the room and the speed to split a track while it plays, a job it was never designed for.</span></li>
|
||||
<li><span class="k new">Next</span><span>Measuring what the DSP has to spare, then deciding.</span></li>
|
||||
</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>
|
||||
<div>
|
||||
<h3>3-band waveform</h3>
|
||||
<div class="by">ours · idea</div>
|
||||
</div>
|
||||
<div class="meta"><span class="state feasibility">feasibility</span></div>
|
||||
</div>
|
||||
<p>The three-band waveform of the newest players: lows, mids and highs drawn as layers in their own colours, so kicks, vocals and hi-hats can be told apart at a glance.</p>
|
||||
<ul class="diff">
|
||||
<li><span class="k">Question</span><span>Whether the band data rekordbox exports for newer players can be read and drawn fast enough on this one.</span></li>
|
||||
<li><span class="k new">Next</span><span>Reading the data on the player, then a renderer.</span></li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<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, our mods 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>
|
||||
<p>Unofficial. Not affiliated with or endorsed by Pioneer DJ or AlphaTheta; CDJ and rekordbox are their trademarks. Flashing modified firmware can leave your player unusable. Everything here is provided as is, without warranty, and used at your own risk.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user