Model Catalog Format
Format notes for the data files shipped inside HX Edit, reconstructed by inspection of a locally installed copy. Nothing here is derived from Line 6 source code.
Reference copy examined:
/Applications/Line6/HX Edit.app/Contents/Resources/
HX Edit 3.82 (CFBundleVersion 3.8.2) for macOS, files dated 2024-11-27,
universal (x86_64 + arm64). All files were opened read-only; none were modified.
Counts and field inventories below are specific to this build — re-run the
snippets against a different version rather than assuming they carry over.
Licensing
The model catalog, the model definitions and the control tables are Line 6 copyrighted data. Do not vendor them into this repository. The intent of this document is to describe the format precisely enough that an open-source tool can locate and parse the user’s own installed copy at runtime, on a machine where the user has already licensed and installed HX Edit. A reader should:
- discover the resource directory at runtime, not bundle it;
- degrade gracefully (fewer features, no model names) when HX Edit is absent;
- treat every model name, parameter name and enum label as data owned by Line 6.
The handful of verbatim JSON records quoted below are the minimum needed to pin down field layout, and are reproduced here for interoperability documentation only.
Confidence key
Statements below are tagged:
- [C] confirmed — directly observed in the files, reproducible by re-running the snippets in this document.
- [I] inferred — consistent with everything observed, but not directly proven.
- [U] undetermined — stated explicitly as unknown.
1. File inventory
| File | Size | Format | Content |
|---|---|---|---|
HX_ModelCatalog.json |
527 KB | JSON | Editor-facing browse tree: categories → subcategories → models, with display names, icons, parameter display order |
HX_ModelCatalog.bin |
154 KB | MessagePack | Decodes to a structure exactly equal to HX_ModelCatalog.json [C] |
HelixModelDefs.bin |
847 KB | MessagePack | Concatenation of all 19 *.models files, 681 records [C] |
*.models (19 files) |
— | JSON | Model definitions: parameter ranges, defaults, types, DSP load, device availability |
Helix.sym |
121 KB | JSON | 833 firmware DSP symbols → ordered parameter-name list |
HelixControls.json |
133 KB | JSON | 301 display/formatting/step definitions referenced by displayType |
default_preset.hlx, default_preset_hxs.hlx, default_preset_hfx.hlx, empty_preset.hlx |
— | JSON | Preset templates |
appStrings_eng.json |
26 KB | JSON | UI string table (24 top-level keys, nested). Not model data. |
prefsDialog.xml and friends |
— | XML | Widget layouts. Occasionally useful — prefsDialog.xml carries device IDs as menu item IDs. |
The 19 .models files: amp, cab, cabmicirs, cabmicirswithpan,
compressor, delay, distortion, eq, filter, fixed, gate, io,
modulation, pitch-synth, preamp, reverb, sendreturn, volumepan, wah.
1.1 The .bin files are MessagePack — with one quirk
Both .bin files are standard MessagePack, and both parse to completion with zero
trailing bytes [C]. The quirk: every string carries a trailing NUL byte
inside the msgpack length. 0xAB "categories\0" is a fixstr of length 11
holding a 10-character name plus a NUL. Verified over all 14 307 strings in
HX_ModelCatalog.bin and all 82 473 strings in HelixModelDefs.bin — zero
exceptions, and no zero-length strings [C]. A reader must strip one trailing
byte from every decoded string.
Type codes actually used [C]:
| File | Codes |
|---|---|
HX_ModelCatalog.bin |
fixstr, str16, fixmap, fixarray, array16, nil, true, false, positive fixint, float32 |
HelixModelDefs.bin |
the above plus uint8/16/32, int8, negative fixint |
No map32, array32, str32, bin*, ext* or float64 appear. float64 is
absent, so all real numbers in HelixModelDefs.bin are IEEE float32 — decoding
load: 28.27 from the .bin yields 28.270000457763672, whereas the .models
JSON has the exact decimal. Prefer the JSON when precision matters; otherwise
compare with a tolerance [C].
HX_ModelCatalog.bin decodes to a structure that compares exactly equal to
json.load(HX_ModelCatalog.json) [C]. HelixModelDefs.bin decodes to a
681-element array that matches the concatenation of the .models files
element-for-element (to float32 tolerance) [C], in this order [C]:
index range file n
0..110 amp.models 111
111..151 cab.models 41
152..197 cabmicirs.models 46
198..243 cabmicirswithpan.models 46
244..261 compressor.models 18
262..309 delay.models 48
310..370 distortion.models 61
371..378 eq.models 8
379..393 filter.models 15
394..404 fixed.models 11
405..406 gate.models 2
407..421 io.models 15
422..477 modulation.models 56
478..508 pitch-synth.models 31
509..621 preamp.models 113
622..646 reverb.models 25
647..664 sendreturn.models 18
665..669 volumepan.models 5
670..680 wah.models 11
Practical consequence: an implementation only needs the JSON files. The .bin
files are a load-time optimisation and carry no extra information [C].
2. Model catalog schema (HX_ModelCatalog.json)
Single top-level key categories, a list of 23 category objects [C].
2.1 Category
{
"id": 1,
"name": "Distortion",
"image": "FX_HX_Category_Distortion.png",
"shortName": "Dist",
"color": "0xf5901e",
"subcategories": [ ... ]
}
id— small integer, not contiguous: the observed set is 0–9, 11–23 (10 is absent) [C].color—0xRRGGBBas a string [C].image— filename resolved undericons_category/[C]: 22 of the 23 categoryimagevalues exist verbatim in that directory.- A category has either
modelsorsubcategories, never both. Category 23 (Favorites) has neither [C].
Full category table [C]:
| id | name | shape | subcategories |
|---|---|---|---|
| 0 | None | models (1) | — |
| 1 | Distortion | subcats | Mono, Stereo, Legacy |
| 2 | Dynamics | subcats | Mono, Stereo, Legacy |
| 3 | EQ | subcats | Mono, Stereo |
| 4 | Modulation | subcats | Mono, Stereo, Legacy |
| 5 | Delay | subcats | Mono, Stereo, Legacy |
| 6 | Reverb | subcats | Mono, Stereo, Legacy |
| 7 | Pitch/Synth | subcats | Mono, Stereo, Legacy |
| 8 | Filter | subcats | Mono, Stereo, Legacy |
| 9 | Wah | subcats | Mono, Stereo |
| 11 | Amp | subcats | Guitar, Bass |
| 12 | Preamp | subcats | Guitar, Bass, Mic |
| 13 | Cab | subcats | Single, Dual, Single Legacy, Dual Legacy |
| 14 | IR | subcats | Single, Dual |
| 15 | Volume/Pan | subcats | Mono, Stereo |
| 16 | Send/Return | subcats | Mono, Stereo |
| 17 | Looper | subcats | Mono, Stereo |
| 18 | Input | models (4) | — |
| 19 | Output | models (4) | — |
| 20 | Split | models (4) | — |
| 21 | Merge | models (1) | — |
| 22 | Connected Devices | models (3) | — |
| 23 | Favorites | empty | — |
2.2 Subcategory
Exactly two keys plus the model list [C]:
{ "name": "Mono", "id": 4, "models": [ ... ] }
Subcategory id is globally unique across the whole catalog, not per-category
— Distortion’s Mono/Stereo/Legacy are 4/5/6 [C]. This matters because
use_subcategory (below) references it by that global id.
2.3 Model entry
872 model entries exist across all categories, but only 679 distinct id
values — 193 ids appear more than once [C]. The duplicates are cross-listings
(a model that is both Mono and Stereo appears in both subcategories).
Full entry (the canonical listing):
{
"id": "HD2_DistKinkyBoost",
"name": "Kinky Boost",
"image": "FX_HX_DIST_KinkyBoost.png",
"params": [
{ "Drive": null },
{ "Boost": null },
{ "Bright": null }
]
}
Cross-reference entry — the same model as listed under Stereo (subcategory 5):
{ "id": "HD2_DistKinkyBoost", "use_subcategory": 4 }
use_subcategory: 4 means “the full record lives in subcategory 4”. A reader must
resolve these — 146 entries carry use_subcategory [C]. 726 of 872 entries
carry name, and the 146 without it are exactly the 146 cross-references —
verified as an identity, not just a count match [C]. So the rule is simply:
an entry with use_subcategory has no other content; an entry without it is
complete.
Cross-referencing is not the only way a model gets listed twice, though: 47 ids
carry a full record in two different subcategories (e.g.
HD2_TremoloOpticalTrem appears complete under both Modulation/Mono and
Modulation/Stereo) [C]. A reader that builds an id-keyed map must decide
whether to keep the first or last; the records are equivalent for parameter
purposes since ranges live in .models, not here.
Entry with pages (the only model in the catalog with page_count) [C]:
{
"id": "HD2_ImpulseResponse1024Dual",
"name": "IR 1024",
"image": "FX_HX_IR_Dual_1024.png",
"params": [
{ "Index": "IR Select A" },
{ "LowCut": "Low Cut A" },
{ "HighCut": "High Cut A" },
{ "A Level": "Level A" },
{ "A Pan": "Pan A" },
{ "A Polarity": "Polarity A" },
{ "Index_1": "IR Select B" },
{ "LowCut_1": "Low Cut B" },
{ "HighCut_1": "High Cut B" },
{ "Level_1": "Level B" },
{ "B Pan": "Pan B" },
{ "B Polarity": "Polarity B" },
{ "Delay": null },
{ "Mix": null }
],
"page_count": 3,
"page_names": [ "IR A", "IR B", "Both" ],
"param_pages": {
"Index": 0, "LowCut": 0, "HighCut": 0,
"A Level": 0, "A Pan": 0, "A Polarity": 0,
"Index_1": 1, "LowCut_1": 1, "HighCut_1": 1,
"Level_1": 1, "B Pan": 1, "B Polarity": 1,
"Delay": 2, "Mix": 2
}
}
Model entry field reference [C] (counts out of 872 entries)
| Field | n | Type | Meaning |
|---|---|---|---|
id |
872 | string | Symbolic model id, e.g. HD2_DistKinkyBoost. This is the model’s identity. |
name |
726 | string | Display name |
image |
726 | string | Icon filename resolved under icons_models/ [C] — 725 of 726 exist verbatim there (the exception, icon-input-category.png, lives elsewhere) |
params |
725 | array | Display order + display-name overrides — see below |
use_subcategory |
146 | int | Cross-reference to the subcategory holding the full record |
stereo |
49 | bool | Marks the entry as the stereo listing |
bass |
24 | bool | Bass-oriented model (used to filter Bass subcategories) [I] |
hidden |
22 | bool | Not shown in the browser [I] |
meterInterval |
17 | float | Metering refresh/scale hint; co-occurs with meterChannels/meterMin/meterMax in .models [I] |
image_native |
8 | string | Alternate icon for Helix Native [I] |
page_count, page_names, param_pages |
1 each | — | Multi-page parameter UI |
The catalog params array is presentation, not definition
Each element is either:
- an object
{ "<paramSymbolicID>": <displayNameOverride|null> }—nullmeans “use the name from the.modelsdefinition”; a string overrides it (e.g.{"Bass": "Bass Cut"}onHD2_DistTeemah) [C]; or - a list of such objects, meaning “these parameters share one UI cell / knob
group” (159 such groups exist) [C]. Example from
HD2_TremoloOpticalTrem:
"params": [
[ { "TempoSync1": null }, { "SyncSelect1": "Note Sync" }, { "Speed": null } ],
{ "Intensity": null },
{ "Level": null }
]
Ranges, defaults and types are not here — they are in the .models files.
3. Parameter definitions (*.models / HelixModelDefs.bin)
This is the part a third-party editor actually needs.
Each .models file is a JSON array of model records. 681 records total,
keyed by symbolicID — the same string used as id in the catalog and as
@model in .hlx presets [C].
3.1 Verbatim example — a small, complete record
From volumepan.models:
{
"symbolicID": "HD2_VolPanGain",
"mono": true,
"stereo": true,
"name": "Gain",
"category": 17,
"load": 0.35,
"load_stereo": 0.51,
"params": [
{
"symbolicID": "Gain",
"name": "Gain",
"valueType": 1,
"displayType": "volume",
"min": -120.0,
"max": 12.0,
"default": 0.0,
"assign": 2
},
{
"symbolicID": "@enabled",
"name": "Enabled",
"valueType": 2,
"min": false,
"max": true,
"default": true
},
{
"symbolicID": "@stereo",
"name": "Stereo",
"valueType": 2,
"min": false,
"max": true,
"default": false
}
]
}
From gate.models, showing metering fields and a discrete-ish control:
{
"symbolicID": "HD2_GateNoiseGate",
"mono": true,
"stereo": true,
"name": "Noise Gate",
"category": 4,
"load": 1.5,
"load_stereo": 1.9,
"meterChannels": 1,
"meterMin": -90.0,
"meterMax": 0.0,
"params": [
{ "symbolicID": "Threshold", "name": "Threshold", "valueType": 1,
"displayType": "volume", "min": -96.0, "max": 0.0, "default": -48.0, "assign": 1 },
{ "symbolicID": "Decay", "name": "Decay", "valueType": 1,
"displayType": "comp_decay_10_1000", "min": 0.01, "max": 1.0, "default": 0.5, "assign": 2 },
{ "symbolicID": "Level", "name": "Level", "valueType": 1,
"displayType": "volume", "min": -60.0, "max": 6.0, "default": 0.0, "assign": 4 },
{ "symbolicID": "@enabled", "name": "Enabled", "valueType": 2,
"min": false, "max": true, "default": true },
{ "symbolicID": "@stereo", "name": "Stereo", "valueType": 2,
"min": false, "max": true, "default": false }
]
}
3.2 Model record fields [C] (counts out of 681)
| Field | n | Type | Meaning |
|---|---|---|---|
symbolicID |
681 | string | Model identity |
params |
681 | array | Parameter definitions (below) |
name |
677 | string | Display name |
category |
662 | int | Category taxonomy — different numbering from the catalog’s category ids, see §3.6 |
load |
645 | float | DSP cost, mono. Used for the “DSP usage” bar [I] |
load_stereo |
186 | float | DSP cost when the block is stereo [I] |
load_320 |
2 | float | DSP cost override for firmware ≥ 3.20 [I] |
devices |
391 | array | Device availability + minimum firmware — see §5 |
exclude_devices |
1 | array | Negative availability list (one model excludes 0x210006) |
mono |
210 | bool | Model can run mono |
stereo |
210 | bool | Model can run stereo |
cablink |
111 | string | Default cab model paired with this amp |
ircablink |
111 | string | Default cab+mic-IR model paired with this amp |
capEdge |
92 | float | Amp-model-specific tone constant, values ~0.16–0.32 [U] — meaning not determined |
meterChannels / meterMin / meterMax |
17 each | int/float | Metering geometry and dB range |
name_stereo |
1 | string | Alternate name in stereo |
3.3 Parameter record fields [C] (counts out of 6 861 parameters)
| Field | n | Required | Meaning |
|---|---|---|---|
symbolicID |
6861 | yes | Parameter key. Names beginning with @ are block-structural (@enabled, @stereo, @trails, @bypassvolume) rather than DSP parameters [C] |
name |
6861 | yes | Display name |
valueType |
6861 | yes | 0/1/2/3 — see §3.4 |
min |
6861 | yes | Inclusive minimum, typed per valueType |
max |
6861 | yes | Inclusive maximum, typed per valueType |
default |
6861 | yes | Default value, typed per valueType |
displayType |
6028 | no | Key into HelixControls.json — formatting, units, step, enum labels |
assign |
1611 | no | Integer 1–9, see §3.5 |
stereo-only |
72 | no | Parameter exists only when the block is stereo |
default_stereo |
37 | no | Default override when stereo |
max_stereo |
11 | no | Max override when stereo |
displayType_stereo |
9 | no | displayType override when stereo |
min_370 |
33 | no | min override on firmware ≥ 3.70 [I] |
max_315 |
16 | no | max override on firmware ≥ 3.15 [I] |
The _<version> suffix convention is read as a three-digit firmware version
(370 → 3.70, 315 → 3.15) [I]. It is consistent with the version strings
in the devices array (§5), which are firmware versions in the same family. A
reader that ignores these suffixes will simply clamp a few parameters slightly too
tightly on new firmware.
Verbatim examples of each override form [C]:
{ "symbolicID": "Delay", "name": "Delay", "valueType": 1,
"displayType": "dualCab_time_ms_withauto",
"min": 0.0, "min_370": -2e-05, "max": 0.05, "default": 0.0 }
{ "symbolicID": "Time", "name": "Time", "valueType": 1,
"displayType": "time_ms", "min": 0.0, "max": 2.0, "max_315": 2.5,
"default": 0.47, "assign": 1 }
{ "symbolicID": "Time", "name": "Time", "valueType": 1,
"displayType": "time_ms_0_8000", "displayType_stereo": "time_ms_0_4000",
"min": 0.0, "max": 8.0, "max_stereo": 4.0, "default": 0.5, "assign": 1 }
{ "symbolicID": "Detector", "name": "Detector", "valueType": 2,
"displayType": "detector", "min": false, "max": true,
"default": true, "stereo-only": true }
3.4 valueType [C]
Determined by checking the Python type of min/max/default across all 6 861
parameters — the correlation is exact, with no exceptions:
valueType |
n | min/max/default type |
Interpretation |
|---|---|---|---|
| 0 | 533 | int | Integer / enumeration. min..max is an inclusive index range; 500 of the 533 point at a displayType marked isDiscrete whose format is a label array |
| 1 | 5108 | float | Continuous. Never isDiscrete. min..max are in the parameter’s own units (dB, seconds, normalised 0..1, …) as decided by displayType |
| 2 | 1217 | bool | Boolean. Always min:false, max:true |
| 3 | 3 | string | String. Only on @global_params: @topology0, @topology1, @cursor_group. min/max/default are all "" |
Four valueType: 1 parameters have an integer-typed default alongside float
min/max — treat default as a float [C].
Note that valueType: 1 with an isDiscrete display type never occurs, but
valueType: 2 with isDiscrete occurs 395 times (booleans rendered as a two-item
segmented control, e.g. off_on) [C].
3.5 assign
Integer 1–9, present on 1 611 parameters, unique within a model for all but 3 of
the 681 models [C]. It is stable across models for equivalent controls —
across amps, Drive=1, Bass=3, Mid=4, Treble=5, Presence/HighMid=6,
Master=7, ChVol=8 [C]. It is not the display order (that is the
catalog params array) and not the DSP parameter index (that is Helix.sym).
[U] The exact device-side meaning is not determined from these files. It is most plausibly the slot index used for quick controller/footswitch assignment, but nothing in the resources proves it.
3.6 category in .models vs id in the catalog
Two different numberings; do not conflate them [C]:
.models category |
source file(s) |
|---|---|
| 1 | amp.models (111), preamp.models (2) |
| 2 | cab.models |
| 3 | distortion.models |
| 4 | compressor.models, gate.models |
| 5 | pitch-synth.models (1) |
| 6 | filter.models |
| 7 | pitch-synth.models (30) |
| 8 | modulation.models |
| 9 | delay.models |
| 10 | reverb.models |
| 11 | wah.models |
| 12 | sendreturn.models |
| 13 | preamp.models (111) |
| 14 | eq.models |
| 15 | fixed.models — loopers |
| 16 | fixed.models — impulse responses |
| 17 | volumepan.models |
| 19 | cabmicirs.models, cabmicirswithpan.models |
For UI grouping, use the catalog tree; .models category is best treated as an
internal tag [I].
4. HelixControls.json — display, units, step, enum labels
301 entries [C]. displayType on a parameter is a key into this map. Keys
observed on the control objects, with counts:
| Key | n | Meaning |
|---|---|---|
format |
239 | Either a printf format string, an array of labels (enum), or an array of range-scoped format objects |
isDiscrete |
193 | Value snaps to integers |
controlType |
105 | Always "segmented" where present [C] |
alias |
58 | Delegate to another control definition ({"alias": "eq_low_cut"}) — resolve recursively |
step |
53 | {fine, coarse} or an array of range-scoped {lowerBound, upperBound, fine, coarse} |
displayToWidgetScale |
39 | Display-value → widget-position scale |
dspToDisplayScale |
29 | DSP value → display value multiplier |
canDisplayHigherRes |
17 | Allow extra decimal places |
formatUnits |
9 | Format string including the unit suffix |
allowDiscreteStates |
7 | Enum where individual states can be independently enabled |
minimumValue / maximumValue |
4 each | Display-domain clamp, independent of the parameter’s own min/max |
dspToDisplayIntegerOffset |
3 | Added to the integer before display (e.g. 1-based enums) |
zeroValue |
1 | Detent centre (only pan) |
Verbatim examples covering each shape [C]:
"generic_knob": { "dspToDisplayScale": 10, "displayToWidgetScale": 10,
"format": "%.1f", "step": { "fine": 0.1, "coarse": 1.0 } }
"percent": { "dspToDisplayScale": 100, "format": "%.0f", "formatUnits": "%.0f %%",
"step": { "fine": 1.0, "coarse": 10.0 } }
"off_on": { "isDiscrete": true, "controlType": "segmented",
"format": ["Off", "On"] }
"wave_shape": { "isDiscrete": true,
"format": ["Saw Up","Saw Down","Triangle","Sine","Square",
"Inverse Sine","Random"] }
"cab_low_cut": { "alias": "eq_low_cut" }
"integer_slider_1based": { "isDiscrete": true, "dspToDisplayIntegerOffset": 1 }
"pan": { "minimumValue": -100, "maximumValue": 100, "zeroValue": 0.0,
"format": [
{ "lowerBound": -99999, "upperBound": -0.5, "format": "%.0f",
"formatUnits": "Left %.0f", "unitsMultiplier": -1 },
{ "lowerBound": -0.5, "upperBound": 0.5, "format": "%.0f",
"formatUnits": "Center" },
{ "lowerBound": 0.5, "upperBound": 999999, "format": "%.0f",
"formatUnits": "Right %.0f" } ],
"step": { "fine": 1.0, "coarse": 10.0 } }
"frequency": { "canDisplayHigherRes": false, "displayToWidgetScale": 10,
"step": [ { "lowerBound": 0.0, "upperBound": 20.0, "fine": 0.1, "coarse": 1 },
{ "lowerBound": 20.0, "upperBound": 100.0, "fine": 1, "coarse": 10 },
{ "lowerBound": 100.0, "upperBound": 1000.0, "fine": 1, "coarse": 10 },
{ "lowerBound": 1000.0, "upperBound": 99999.0, "fine": 100, "coarse": 1000 } ],
"format": [ { "lowerBound": 0, "upperBound": 20, "format": "%.1f", "formatUnits": "%.1f Hz" },
{ "lowerBound": 20, "upperBound": 1000, "format": "%.0f", "formatUnits": "%.0f Hz" },
{ "lowerBound": 1000, "upperBound": 999999, "unitsMultiplier": 0.001,
"format": "%.0f", "formatUnits": "%.1f kHz" } ] }
Rendering algorithm (reconstructed from field names and consistency with the observed data — [I], not proven):
- Resolve
aliaschains. display = dsp * (dspToDisplayScale ?? 1) + (dspToDisplayIntegerOffset ?? 0).- Clamp to
minimumValue/maximumValuewhen present. - If
formatis an array of strings → the value is an index into it; the label is the display text. - If
formatis an array of objects → pick the entry whose[lowerBound, upperBound)contains the display value; apply itsunitsMultiplierif present; render withformatUnits(orformat). - Otherwise
format/formatUnitsis a printf string applied directly.
step.fine/step.coarse are the increments for fine (modifier-held) and coarse
knob movement, in display units [I].
Enum length note: for a valueType: 0 parameter, max - min + 1 should equal the
length of the resolved label array. Worth asserting in a reader, but not verified
exhaustively here [U].
5. Device IDs
Device ids appear in three places: data.device in .hlx presets, the devices
arrays in .models, and menu_item id attributes in prefsDialog.xml [C].
The encoding is (family << 16) | member over the Line 6 SysEx device family
identifiers already documented in PROTOCOL.md — family 0x0021, so all HX ids
have the form 0x0021_XXXX [C]. This is corroborated by HX Edit.prefs, which
keys per-device UI settings under the string "0x00210006" for a connected
HX Stomp.
Exactly eight ids appear anywhere in the resources [C]:
| id (dec) | id (hex) | Device | Evidence |
|---|---|---|---|
| 2162689 | 0x210001 |
Helix Floor | [C] arm64 disassembly: L6Device::isDeviceHelixFX() → id 0x210001 and the display-name global holding "Helix Floor" (3 independent call sites at 0x10003f5fc, 0x10003f7a8, 0x10003f924). Also the device value of default_preset.hlx. |
| 2162690 | 0x210002 |
Helix Rack | [C] same code: L6Device::isDeviceHelixRack() → 0x210001 + 1, name "Helix Rack" |
| 2162692 | 0x210004 |
Helix LT | [C] same code: L6Device::isDeviceHelixLT() → 0x210001 + 3, name "Helix LT" |
| 2162693 | 0x210005 |
HX Effects | [C] prefsDialog.xml <menu_item id="2162693"><label>HX Effects</label>; HelixFx_AppDSPFlow* models list exactly [0x210005]; default_preset_hfx.hlx has device: 2162693 |
| 2162694 | 0x210006 |
HX Stomp | [C] prefsDialog.xml <menu_item id="2162694"><label>HX Stomp</label>; default_preset_hxs.hlx has device: 2162694; matches the "0x00210006" prefs key of the connected HX Stomp |
| 2162699 | 0x21000B |
HX Stomp XL | [I] the only remaining HX Edit-supported hardware; shares HelixStomp_AppDSPFlowInput / ...OutputMain / ...OutputSend with 0x210006 and appears with it in a single {0x210005, 0x210006, 0x21000B} range test at 0x100003ad0 |
| 2162944 | 0x210100 |
Helix Native, Helix-hardware compatibility mode | [C] prefsDialog.xml <menu_item id="2162944"><label>Helix Floor/Rack/LT</label> in the Hardware Compatibility Mode menu; HelixPlugin_AppDSPFlow* list exactly [0x210100, 0x210101] |
| 2162945 | 0x210101 |
Helix Native, compatibility off | [C] <menu_item id="2162945"><label>Off</label> in the same menu |
0x210003 is absent from every resource. [I] it is Helix Control, the
foot-controller accessory — HX Edit knows the strings "Helix Control" /
"HELIX CONTROL" and has an L6Device::isDeviceHelixControl(), but the device has
no DSP so no model lists it.
Notes on the 391 models that carry an explicit devices list [C]:
- The two Helix Native ids reference an identical model set (386 each), a strict
superset of the Helix Floor set (384) — the extra two are
HelixPlugin_AppDSPFlow1InputandHelixPlugin_AppDSPFlowOutput. - The Helix Rack set is identical to the Helix Floor set (384).
- Helix LT, HX Stomp and HX Stomp XL are 375 each; HX Effects is the smallest at 352.
5.1 The devices array
"devices": [
{ "id": 2162944, "version": "0x03190100" },
{ "id": 2162945, "version": "0x03190100" },
{ "id": 2162693, "version": "0x03190100" },
{ "id": 2162699, "version": "0x03190100" },
{ "id": 2162694, "version": "0x03190100" },
{ "id": 2162689, "version": "0x03190100" },
{ "id": 2162690, "version": "0x03190100" },
{ "id": 2162692, "version": "0x03190100" }
]
Presence of an {id} object = the model is available on that device. The optional
version is the minimum firmware on which the model exists, formatted
0xMMmmpprr where 0x03190100 reads as 3.19 [I] — the same encoding family as
the 03 50 bytes in the SysEx identity reply documented in PROTOCOL.md, and
consistent with the min_370 / max_315 parameter suffixes.
[U] Whether the last two bytes are patch/build, and whether 0x19 is decimal
19 or BCD, is not settled by these files. As with the firmware-revision question in
PROTOCOL.md, this needs a device at a known version.
290 of 681 models have no devices key at all [C]. The key appears only on
models that post-date the original shipping set [I] — which is why a list often
mixes versioned and unversioned entries:
[ {"id": 2162944}, {"id": 2162945}, {"id": 2162699},
{"id": 2162694, "version": "0x02790000"},
{"id": 2162693, "version": "0x02790000"},
{"id": 2162689, "version": "0x02790000"}, ... ]
Read as: the model is present on Helix Native and HX Stomp XL from their first firmware (those products shipped after 2.79), but on HX Stomp / HX Effects / Helix Floor it requires firmware ≥ 2.79 [I].
Do not treat devices as “this model is selectable on this device.” Two facts
block that reading [C]:
- Genuine hardware filtering does occur —
HD2_FXLoopMono3lists only{0x210001, 0x210002, 0x210100, 0x210101}, i.e. only the units with four FX loops, and 9 send/return models do the same. - But 38 of the 111 amp models list HX Effects (
0x210005), and HX Effects has no Amp block at all. Only 11 amp models exclude it.
[U] The most plausible reconciliation is that devices records which devices’
firmware carries the model’s DSP code (a shared codebase ships more than the UI
exposes), while the user-visible gate is the category/block-type the device
supports. This is not proven. A reader should use devices for firmware-version
gating and for the send/return-style capability cases, and rely on the catalog
category tree for what the user may actually place.
6. The .hlx preset format
Plain UTF-8 JSON, no wrapper. Top level [C]:
{ "version": 6, "data": { "meta": {...}, "device": 2162689, "tone": {...} } }
version: 6 in all four shipped templates [C].
6.1 data.meta
{
"name": "New Preset",
"application": "Helix Edit",
"build_sha": "30848a7",
"modifieddate": 1478293021,
"appversion": 327680
}
empty_preset.hlx carries only {"name": "New Preset"}, so every field except
name is optional [C]. modifieddate is a Unix timestamp (1478293021 =
2016-11-04) [C]. appversion 327680 = 0x050000 [I] — presumably a
packed editor version.
6.2 data.device
The device id from §5. It identifies the model of hardware the preset targets and therefore which I/O block symbols and which model subset are legal [C].
6.3 data.tone
Keys observed in the templates [C]: dsp0, dsp1, global,
snapshot0…snapshot7.
Note default_preset.hlx has snapshot1…snapshot6 only, while
empty_preset.hlx has snapshot0…snapshot7 — so the snapshot set is sparse and
a reader must not assume all eight are present [C]. There are 8 snapshots
maximum [I] (snapshot%d format string, SNAPSHOT %d default name, and the
templates never exceed index 7).
6.4 data.tone.dspN
Two DSPs (dsp0, dsp1) on every template, including the single-DSP HX Stomp
template [C]. Fixed-name slots plus numbered block slots:
| Key | Present in templates | Purpose |
|---|---|---|
inputA, inputB |
yes | Path A / path B input |
outputA, outputB |
yes | Path A / path B output |
split |
yes | Path splitter |
join |
yes | Path mixer |
block0, block1, … |
no (templates are empty) | Effect block slots |
The blockN naming is [C] from the app binary, which contains the literals
block%d, block0, block1, blocks.dsp%d.block%d, blocks.dsp%d.%s and
blocks.dsp%.split (their typo). None of the four shipped templates contains a
populated block, so the block object’s exact key set could not be observed
directly — but the same string region enumerates the preset key vocabulary:
@model @position @path @enabled bypass @input @output
@stereo @trails @cab @type @favorite @uuid @uuid2
@no_snapshot_bypass category cab block
[I] A populated block is therefore an object of the form
{"@model": "<symbolicID>", "@position": <int>, "@path": <int>, "@enabled": <bool>,
…parameters by symbolicID…}.
Confirmed slot examples from default_preset.hlx [C]:
"split": {
"@model": "HD2_AppDSPFlowSplitY",
"@enabled": true,
"bypass": false,
"@position": 0,
"BalanceA": 0.5,
"BalanceB": 0.5
},
"inputA": {
"@model": "HD2_AppDSPFlow1Input",
"@input": 0,
"noiseGate": false,
"decay": 0.5,
"threshold": -48
},
"join": {
"@model": "HD2_AppDSPFlowJoin",
"@position": 8,
"@enabled": true,
"A Level": 0, "A Pan": 0.5,
"B Level": 0, "B Pan": 0.5, "B Polarity": false,
"Level": 0
},
"outputA": {
"@model": "HD2_AppDSPFlowOutput",
"@output": 1,
"pan": 0.5,
"gain": 0
}
Observations [C]:
- A block references its model by the
@modelstring, which is exactly thesymbolicID/ catalogid. There is no numeric model reference in the format. - Every other key in the object is a parameter
symbolicIDfrom that model’s.modelsrecord, with a raw DSP-domain value. CompareinputA’sthreshold: -48againstHD2_AppDSPFlow1Input’sthresholddefinition, andjoin’s"A Pan": 0.5against thepandisplay type. - Parameter keys may contain spaces (
"A Level","B Polarity") — they are the literalsymbolicIDvalues, not identifiers. @positionis the column index along the signal path;splitsits at 0 andjoinat 8 in the stock templates. [I] the block grid is 0–8 wide per DSP on Helix Floor.@enabled= the block is on (not bypassed). A separatebypassboolean also appears onsplit. [U] the precise division of labour between@enabledandbypassis not determined;@enabledis the one defined as a parameter in every.modelsrecord, so treat it as authoritative andbypassas split-specific.@input/@outputare integer selectors into the device’s physical I/O, whose labels come fromHelixControls.jsoninput_type/input_type_lt/input_type_native(which is why those three exist) [I].
The device-specific input/output models are chosen by data.device [C] — the
selection logic is visible verbatim in the disassembly at 0x100003b08:
0x210005 → HelixFx_AppDSPFlowInput, 0x210006 and 0x21000B →
HelixStomp_AppDSPFlowInput, everything else → HD2_AppDSPFlow1Input.
6.5 data.tone.global
Verbatim from default_preset.hlx [C]:
{
"@model": "@global_params",
"@tempo": 120,
"@pedalstate": 2,
"@guitarinputZ": 0,
"@current_snapshot": 0,
"@topology0": "A",
"@topology1": "A",
"@cursor_dsp": 0,
"@cursor_path": 0,
"@cursor_position": 0,
"@cursor_group": "",
"@variax_model": 0,
"@variax_volumeknob": -0.1,
"@variax_toneknob": -0.1,
"@variax_lockctrls": 0,
"@variax_customtuning": true,
"@variax_magmode": true,
"@variax_str1tuning": 0,
"@variax_str2tuning": 0,
"@variax_str3tuning": 0,
"@variax_str4tuning": 0,
"@variax_str5tuning": 0,
"@variax_str6tuning": 0
}
@model: "@global_params" points at the @global_params pseudo-model in
fixed.models, which defines every one of these keys with a type and range —
so the global block is validated the same way as any other block [C]. This is
also where the three valueType: 3 (string) parameters live.
@topology0 / @topology1 are the per-DSP routing topology [C]. Observed
value "A"; the app binary contains the complete alphabet of topology strings
"A", "AB", "ABJ", "SAB", "SABJ" [C] — read as S=split, A=path A,
B=path B, J=join [I].
fixed.models also defines the pseudo-models @dt, @powercab and @variax,
matching the binary’s dt0/dt1/dtdual, powercab0/powercab1/powercabdual
and variax preset keys — external L6 Link device state [I].
6.6 data.tone.snapshotN
{ "@name": "SNAPSHOT 2", "@tempo": 120, "@pedalstate": 2, "@ledcolor": 0 }
The binary additionally references @valid in the snapshot key group [C], so a
real snapshot may carry it. [U] How per-snapshot parameter values are stored
could not be determined — no shipped template has a modified snapshot. The binary’s
@snapshot_disable and @no_snapshot_bypass keys indicate per-parameter and
per-block snapshot opt-outs exist [C].
6.7 Sections not present in the templates
The app binary enumerates three more preset sections that none of the four templates exercises [C]:
@assignments— controller assignments, with keys@param,@min,@max,@controller,@globaldsp,@globalblock,@snapshot_disable, and address pathscontrollers.%s.%s./controllers.dsp%d.%s.%s.footswitch—@fs_index,@fs_label,@fs_enabled,@fs_momentary,@fs_ledcolor,@fs_customcolor,@fs_customlabelcommands.%s.— Command Center:@command,@cc,@overthresh,@wait,@behavior,@valueirUuidTable— maps preset-local IR slots to IR UUIDs [I], related to the@uuid/@uuid2block keys
[U] Their exact JSON shape is undetermined. Obtaining a real user preset that uses snapshots, controller assignments and an IR is the single highest-value next step for completing this section.
6.8 Dot-path addressing
The format strings blocks.dsp%d.block%d, blocks.dsp%d.%s,
controllers.dsp%d.%s.%s., commands.%s., dsp%d.%s.%s, %s.%d. and
global_device [C] show that HX Edit addresses individual parameters by a
dotted string path, e.g. blocks.dsp0.block2.Drive. This is likely the same
addressing used on the wire for parameter edits — worth checking against
tools/hxsniff captures.
7. ID ↔ name mapping
7.1 There is no numeric model id
[C] A model’s identity throughout the entire format is the symbolic string
(HD2_DistKinkyBoost). It is used as:
idinHX_ModelCatalog.json,symbolicIDin the.modelsfiles andHelixModelDefs.bin,symbolinHelix.sym,@modelin.hlxpresets.
All 872 catalog id values are strings; not one is numeric [C]. The only
numeric ids in these files are category ids, subcategory ids and device
ids. If the USB/MIDI wire protocol uses a numeric model index, that mapping is not
in these resource files [U] — the only implicit index available is a model’s
position in HelixModelDefs.bin (0–680), and there is no evidence it is the wire
value.
Coverage between the two tables [C]:
- catalog ids not in
.models: exactly one, the sentinel"None"(the empty block, category 0); .modelssymbols not in the catalog: three —@global_params,HelixPlugin_AppDSPFlow1Input,HelixPlugin_AppDSPFlowOutput.
So the useful mapping is symbolicID → display name, and it exists in two
places: the catalog (which also gives you the browse tree) and the .models files
(which also give you the parameters).
7.2 Helix.sym — the parameter index table
833 entries [C]:
{ "symbol": "HD2_DelaySimpleDelayMono",
"parameters": ["Time","Feedback","Mix","Level","SyncSelect1","TempoSync1"] }
{ "symbol": "HD2_DelaySimpleDelayStereo",
"parameters": ["Time","Feedback","Mix","Level","Scale","SyncSelect1","TempoSync1"] }
This is the firmware DSP-level table, one entry per built DSP variant, and it
is where mono and stereo become separate symbols. The editor-level .models record
merges them:
HD2_DelaySimpleDelay mono:true stereo:true
params: Time, Feedback, Mix, Level, Scale, SyncSelect1, TempoSync1,
@enabled, @trails, @stereo
("Scale" carries "stereo-only": true — exactly the parameter the Mono
symbol lacks)
The two tables therefore reconcile [C]. Relationship counts: 348 Helix.sym
symbols have no .models record (they are the …Mono / …Stereo split forms),
and 196 .models symbols have no Helix.sym entry (they are the merged forms)
[C]. Comparing the 485 that share a name, 376 have an identical parameter
order and 109 differ [C] — the differences are either stereo-only parameters
or, as with HD2_AppDSPFlow1Input, Helix.sym listing device-specific extras
(select, gain, guitarSense, auxSense, micLowCut) that the .models
record omits.
Why this matters: Helix.sym order is the closest thing in these files to a
numeric parameter address. If the wire protocol addresses parameters by index
rather than by name, Helix.sym is the table to try first [I].
Helix.sym is not sorted alphabetically; the order looks like build order
[C].
7.3 Extraction snippet
#!/usr/bin/env python3
"""Read the model tables from a locally installed HX Edit.
Reads the user's own installed copy at runtime. Nothing is redistributed.
"""
import json
import os
RES = "/Applications/Line6/HX Edit.app/Contents/Resources"
MODEL_FILES = [
"amp", "cab", "cabmicirs", "cabmicirswithpan", "compressor", "delay",
"distortion", "eq", "filter", "fixed", "gate", "io", "modulation",
"pitch-synth", "preamp", "reverb", "sendreturn", "volumepan", "wah",
]
def load_catalog(res=RES):
"""symbolicID -> {name, category, subcategory, image, params}
Resolves `use_subcategory` cross-references so each id appears once.
"""
doc = json.load(open(os.path.join(res, "HX_ModelCatalog.json")))
subs = {} # subcategory id -> (category, subcategory name)
entries = [] # (category, subcat_name, subcat_id, model)
for cat in doc["categories"]:
for m in cat.get("models", []):
entries.append((cat, None, None, m))
for sub in cat.get("subcategories", []):
subs[sub["id"]] = (cat, sub["name"])
for m in sub.get("models", []):
entries.append((cat, sub["name"], sub["id"], m))
out = {}
for cat, sub_name, _sub_id, m in entries:
if "use_subcategory" in m: # cross-reference; canonical record elsewhere
continue
out[m["id"]] = {
"name": m.get("name"),
"category_id": cat["id"],
"category": cat["name"],
"subcategory": sub_name,
"image": m.get("image"),
"params": m.get("params", []), # display order + name overrides
}
return out
def load_defs(res=RES):
"""symbolicID -> full model definition (ranges, defaults, devices, load)."""
defs = {}
for stem in MODEL_FILES:
for m in json.load(open(os.path.join(res, stem + ".models"))):
defs[m["symbolicID"]] = m
return defs
def load_controls(res=RES):
"""displayType -> control definition, with `alias` chains resolved."""
raw = json.load(open(os.path.join(res, "HelixControls.json")))
def resolve(key, depth=0):
c = raw.get(key)
if c and "alias" in c and depth < 8:
return resolve(c["alias"], depth + 1)
return c
return {k: resolve(k) for k in raw}
def load_sym(res=RES):
"""firmware symbol -> ordered parameter-name list."""
return {e["symbol"]: e["parameters"]
for e in json.load(open(os.path.join(res, "Helix.sym")))}
if __name__ == "__main__":
cat, defs = load_catalog(), load_defs()
print(f"{len(cat)} catalog models, {len(defs)} definitions")
for mid in sorted(cat)[:15]:
d = defs.get(mid, {})
n = len([p for p in d.get("params", [])
if not p["symbolicID"].startswith("@")])
print(f"{mid:34s} {cat[mid]['name'] or '':22s} "
f"{cat[mid]['category']:12s} {n:2d} params")
To read the MessagePack .bin files instead, remember the trailing-NUL rule:
def decode_str(raw: bytes) -> str:
# every string in the .bin files carries a NUL inside the msgpack length
return raw.decode("utf-8").rstrip("\0")
In Rust, rmp-serde works if you deserialize strings through a wrapper that
trims one trailing \0; or use rmpv::decode::read_value and post-process.
But since the .bin files carry no information the JSON lacks, a Rust reader is
better off with serde_json on the .json / .models files [I].
7.4 Sample rows
15 rows produced by the snippet above, showing the shape of the mapping (not a reproduction of the catalog) [C]:
| category | subcategory | symbolic id | display name |
|---|---|---|---|
| None | — | None |
(empty block) |
| Distortion | Mono | HD2_DistKinkyBoost |
Kinky Boost |
| Distortion | Mono | HD2_DistDerangedMaster |
Deranged Master |
| Distortion | Mono | HD2_DistMinotaur |
Minotaur |
| Dynamics | Mono | HD2_GateNoiseGate |
Noise Gate |
| Delay | Legacy | HD2_DL4AnalogDelayStereoMod |
Analog w/Mod |
| Delay | Legacy | HD2_DL4MultiheadStereo |
Multi-Head |
| Delay | Legacy | L6BubbleEcho |
Bubble Echo |
| Delay | Mono | HD2_DelaySimpleDelay |
Simple Delay |
| Amp | Guitar | HD2_AmpGermanMahadeva |
German Mahadeva |
| Amp | Bass | HD2_AmpCali400Ch1 |
Cali 400 Ch1 |
| Cab | Dual | HD2_CabMicIr_1x12CaliEXTWithPan |
1x12 Cali EXT |
| Cab | Dual | HD2_CabMicIr_2x12BlueBellWithPan |
2x12 Blue Bell |
| Volume/Pan | Mono | HD2_VolPanGain |
Gain |
| IR | Dual | HD2_ImpulseResponse1024Dual |
IR 1024 |
Note the naming convention, useful for heuristics but not a substitute for the
table [I]: HD2_ = the main HX model set, VIC_ / Victoria / L6SPB /
L6PhazeEko / bare names (TapeEater, RezSynth, SynthLead) = legacy M-series
and stompbox models, HelixStomp_ / HelixFx_ / HelixPlugin_ = device-specific
I/O blocks, @-prefixed = pseudo-models for global/external state.
8. What could not be determined
| Question | Status |
|---|---|
Exact JSON shape of a populated blockN object |
[U] — key vocabulary known from the binary, but no shipped template contains a block. Needs a real user preset. |
| How snapshots store per-parameter values | [U] — needs a real preset with edited snapshots |
Shape of @assignments, footswitch, commands, irUuidTable |
[U] — key names known, structure not |
Meaning of assign (1–9) |
[U] — behaviour characterised, semantics unproven |
Meaning of capEdge (92 amp/preamp models, ~0.16–0.32) |
[U] |
Whether firmware version fields are BCD or plain hex |
[U] — same open question as the SysEx revision bytes in PROTOCOL.md |
| Whether a numeric model id exists on the wire | [U] — absent from all resource files |
Difference between @enabled and bypass |
[U] |
Exact semantics of devices (firmware presence vs. user-visible availability) |
[U] — see §5.1 |
0x210003 = Helix Control |
[I] — never appears in the resources |
9. Reproducing this analysis
Everything above is reproducible with python3 and the standard library, except
the device-id confirmations in §5, which used:
otool -arch arm64 -tV "/Applications/Line6/HX Edit.app/Contents/MacOS/HX Edit"
then locating L6Device::isDeviceHelixFX / isDeviceHelixRack / isDeviceHelixLT
and reading the movk w23, #0x21, lsl #16 immediates alongside the display-name
globals. The binary is a universal x86_64 + arm64 fat file, so the arm64 slice
starts at file offset 22 740 992 and __TEXT has vmaddr 0x100000000 — a raw
byte offset into the fat file must have both subtracted and added respectively
before it can be matched against disassembly addresses.