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:
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.
@@ -126,7 +129,8 @@
the big waveform stays black and appears once all are ready;
PLAY, CUE and the hot cue keys are locked, so nothing interrupts the loading;
-
the key of the hot cue being loaded blinks.
+
the key of the hot cue being loaded blinks;
+
nothing is heard: the output stays silent until the deck stands at its CUE point again.
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.
@@ -139,6 +143,7 @@
held less than 0.5 s: it simply plays on (a normal hot cue);
held 0.5 s or longer: it jumps back to the hot cue and stands there, ready to go again.
+
From 0.5 s on the key blinks slowly while you hold it, so you can see the gate is armed.
Set a hot cue
On a key without a hot cue (LED off):
@@ -163,6 +168,7 @@
off — no hot cue dim — hot cue there blinking — pressed, set, deleted or being loaded
+ blinking slowly — a gate held (0.5 s or longer)
Good to know
@@ -173,6 +179,15 @@
No slip hot cues yet: with SLIP on, a hot cue plays as without SLIP.
+
beatjump oursexperimental
+
The TRACK SEARCH keys ◀◀ / ▶▶ jump along the track's beat grid instead of changing the track. How far is chosen in UTILITY › BEAT JUMP (where SCREEN SAVER was): OFF, 1, 2, 4 or 8 beats; 4 until you choose, and your choice stays when the player is switched off.
+
+
The jump is exactly that many grid beats, so with 4 or 8 the phase meter shows the same place in the bar after the jump; with 1 or 2 the beats still line up, the bar moves on by one or two beats.
+
Playing, it is made at once in the audio the player has already decoded: no gap, no pre-roll. Paused or at the cue it moves the deck there; PLAY plays on from it.
+
The player always keeps 10 s of decoded audio ahead; a press that would leave less (very fast clicks with 8 beats) is ignored and the next one counts again. Nothing is put off, so the player never runs dry (no EMERGENCY LOOP).
+
It needs hotcuekeys (its engine makes the jumps) and cuecolours; the builder ticks them for you.
+
+
ledsaver ours
UTILITY › LED ANIMATION (the last item) is ON from the first start; set it to OFF to switch it off (the player keeps the choice). When the player's own screen saver dims the display (5 minutes without a track loaded, or 100 minutes paused or at the cue), the LEDs play a loop of about four and a half seconds:
@@ -183,11 +198,24 @@
Any key ends it, as it ends the screen saver.
+
keynotation oursexperimental
+
rekordbox writes each track's key as text, in whatever notation was set in its preferences when the stick was exported: 4A, 9m or Fm for the same key. This mod shows every key in one notation of your choice, UTILITY › KEY NOTATION (the last item):
+
+
CAMELOT (default) — 1A–12A minor, 1B–12B major, as on the Camelot wheel;
+
OPEN KEY — 1m–12m minor, 1d–12d major (8A = 1m = Am);
+
CLASSIC — Am, F#m, Db … as rekordbox writes them;
+
OFF — the key as exported.
+
+
The player converts with a table of all spellings, sharps and flats included (G#m, D#m, A#, Gbm …). It works on the main screen, in INFO and in BROWSE › KEY (the key list and its compatible-key groups such as “1A, 1B, 12A, 2A”); a new choice shows from the next track loaded.
+
Each key is drawn in the colour of its place on the Camelot wheel (8A pink, 5A orange, 4A yellow …), in all three notations; with OFF it stays white. A highlighted row keeps its black text, and the letters you type in SEARCH are left as they are. The player's text colour is set per text box, so the mod sets it for a key's box just before the player draws it.
+ 5A in its wheel colour.
+ UTILITY › KEY NOTATION.
+
credit ours
The boot screen says Pionaarr Mods 1.0 where it said Ver 1.31. Ticked at the start; untick it to keep the stock text. UTILITY and the LINK screen still show the real version, 1.31.
The boot screen.
-
Beat jump, stems, a uniform key notation and a 3-band waveform are in the works: see Upcoming.
+
Quick search, USB auto load, a reversed phase meter, stems and a 3-band waveform are in the works: see Upcoming.
Status
@@ -200,7 +228,9 @@
phasemeter
arnoldasr
stable
ran on a player
cuecolours
ours
stable
ran on a player
hotcuekeys
ours
experimental
ran on a player
+
beatjump
ours
experimental
ran on a player
ledsaver
ours
stable
ran on a player
+
keynotation
ours
experimental
emulator only
credit
ours
stable
ran on a player
@@ -224,7 +254,7 @@
Made with Claude Code · Claude Opus 5.5
-
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.
+
This website, the builder, the mods cuecolours, hotcuekeys, ledsaver, keynotation 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.
A set of firmware mods that bring features of newer players to the CDJ-900NXS:
- an RGB waveform, a BPM to two decimals, a phase meter, rekordbox cue colours and
- hot cues you can play from the deck itself. Build your own update file
+ an RGB waveform, a BPM to two decimals, a phase meter, rekordbox cue colours,
+ beat jump 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.
TRACK SEARCH ◀◀ / ▶▶ jumps 1, 2, 4 or 8 beats along the rekordbox beat grid instead of changing the track — at once, without a gap and in phase with the other deck, as on the newest players. Chosen in UTILITY › BEAT JUMP (default 4 beats, kept when the player is off). Works playing, paused and at the cue. The jump is made in the audio the player has already decoded; a press it cannot jump yet (clicked very fast with 8 beats) is simply ignored instead of running the player dry.
+
+
StockTRACK SEARCH goes to the previous / next track.
+
ModdedWhole bars (4, 8) or beats (1, 2) forward and back, the phase meter stays where it was.
Every track's key in one notation, whatever rekordbox exported: Camelot (8A), Open Key (1m) or classic (Am), chosen in UTILITY › KEY NOTATION. The player converts on its own with a table of all spellings, sharps and flats included — on the main screen, in INFO and in BROWSE › KEY. The key shows in the colour of its place on the Camelot wheel.
+
+ As exported (Camelot): 4A
+ KEY NOTATION › OPEN KEY: 9m
+ Wheel colour: 5A in orange
+ UTILITY › KEY NOTATION
+
+
+
StockThe key as rekordbox's export setting wrote it.
+
ModdedOFF, CAMELOT (default), OPEN KEY or CLASSIC — the same notation on every stick, in its wheel colour.
More in the works — beat jump, stems, a uniform key notation and a 3-band waveform: see Upcoming.
+
More in the works — beat jump, coloured Camelot keys, stems and a 3-band waveform: see Upcoming.
@@ -258,7 +301,7 @@
Made with Claude Code · Claude Opus 5.5
-
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.
+
This website, the builder, the mods cuecolours, hotcuekeys, ledsaver, keynotation 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/script.js b/script.js
index 3972132..d477b53 100644
--- a/script.js
+++ b/script.js
@@ -21,6 +21,41 @@
label();
});
+ // version badge and changelog (assets/versions.js, made by tools/make_builder_data.py)
+ var V = window.CDJ_VERSIONS;
+ var head = document.querySelector('.top .wrap');
+ if (V && head) {
+ var esc = function (t) { return String(t).replace(/[&<>]/g, function (c) { return { '&': '&', '<': '<', '>': '>' }[c]; }); };
+ var vb = document.createElement('button');
+ vb.type = 'button'; vb.className = 'ver-btn'; vb.textContent = 'v' + V.site;
+ vb.setAttribute('aria-haspopup', 'dialog'); vb.title = 'Changelog';
+ var brand = head.querySelector('.brand');
+ if (brand && brand.nextSibling) head.insertBefore(vb, brand.nextSibling); else head.appendChild(vb);
+ var pop = document.createElement('div');
+ pop.className = 'ver-pop'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-label', 'Changelog');
+ var html = '
' +
+ '
Changelog
Site ' + esc(V.site) + ' · ' + esc(V.date) + ' · Semantic Versioning, dates ISO 8601
';
+ pop.innerHTML = html;
+ document.body.appendChild(pop);
+ var close = function () { pop.classList.remove('open'); vb.focus(); };
+ vb.addEventListener('click', function () { pop.classList.add('open'); pop.querySelector('.close').focus(); });
+ pop.querySelector('.close').addEventListener('click', close);
+ pop.addEventListener('click', function (e) { if (e.target === pop) close(); });
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && pop.classList.contains('open')) close(); });
+ // every mod card's version beside its name
+ document.querySelectorAll('.card h3 code').forEach(function (c) {
+ var v = V.mods[c.textContent.trim()];
+ if (v) { var s = document.createElement('span'); s.className = 'ver'; s.textContent = 'v' + v; c.parentNode.appendChild(s); }
+ });
+ }
+
// before / after
document.querySelectorAll('.compare').forEach(function (box) {
var range = box.querySelector('input[type="range"]');
diff --git a/style.css b/style.css
index 9abe7af..dfcabef 100644
--- a/style.css
+++ b/style.css
@@ -376,3 +376,39 @@ button.btn { font: inherit; font-weight: 700; cursor: pointer; }
@media (max-width: 360px) {
.tabs a { padding: 5px 6px; font-size: .74rem; }
}
+
+/* ---- version badge and changelog ---- */
+.ver-btn {
+ border: 1px solid var(--line); background: var(--card); color: var(--muted);
+ font: 600 .74rem/1 var(--mono); padding: 7px 10px; border-radius: 999px; cursor: pointer; white-space: nowrap;
+}
+.ver-btn:hover { color: var(--text); border-color: var(--accent); }
+.ver-pop {
+ position: fixed; inset: 0; z-index: 50; display: none; align-items: flex-start; justify-content: center;
+ background: color-mix(in srgb, var(--bg) 55%, transparent); padding: 80px 16px 16px;
+}
+.ver-pop.open { display: flex; }
+.ver-box {
+ width: min(560px, 100%); max-height: calc(100vh - 100px); overflow: auto;
+ background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow);
+ padding: 20px 22px;
+}
+.ver-box h2 { margin: 0 0 4px; font-size: 1.15rem; }
+.ver-box .sub { color: var(--muted); font-size: .85rem; margin: 0 0 14px; }
+.ver-box h3 { margin: 16px 0 6px; font-size: .95rem; }
+.ver-box h3 .d { color: var(--faint); font: 500 .78rem var(--mono); margin-left: 8px; }
+.ver-box ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: .9rem; }
+.ver-box .close {
+ float: right; border: 1px solid var(--line); background: var(--card-2); color: var(--muted);
+ width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 1rem;
+}
+.ver-box .close:hover { color: var(--text); }
+.ver-mods { display: grid; grid-template-columns: 1fr auto; gap: 2px 16px; font-size: .88rem; margin-top: 6px; }
+.ver-mods code { background: none; border: 0; padding: 0; }
+.ver-mods span { color: var(--muted); font-family: var(--mono); }
+.ver { font: 600 .7rem/1 var(--mono); color: var(--faint); margin-left: 6px; }
+@media (max-width: 560px) {
+ .top .brand { display: none; }
+ .ver-btn { padding: 6px 7px; font-size: .68rem; }
+ .tabs a { padding: 6px 10px; }
+}
diff --git a/tools/changelog.json b/tools/changelog.json
new file mode 100644
index 0000000..57d6e93
--- /dev/null
+++ b/tools/changelog.json
@@ -0,0 +1,38 @@
+{
+ "_about": "The site's changelog, newest first. Versions follow Semantic Versioning (MAJOR.MINOR.PATCH), dates ISO 8601 (DIN EN 28601). make_builder_data.py adds every mod's current version from its module and writes assets/versions.js.",
+ "site": [
+ {
+ "version": "1.1.0",
+ "date": "2026-10-20",
+ "changes": [
+ "Beat jump is in the builder (experimental): TRACK SEARCH jumps 1/2/4/8 beats, gapless, in phase.",
+ "Key notation 1.2.0: the key in its Camelot wheel colour, also in BROWSE › KEY and INFO; lists of keys converted too.",
+ "Hot cue keys 1.2.0: hot cues load in the background, the deck's keys are locked and HOT KEYS blinks LOADING meanwhile; search and slip no longer upset after a load.",
+ "Version shown at the top, with this changelog; every mod has its own version.",
+ "Upcoming: quick search, USB auto load and a reversed phase meter."
+ ]
+ },
+ {
+ "version": "1.0.0",
+ "date": "2026-10-19",
+ "changes": [
+ "First release of the site: the mods, the in-browser builder, docs and upcoming."
+ ]
+ }
+ ],
+ "mods": {
+ "beatjump": [
+ {"version": "1.0.0", "date": "2026-10-20", "changes": ["Jumps in the DSP's decoded audio, playing, paused or at cue standby; presses the DSP cannot jump yet are ignored, so no EMERGENCY LOOP."]}
+ ],
+ "hotcuekeys": [
+ {"version": "1.2.0", "date": "2026-10-20", "changes": ["Background loading invisible; keys locked and LOADING blinking while a track loads; no more interference with search and slip."]},
+ {"version": "1.1.0", "date": "2026-10-19", "changes": ["Hot cue engine: instant hot cues from the DSP's slots, saved on the USB in rekordbox colours."]},
+ {"version": "1.0.0", "date": "2026-10-18", "changes": ["Hot cues A-C on the BEAT DIVIDE keys: play, gate, set, delete."]}
+ ],
+ "keynotation": [
+ {"version": "1.2.0", "date": "2026-10-20", "changes": ["Keys in their notation and colour everywhere on the screen, BROWSE › KEY included; the SEARCH input is left alone."]},
+ {"version": "1.1.0", "date": "2026-10-19", "changes": ["The key in its Camelot wheel colour in the title bar."]},
+ {"version": "1.0.0", "date": "2026-10-19", "changes": ["Camelot, Open Key or classic notation, UTILITY › KEY NOTATION."]}
+ ]
+ }
+}
diff --git a/tools/make_builder_data.py b/tools/make_builder_data.py
index 122a009..10611f2 100644
--- a/tools/make_builder_data.py
+++ b/tools/make_builder_data.py
@@ -4,13 +4,14 @@
usage: make_builder_data.py [--repo ../CDJ900NXSVirtual]
-Our mods (cuecolours, hotcuekeys, ledsaver, credit) are made by the emulator
+Our mods (beatjump, cuecolours, hotcuekeys, ledsaver, keynotation, credit) are made by the emulator
checkout's mods/patch_cdj900nxs.py for every combination the builder offers;
their bytes do not depend on which of arnoldasr's mods are chosen (checked
below). For each combination the bytes our mods change in the application
go into data.js, except what is the player's own firmware:
- copy the UTILITY item table (hotcuekeys and ledsaver extend a copy of it)
+ copy the UTILITY item table (hotcuekeys, ledsaver and keynotation extend a
+ copy of it)
is copied from the user's own application in the browser;
box the HOT KEYS box is the QUANTIZE label's image recoloured with
HOT KEYS written on it; the browser makes it from the user's image.
@@ -38,8 +39,8 @@ import sys
HERE = os.path.dirname(os.path.abspath(__file__))
WEB = os.path.dirname(HERE)
-OURS = ('cuecolours', 'hotcuekeys', 'ledsaver', 'credit')
-NEEDS = {'hotcuekeys': 'cuecolours'}
+OURS = ('beatjump', 'cuecolours', 'hotcuekeys', 'ledsaver', 'keynotation', 'credit')
+NEEDS = {'hotcuekeys': 'cuecolours', 'beatjump': 'hotcuekeys'}
UT_TABLE_STOCK = 0x08C6C8DC
UT_TABLE_LEN = 17 * 52
BASE = 0x08000000
@@ -71,6 +72,17 @@ def runs(base, final):
return out
+def spans(idx):
+ """[(lo, hi)] of the runs of consecutive numbers in idx (sorted)."""
+ out = []
+ for i in idx:
+ if out and out[-1][1] == i:
+ out[-1] = (out[-1][0], i + 1)
+ else:
+ out.append((i, i + 1))
+ return out
+
+
def ops_for(stock, final, boxes):
"""The browser's operations for one combination: data, copy, box."""
table = stock[UT_TABLE_STOCK - BASE:UT_TABLE_STOCK - BASE + UT_TABLE_LEN]
@@ -81,18 +93,28 @@ def ops_for(stock, final, boxes):
off = box['addr'] - BASE
masked[off:off + box['len']] = padded[off:off + box['len']]
for addr, data in runs(stock, bytes(masked)):
- cuts = [] # (start, end, op) inside this run
- k = data.find(table)
+ # (start, end, ops) inside this run: a copy of the table, a few of its
+ # items changed (beatjump renames SCREEN SAVER), is the copy and then
+ # our bytes over it
+ cuts = []
+ k = data.find(table[:52])
while k >= 0:
- cuts.append((k, k + UT_TABLE_LEN, {'op': 'copy', 'addr': addr + k, 'src': UT_TABLE_STOCK,
- 'len': UT_TABLE_LEN}))
- k = data.find(table, k + UT_TABLE_LEN)
+ seg = data[k:k + UT_TABLE_LEN]
+ diff = [i for i in range(len(seg)) if seg[i] != table[i]] if len(seg) == UT_TABLE_LEN else None
+ if diff is not None and len(diff) <= 2 * 52:
+ ops = [{'op': 'copy', 'addr': addr + k, 'src': UT_TABLE_STOCK, 'len': UT_TABLE_LEN}]
+ for lo, hi in spans(diff):
+ ops.append({'op': 'data', 'addr': addr + k + lo, 'data': seg[lo:hi]})
+ cuts.append((k, k + UT_TABLE_LEN, ops))
+ k = data.find(table[:52], k + UT_TABLE_LEN)
+ else:
+ k = data.find(table[:52], k + 1)
cuts.sort(key=lambda c: c[0])
pos = 0
- for lo, hi, op in cuts:
+ for lo, hi, ops in cuts:
if lo > pos:
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:lo]})
- out.append(op)
+ out.extend(ops)
pos = hi
if pos < len(data):
out.append({'op': 'data', 'addr': addr + pos, 'data': data[pos:]})
@@ -112,7 +134,7 @@ def apply_ops(app, stock, ops, hk, size):
elif o['op'] == 'assets':
out[off:off + o['len']] = ASSETS(stock)[0][:o['len']]
else:
- _, _, px = hk.box_pixels(stock, o['res'], o['bg'], o['ink'])
+ _, _, px = hk.box_pixels(stock, o['res'], o['bg'], o['ink'], o['text'])
out[off:off + o['len']] = px
return bytes(out)
@@ -135,8 +157,12 @@ def main():
ASSETS = p.assets
_, labels = hk.routine(hk.DECODER, cuecolours.HOT_TABLE)
w, h, on = hk.box_pixels(stock, hk.RES_QUANTIZE, 0xF800, 0x0000)
- boxes = [{'addr': labels['PIX_ON'], 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xF800, 'ink': 0x0000},
- {'addr': labels['PIX_OFF'], 'len': len(on), 'res': hk.RES_QUANTIZE_OFF, 'bg': 0x0000, 'ink': 0x4208}]
+ boxes = [{'addr': labels['PIX_ON'], 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xF800, 'ink': 0x0000,
+ 'text': 'HOT KEYS'},
+ {'addr': labels['PIX_OFF'], 'len': len(on), 'res': hk.RES_QUANTIZE_OFF, 'bg': 0x0000, 'ink': 0x4208,
+ 'text': 'HOT KEYS'},
+ {'addr': hk.PIX_LOAD, 'len': len(on), 'res': hk.RES_QUANTIZE, 'bg': 0xFC60, 'ink': 0x0000,
+ 'text': 'LOADING'}]
combos = [c for n in range(1, len(OURS) + 1) for c in itertools.combinations(OURS, n) if valid(c)]
variants, chunks = {}, {}
@@ -153,7 +179,7 @@ def main():
elif o['op'] == 'assets':
enc.append(['assets', o['addr'], o['len']])
else:
- enc.append(['box', o['addr'], o['res'], o['bg'], o['ink']])
+ enc.append(['box', o['addr'], o['res'], o['bg'], o['ink'], o['text']])
return enc
# arnoldasr's mods, split up: per combination their bytes, but the asset
@@ -208,7 +234,7 @@ def main():
'ours': variants,
'expect': expect,
'box': {'resTable': hk.RES_TABLE, 'width': w, 'height': h, 'glyphs': glyphs,
- 'text': 'HOT KEYS', 'gap': 3, 'space': 5},
+ 'gap': 3, 'space': 5},
'chunks': chunks,
}
sys.path.insert(0, os.path.join(os.path.dirname(WEB), 'CDJ900NXSPatcher'))
@@ -228,6 +254,36 @@ def main():
json.dump(data, f, separators=(',', ':'))
f.write(';\n')
print('wrote %s (%d bytes, %d + %d variants, %d chunks)' % (out, os.path.getsize(out), len(upstream), len(variants), len(chunks)))
+ write_versions(p)
+
+
+def write_versions(p):
+ """assets/versions.js: the site's version and changelog (tools/changelog.json)
+ and every mod's version as patch_cdj900nxs.VERSIONS has it."""
+ with open(os.path.join(HERE, 'changelog.json'), encoding='utf-8') as f:
+ log = json.load(f)
+ data = {'site': log['site'][0]['version'], 'date': log['site'][0]['date'], 'log': log['site'],
+ 'mods': dict(p.VERSIONS), 'modlog': log.get('mods', {})}
+ out = os.path.join(WEB, 'assets', 'versions.js')
+ with open(out, 'w', encoding='utf-8', newline='\n') as f:
+ f.write("// Generated by tools/make_builder_data.py from tools/changelog.json and the mods' versions.\n")
+ f.write('window.CDJ_VERSIONS = ' + json.dumps(data, indent=1, ensure_ascii=False) + ';\n')
+ print('wrote %s (site %s)' % (out, data['site']))
+ # the pages load the site's own CSS and scripts with ?v=, so
+ # a browser takes the new ones after an update
+ import re
+ for name in sorted(os.listdir(WEB)):
+ if not name.endswith('.html'):
+ continue
+ path = os.path.join(WEB, name)
+ with open(path, encoding='utf-8') as f:
+ html = f.read()
+ new = re.sub(r'(href|src)="((?:style\.css|script\.js|assets/versions\.js|builder/[a-z]+\.js))(\?v=[^"]*)?"',
+ lambda m: '%s="%s?v=%s"' % (m.group(1), m.group(2), data['site']), html)
+ if new != html:
+ with open(path, 'w', encoding='utf-8', newline='\n') as f:
+ f.write(new)
+ print('stamped %s with ?v=%s' % (name, data['site']))
if __name__ == '__main__':
diff --git a/upcoming.html b/upcoming.html
index d142a13..b8b7c26 100644
--- a/upcoming.html
+++ b/upcoming.html
@@ -4,12 +4,13 @@
CDJ-900NXS Mods Upcoming
-
+
-
-
+
+
+
@@ -29,7 +30,7 @@
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.
+
Ideas we are working on or looking into. None of them is in the builder yet. In testing means built and working in the emulator, waiting for a player; planned means we know how to do it; feasibility means we are still finding out whether the player can do it at all.
plannedfeasibility
@@ -39,17 +40,49 @@
-
⏭️
+
🔎
-
Beat jump beatjump
+
Quick search quicksearch
ours · made with Claude Code
-
emulator onlyplanned
+
emulator onlyin testing
-
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.
+
BROWSE held opens SEARCH straight away, from wherever you are; the next plain BROWSE takes you back to the folder you were in. BROWSE pressed twice goes to the list the playing track was loaded from, with the track picked.
-
NowBuilt and working in the emulator, on top of the hot cue engine.
-
NextTesting on a player before it goes into the builder.
+
NowBuilt and working in the emulator; it walks the player's own menus for you, so it takes a moment to get to SEARCH.
+
NextTesting on a player, then the builder.
+
+
+
+
+
+
💾
+
+
USB auto load usbautoload
+
ours · made with Claude Code
+
+
emulator onlyin testing
+
+
The MY SETTINGS on your stick are loaded as soon as the player finds them — no more “Push the MENU button to LOAD”. UTILITY › USB AUTO LOAD switches it on and off.
+
+
NowBuilt and working in the emulator (“Your settings were loaded to PLAYER.” right after the stick is read).
+
NextTesting on a player, then the builder.
+
+
+
+
+
+
↔️
+
+
Phase meter reverse phasereverse
+
ours · made with Claude Code · needs phasemeter
+
+
emulator onlyin testing
+
+
A third value for UTILITY › PHASE METER: OFF, ON or REVERSE — the line meter with its markers running from right to left.
+
+
NowBuilt and working in the emulator, on top of arnoldasr's phase meter.
+
NextTesting on a player, then the builder.
@@ -69,22 +102,6 @@
-
-
-
🎼
-
-
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.