diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0040425 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_test/ diff --git a/assets/boot-mods.png b/assets/boot-mods.png new file mode 100644 index 0000000..d42d7c4 Binary files /dev/null and b/assets/boot-mods.png differ diff --git a/assets/boot-stock.png b/assets/boot-stock.png new file mode 100644 index 0000000..f2a731d Binary files /dev/null and b/assets/boot-stock.png differ diff --git a/builder.html b/builder.html new file mode 100644 index 0000000..7687409 --- /dev/null +++ b/builder.html @@ -0,0 +1,110 @@ + + + + + + CDJ-900NXS Mods Builder + + + + + + + + + + + + +
+
+ CDJ-900NXS Mods + + +
+
+ +
+
Builder
+

Build your update

+

Upload the official CDJ-900NXS 1.31 update, tick the mods and download your C900NXS.UPD. Everything runs in your browser: the file never leaves your computer.

+ + +
+
1
+
+

The official update

+

Pioneer DJ's C900NXS.UPD for firmware 1.31. The builder checks its SHA-256 and refuses any other file.

+ +
+
+ + +
+
2
+
+

The mods

+

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).

+
+

+
+
+ + +
+
3
+
+

Before you build

+
+ Unofficial firmware. 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. +
+ + +
+
+ + +
+
4
+
+

Build

+ + + + +
+
+
+ + + + + diff --git a/builder/builder.js b/builder/builder.js new file mode 100644 index 0000000..fcb6696 --- /dev/null +++ b/builder/builder.js @@ -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 }; +})(); diff --git a/builder/data.js b/builder/data.js new file mode 100644 index 0000000..f0a54dc --- /dev/null +++ b/builder/data.js @@ -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,"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,"9e7532da7f56183c"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,49760]]},"extended_bpm,rgb_waveform":{"size":16577120,"ops":[["data",144311836,"ca71ec9290d6ed48"],["data",145914824,"45b88bcd5afea839"],["data",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["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,"2853d86b697a9dfa"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"phasemeter,rgb_waveform":{"size":16577120,"ops":[["data",135069542,"845677fae2e67916"],["data",144311836,"ca71ec9290d6ed48"],["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",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147243844,"d77596e8499df246"],["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",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,"acf759afff056d54"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]},"extended_bpm,phasemeter,rgb_waveform":{"size":16577120,"ops":[["data",135069542,"845677fae2e67916"],["data",144311836,"ca71ec9290d6ed48"],["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",146565168,"52b8fe18a1fed3bf"],["data",146565756,"a2721044eff0d2f6"],["data",146586208,"c465a030b70c781e"],["data",146586876,"c465a030b70c781e"],["data",146603378,"620bfdaa346b088f"],["data",146603856,"582d807f9ed58432"],["data",146618952,"c3967666e153796f"],["data",146619984,"7f0079b1f754ae7e"],["data",146620120,"a91a9b3491491b64"],["data",146829820,"f9e6d34295c320f0"],["data",147236557,"371b074706e32cac"],["data",147243844,"d77596e8499df246"],["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",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,"9e7532da7f56183c"],["data",150742272,"7c188559c27b7ceb"],["data",150742528,"e3f375bd0e18a505"],["data",150742784,"4617c585a7472b06"],["data",150743424,"163da0f1e1acc40a"],["data",150744064,"5e4e42864642a517"],["assets",150744320,50528]]}},"ours":{"cuecolours":{"size":16599808,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",150796800,"6db452c680a7f8c7"]]},"ledsaver":{"size":16612352,"ops":[["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"f299791cddd3d666"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"credit":{"size":16578880,"ops":[["data",146656652,"f445e63209e0be24"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"]]},"cuecolours,hotcuekeys":{"size":16608256,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"b40832cbfea8a56a"],["data",146166692,"aedd01b3bf32874f"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"7728b2e2f6f14165"],["data",146648232,"7728b2e2f6f14165"],["data",146649044,"7728b2e2f6f14165"],["data",146649196,"7728b2e2f6f14165"],["data",147246160,"f299791cddd3d666"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"]]},"cuecolours,ledsaver":{"size":16612352,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"f299791cddd3d666"],["data",150796800,"6db452c680a7f8c7"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,credit":{"size":16599808,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146656652,"f445e63209e0be24"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"]]},"ledsaver,credit":{"size":16612352,"ops":[["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,hotcuekeys,ledsaver":{"size":16612352,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",147246160,"ab897fbdedfa502b"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"34dfc332e32a32ec"],["copy",150828024,147245276,884],["data",150828908,"cfdb635170a79a20"]]},"cuecolours,hotcuekeys,credit":{"size":16608256,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"b40832cbfea8a56a"],["data",146166692,"aedd01b3bf32874f"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"7728b2e2f6f14165"],["data",146648232,"7728b2e2f6f14165"],["data",146649044,"7728b2e2f6f14165"],["data",146649196,"7728b2e2f6f14165"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"]]},"cuecolours,ledsaver,credit":{"size":16612352,"ops":[["data",144150352,"ac638ec83c8cae54"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"f299791cddd3d666"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"1f38751fbe9c8e00"],["copy",150828024,147245276,884],["data",150828908,"3a11950e2934091a"]]},"cuecolours,hotcuekeys,ledsaver,credit":{"size":16612352,"ops":[["box",150817984,424,63488,0],["box",150820640,425,0,16904],["data",134948828,"8815309ed1a1d33f"],["data",144150352,"ac638ec83c8cae54"],["data",144257356,"03c9e94b28441a1f"],["data",144647420,"9266839af92b27a2"],["data",144648700,"8d0d8900ac2cf436"],["data",145816836,"90b1b75e0586814d"],["data",145945476,"5f89ea7b823bb508"],["data",146154152,"f528282873b9c40b"],["data",146166692,"62ab7ebb0d777c57"],["data",146566136,"ac4b9d28c5bfb9dd"],["data",146588136,"03bdb1d5e2d1ccb6"],["data",146601454,"38712f352f6a6c0e"],["data",146601494,"e7cf46a078fed4fa"],["data",146601840,"aca9c8d7ca83a619"],["data",146602956,"030cb5345274e947"],["data",146647420,"28dd5e2b2f412bd3"],["data",146648232,"28dd5e2b2f412bd3"],["data",146649044,"28dd5e2b2f412bd3"],["data",146649196,"28dd5e2b2f412bd3"],["data",146656652,"f445e63209e0be24"],["data",147246160,"ab897fbdedfa502b"],["data",147247660,"95951541115495c3"],["data",147341798,"e9b0c031f0493d3f"],["data",150796544,"c76a1882879da374"],["data",150796800,"6db452c680a7f8c7"],["data",150804512,"1b99b878a8b033a3"],["copy",150809552,147245276,884],["data",150810436,"cbf33f29584fb8c8"],["data",150810640,"3854155ee65e476b"],["data",150823379,"4f72ba82f5e21909"],["data",150825984,"4bf5122f344554c5"],["data",150826016,"34dfc332e32a32ec"],["copy",150828024,147245276,884],["data",150828908,"cfdb635170a79a20"]]}},"expect":{"cuecolours":"33f565dc1b85c0fb21b96d6474d65c220bfdf7e67edb46adcae5dd052eac5cf0","ledsaver":"a72fdfed0d9096a0f46797d2aed9eaf53f0539673e2b31cf682e71fada1c1f13","credit":"cdbbeedd453c58439790d38d901efdb5be846a036f9a7fd0b8085faa05a7dc52","cuecolours,hotcuekeys":"dc6ae56d092de0e0e77e02a6f10ef849e31411f0d742e020257d411386e73904","cuecolours,ledsaver":"c4515ef4a0f6a584b40b8415f9926a6fc0210a1abfadffa7fba04ca41a2922f5","cuecolours,credit":"4cba769597783c7db286c59bc07cd44bf74cb928388e82f46d8a2634885c9806","ledsaver,credit":"8173b9240f82db6a0066372c6ff030ede8e3288977212f8e998a4c786c3415f8","cuecolours,hotcuekeys,ledsaver":"ab7ba9a1ab392dcb4371403a013a273a182290a7810cdcda10b7519d88a50c67","cuecolours,hotcuekeys,credit":"c924eb719a0ebb7ce26407d179932d7d1b3c31ac5197da0ec18c76c357cb0116","cuecolours,ledsaver,credit":"adb0fca5facf9154c9e480a18305b410aad903081805bbb23f03060cb4d4c13b","cuecolours,hotcuekeys,ledsaver,credit":"2051eb058c7eb470e5184d7b3732f843af26b68b8f55c8720a6374db1c766c38","extended_bpm":"1150cc0099d890fb4b9e09cf19f03464106ffd5fd04b1fa940d5af0b41008c4a","extended_bpm,cuecolours":"1d654429265d128f6f57428f696ef43d891cff7ab7f781ff070fd0f8691d0bb6","extended_bpm,ledsaver":"3ff12eafdb2fa13571e3e00fddd9627711300e6a3fb7240f2e3743b0ecba40cd","extended_bpm,credit":"5ce2c13313f4f29b75d03ae3076aae93230940ba30689c1413eea6665f70335e","extended_bpm,cuecolours,hotcuekeys":"ec97fc75a0f060c583a948d0f1e11a70b90eb77823bc6a3e928a237cb465e24e","extended_bpm,cuecolours,ledsaver":"ebe14934834529003bcaf8bd4811438b0ed3758c8edb82a0c75db37ddbb488fd","extended_bpm,cuecolours,credit":"1de56f1275188d082408d91c5e21cdc8cb6dcc1c98ea99bc424f477b7f58890b","extended_bpm,ledsaver,credit":"1b48a24ef744b2e2ac4f2952ae72c717ef554abab3ce8c46049b714221c31e67","extended_bpm,cuecolours,hotcuekeys,ledsaver":"7887783d81bc355901059f121780569fc61e90a92c7fa5ebe5a6bf0f14ab0183","extended_bpm,cuecolours,hotcuekeys,credit":"8e65445037898be915b09ffb56eb84c83d263505bff1e437981411d8c44abd86","extended_bpm,cuecolours,ledsaver,credit":"237764b0b08e357cf41d0c242397e5823ddb00c06b85ce2e0e6abcd3be656f76","extended_bpm,cuecolours,hotcuekeys,ledsaver,credit":"e4c246d634fca90badbd813b5b9deb701b64b9c87b6ed1fc51c1a9fa8be8f3af","phasemeter":"dca533375e39fed109c5b35aef80bcc036ed4500ee2e67f8f3ea3bb0309cdedb","phasemeter,cuecolours":"7f6599ccb6b94ad8d8b85140fdce6a51a41381e7bf9f05f1a51700bbeacd7513","phasemeter,ledsaver":"13ed14e1750ec1ad52811773f6f5d82c8285e49cc0312f4faa2e52d4c6e69250","phasemeter,credit":"9b6c48e501f784c65d157147fd07c9a3d0585ca5de7b5c6bc6e12dca23f55f59","phasemeter,cuecolours,hotcuekeys":"c830d64d7a455526cccc05eaaae68ad6a191bd3881278d91143bad31d7ffd068","phasemeter,cuecolours,ledsaver":"b5b7570f42355bd5f1348763fec8c7379c37addd34a51469ce46758f25935e29","phasemeter,cuecolours,credit":"d99f6f2423c82ff1e083e618b875ea9b77f0aec740c5ad9441d1b106a3e925ee","phasemeter,ledsaver,credit":"ae6f07e4823803feff9ba5ffc99c5a99bd6fd7528e399c196a1ebeb97cad5227","phasemeter,cuecolours,hotcuekeys,ledsaver":"a30e8e6396e2be96310e99c4ab0beb3f4d9060132e8fda055d9e5a3490a4d598","phasemeter,cuecolours,hotcuekeys,credit":"133f7e8112a58c826da783dda18e84e0cb7cab82696e35e8eea4040a5736b91d","phasemeter,cuecolours,ledsaver,credit":"87611265c7484e9b62aeb09e7db0fb9e0cc10aa7250845a4d4d1fbf0defedfa8","phasemeter,cuecolours,hotcuekeys,ledsaver,credit":"928bd8540edd0cadaaad2ef1adceea0d022f910eae999387608728343f847908","rgb_waveform":"0cd6e156824b70f4c010e49348e9cbf2d26e83e12cd9feba14e26c539e64e6be","rgb_waveform,cuecolours":"90d6810eaf2c3c8200a8e6c9f4a43089310f966bf34c4d81f88fdd68f13eedd2","rgb_waveform,ledsaver":"0e9b068fa49feddd262127ba90d0348f3a83e5797fe38c6ac43e97ef4bcb94ad","rgb_waveform,credit":"90a64a0d2044daf0d2befdeb78e3f0bf09aea77cbbd5b8046a6127356af5fc88","rgb_waveform,cuecolours,hotcuekeys":"8985dd4e90229467727f8622ac095fafec4fe20199336edcb2aefb4add87a186","rgb_waveform,cuecolours,ledsaver":"f61280851486e9ec766cecb15a3b6a3400f0d768a6a90479469400889fabf723","rgb_waveform,cuecolours,credit":"8763afcb40230b67916f64beedf8251d5135c0decdea1413e71062b889d92557","rgb_waveform,ledsaver,credit":"3c9234187a75f8673cd23cf3817e4af9aef34c2cf174eb7d20510902f864c47e","rgb_waveform,cuecolours,hotcuekeys,ledsaver":"8bf5fab6b4b0ce24eb9c517f47b2dd903ed0fde6a878003f912757441ecbbe4e","rgb_waveform,cuecolours,hotcuekeys,credit":"4e23c4ea1c16bf2292669a953e6e244337d88cc695369038049fdf0d5c66f806","rgb_waveform,cuecolours,ledsaver,credit":"58a0dfc9bfdf0b335b529b5adb22e48813cc843bb8a048facd6dd722924e1e19","rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"e79c7938bdc422e698b129a0f4f48cc59306cd3bc45eb4254d926ee2d0eaaa38","extended_bpm,phasemeter":"5c663e5fdb53c791a75ee45de1a0875eb83fd247534a916403427cb35e578f14","extended_bpm,phasemeter,cuecolours":"d2cc915162acdc88fe2145442ff10ad64add3e5d0c9289c51c24c831a362c653","extended_bpm,phasemeter,ledsaver":"9ac0a6223d37aa09ed7c71b53d18a3f791646882ac2d5c4d149f0507977875de","extended_bpm,phasemeter,credit":"3eac8591830185176f578076dc7fd2ee233c30bde2fa38a0afb6f7470241c837","extended_bpm,phasemeter,cuecolours,hotcuekeys":"fccd05fea2d648a1345f08c89f7b12c79e146cfd68a3ed789b656b6b12489457","extended_bpm,phasemeter,cuecolours,ledsaver":"cf64b3555cb02dea6b0e129e6f10c7982d203a2319c41ec64b400c1d82f6514b","extended_bpm,phasemeter,cuecolours,credit":"0d46e2a41d5717d533ab2262517587071425b8e2445ea31757831b1f5f0208b5","extended_bpm,phasemeter,ledsaver,credit":"54dfe2b040666cb9b8b808bae55e1f12216a5c75c9c89ee91744ef5bb62b3f5b","extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver":"7006769ff35db824897c828f1046877c19901fc084c9059186dca59e98ce27f4","extended_bpm,phasemeter,cuecolours,hotcuekeys,credit":"c0e1fca211b5545b3dfe274d41014a6c91bbf1f4d97925da434cc0fa0a858f52","extended_bpm,phasemeter,cuecolours,ledsaver,credit":"3ae85f0a5c473b3b9a69ada56f22a6a04c825102462eab9b926625053f6c0414","extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,credit":"e03ca29ed09132a698818448ef582739ed05fe1ae4d191e81be74c567586dacd","extended_bpm,rgb_waveform":"6ffa6d0392ba2245cc40d6a44c8268e87f98388496fbbd5fe8da3b60632b8110","extended_bpm,rgb_waveform,cuecolours":"4cf527c7bd6f5ca80724fafddc34332e67bbb4132a6d05f06fc55b33b3abd692","extended_bpm,rgb_waveform,ledsaver":"76d382d74d2a24aa8cc47e755227a550cfcc52e9e74f77904b64b660725519cb","extended_bpm,rgb_waveform,credit":"7cbc6d8b109c11795f9cd164a9e847db6eff16f4253535fa7af252871a498cd8","extended_bpm,rgb_waveform,cuecolours,hotcuekeys":"67588b2b3146350379eb577071e73a61fe3a39549c12529a667ee7cd3dbff6e6","extended_bpm,rgb_waveform,cuecolours,ledsaver":"7c43ec2ad24b1dbdd0b10c6a6b9493554c9de57fbfa8b670e3c01ff3ceb953b3","extended_bpm,rgb_waveform,cuecolours,credit":"ad5e5143a41be8d5ab3756ef0daec8d661a0f7be5437bc1c99e7259122e9f9f1","extended_bpm,rgb_waveform,ledsaver,credit":"29b5e2c1e71d510695fdf28304bbb73a323ca81b167b5d81f059440bd91cd980","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"6102432499ab0c1193eb6350bc4398018078d8bc49395ce4ca1a6fd06efd457f","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,credit":"ebef3ce31f5d8ccfd0ce76e2cbd598ddefc3d1927c23f5233d08590d38737f3b","extended_bpm,rgb_waveform,cuecolours,ledsaver,credit":"61e9ff151fa07cf130f1b4986394119477cf6812db993935d20353bf8fe5e0b7","extended_bpm,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"fff6958216607602c6eebc865139418a9361df4e6202a03bcfe1e4895466fdd0","phasemeter,rgb_waveform":"afeb3fe6b77de187570ede488048a47213b92cd472bad8400d05d81c0f5ce8a4","phasemeter,rgb_waveform,cuecolours":"7415cf562c6cc94be320cef872f926f2de1c7a70850462b79c7c5162fa33f207","phasemeter,rgb_waveform,ledsaver":"08a3eda0a745ff7cd09eae2799d5a6dd485e45ffa58faccca42261ec7f24eb33","phasemeter,rgb_waveform,credit":"eadf29d05d65e9786d7400ba5d8ca922b85dd89680d65a377cc2ef90730cfd54","phasemeter,rgb_waveform,cuecolours,hotcuekeys":"15b0004c56aae605196c0b9f1d134545e9b9847b0c894da3cb2a199096291374","phasemeter,rgb_waveform,cuecolours,ledsaver":"1ccabf4c87becd24b7ca92941ad11c7ac7dc6e8eaef16cfd754b0ec2645f10b2","phasemeter,rgb_waveform,cuecolours,credit":"ac17dd20347e853167dc6fa1976e51339256b7c895ab40fd1b728dfb8e7db341","phasemeter,rgb_waveform,ledsaver,credit":"3438bf64e79d64495f1702d9f49523ba426a0c424b352ba2e36ad285cc7c7bcc","phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"c26a9f75bdc4f1b5e8ae17307eba9e9a729640704860d529c096b0c96ff8db24","phasemeter,rgb_waveform,cuecolours,hotcuekeys,credit":"bccde295951b8672462101a1cc8a546490b55232104b50d027454b30fb28c75d","phasemeter,rgb_waveform,cuecolours,ledsaver,credit":"3556504dc8d5673eb944e649922faff3216722e59b965f2e65b759019e5efc05","phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"9bb02d1f06b54cba893da7b31b264aa4ff86bd91165b8c802382ba5c8208d1b6","extended_bpm,phasemeter,rgb_waveform":"cba7f8e4748d4adfbb3409e03c519cd9a56a745314b273ef416e5a1e4786ae5d","extended_bpm,phasemeter,rgb_waveform,cuecolours":"547146d47c986f7540fe5923ecf3222ae0b669c4b9fc2dc5ba4c0ab7b9469472","extended_bpm,phasemeter,rgb_waveform,ledsaver":"962686fec8eaaa3dcb001e38954c50bc7024ea5b92922aa8830febffd2bfd143","extended_bpm,phasemeter,rgb_waveform,credit":"f953ec56ceb305fd690974b207cd8ea05f437a9eabc12652cc70b4f70eb63318","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys":"06bfd6289a1aa3d643205fb1a69ff8f872d71426fb0fa10c7acc226a4eb27038","extended_bpm,phasemeter,rgb_waveform,cuecolours,ledsaver":"c2f95202abc426be20cb0004a8c01871d516089aec96d18846eb83d977211275","extended_bpm,phasemeter,rgb_waveform,cuecolours,credit":"8eb1a06c4be3f1629fba26a0ae20487fe6e759281b568bb39ee5affdc73903cd","extended_bpm,phasemeter,rgb_waveform,ledsaver,credit":"48746eefbc2f33f028173b670c9f80d6d7e5c8e48b2acbec8004a52e4b915de7","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver":"96cc618e283bc3db37e82ef29df953e9ed9a3b0601285804e2c6c8bfedc14e78","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,credit":"e500e0658b57afa3b605fe8df51b0ff0d26c74586d673f019fe7b9614f05a260","extended_bpm,phasemeter,rgb_waveform,cuecolours,ledsaver,credit":"0346a1e8675372e5fcc7783b8ca6d81bef6903fdee3e2c25548ae6cd63dc8f56","extended_bpm,phasemeter,rgb_waveform,cuecolours,hotcuekeys,ledsaver,credit":"d2694918ff7c61a4dce4717a4a8a1e9e31e25c7cf490b9ca3c43de609017da25"},"box":{"resTable":1048576,"width":88,"height":15,"glyphs":{"H":["X.....X","X.....X","X.....X","XXXXXXX","X.....X","X.....X","X.....X"],"O":[".XXXXX.","X.....X","X.....X","X.....X","X.....X","X.....X",".XXXXX."],"T":["XXXXXXX","...X...","...X...","...X...","...X...","...X...","...X..."],"K":["X....X.","X...X..","X..X...","XXX....","X..X...","X...X..","X....X."],"E":["XXXXXX","X.....","X.....","XXXXX.","X.....","X.....","XXXXXX"],"Y":["X.....X",".X...X.","..X.X..","...X...","...X...","...X...","...X..."],"S":[".XXXXX.","X......","X......",".XXXXX.","......X","......X",".XXXXX."]},"text":"HOT KEYS","gap":3,"space":5},"chunks":{"45b88bcd5afea839":"ACX8","c465a030b70c781e":"ACL8","c3967666e153796f":"ACH8","7f0079b1f754ae7e":"AdErQQkACQAAEPwI","a91a9b3491491b64":"AdYrRgkACQAAEvwI","f9e6d34295c320f0":"ACT8","6d11cfe939a0630a":"hi+WLyJPCdj/4AEYCNELQQkAA2kCKAfREmEYIQGJ/+ABGJNgJk/2aQsA9mgALPwIACD8CKDSTQo=","a4d77ab57ab896dd":"Bi8WLyYvNi9GL1YvZi92LyJPEk8CTwNl42QM0QtBCQAL0QERBk8WTyZP9mf2ZvZl9mT2Y/Zi9mH2YGoFJ+YYRgTREHYE1wXSK0IJAAAU/AgALPwIhIurC1DDAADkPr0I","906ea135b6ba2cef":"Ek8CTw3REjQTiQ3REzUQiwzRFzUNiQzRHDVHBRoAC9EcMAvSJTAKAPPhHUAJ0RIwAIv/4AZPCwAWTwkA//8AAPDY////fwAAECcAAIgTAABZF7fRoIYB","d6db78bde1edbcae":"hi+WL6Yvti/GL9Yv5i8iTxJPAk/sfz7YglAIIAKJg1AIIAGLaKAJAIhbOtESOweJiVADagGIDYkCiAuJA4gJiTXRglQLQQDlM9GDVAtBAOVSoAkAM9ESabNk82XQsQkAo2ADiCkMgl2DVC/eNNrILAOJglSDXS3eMton0QtBAOXxVQBFKtEcNcw102Qj0QtBCQDTZAHlINELQQkAk2Q35fJmyCwDiSXlAEYi0Rw2C04JAADrs2AIQK4FCHD+BsgsAokARhzRHDYLTpNks2AIQK4Fk2QZ0QtBCQADZIhSs2AIQBnRHgESMikFC9ELQQkAAXsD4RI73YsUfwZPFk8mT/Zu9m32bPZr9mr2aQsA9mgALPwIoIYBALSvsggAprII8IurC5RwEBHYVrwIila8COoAAADrAAAA1wAAADRWvAhMF/wIWBf8CGQX/AhEAAAAUAAAAFwAAAAnAAAAKAAAACkAAAAAAAAA6AMAABAn","786195378d06baff":"CdYF52U0CgIJQgFCI2MIQywzAENDYDgwAiUEdSNkEEfwiwsACQAJAM3MzMw=","503c09b6b0baefb3":"4RkAJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRAsAQ2A=","160adecaa849e284":"hi+WL6Yvti/GL9Yv5i8iT/B/Q2hTaWNqc2uCUAggBImDUAggMIlnoAkAg1AIICuLk2RssAkACCAmiaNkZ7AJAAggIYmQOh+JsGABiByLs2EYcRBgAYgXi0DREmIoIhOJI2ADyBCLElMzYAPIDIs81Dw0MjQIixFVUjQFiSNmTDYiNgGLA6AJAADgNqAJADwyNuMA4AIiBHIQQ/uLMd7zZAh0C06zZfNkDHSzZQtOGHUt3gDkC04c5QNsAOQLThzlA22TZMNlN7DyVqNk02UzsPNWB+ACLwDgAR8k3gDkk2XDZgtOAOcA5KNl02YLTgDn8lACGPNQAxgB4BB/Jk/2bvZt9mz2a/Zq9mkLAPZoSCQQiUSEDogNi0NhIHESYhTTIDMHixWFBogEixFQCCABiQsAAeALAADgSVEG4hZgAiUEdRBC+otiJUIWQ2EQcRFgY2EQcQEh/+ALAGOBqEsQEdgAAAC4LLMIZiyzCJgmswhsJwuo","d82da843aae59533":"hi+WLyJPW9hAeFvREmmYKQ6JilAAOQuJAOADGAgYCRhW0ZNkC0Fb5YNkA2UEsJNmJk/2aQsA9miGL5Yvpi+2L8Yv1i/mLyJP8H9DaFNpY2qYKWuJlIQOiGiLk2EgcRJiSNMgM2KLEVAIIF+JFYUDaxVLW4sQ4ic7WImzbAJ8CEzDbVB9UH1A0RJiKCJOiSNgA8hLixJTM2ADyEeL02Q8NDI0Q4sRVVI0QIkjZkw2IjY8izwy02MJQwDgAiIEchBD+4sy3i7V82QLTgh0LNUYdfNkC04MdC7RAOQLQcNlA22ZUdNis2MWYAIiBHIQQ/qL8lRCIvNVURKSFJIVmIVIgViB/+BDgVOBs2ACcAIvAOABHyDRAOSTZdNmC0EA5/JQCBjzUAkYqhgBoAHgAOAQfyZP9m72bfZs9mv2avZpCwD2aAkAAQAAAAAAAADZACQAowIAAP8A//8BAAAAAQAAAAAAAADZADwApAIAAP8A//8BAAAAACz8CJhwEBE0VrwIRB/8CGwnC6ioSxARuCyzCGYsswiYJrMI","7010683be462baa0":"1i/mLyJPEk8CT/x/AdErQQkACQBcPr0I","ebd1070ec574dec1":"Ni9GL1YvZi92L4YvIk8R2IRTAXM0GBDTC0MJAAYvFi8O0RJjNhiCZDA0/+AAi4FQBRgL0RJgBxiEUwFzNBj2YfZgJk/2aPZn9mb2ZfZkCwD2YwkAACz8CIxpwggkjKsL8IurCw==","172e933902cb3738":"hi+WLyJPFNiEWZNgAcgMi4VQBi+HUAYvhFEQOQSL9mAJGPZgAaAIGAh/mhgL0QtBCQAGL4VR/+AAMQSJh1GJUAAxAYn/4AgYBdELQQkA9mAmT/ZpCwD2aAAs/AgQgbwIABb8CA==","2853d86b697a9dfa":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdELQQkACCAAiZwYC9EJAAkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","7c188559c27b7ceb":"hi8iTwjRC0EJAANoB9EA4AIRAxEMEUBxAxEIEQkRChGDYCZPCwD2aD4sswgALPwI","e3f375bd0e18a505":"AQAAAAAAAACKAgQA6gAAAP8A//8BAAAAAQAAAAAAAACKAgQA6wAAAP8A//8B","4617c585a7472b06":"hi+WL6Yvti/GL9Yv5i8iTxJPAk/4f23RQHH/4AURBhFl2IhRGCFTicTgDGAeAigiTolB4AhALgkIcC4KmClHiQPgGEDMcI4LkVwF4RI8P4uTZKNlWdELQbNmFUA4i/9wA20DYQhBCEGcMRRxEWItYv9yA+M2MiuJKD0RTSiL02EEccIxJInTZAhECEScNBR0Q25L0QtBo2UDbONkQHRI0QtBo2XIMANtFU0Ri0jREj0OibNkyDQRRAqL0jQIiShEPrnTZQNuQdFAcQURAqAJAGKgAOA92EB4OtmdWhTgnAu8a5Fckl2UUZ1SoDIpi4JioDImibNi/3ID4zYyIYnGPR+L02PIMzPQAjMaiRlBGCEXiTNkGEQIRAhEEbkTZS7RBTEKAxoAKEMpQDsgCCAIibNi/3InABoDo2I4MqIoIRgCGIJcyCzDiR/RC0HzZPFRw20ITR7SIj26iYJiE2MoM9IztYmBUhNkKDTSNACL2DTSNK2JKETjuNNlFNJAcgYS42EIMQhBHWEfYRTSJwEaABPSLDDw4ixADNhAeAQYCH8GTxZPJk/2bvZt9mz2a/Zq9mkLAPZoCQDcRZQJTH2iCLbvoAjAUBAR/NnACAAs/AgAAAEAAAAIAAAAAQAyAAAAAIA=","163da0f1e1acc40a":"hi+WLyJPEk9f0QtBCQAGL2XYQHhd0QtBCQABiCkJiFGJUhghAYkoIgCLAOmDUZAxAolDsAkAkxiYKQGJoL4JAIhUhlUKsJNmiVSFVQawk2b2YBZPJk/2aQsA9mhIJCuJ1i/mLyJPQ25oJhqJ/+AANReJS9EXBRoNKU1K0Rw97IXQMAWJ02DsgQhw7oEDoAkA41AIIAyL42RA0QtBAeUHoAkA41AIIAOJ42Q80QtBAOUmT/ZuCwD2bQsACQCmL7Yvxi/WL+YvIk8x0RJsyCw0iSrdLNoq2wTu1GVcZS3RC0HDZAggJYkDZCrgTQEdYQXiJjEAiyNhsWM9YzYvFi9JUiYvpi/yURghEon/cRIf8VImZCEf8mM1ZTIvXWWYKQCJ81VIJO6JHNELQQkA6q8JABB/AnsQTs+PCnomT/Zu9m32bPZrCwD2agxTWU2hAqICogKiAsMBxAHFAcYBxwHIAckBygHLAcwByAHJAdAB0QHSAcgByQHNAc4BzwHIKvwIzCr8CNQq/AjoAr0I+NmvCJhwEBE0VrwIAKayCLSvsgjDAAAA2QAAAAAs/Ag=","5e4e42864642a517":"//////////8AAAAAAAAAAAAAAAD//////////wAAAAD/////","845677fae2e67916":"UABIAEEAUwBFACAATQBFAFQARQBSAAAAAA==","6922e93e3827642c":"pQ==","84014a74358fe2ac":"0q+y","371b074706e32cac":"Lfw=","060f2c4b6cd4c5e4":"gCn8","acf759afff056d54":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdEA4AkACCAAiZwYC9ELQQkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","ca71ec9290d6ed48":"ZCL8","52b8fe18a1fed3bf":"ABj8","a2721044eff0d2f6":"Ghj8","620bfdaa346b088f":"fw==","582d807f9ed58432":"YO/8CA==","d77596e8499df246":"Q5FDkUORQ5Gl/KX8opCD4WP85875NZ+W","91aaac306bd43135":"Ik8i0QtBCQABiAOLIdFAcQHiLBEmTwsACQAiTx3RQHEcUAggAokA4AawDBEY0QtBCQAmTwsACQCGL5Yvpi8W0RNYiCgdiRXREmmYKRmJFNAeCqgqFYmTYpwyF3L84AkijDIiYA/TMDALiyFQkDAIi4NiFHKcMgF6JGAAKhBJ+48CevZq9mkLAPZoCQASKb0IVCq9CAAs/AjEBKoLYAd9CxjuAgBSR0I1","d5326732238d3e24":"4RkAJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRFQxJERUMSREVDEkRAsAQ2CWL6Yvti/GL9Yv5i8iT/R/Q25TbGNpc2r/7eguLonia7grK4kb0QtBs2QIICaJGdELQUjkCCAhiQEfAh8A4RIv42QDZRjWFNELQQHn8VLGEhfRFxIX0RgSs2TyVRDRC0EJAAggCoumL/NUD9XzZgR2k2cL0QtBCQADbQR/02AMfyZP9m72bfZs9mv2agsA9mkJAPI2lwgeeZoIUDyXCPA4lwiYQZcIBCwAAAJPAABQV1Y1RVhU","f9699e90e078917d":"1i/mLyJPEk8CT/x/AdErQQkACQBcPr0IIk9DYUDiQ2MUc1wzEGBQiB6LEYRXiBuLEoRWiBiLE4Q1iBWLE2casAx3AogQixNnFbAQd2IwC4sTZxCwBHdA5EYwBYkcMANnbDdsNzY3A4sQQtuPAXEA4CZPCwAJAHRgDGB0ZVxlGEBbIHRlXGUYQFsgdGVcZRhACwBbIA==","bb010f4579136412":"hi+WLyJPFNiEWZNgAcgMi4VQBi+HUAYvhFEQOQSL9mAJGPZgAaAIGAh/mhgL0QtBCQAGL4VR/+AAMQSJh1GJUAAxAYn/4AgYBdELQQkA9mAmT/ZpCwD2aAAs/AgQgbwIABb8CIYvli+mL7Yvxi/WL+YvIk/0f0NoU2ljanNrM9ELQQkAA2wRTFWLom2ybuguUYnYLU+JLtEWPUyJAOACLwEfg2STZfNm82cOvAR38VRIJECJEUA6i/Jlrb7TZgggNYkDaNNk3DQXdPzgCSQIdCDRC0EJAAggKYkDaeNhk2IF4xZgAiIQQ/uPBHLTYxRgACIQQ/uPAXIY0QDl02OEYAxgAECEZABEXjAcAAAiEEP1jwFy02DcMBdw/OEZIJwwD9ESINEQkisL0QtB42TxVAnRC0EJAMNgDH8mT/Zu9m32bPZr9mr2aQsA9miAhpcIsB4EAOzGpQjC66UIYPD8CFJHQjU=","9bed08a38f1020b6":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdEA4AkACCAAiZwYC9EJAAkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","9e7532da7f56183c":"hi+WL6Yvti8iTxbRC0EJAANrFdgV0RJpmCkYiYxQADkCiQDgAhgDGBHRk2QLQV7lA2oP0ZNkC0Er5QNmg2SjZQzXDdELQQkACCAAiZwYC9ELQQkAs2AmT/Zr9mr2aQsA9mgJAEzDvQgALPwIlHAQETRWvAgAJvwIABz8CAAe/Ag=","ac638ec83c8cae54":"APr8","ac4b9d28c5bfb9dd":"vvz8","03bdb1d5e2d1ccb6":"yP38","38712f352f6a6c0e":"B+0LR9ofagz3VQg=","e7cf46a078fed4fa":"Cw==","aca9c8d7ca83a619":"Fv38","6db452c680a7f8c7":"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","f528282873b9c40b":"IGz9","62ab7ebb0d777c57":"mmz9","28dd5e2b2f412bd3":"+HP9CA==","f299791cddd3d666":"Eg==","4bf5122f344554c5":"AQ==","1f38751fbe9c8e00":"Ik9w0AtACQAGL3DREYQIIAGLwLAJAG3REGAIIAiJbNIiY2zQBjMGiSFTa9AGMwKJAOAeoBKAadIiYhKECCAEiwHgEoAA4AERIhESUyNgODARQAmLBXMyERFQAXBa4zIwAIsA4PGvARFd0igiAYkA4AAi9mAmTwsACQBU0RKECCAoiUYvVi92LxFQCEAIQFXSDDJV1QjnVGAMYGNjDDMkZDBgSSAkZEsgEEf0jwAjY2IW5wDkJGAMYAw0Q2AZQAggAYlMZAF0EEf0i0NgACL2Z/Zl9mQ80StBCQBEUP+IAolC0StBCQCGLyJPQ2g30RBgDGAEFD3RC0EJADTREGEcYRMY/+EUGCZPCwD2aERVRIVdZg1iIzYMiUNSIDUJiVMUKtFTYAAhIk8FsFNkJk8LAAHgCwAA4IYvli8iT/x/Q2gr0RJk82UA5ivRC0EJAAggHIvyaZgpGYmTZADljOYp0QtBbGYF4JSAJ9CUgYNglYEg0RJkk2Uh0QtBCQAIIAWJG9ESZJNlHtELQQkABH8mT/ZpCwD2aAHgEYAc0hzTGtX/5m1mMjIKiSFgDWBgMAaJUDACiyGFAckAIfOvBHILAAkACQB4MrYIjkivCABs/QjYlXwLfJIAALBxCwCoUjILAAAAADRu/QjUc/0IfJy9CKTXEQqg1xEK3NbACFC6wAgU2MAIOIDCCOKDAAAAYH+gAIB/oDAC8AbwDAAA/AAAAM8wAAAwAvAJ8AwAAfwAAADPMAAAMALwCfAMAAH8AAAAzzAAADAC8AnwDAAB/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMALwCfAMAAL8AAAAzzAAADAC8AnwDAAC/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMAHwCfAMAAP8AAAAzzAAADAB8AnwDAAH/AAAAM8wAAAwAfAJ8AwAB/wAAADPMAAAMAHwDPAMAAf8AAAAzzAAADAB8AzwDAAL/AAAAM8wAAAwAfAM8AkAC/wAAADPMAAAMAHwDPAJAAv8AAAAzzAAADAE8AzwCQAL/AAAAM8wAAAwBPAM8AkAH/wAAADPMAAAMATwDPAJAB/8AAAAzzAAADAE8AzwCQAe/AAAAM8wAAAwBPAM8AkALvwAAADPMAAAMATwDPAJAC78AAAAzzAAADAE8AzwCQAu/AAAAM8wAAAwBPAM8AkALfwAAADPMAAAMIDwDPAGAD38AAD/zwAAADCA8AzwBgB9/AAA/88AAAAwgPAM8AYAfPwAAP/PAAAAMIDwDPAGAHj8AAD/zwAAADCA8AzwBgC4/AAA/88AAAAwgPAM8AYAuPwAAP/PAAAAMIDwDPAGALj8AAD/zwAAADAI8AzwBgC0/AAA/88AAAAwCPAM8AYA9PwBAP/PAAAAMAjwDPAGAPT8AQD/zwAAADAI8AzwAwDg/AEA/88AAAAwCPAM8AMA4PwCAP/PAAAAMAjwCfADAOD8AgD/zwAAADAI8AnwAwDg/AIA/88AAAAwCPAJ8AMA0PwCAP/PAAAAMEDwCfADAND8AwD/zwAAADBA8AnwAwDQ/AMA/88AAAAwQPAJ8AMAwPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwCA/AMA/88AAAAwQPAJ8AMAgPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwBA/AMAAM8AAAMwQPAG8AMAQPwDAADPAAADMEDwBvADAED8AwAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMEDwBvADAAD8AgAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMAjwBvADAAD8AwAAzwAAAzAI8AbwAwBA/AMAAM8AAAMwCPAG8AMAQPwDAADPAAADMAjwA/ADAED8AwAAzwAAAzAI8APwAwCA/AMAAM8AAAMwCPAD8AYAgPwDAADPAAADMAjwA/AGAID8AwAAzwAAAzCA8APwBgCA/AMAAM8AAAMwgPAD8AYA0PwDAADPAAADMIDwA/AGAND8AwAAzwAAAzCA8APwBgDQ/AIAAM8AAAMwgPAD8AYA4PwCAADPAAADMIDwA/AGAOD8AgAAzwAAAzCA8APwBgDg/AIAAM8AAAMwgPAD8AYA4PwBAADPAAADMATwA/AJAPD8AQD/zwAAADAE8APwCQD0/AEA/88AAAAwBPAD8AkA9PwAAP/PAAAAMATwA/AJALT8AAD/zwAAADAE8APwCQC4/AAA/88AAAAwBPAD8AkAuPwAAP/PAAAAMATwA/AJALj8AAD/zwAAADAB8APwCQB4/AAA/88AAAAwAfAD8AkAffwAAP/PAAAAMAHwA/AJAH38AAD/zwAAADAB8APwDAAt/AAA/88AAAAwAfAD8AwALvwAAP/PAAAAMAHwBvAMAC78AAD/zwAAADAB8AbwDAAu/AAA/88AAAAwAfAG8AwAHvwAAP/PAAAAMALwBvAMAB/8AAD/zwAAADAC8AbwDAAf/AAA/88AAAAwAvAG8AwAD/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAADAC8AbwDAAL/AAA/88AAAAwAvAG8AwAC/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAAAABAwQFBwgJTABFAEQAIABBAE4ASQBNAEEAVABJAE8ATgAAAA==","3a11950e2934091a":"3HP9CHhkEBECAAAA//////////8BAAAAKpG9CPps/QhEn70ISqC9CCxt/QhYpr0I8qa9CA==","f445e63209e0be24":"IPn8","95951541115495c3":"APn8","e9b0c031f0493d3f":"iQ==","c76a1882879da374":"UGlvbmFhcnIgTW9kcyAxLjAAAAAAAAAAAAAAAAAAAAAB0AsACQAJABH5/Kg=","8815309ed1a1d33f":"aCr9","03c9e94b28441a1f":"KCf9","9266839af92b27a2":"UiT9","8d0d8900ac2cf436":"OiT9","90b1b75e0586814d":"cCf9","5f89ea7b823bb508":"Hiv9","b40832cbfea8a56a":"IBj9","aedd01b3bf32874f":"DCn9","030cb5345274e947":"AdArQAkACQBAIf0I","7728b2e2f6f14165":"0Cv9CA==","1b99b878a8b033a3":"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","cbf33f29584fb8c8":"eC/9CHhkEBECAAAA//////////8BAAAAKpG9CHop/QhEn70ISqC9CKwp/QhYpr0I8qa9CEgATwBUACAAQwBVAEUAIABLAEUAWQBT","3854155ee65e476b":"BAAEAgQEBAYFAAAA/////////////////////////////////////////////////////w==","4f72ba82f5e21909":"AQAAAAAAAAAAAAAAAAAABAAAABMA/zAAAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAAm/14AAAAAAAABAAAAAAAAAAAAAAAAAAAEAAAACADE/wAAAAAAAQAAAAAAAAAAAAAAAAAABAAAADxNAP8AAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAATAP8wAAAAAAABAAAAAAAAAAAAAAAAAAAEAAAAFzP/AAAAAAAAAQAAAAAAAAAAAAAAAAAABAAAACv/ABcAAAAAAAEAAAAAAAAAAAAAAAAAAAQAAAAm/14AAADmB+D6PwYfSOYH4DcC+OD6","ab897fbdedfa502b":"Ew==","34dfc332e32a32ec":"Ik9w0AtACQAGL3DREYQIIAGLwLAJAG3REGAIIAiJbNIiY2zQBjMGiSFTa9AGMwKJAOAeoBKAadIiYhKECCAEiwHgEoAA4AERIhESUyNgODARQAmLBXMyERFQAXBa4zIwAIsA4PGvARFd0igiAYkA4AAi9mAmTwsACQBU0RKECCAoiUYvVi92LxFQCEAIQFXSDDJV1QjnVGAMYGNjDDMkZDBgSSAkZEsgEEf0jwAjY2IW5wDkJGAMYAw0Q2AZQAggAYlMZAF0EEf0i0NgACL2Z/Zl9mQ80StBCQBEUP+IAolC0StBCQCGLyJPQ2g30RBgDGAEFD3RC0EJADTREGEcYRMY/+EUGCZPCwD2aERVRIVdZg1iIzYMiUNSIDUJiVMUKtFTYAAhIk8FsFNkJk8LAAHgCwAA4IYvli8iT/x/Q2gr0RJk82UA5ivRC0EJAAggHIvyaZgpGYmTZADljOYp0QtBbGYF4JSAJ9CUgYNglYEg0RJkk2Uh0QtBCQAIIAWJG9ESZJNlHtELQQkABH8mT/ZpCwD2aAHgEYAc0hzTGtX/5m1mMjIKiSFgDWBgMAaJUDACiyGFAckAIfOvBHILAAkACQAgGP0IDCn9CABs/QjYlXwLfJIAALBxCwCoUjILDRj9CDRu/QjUc/0IfJy9CKTXEQqg1xEK3NbACFC6wAgU2MAIOIDCCOKDAAAAYH+gAIB/oDAC8AbwDAAA/AAAAM8wAAAwAvAJ8AwAAfwAAADPMAAAMALwCfAMAAH8AAAAzzAAADAC8AnwDAAB/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMALwCfAMAAL8AAAAzzAAADAC8AnwDAAC/AAAAM8wAAAwAvAJ8AwAAvwAAADPMAAAMAHwCfAMAAP8AAAAzzAAADAB8AnwDAAH/AAAAM8wAAAwAfAJ8AwAB/wAAADPMAAAMAHwDPAMAAf8AAAAzzAAADAB8AzwDAAL/AAAAM8wAAAwAfAM8AkAC/wAAADPMAAAMAHwDPAJAAv8AAAAzzAAADAE8AzwCQAL/AAAAM8wAAAwBPAM8AkAH/wAAADPMAAAMATwDPAJAB/8AAAAzzAAADAE8AzwCQAe/AAAAM8wAAAwBPAM8AkALvwAAADPMAAAMATwDPAJAC78AAAAzzAAADAE8AzwCQAu/AAAAM8wAAAwBPAM8AkALfwAAADPMAAAMIDwDPAGAD38AAD/zwAAADCA8AzwBgB9/AAA/88AAAAwgPAM8AYAfPwAAP/PAAAAMIDwDPAGAHj8AAD/zwAAADCA8AzwBgC4/AAA/88AAAAwgPAM8AYAuPwAAP/PAAAAMIDwDPAGALj8AAD/zwAAADAI8AzwBgC0/AAA/88AAAAwCPAM8AYA9PwBAP/PAAAAMAjwDPAGAPT8AQD/zwAAADAI8AzwAwDg/AEA/88AAAAwCPAM8AMA4PwCAP/PAAAAMAjwCfADAOD8AgD/zwAAADAI8AnwAwDg/AIA/88AAAAwCPAJ8AMA0PwCAP/PAAAAMEDwCfADAND8AwD/zwAAADBA8AnwAwDQ/AMA/88AAAAwQPAJ8AMAwPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwCA/AMA/88AAAAwQPAJ8AMAgPwDAP/PAAAAMEDwCfADAID8AwD/zwAAADBA8AnwAwBA/AMAAM8AAAMwQPAG8AMAQPwDAADPAAADMEDwBvADAED8AwAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMEDwBvADAAD8AgAAzwAAAzBA8AbwAwAA/AIAAM8AAAMwQPAG8AMAAPwCAADPAAADMAjwBvADAAD8AwAAzwAAAzAI8AbwAwBA/AMAAM8AAAMwCPAG8AMAQPwDAADPAAADMAjwA/ADAED8AwAAzwAAAzAI8APwAwCA/AMAAM8AAAMwCPAD8AYAgPwDAADPAAADMAjwA/AGAID8AwAAzwAAAzCA8APwBgCA/AMAAM8AAAMwgPAD8AYA0PwDAADPAAADMIDwA/AGAND8AwAAzwAAAzCA8APwBgDQ/AIAAM8AAAMwgPAD8AYA4PwCAADPAAADMIDwA/AGAOD8AgAAzwAAAzCA8APwBgDg/AIAAM8AAAMwgPAD8AYA4PwBAADPAAADMATwA/AJAPD8AQD/zwAAADAE8APwCQD0/AEA/88AAAAwBPAD8AkA9PwAAP/PAAAAMATwA/AJALT8AAD/zwAAADAE8APwCQC4/AAA/88AAAAwBPAD8AkAuPwAAP/PAAAAMATwA/AJALj8AAD/zwAAADAB8APwCQB4/AAA/88AAAAwAfAD8AkAffwAAP/PAAAAMAHwA/AJAH38AAD/zwAAADAB8APwDAAt/AAA/88AAAAwAfAD8AwALvwAAP/PAAAAMAHwBvAMAC78AAD/zwAAADAB8AbwDAAu/AAA/88AAAAwAfAG8AwAHvwAAP/PAAAAMALwBvAMAB/8AAD/zwAAADAC8AbwDAAf/AAA/88AAAAwAvAG8AwAD/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAADAC8AbwDAAL/AAA/88AAAAwAvAG8AwAC/wAAP/PAAAAMALwBvAMAAv8AAD/zwAAAAABAwQFBwgJTABFAEQAIABBAE4ASQBNAEEAVABJAE8ATgAAAA==","cfdb635170a79a20":"eC/9CHhkEBECAAAA//////////8BAAAAKpG9CHop/QhEn70ISqC9CKwp/QhYpr0I8qa9CNxz/Qh4ZBARAgAAAP//////////AQAAACqRvQj6bP0IRJ+9CEqgvQgsbf0IWKa9CPKmvQg="}}; diff --git a/builder/ui.js b/builder/ui.js new file mode 100644 index 0000000..e45998b --- /dev/null +++ b/builder/ui.js @@ -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 = + '' + + '' + m.name + ' ' + m.id + '' + + '' + m.what + '' + + '' + (m.by === 'ours' ? 'ours · made with Claude Code' : 'by arnoldasr') + + (m.needs ? ' · needs ' + m.needs + '' : '') + '' + + '' + m.state + ''; + 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 ' + r.updSha + '
of the application ' + r.appSha + ''; + 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); + }); +})(); diff --git a/docs.html b/docs.html index 3e2b571..5f017f6 100644 --- a/docs.html +++ b/docs.html @@ -4,7 +4,7 @@ CDJ-900NXS Mods Docs - + @@ -18,6 +18,8 @@ CDJ-900NXS Mods @@ -28,16 +30,19 @@
Documentation

Using the mods

-

Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware 1.31; the patcher only accepts that update.

+

Everything you need to build the update, put it on your CDJ-900NXS and use each mod. The player must run the official firmware 1.31; the builder only accepts that update.

Install

    -
  1. Get the official C900NXS.UPD for firmware 1.31 from Pioneer DJ. The patcher checks its SHA-256 and refuses any other file.
  2. -
  3. Run the patcher with Python 3. Tick the mods with the arrow keys and Space, build with Enter. -
    $ python cdj900nxs_patcher.py C900NXS.UPD
    - Ticking a mod also ticks what it needs (utility needs code, hotcuekeys needs cuecolours); unticking one unticks what depends on it (beatjump needs hotcuekeys). credit (ticked at the start) makes the boot screen say Pionope Mods by Givou where it said Ver 1.31; untick it to keep the stock text. -
  4. -
  5. Read the disclaimer and type yes to accept it. Nothing is built without it.
  6. -
  7. The new file is written to out/<date>-<mods>/C900NXS.UPD, with a report.txt listing every check.
  8. -
  9. Copy C900NXS.UPD to the root of a FAT32 USB stick and run the update from the player as you would for an official update.
  10. +
  11. Get the official C900NXS.UPD for firmware 1.31 from Pioneer DJ.
  12. +
  13. Open the builder and choose the file. It checks its SHA-256 and refuses any other file.
  14. +
  15. Tick the mods. Ticking a mod also ticks what it needs (hotcuekeys needs cuecolours); unticking one unticks what needs it.
  16. +
  17. Confirm the two points (no liability, code written with Claude Code) and build. Everything happens in your browser: nothing is uploaded, nothing is downloaded.
  18. +
  19. Download C900NXS.UPD, copy it to the root of a FAT32 USB stick and run the update from the player as you would for an official update.
-
What the patcher guarantees. 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.
-

Without a terminal that can read single keys, name the mods instead and accept the disclaimer with --accept:

-
$ python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys --accept
+
What the builder guarantees. 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.
+ +

Command line

+

The Python patcher builds exactly the same file, byte for byte. Tick the mods with the arrow keys and Space, build with Enter, accept the disclaimer by typing yes:

+
$ python cdj900nxs_patcher.py C900NXS.UPD
+

Or name the mods and accept with --accept:

+
$ python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys,ledsaver,credit --accept

Back to stock

-

Build the original application, relabelled so the player accepts it, and update as above:

-
$ python cdj900nxs_patcher.py C900NXS.UPD --stock
-

Keep this file on a stick before you try any mod.

+

Build with no mod ticked in the builder (or --stock 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.

-

code arnoldasr

-

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.

+

rgb_waveform arnoldasr

+

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.

RGB detail waveform
RGB detail waveform.
-

utility arnoldasr

-

UTILITY › SLIP FLASHING becomes PHASE METER; its value chooses which phase meter is shown. Needs code.

-
UTILITY with PHASE METER
UTILITY › PHASE METER.
+

extended_bpm arnoldasr

+

The BPM to two decimals: the tenths as a small digit above, the hundredths in the place of the stock decimal.

-

overview arnoldasr

-

The overview waveform's colour table becomes a red-to-white gradient. Data only, no code.

+

phasemeter arnoldasr

+

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 PHASE METER: ON shows the lines, OFF the stock boxes. The orange marker needs another player as tempo master.

+
UTILITY with PHASE METER
UTILITY › PHASE METER.

cuecolours ours

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.

Overview with coloured memory cues and hot cues A to F
Coloured memory cues and hot cues A–F on the overview.
-

hotcuekeys ours

+

hotcuekeys ours experimental

The CDJ-900NXS has no hot cue pads. This mod lends it three: the top three BEAT DIVIDE keys left of the jog. It needs cuecolours, which reads the hot cues from your USB.

-
UTILITY with HOT CUE KEYS
UTILITY › HOT CUE KEYS, the new last item.
+
UTILITY with HOT CUE KEYS
UTILITY › HOT CUE KEYS.

The hot key mode

    @@ -115,14 +119,22 @@
    –
    1/4
    –
    1/8
    -

    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.

    -

    With the mode off, or HOT CUE KEYS set to OFF, the keys do what they always did.

    +

    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.

    + +

    Loading

    +

    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:

    + +

    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.

    Play a hot cue

    -

    Press the key briefly. The deck jumps to the hot cue and plays — also when it was paused.

    +

    Press the key briefly. The deck jumps to the hot cue and plays — also from a pause, in CDJ and in VINYL mode.

    Gate

    -

    Press the key and keep holding it. The deck jumps and starts playing at once, exactly as with a short press; nothing waits. When you let go:

    +

    Press the key and keep holding it. The deck jumps and starts playing at once, exactly as with a short press. When you let go:

@@ -218,7 +224,7 @@
Made with Claude Code · Claude Opus 5.5
-

This website, the mods beatjump, cuecolours and hotcuekeys, 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, utility and overview are by arnoldasr.

+

This website, the builder, the mods cuecolours, hotcuekeys, ledsaver and credit, 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 rgb_waveform, extended_bpm and phasemeter are by arnoldasr.

Disclaimer

diff --git a/index.html b/index.html index 0f43aef..c9b22bc 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ CDJ-900NXS Mods - + @@ -18,6 +18,8 @@ CDJ-900NXS Mods @@ -31,13 +33,13 @@

New tricks for an old favourite.

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 - hot cues you can play from the deck itself. 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 + hot cues you can play from the deck itself. Build your own update file + right here in the browser — the boot loader stays untouched.

@@ -56,10 +58,10 @@
-
6
mods, pick the ones you want
-
A–C
hot cues on the BEAT DIVIDE keys, at once
+
7
mods, pick the ones you want
+
A–C
hot cues on the BEAT DIVIDE keys, loaded in the background
0 bytes
changed in the boot loader and recovery
-
1 file
a normal C900NXS.UPD for the player's USB update
+
1 file
a normal C900NXS.UPD, built in your browser
@@ -67,83 +69,82 @@

The mods

-

Each one compared with stock. Three are by arnoldasr, three are ours. Choose any combination in the patcher.

+

Each one compared with stock. Three are by arnoldasr, four are ours. Choose any combination in the builder.

- ran on a player - emulator only - partly tested - known faults + stable + experimental + ran on a player
- +
-

RGB waveform, BPM & phase meter code

+

RGB waveform rgb_waveform

by arnoldasr
-
ran on a player
+
ran on a playerstable
-

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.

+

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.

Stock blue detail waveform
Stock
-
RGB detail waveform
Modded
+
RGB detail waveform
RGB
-
-
Stock BPM 145.0
BPM · stock
-
BPM with two decimals
BPM · two decimals
+
+
Stock blue overview waveform
Overview · stock
+
Gradient overview waveform
Overview · gradient (with cue colours and hot cues)
    -
  • StockBlue waveform, BPM to one decimal, phase meter as four boxes.
  • -
  • ModdedRGB waveform, BPM to two decimals (stacked), phase meter on two lines.
  • +
  • StockBlue waveform, flat blue overview.
  • +
  • ModdedRGB waveform, red-to-white overview.
- + +
+
+ +
+

Extended BPM extended_bpm

+
by arnoldasr
+
+
ran on a playerstable
+
+

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.

+
+
Stock BPM 145.0
Stock
+
BPM with two decimals
Two decimals
+
+
    +
  • StockBPM to one decimal.
  • +
  • ModdedBPM to two decimals, stacked.
  • +
+
+ +
-

Phase meter switch utility

-
by arnoldasr · needs code
+

Phase meter phasemeter

+
by arnoldasr
-
ran on a player
+
ran on a playerstable
-

Choose the phase meter in UTILITY. The rarely used SLIP FLASHING item becomes PHASE METER.

+

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.

-
Stock phase meter: four boxes
Phase meter · stock
-
Two-line phase meter
Phase meter · two lines
+
Stock phase meter: four boxes
Stock
+
Two-line phase meter
Two lines
UTILITY menu with PHASE METER item
UTILITY › PHASE METER
    -
  • StockUTILITY › SLIP FLASHING.
  • -
  • ModdedUTILITY › PHASE METER picks the meter.
  • -
-
- - -
-
- -
-

Overview colour gradient overview

-
by arnoldasr
-
-
ran on a player
-
-

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.

-
-
Stock blue overview waveform
Stock
-
Gradient overview waveform with coloured cues and hot cues
Modded (with cue colours and hot cues)
-
-
    -
  • StockFlat blue overview.
  • -
  • ModdedRed-to-white gradient.
  • +
  • StockFour boxes; UTILITY › SLIP FLASHING.
  • +
  • ModdedTwo lines with markers; UTILITY › PHASE METER picks the meter.
@@ -155,9 +156,9 @@

Cue colours & hot cues shown cuecolours

ours · made with Claude Code
-
ran on a player
+
ran on a playerstable
-

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.

+

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.

Stock: red memory triangles only
Stock
Hot cue A box on the detail waveform
Hot cue A on the waveform
@@ -177,15 +178,15 @@

Hot cue keys hotcuekeys

ours · made with Claude Code · needs cuecolours
-
partly tested
+
ran on a playerexperimental
-

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.

+

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).

Stock: QUANTIZE label only
Stock
HOT KEYS box beside QUANTIZE
HOT KEYS mode on
-
UTILITY menu with BEAT JUMP and HOT CUE KEYS items
UTILITY › HOT CUE KEYS
+
UTILITY menu with the HOT CUE KEYS item
UTILITY › HOT CUE KEYS
+

More in the works — beat jump, stems, a uniform key notation and a 3-band waveform: see Upcoming.

-
+
-

One patcher, your choice

-

The patcher takes the official 1.31 update, adds the mods you tick and writes a new C900NXS.UPD. 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.

+

Build it in your browser

+

The builder takes the official 1.31 update, adds the mods you tick and gives you a new C900NXS.UPD — 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.

+ +

Prefer the command line? The Python patcher builds the same file, byte for byte:

# pick the mods in a list $ python cdj900nxs_patcher.py C900NXS.UPD # or name them -$ python cdj900nxs_patcher.py C900NXS.UPD --mods code,utility,overview,cuecolours,hotcuekeys +$ python cdj900nxs_patcher.py C900NXS.UPD --mods rgb_waveform,extended_bpm,phasemeter,cuecolours,hotcuekeys # back to stock $ python cdj900nxs_patcher.py C900NXS.UPD --stock
-

Before it builds, the patcher shows a disclaimer that you must accept. Full install guide →

+

Both ask you to accept a disclaimer first. Full install guide →

@@ -237,7 +258,7 @@
Made with Claude Code · Claude Opus 5.5
-

This website, the mods beatjump, cuecolours and hotcuekeys, 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, utility and overview are by arnoldasr.

+

This website, the builder, the mods cuecolours, hotcuekeys, ledsaver and credit, 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 rgb_waveform, extended_bpm and phasemeter are by arnoldasr.

Disclaimer

diff --git a/style.css b/style.css index 3ae0a85..9abe7af 100644 --- a/style.css +++ b/style.css @@ -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; } +} diff --git a/tools/make_builder_data.py b/tools/make_builder_data.py new file mode 100644 index 0000000..122a009 --- /dev/null +++ b/tools/make_builder_data.py @@ -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() diff --git a/upcoming.html b/upcoming.html new file mode 100644 index 0000000..d142a13 --- /dev/null +++ b/upcoming.html @@ -0,0 +1,121 @@ + + + + + + CDJ-900NXS Mods Upcoming + + + + + + + + + +
+
+ CDJ-900NXS Mods + + +
+
+ +
+
Upcoming
+

What comes next

+

Ideas we are working on or looking into. None of them is in the builder yet. Planned means we know how to do it; feasibility means we are still finding out whether the player can do it at all.

+
+ planned + feasibility +
+ +
+ +
+
+ +
+

Beat jump beatjump

+
ours · made with Claude Code
+
+
emulator onlyplanned
+
+

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.

+
    +
  • NowBuilt and working in the emulator, on top of the hot cue engine.
  • +
  • NextTesting on a player before it goes into the builder.
  • +
+
+ +
+
+ +
+

Stems

+
ours · idea
+
+
feasibility
+
+

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.

+
    +
  • QuestionWhether 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.
  • +
  • NextMeasuring what the DSP has to spare, then deciding.
  • +
+
+ +
+
+ +
+

Uniform Camelot / Open Key

+
ours · idea
+
+
planned
+
+

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.

+
    +
  • NowThe player shows the key as it was exported.
  • +
  • PlannedOne notation everywhere, set on the player.
  • +
+
+ +
+
+ +
+

3-band waveform

+
ours · idea
+
+
feasibility
+
+

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.

+
    +
  • QuestionWhether the band data rekordbox exports for newer players can be read and drawn fast enough on this one.
  • +
  • NextReading the data on the player, then a renderer.
  • +
+
+ +
+
+ + + + +