Reference for VirtualDJ controller and keyboard mapper files, and the device-definition XML they bind to.
Ground truth for this doc: four real working mappers copied into examples/Mappers/Local/ (two factory Atomix mappings, one Atomix keyboard mapping, one user-authored controller mapping), plus the official VDJPedia pages ControllerMappingFile_v8.html, ControllerDefinitionMIDIv8.html, and ControllerDefinitionHIDv8.html.
An earlier revision of this document described an inline schema (
<button note="36" action="..."/>directly inside<mapper>). That schema does not match any real mapper file and has been removed. Real mappers bind named controls to VDJScript with<map value="" action=""/>; the hardware I/O lives in a separate device definition.
Source labels match the rest of this repo:
Official, Official forum, Community, Built-in app resource, Local test, Inference.
VirtualDJ splits controller support into two XML layers:
- Device definition (
<device>root) — declares the hardware: which MIDI notes/CCs or HID byte offsets exist, what they are named, LEDs, encoders, value ranges. No VDJScript is evaluated here. - Mapper (
<mapper>root) — binds each named control from the definition to a VDJScript action:<map value="PLAY" action="play_pause"/>.
Most shipped controllers have a compiled built-in definition: the app bundle contains Resources/controllers.dat (binary, not XML) and no Resources/Mappers/ or Resources/Devices/ folders (Local test, bundle 18.0.9482). You only write a device definition XML for hardware VirtualDJ does not already know; you write or edit a mapper whenever you want custom behavior on any controller.
Mapper actions are VDJScript. Device definitions are not — do not expect variables, conditionals, backticks, or actions to be evaluated inside definition elements such as <button>, <led>, cc="", value="", or zero="" (Official forum, staff reply in "Sending MIDI CC Commands"). Put dynamic behavior in the mapper:
<!-- Device definition: static hardware output declarations -->
<led name="LED_CC_000" cc="0x00" channel="0"/>
<led name="LED_CC_001" cc="0x01" channel="0"/>
<!-- Mapper: VDJScript decides which output is active -->
<map value="LED_CC_000" action="var_equal 'CCOut' 0"/>
<map value="LED_CC_001" action="var_equal 'CCOut' 1"/><?xml version="1.0" encoding="UTF-8"?>
<mapper device="DDJXP2" author="Atomix Productions" version="850" date="2025-11-09">
<info>http://www.virtualdj.com/manuals/hardware/pioneer/ddjxp2/index.html</info>
<map value="SHIFT" action="shift" />
<map value="PLAY" action="play_pause" />
</mapper>| Attribute | Description | Source |
|---|---|---|
device |
Unique string identifying the controller; must match the device definition's name |
Official, Local test |
author |
Optional author name; factory mappings saved by the app carry Atomix Productions |
Official, Local test |
version |
Minimum VirtualDJ version, e.g. 850 |
Official, Local test |
date |
Optional creation/update date | Official, Local test |
priority |
Optional precedence: -1 higher, 1 lower |
Official |
Children:
| Element | Description | Source |
|---|---|---|
<info> |
Optional URL documenting the mapping | Official, Local test |
<map value="" action="" /> |
One binding per named control | Official, Local test |
<map> may also carry a name="" attribute (seen empty in factory keyboard mappings; purpose unconfirmed) (Local test).
End-to-end firing verified (
Local test, VirtualDJ 2026, AlphaTheta DDJ-GRV6, 2026-07-27). A minimal two-line mapper fordevice="DDJGRV6"—<map value="ONINIT" …>plus<map value="PLAY_PAUSE" action="set '$v' 1"/>— was loaded on the real controller; pressing the physical play button set the global, read back1over the HTTP interface. This is the first local confirmation that the<map value action>schema binds and fires on hardware, not just that the format parses.
value=""is the control name declared by the device definition (PLAY_PAUSE,CUE,BROWSE,LED_PLAY_PAUSE, …). The name must match the device definition exactly. A wrong name binds nothing and fails silently —value="PLAY"on the DDJ-GRV6 (whose control isPLAY_PAUSE) loaded without error and simply never fired (Local test, 2026-07-27), consistent with VDJScript's no-error parsing. Crib the exact names from a working mapper for that device (rg -o 'value="[^"]*"'), never guess.- Getting the ground-truth names for a recognized controller: export its factory mapping. In Settings → Controllers, pick the device, select Factory default, and Save — VirtualDJ writes the full factory
<mapper>(every control name + its canonical action) toMappers/. Quirk (Local test, DDJ-GRV6, 2026-07-27): saving the unrenamed "factory default" was a no-op; you must rename it first (it saved asAlphaTheta DDJ-GRV6 - factory default copy.xml). The DDJ-GRV6 export was 293 bindings and linted clean against the verb index. This is a<mapper>only — it does not contain the<device>definition (no<button>/<led>/note/cc), so it gives you control names and canonical actions but not the MIDI address map. Factory play action:<map value="PLAY_PAUSE" action="pioneer_play"/>.
- Getting the ground-truth names for a recognized controller: export its factory mapping. In Settings → Controllers, pick the device, select Factory default, and Save — VirtualDJ writes the full factory
action=""is VDJScript. For buttons it runs on press; for sliders/encoders the moved value is passed to the action implicitly (e.g.action="volume"); for LED-out controls the action is evaluated as a query whose result drives the LED.- The same query rules as skin/pad
query=""apply to LED bindings, includingblink(Local test:<map value="DNC_MODE" action="blink 150ms"/>in the factory DDJ-XP2 mapping).
Observed in factory and user mappings (Local test, examples/Mappers/Local/):
| Name | Fires |
|---|---|
ONINIT |
When the mapper loads (controller connect / app start) — used for setup chains (effect_3slots_layout on, setting_setsession …). Firing HTTP-verified (Local test, DDJ-GRV6, 2026-07-27): its action set a global read back as 1. Use it to seed controller state on connect. |
ONEXIT |
When the controller disconnects — used to undo ONINIT state |
UNMAPPED |
Fallback for controls with no explicit <map> (factory keyboard maps it to search) |
SHIFT |
Declares the shift modifier: <map value="SHIFT" action="shift" /> |
SHIFT_<NAME> |
Binding for <NAME> while shift is held (a separate <map> row, not an attribute) |
LED_<NAME> |
Output binding driving an LED; action evaluated as query |
DNC_MODE, DNC_LOADED |
Display/notification controls on supported hardware (e.g. blink 150ms, load_pulse) |
Shift layers are therefore expressed as parallel SHIFT_-prefixed control names, not as a shift="" attribute:
<map value="BROWSE" action="browser_scroll" />
<map value="SHIFT_BROWSE" action="browser_scroll" />
<map value="BROWSE_PUSH" action="browser_window 'folders' ? browser_enter : browser_window 'folders'" />Keyboard mappers use the same <mapper>/<map> schema with device="KEYBOARD". Key identifiers are the value="" names — there is no key="" attribute (Local test, factory keyboard mapping):
<mapper device="KEYBOARD" author="Atomix Productions" version="850" date="2026-03-14">
<map value="UNMAPPED" action="search" />
<map value="ALT" action="keyboard_shortcuts" name="" />
<map value="RIGHT ALT" action="keyboard_shortcuts" name="" />
<map value="ALT+1" action="deck 1 select" />
<map value="ALT+Q" action="deck 1 play_pause" />
</mapper>Observed key-name forms: bare keys (A, 1, SPACE-style names), positional modifiers (RIGHT ALT), and MOD+KEY combos (ALT+1, ALT+Q). Full key-name enumeration has not been captured locally; harvest more names from a saved keyboard mapping before relying on unobserved ones (Inference).
Deck scoping happens inside the VDJScript action, not via a deck="" attribute on <map>:
<map value="DECK_LEFT" action="deck 3 leftdeck ? deck 1 leftdeck : deck 3 leftdeck" />
<map value="LED_DECK_LEFT" action="deck 3 leftdeck" />(Local test, factory DDJ-XP2 mapping. Device definitions may declare per-control deck="" so a control name is deck-scoped before the mapper sees it — see below.)
Summary of the official schema (Official: ControllerDefinitionMIDIv8.html, ControllerDefinitionHIDv8.html). No local device-definition XML has been tested yet; treat details below as official-doc-derived, not locally verified.
<device name="DDJSX" author="Atomix Productions"
description="Pioneer DDJ-SX" version="800"
type="MIDI" vid="0x08E4" pid="0x0171"
decks="4" padColumns="4" padRows="2" padSides="2">
<audio description="Pioneer DDJ-SX" input="1" output="2"
mixer="yes" vid="0x08E4" pid="0x0171"
asio="Pioneer DDJ_SX ASIO" />
<button note="0x34" name="PLAY_PAUSE" deck="1" channel="1" />
<slider ccmsb="0x08" cclsb="0x28" name="LEVEL" deck="1" channel="0"/>
<init sendsysex="F00001020304057F" />
</device>Key root attributes: name (the string mappers reference via device=""), type="MIDI", description, version, author, decks, detection ids (vid/pid, sysexid, drivername), and optional singledeck, motor, platform="pc|mac", padColumns/padRows/padSides.
MIDI input elements: <button> (note or cc, value/off, inverted, autoled, channel, deck, nbdecks), <toggle>, <slider> (cc/ccmsb 14-bit, note for velocity, pitch for pitch-bend, min/max/zero/zerorange, inverted, ghost soft-takeover, nozero), <jog>/<fulljog> (incremental zero/full vs absolute max/mask), <encoder>/<fullencoder>, <touchstrip>, <sysexin>.
MIDI output elements: <led> (note- or CC-based, noteoff/ccoff, default linked button), <color> (RGB CCs or velocity→color values="0x00=#000000,0x01=#FF0000"), <bar> (VU/progress), <digit> (LCD digits), <text> (CC- or SysEx-based character displays, encoding), <init>/<exit>/<ledsysex>/<sysex>.
Relative encoders are handled by the definition layer (<encoder zero="">, <jog zero="0x40">), so the mapper only ever sees clean movement values — encoder two's-complement handling never appears in mapper XML.
type="HID" with reportsize/outreportsize, then <page type="in|out|init|wait|exit"> blocks containing the same logical elements positioned by bit/byte/word/dword + nbbits/size + endian instead of notes/CCs.
Built-in definitions are compiled into controllers.dat (app bundle and ~/Library/Application Support/VirtualDJ/Devices/); they are not inspectable XML (Local test). Custom definition XML files go in the Devices/ folder of the VirtualDJ home directory.
Exporting the factory mapping (Factory default → Save, above) does not expose the device definition — it writes only the <mapper> bindings, with no <button>/<slider>/<led> elements or MIDI addresses (Local test, DDJ-GRV6, 2026-07-27). So on a recognized controller the definition layer stays opaque: the mapper gives you the control names, but the note/CC map behind them remains compiled. Testing the custom <device> definition schema still requires hardware VirtualDJ does not recognize (so it must use your XML), or a virtual MIDI port whose messages you inject.
Pad pages (examples/Pads/*.xml) and mapper files are separate systems with a shared scripting language.
| Pad pages | Mapper files | |
|---|---|---|
| Triggered by | On-screen pads in VirtualDJ UI | Physical controller hardware |
| File location | Pads/ in the VirtualDJ home folder |
Mappers/ in the VirtualDJ home folder |
| Root element | <page> |
<mapper> |
| Input elements | <pad1> … <pad16>, <param1>, <param2> |
<map value="" action="" /> |
| LED feedback | query="" drives pad color and blink |
LED_* map bindings; action evaluated as query |
A physical pad-grid controller typically pairs a mapper (hardware → VDJScript) with a pad page shown in the UI. Factory pad-controller mappings drive mode switching with global variables plus refresh_controller (Local test, APC Mini MK2: set '$apclivemode' 0 & wait 300ms & refresh_controller).
| Path | Purpose |
|---|---|
~/Library/Application Support/VirtualDJ/Mappers/ |
User and factory-saved mappers (XML) |
~/Library/Application Support/VirtualDJ/Devices/ |
Custom device definitions (XML) + compiled controllers.dat |
Edit/reload cycle (Local test, 2026-07-27): VirtualDJ scans the Mappers/ folder at startup and caches what it finds. A newly added mapper file does not appear in the mapping selector until a restart, and editing a file that is already loaded does not hot-reload — re-selecting the same mapping serves the cached copy (the MIDI-learn monitor kept showing the pre-edit binding). A full VirtualDJ restart was required both to make the new file appear and, after each edit, to load the change. Switching between mappings VirtualDJ already knows applies live; only new files and in-place edits need the restart. Plan file-based mapping iteration around a restart per change, or edit through the in-app mapper editor instead of the file.
| /Applications/VirtualDJ.app/Contents/Resources/controllers.dat | Compiled built-in definitions (binary) |
The v8-era official docs reference Documents/VirtualDJ/Mappers/; on this Mac install the live folder is under Application Support (Local test, 2026-07-12).
<map name="">attribute purpose (always empty in observed factory files).- Full keyboard key-name enumeration.
- Whether
priorityinteracts with multiple mappers for one device. - No custom device-definition XML has been authored and load-tested locally yet; the definition schema above is official-doc-derived.