Document revision: v4.2 — repository cross-reference and G3Xn corpus validation, 2026-09-14.
Status: reverse-engineered; not an official ZOOM specification.
Filename retained:ZPTC_FORMAT_SPEC_v4.1_EN.mdso existing links continue to work.
- 1. Overview and reading conventions
- 2. Binary format
- 3. Reading, editing and implementation
- 4. Effect catalogs and external formats
- 5. Validation and open questions
- Appendix A. Reviewed sources
- Appendix B. Historical samples and cross-checks
- Appendix C. Revision history
This document describes raw .zptc patch containers with the PTCF signature. It covers the header, effect data, global parameters, editing rules and related transport. Measurements use 150 files in zptc examples, identified by the user as extracted from a G3Xn; repository code supplies additional layouts and interpretations.
| Topic | Reference |
|---|---|
| File signature and header | PTCF; 36-byte fixed header; field offsets |
| Structural integers | Unsigned little-endian; container length and padding |
| First section | 36 + 4 * fx_count; section envelopes |
| Effect records | 24 bytes per slot; EDTB bit layout |
| Global parameters | PPRM: 12 bytes; PRM2: 32 bytes in the source newer branch |
| Measured device profile | G3Xn; version 1, target 0x04; validation results |
| Reproducible validation | Per-file report and validation script |
- MEASURED: a result reproduced from the supplied 150-file G3Xn corpus in this revision. Device provenance is user-reported; firmware version was not supplied.
- HISTORICAL SAMPLE: a raw-byte result reported by v4.1 or an earlier revision. Where the matching file is now available, its new validation is explicitly stated.
- SOURCE: a field, branch, formula or interpretation explicitly implemented in a reviewed repository.
- DERIVED: an offset/formula calculated from that source; synthetic checks can validate the calculation.
- INFERRED: a likely device behavior that still needs controlled patch pairs or hardware verification.
- UNKNOWN: no reliable semantic interpretation in these sources.
Source agreement and successful serialization do not establish musical semantics. A codec can preserve every bit while assigning incorrect control names. Device-specific acceptance, parameter ranges and UI mappings require separate evidence.
Byte offsets are relative to the first P in PTCF unless a section explicitly uses payload-relative offsets. Slot indices in the binary layout are zero-based; the MIDI CLI's one-based patch numbers are documented separately. Bit tables use the original little-endian payload, with bit 0 at the least significant bit of byte 0.
References S1–S8 identify the pinned local files in Appendix A. The document revision v4.2, the header's format version and the pedal's firmware version are separate identifiers.
Editing principle: preserve unknown bytes and modify only understood fields.
Read these sections in file order: container and target header, tagged payloads, effect blocks, and global parameters.
All offsets below are byte offsets from the first P in PTCF, unless explicitly relative to a payload. Structural integers are unsigned little-endian.
Fixed header 36 bytes
Effect-ID table 4 * fxCount bytes
TXJ1 8 + payload length
TXE1 8 + payload length
EDTB 8 + 24 * fxCount
PPRM (source branch version <= 1) 8 + 12
or PRM2 (source branch > 1) 8 + 32
NAME (optional in source > 1) 8 + payload length, if present
External padding/trailing bytes outside declared container, if present
This is the sequence implemented by S1. A general inspection tool should preserve unknown tags rather than treating this sequence as universal across all future versions.
| Offset | Size | Type | Meaning | Evidence |
|---|---|---|---|---|
0x00 |
4 | ASCII | Signature PTCF |
SOURCE; HISTORICAL SAMPLE |
0x04 |
4 | uint32 LE |
Declared container length | SOURCE; HISTORICAL SAMPLE |
0x08 |
4 | uint32 LE |
version, used to select the global-parameter schema |
SOURCE |
0x0C |
4 | uint32 LE |
fx_count |
SOURCE; HISTORICAL SAMPLE |
0x10 |
4 | uint32 LE |
target, device bitmask |
SOURCE |
0x14 |
6 | raw bytes | data, unknown metadata |
SOURCE; semantics UNKNOWN |
0x1A |
10 | ASCII-compatible bytes | Short patch name | SOURCE; HISTORICAL SAMPLE |
0x24 |
4 * fx_count |
uint32 LE[] |
Effect IDs in slot order | SOURCE; HISTORICAL SAMPLE |
The first tagged section begins at 36 + 4 * fx_count. The six bytes at 0x14..0x19 are one opaque field in S1. Earlier subdivisions into a uint32 and uint16 only describe how v4.1 displayed their zero values.
Correction: 0x04 is not a standalone version byte. A 336-byte container stores 50 01 00 00; a 392-byte container stores 88 01 00 00. The repository's version field is at 0x08, not 0x04.
S1 calls uint32 @ 0x08 version and uses version > 1 to select PRM2; otherwise it selects PPRM. The historical factory corpus and all 150 newly supplied files have value 1 throughout. No physical PRM2/NAME sample was supplied.
“V2” in this document means the repository's > 1 branch. It is not evidence that every integer above 1 has the same layout. The reviewed parser also routes 0 to the older layout; that implementation behavior does not establish version 0 as valid. The format version is distinct from this document's v4.2 revision and from a pedal firmware version. [S1]
For the implemented layouts:
L_v1 = 68 + 4*N + len(TXJ1) + len(TXE1) + len(EDTB) + len(PPRM)
L_v2_with_NAME = 76 + 4*N + len(TXJ1) + len(TXE1)
+ len(EDTB) + len(PRM2) + len(NAME)
Here N is fx_count; every len(TAG) above is its payload length. These formulas come from S1. A generic serializer should calculate 36 + 4*N + sum(8 + payload_length) over the sections actually emitted. Without NAME, the fixed overhead remains 68, including for a PRM2 container.
S1's --pad appends zero bytes after building the container; it does not include them in the declared length. Therefore:
declared_length > physical_lengthis truncation and must be rejected.declared_length == physical_lengthdescribes the historical unpadded factory files.declared_length < physical_lengthmay be explicit external padding. Preserve and report the suffix; do not parse it as sections or automatically call the file corrupt.
For a strict unpadded-file profile, equality is still an appropriate requirement. Padding length required by a specific device is separate from the container layout. [S1, S2]
S1 exposes target as a writable uint32 LE and targets as a decoded, informational view of those same bits. Multiple bits can be represented; their presence is not proof that a patch's installed effects work on every listed device.
| Mask | Label in S1 |
|---|---|
0x00000001 |
G5n |
0x00000002 |
G3n |
0x00000004 |
G3Xn |
0x00000008 |
B3n |
0x00000010 |
G1 FOUR |
0x00000020 |
G1X FOUR |
0x00000040 |
B1 FOUR |
0x00000080 |
B1X FOUR |
0x00000100 |
A1 FOUR |
0x00000200 |
A1X FOUR |
0x00000400 |
G11 |
0x00000800 |
H8 |
0x00001000 |
G6 |
0x00002000 |
B6 |
0x00004000 |
R20 |
0x00010000 |
B2 FOUR |
0x00040000 |
MS-50G+; source comment also mentions MS-70CDR+ |
0x00080000 |
MS-60B+ |
Other bits are unnamed/padding in that view; preserve them in the raw target integer. The historical target == 4 can now be interpreted as G3Xn according to S1's mapping. It does not describe the four-byte width of an effect-ID entry. All 150 user-identified G3Xn examples also store 4, providing corpus agreement with the source mapping.
The .ZD2 target field has a different map in S2: for example, its 0x80 bit is labeled MS-50G+ and related plus models. Do not reuse either target map for the other format. Changing the .zptc mask alone does not convert effect binaries, parameters or slot metadata. [S1, S2]
Each known section has the following envelope:
| Relative offset | Size | Field |
|---|---|---|
+0 |
4 | ASCII tag |
+4 |
4 | uint32 LE payload length |
+8 |
length | Payload |
Advance by 8 + length, starting at the end of the ID table. Do not locate sections by searching for tag strings: a description or unknown payload can contain EDTB, PPRM, etc. Section boundaries come from lengths.
| Tag | Payload in reviewed source | Evidence and limits |
|---|---|---|
TXJ1 |
Arbitrary bytes | S1 preserves bytes; Shift-JIS/CP932-compatible Japanese descriptions were reported in v4.1 |
TXE1 |
ASCII PaddedString |
S1; historical ASCII text is also UTF-8-compatible, which does not prove arbitrary Unicode support |
EDTB |
fx_count blocks of 24 bytes |
S1; historical length agreement |
PPRM |
12 bytes | S1 older branch; partially named PPRM fields |
PRM2 |
32 bytes | S1 newer branch; partially named PRM2 fields |
NAME |
ASCII PaddedString |
Optional parsing in S1 newer branch; separate from the fixed short name |
The fixed short name occupies 0x1A..0x23 in both source branches. Historical factory files pad it with ASCII spaces (0x20). S1's PaddedString(10, "ascii") builds unused space with NUL bytes, so a decode/build cycle is not a universal factory-padding guarantee after a name edit.
NAME adds a length-prefixed name in the newer branch. S1 continues to print config['name'] in its summary, and does not define a policy for synchronizing the two names or for which one the hardware displays. Expose both fields, preserve their bytes, and avoid asserting a universal long-name maximum from this code. [S1]
The historical TXJ1 and TXE1 payload lengths are multiples of four. Some exactly fill their payload without a NUL terminator. Existing payload bytes and lengths should be preserved during unrelated edits.
For TXE1 and NAME, S1 rebuilds the length as:
(len(text) + 4) & 0xFFFC
For ordinary ASCII names/descriptions, this rounds up while reserving at least one NUL byte: text lengths 3, 4 and 5 produce payload lengths 4, 8 and 8. It differs from (n + 3) & ~3, which would allocate only 4 bytes for a 4-byte string. Thus an unchanged historical text that exactly fills its aligned payload can grow during source reserialization.
The literal 0xFFFC mask also drops bits above bit 15; it is a source implementation detail, not evidence of a universal text-size limit. New code should validate encoded byte lengths and use explicit bounds. S1 rebuilds TXJ1.length from the raw bytes without adding alignment or interpreting its encoding. [S1]
For slot index i, starting at zero:
block_offset = EDTB_section_offset + 8 + 24*i
EDTB_payload_length = 24 * fx_count
S1's EDTB2 reverses the 24 bytes, preserves the first three reversed bytes, then reads 168 bits MSB-first. Its parameter order and widths exactly match the old document's codec. That upgrades the partition from a historical codec assumption to an explicitly sourced implementation; musical meanings still require independent validation.
Treat the original 24 bytes as an unsigned little-endian 192-bit integer. Bit 0 is the least significant bit of original byte 0. The following offsets are derived from EDTB2:
| Field | Starting bit | Width | Storage range / meaning |
|---|---|---|---|
enabled |
0 | 1 | Source boolean: 0 off, 1 on |
id |
1 | 29 | 0..0x1FFFFFFF |
param1 |
30 | 12 | 0..4095 |
param2 |
42 | 12 | 0..4095 |
param3 |
54 | 12 | 0..4095 |
param4 |
66 | 12 | 0..4095 |
param5 |
78 | 12 | 0..4095 |
param6 |
90 | 8 | 0..255 |
param7 |
98 | 8 | 0..255 |
param8 |
106 | 8 | 0..255 |
param9 |
114 | 12 | 0..4095 |
param10 |
126 | 12 | 0..4095 |
param11 |
138 | 12 | 0..4095 |
param12 |
150 | 12 | 0..4095 |
| Unknown six-bit field | 162 | 6 | Preserve |
| Unknown three bytes | 168 | 24 | Original bytes 21..23; preserve |
The old unknownPrefix representation is raw[21:24][::-1], because it names the bytes after reversal. All 192 bits are accounted for. This table is equivalent to reversing the block; it does not introduce a different on-disk layout. [S1; DERIVED]
The final flag in the reversed stream is therefore the first physical byte's low bit. S1 explicitly names it enabled and displays it in patch summaries. The historical 121-slot corpus reported 117 values of 1 and four values of 0; that observation alone was not a hardware on/off experiment.
The ID table stores 32-bit values; the block stores 29 bits. S1's conversion operations write the table ID and write id & 0x1FFFFFFF to the block. Historical files have exact equality because their upper table bits are zero. Outside that corpus, compare the low 29 bits and report any upper bits as unmapped; do not discard them silently.
S1 treats table ID 1 as a Bypass effect in --bypass. This is distinct from setting enabled = 0 on a non-bypass effect. Its algorithm moves the associated table entry and effect block together.
Parameter widths are storage capacities, not musical ranges. A parameter may be a level, time, enum, switch, dummy paging control or unused field. No universal param1 = gain mapping follows from S1. [S1, S4]
S1's older branch uses tag PPRM and a fixed 12-byte payload. PPRM_rev reverses these bytes and defines two named fields. In original payload bit order, using the EDTB little-endian bit convention:
| Field | Starting bit | Width | Source interpretation |
|---|---|---|---|
| Unknown low region | 0 | 63 | Preserve |
patchVolume |
63 | 7 | Patch volume, raw 0..127 |
editSlot |
70 | 3 | Active/editing slot; comment says 0..4 on G1 FOUR/etc. |
| Unknown high region | 73 | 23 | Preserve |
Equivalent byte formulas, with p indexing the 12-byte payload:
patchVolume = ((p[7] >> 7) | (p[8] << 1)) & 0x7F
editSlot = ((p[8] >> 6) | (p[9] << 2)) & 0x07
Offsets are DERIVED from S1; field names and comments are SOURCE. Volume units/scaling and the allowed edit-slot range on every other device are not established.
Historical AutoFunk payload:
00 00 00 00 02 00 00 00 32 84 0C 00
Applying the source layout gives patchVolume = 100 and editSlot = 0. Both the payload and these decoded values were rechecked in the supplied 58 - AutoFunk .zptc. The remaining bits retain unknown meanings.
For version > 1, the Python member still named PPRM uses the PPRM_v2 schema, whose actual on-disk tag is PRM2. The source reads a fixed 32-byte payload. Do not emit a 32-byte PPRM merely because the object key has that name. [S1]
The following bit offsets are derived from PPRM_v2_rev, relative to the original 32-byte payload, LSB-first:
| Field/region | Starting bit | Width | Source description |
|---|---|---|---|
| Unknown | 0 | 22 | Preserve |
invalidFXSlot |
22 | 11 | Slot bitfield |
expressionSlot |
33 | 11 | Slot bitfield |
| Unknown | 44 | 11 | Preserve |
looperSlot |
55 | 11 | Slot bitfield |
rhythmSlot |
66 | 11 | Slot bitfield |
patchVolume |
77 | 7 | Raw patch volume |
| Unknown | 84 | 1 | Preserve |
editSlot |
85 | 3 | Source field name |
editSlotBits |
88 | 16 | Source field name; relationship to editSlot unproven |
| Unknown | 104 | 1 | One of the two fields named byte13 |
rightPosition |
105 | 4 | Source field name; UI meaning unverified |
| Unknown | 109 | 52 | Includes the other byte13 field |
preampSlot |
161 | 11 | Slot bitfield |
bpmSlot |
172 | 11 | Slot bitfield |
lineselSlot |
183 | 11 | Slot bitfield |
| Unknown | 194 | 50 | Preserve |
tempo |
244 | 8 | Raw source tempo field; unit/range not tested here |
| Unknown | 252 | 4 | Preserve |
An 11-bit slot field should be exposed as a mask, not assumed to be a numerical slot index. The source does not document zero-mask behavior, multi-bit semantics, device-specific slot capacity or consistency rules when moving effects. In particular, the three-bit editSlot cannot by itself explain all possible bits in the 11-bit masks.
PPRM_v2_rev defines byte13 twice: a three-bit field at original bits 109..111, and a one-bit field at bit 104. With Construct's named container, these keys collide. A subsequent build cannot independently recover both values from that container: it can overwrite bits or reject a value that does not fit.
Preserve the raw 32 bytes and edit named bit ranges directly, or assign unique keys in a separately validated implementation. Do not describe the repository's full PRM2 decode/rebuild as lossless for arbitrary input. [S1]
- Require at least 36 bytes,
PTCF, and36 <= declared_length <= physical_length. - Read version, count, target and opaque metadata without normalizing them.
- Check
fx_count <= (declared_length - 36) // 4before reading the ID table. - Walk section envelopes from
36 + 4*fx_countto exactlydeclared_length; reject truncated headers and out-of-bounds payloads. - Preserve tag order, duplicate/unknown sections and any external suffix. Reject ambiguous duplicates of required sections before editing them.
- For the source profiles, require
TXJ1,TXE1,EDTB, followed byPPRMorPRM2as appropriate. CheckEDTB.length == 24*fx_count, olderPPRM.length == 12, and newerPRM2.length == 32. - Inspect optional
NAMEin the newer profile; report unsupported versions rather than blindly assuming the> 1source branch works for them. - Compare block IDs to the low 29 bits of the corresponding table IDs; separately report upper bits and mismatches.
Source parsing is not a substitute for these checks. S1 uses a counted Array for EDTB rather than bounding it by the declared EDTB length, reads fixed PPRM/PRM2 payload sizes, and does not enforce the outer declared length or end-of-stream.
For a known layout, copy the original bytes and replace only the intended bit field or fixed-size payload. Preserve all unknown bits, text padding, target bits and global metadata. Validate field width and the effect's logical range independently.
When changing an effect ID, update both the table entry and its block, using the appropriate family policy for upper ID bits. When moving/removing slots, keep the table and blocks aligned and account for slot-dependent global metadata in PPRM/PRM2. Do not assume moving only EDTB blocks is sufficient.
For resizing, construct actual section payloads first, calculate each length, then compute the outer container length from the emitted sections. Keep external padding separate. Preserve the original if a device's fixed transfer-size rules are not known. Reparse the result and verify that only intended regions changed.
Use a binary layer with raw bytes, offsets, lengths and bounds; a patch layer for version, target, both names, descriptions, IDs, raw controls and global fields; and a device-specific catalog for musical labels, ranges, enums and units. Keep raw sections and external padding available even when a semantic view exists.
These Python examples use only the standard library. They are small inspection/editing primitives, not a complete hardware-compatible writer. Bit offsets use the original little-endian payload convention used by EDTB, PPRM and PRM2.
import struct
def inspect_zptc(blob):
if len(blob) < 36 or blob[:4] != b"PTCF":
raise ValueError("Invalid or truncated PTCF header")
length, version, count, target = struct.unpack_from("<4I", blob, 4)
if not 36 <= length <= len(blob):
raise ValueError("Invalid declared container length")
if count > (length - 36) // 4:
raise ValueError("Effect-ID table exceeds container")
ids = [struct.unpack_from("<I", blob, 36 + 4*i)[0]
for i in range(count)]
pos = 36 + 4*count
sections = []
while pos < length:
if length - pos < 8:
raise ValueError("Truncated section header")
tag = blob[pos:pos + 4]
size = struct.unpack_from("<I", blob, pos + 4)[0]
start = pos + 8
if size > length - start:
raise ValueError("Section exceeds declared container")
sections.append({"tag": tag, "offset": pos,
"data": blob[start:start + size]})
pos = start + size
return {"length": length, "version": version, "target": target,
"fx_count": count, "ids": ids, "metadata": blob[20:26],
"name_raw": blob[26:36], "sections": sections,
"trailing": blob[length:]}This walker deliberately does not declare musical or version-profile validity. Apply the inspection and profile checks before editing a parsed container. Keeping sections in a list avoids silently losing duplicate or unknown tags.
def read_field(raw, start, width):
if start < 0 or width < 1 or start + width > len(raw)*8:
raise ValueError("Invalid bit range")
return (int.from_bytes(raw, "little") >> start) & ((1 << width) - 1)
def replace_field(raw, start, width, value):
if start < 0 or width < 1 or start + width > len(raw)*8:
raise ValueError("Invalid bit range")
if not isinstance(value, int) or not 0 <= value < (1 << width):
raise ValueError("Field value does not fit")
mask = ((1 << width) - 1) << start
bits = int.from_bytes(raw, "little")
return ((bits & ~mask) | (value << start)).to_bytes(len(raw), "little")
PARAM_FIELDS = [
(30, 12), (42, 12), (54, 12), (66, 12), (78, 12),
(90, 8), (98, 8), (106, 8),
(114, 12), (126, 12), (138, 12), (150, 12),
]
def decode_effect(raw):
if len(raw) != 24:
raise ValueError("EDTB block must be 24 bytes")
return {"enabled": bool(read_field(raw, 0, 1)),
"id": read_field(raw, 1, 29),
"params": [read_field(raw, s, w) for s, w in PARAM_FIELDS],
"unknown6": read_field(raw, 162, 6),
"unknownPrefix": raw[21:24][::-1]}Examples: replace_field(block, 0, 1, 0) disables an effect while preserving its ID and every other bit; replace_field(pprm, 63, 7, 100) changes the source older-layout raw patch volume. Require the respective 24-byte or 12-byte payload before applying those calls. Using bit 63 on PRM2 would edit a different field.
From the workspace root, with the repository dependencies available:
python zoom-zt2/decode_preset.py patch.zptc --dump
python zoom-zt2/decode_preset.py patch.zptc --summary
python zoom-zt2/decode_effect.py effect.ZD2 --summary --targets
python zoom-zt2/decode_effect.py effect.ZD2 --xml effect-parameters.xml
S1 requires Construct; requirements.txt pins construct == 2.10.70. Effect and MIDI tools additionally import dependencies such as Mido and crcmod. decode_preset.py --midi recognizes hard-coded Midi_45/Midi_64 envelopes; it is not a generic SysEx validator.
| Source behavior | Consequence for consumers |
|---|---|
RestreamData creates parsed reversed views of autorev |
Editing the view alone does not update stored bytes. S1 explicitly rebuilds EDTB2 and the selected global schema into autorev before building the container. |
| Outer length is built before rebuilt child lengths | S1 performs parse/build twice to fix the top-level length. A new serializer should calculate lengths from final payloads first. |
Newer NAME parsing is optional, but length building dereferences NAME.length |
A newer patch without NAME may parse but cannot be assumed to rebuild successfully. |
| Text is rebuilt with NUL padding and the source alignment formula | Full-file byte equality can fail even without a meaningful text change. |
PRM2 has duplicate byte13 field names |
Parsed unknown values can collide during rebuilding; preserve raw bytes. |
--bypass/--limit manipulate IDs and EDTB blocks |
They do not implement a complete recalculation of slot-related global fields. |
--target changes the integer; --convert1/2 change IDs |
These operations alone do not establish device compatibility. |
Source MIDI receive slices use n + floor(n/7) + 1 |
At exact multiples of seven, this requests one extra packed byte; use exact packed lengths and framing validation in new code. |
decode_preset.py MIDI schemas consume Bytes(length) before unpacking |
Do not assume this handles the packed expansion or validates the transfer CRC like S2's patch methods. |
These are properties of the reviewed snapshot, not additional format requirements. Neither repository was modified as part of this documentation update. The supplied example files were read without modification.
The following layers provide effect metadata, transport or firmware storage around the raw patch container.
A .zptc stores effect references and control values, not DSP executable code, effect display names, icons or a complete schema of parameter labels.
- S2's
.ZD2schema starts with signatureZDLFand includes target, effect version, group, ID, name, description and optional parameter sections. - S2's
FLST_SEQ.ZT2schema records a 12-byte effect filename, four-character version, installed byte and 32-bit effect ID, organized by group. The record's group is(id >> 24) & 0xFFand is checked against the enclosing group. - Optional
.ZD2PRMEholds English XML in S2;PRMJis the Japanese/raw counterpart. S3 can export them using--xmland--japan. - S5/S8 provide firmware extraction of effect binaries, which can then be inspected using S2/S3.
Build a catalog from the actual installed or extracted files for the target device. Keep effect ID, device/effect target maps, firmware/library version, effect binary version, parameter metadata and file identity together. The presence of XML is a route to further mapping, not proof that all effects provide complete parameter labels/ranges or that XML order directly equals param1..12.
The standard group enum in S2 includes Dynamics 1, Filter 2, Drive 3, Amp 4, Cabinet 5, Modulation 6, SFX 7, Delay 8, Reverb 9, Pedal 11, AG Model 20, Acoustic 29, Rhythm 30, and Looper 31. S4 states that MS-plus uses a different ID/group scheme; do not generalize the labels to it.
These are selected entries from the source's two-screen to one-screen conversion table:
| Effect label | Two-screen ID | One-screen ID |
|---|---|---|
| MS 800 | 0x04000010 |
0x04000011 |
| FD TWNR | 0x04000020 |
0x04000021 |
| Delay | 0x08000010 |
0x08000011 |
| AnalogDly | 0x08000020 |
0x08000021 |
| PDL Delay | 0x0B0000A0 |
0x0B0000A1 |
Use explicit pairs from convert; do not implement conversion as universally adding or subtracting 1. For example, FD DLXR maps 0x0400002A to 0x0400002C.
S4 describes 1U effect variants with dummy paging parameters and 3S variants for restricted delay durations. S1 changes IDs during conversion; it does not perform a documented general parameter remapping or validate hardware acceptance. The .ZDL generation, .ZD2 generation and newer MS-plus variants must retain separate compatibility information. [S1, S4, S5]
The raw patch begins 50 54 43 46 (PTCF). A MIDI SysEx message instead has framing bytes F0 ... F7, address/command fields, 7-bit-packed data, and in the checked transfer methods a five-byte CRC representation. These transport fields do not become fields in the raw .zptc header. [S1, S2]
S2's pack transforms each group of up to seven raw bytes into:
one high-bit byte + up to seven low-seven-bit bytes
The high bit of the first raw byte goes into bit 6 of the prefix; the second into bit 5; through bit 0 for the seventh. Short groups use those same high prefix positions. For n raw bytes, packed size is n + ceil(n/7), with zero producing zero bytes.
Example:
raw: 80 01 FF
packed: 50 00 01 7F
Lengths and bank/location numbers encoded as two 7-bit bytes use low + (high << 7); that numeric encoding is separate from packing arbitrary binary payloads. [S1 Midi2u; S2 pack/unpack]
The source uses address bytes 52 00 6E; do not claim this address is universal. Entries below are command bytes after that address. Mido's msg.data excludes F0/F7.
| Operation | Command | Behavior |
|---|---|---|
| Patch capacity query | 44 |
Reads patch count, patch size and bank size |
| Numbered patch download request | 46 00 00 |
Bank/location each represented by two 7-bit bytes |
| Numbered patch upload | 45 00 00 |
Bank/location, length, packed data and CRC |
| Old numbered download | 09 00 |
One byte each for bank/location |
| Old numbered upload | 08 00 |
Bank/location, length, packed data and CRC |
| Current patch download | 64 13 |
Response unpacked and CRC checked |
| Old current patch download | 29 |
patch_download_current_old unpacks without a CRC check |
CLI patch numbers are one-based. For bank size B, S2 derives bank = (location - 1) // B and slot = (location - 1) % B. patch_upload_current is commented out; current-patch upload is not an implemented public operation in this snapshot.
Numbered upload and checked download methods use:
import binascii
crc = binascii.crc32(raw_transfer_data) ^ 0xFFFFFFFF
crc_bytes = bytes([
crc & 0x7F,
(crc >> 7) & 0x7F,
(crc >> 14) & 0x7F,
(crc >> 21) & 0x7F,
(crc >> 28) & 0x0F,
])raw_transfer_data is the complete unpacked transfer payload, including any padding sent. The checksum covers neither the 7-bit-packed representation nor only buffer[:declared_length] unless those are precisely the transmitted raw bytes. Download reconstructs the integer from the five transport bytes and checks (received_crc ^ 0xFFFFFFFF) == binascii.crc32(data). The source prints an error on mismatch but still returns data; a robust importer should reject a mismatch. [S2]
No embedded checksum field is identified in S1's PTCF schema. Separately, .ZD2 has a checksum at offset 0x08; S3 calculates its effect-binary CRC over data[12:-16]. Do not transplant that offset or coverage into .zptc, where 0x08 is the source format version. [S1–S3]
The following structure is relevant when extracting files with Zoom-Firmware-Editor. It is not part of the standalone PTCF byte stream.
| Firmware structure | Layout in S6–S8 |
|---|---|
| File-table system prefix | 8 bytes |
| File-table record | 32 bytes |
Record +0 |
2-byte starting block address |
Record +4 |
4-byte file size |
Record +8 |
12-byte filename area |
| Content block | 4096 bytes |
Block +0, +2, +4 |
2-byte previous address, next address, payload size |
Block +6 |
Up to 4090 payload bytes |
fillPatchContent follows the block chain, appends only payload bytes, checks previous-address consistency and verifies the reconstructed size. savePatchFile writes patch.getContent(). Consequently an extracted file excludes the firmware block headers and filler. calculatePatchBlocksCount uses ceil(file_size / 4090).
Patch.extractNameFromContent() searches for OnOff and a four-byte FF pattern to derive an effect name. This heuristic does not define the .zptc short-name offset. Firmware table names, effect display names and the patch name at 0x1A are different fields. [S6–S8]
The G3Xn validation report records every input filename, SHA-256, section offset/length, effect count, decoded raw patch volume/edit slot and source full-file round-trip result. Device provenance is the user's G3Xn extraction; the pedal's firmware version was not supplied.
| Check | Measured result |
|---|---|
| Files / distinct SHA-256 values | 150 / 85 |
| Physical size range | 248..500 bytes |
| Version / target | 1 / 0x00000004 in 150/150 |
| Declared size equals physical size | 150/150; no external suffix |
Header metadata 0x14..0x19 |
Six zero bytes in 150/150 |
| Section sequence | TXJ1 -> TXE1 -> EDTB -> PPRM in 150/150 |
| Payload alignment | All payload lengths divisible by four, including zero |
| EDTB length / effect count | 24 * count in 150/150 |
| EDTB blocks rebuilt byte-for-byte using S1 | 993/993 |
| Full 32-bit header IDs equal 29-bit block IDs | 993/993 |
| PPRM size / position | 12 bytes, ending at EOF in 150/150 |
| PPRM source decode/build equality | 150/150 |
| TXJ1 CP932 / TXE1 ASCII decode | 150/150 for each; includes empty descriptions |
| Enabled flag values | 970 ones, 23 zeros |
| Effect counts | 3 files with 4; 15 with 5; 18 with 6; 114 with 7 |
| Distinct effect IDs / slots with Bypass ID 1 | 71 / 75 |
| Source-decoded raw patch-volume range | 35..120 |
| Source-decoded edit-slot values | 0..4 |
| Complete S1 rebuild byte-for-byte equality | 54/150 |
Why only 54 full-file matches: all other 96 files grow by exactly four bytes because S1 enlarges TXE1 using its text-length formula. Section payload comparisons isolate the change to TXE1; the outer size and following section offsets change accordingly. This includes 67 zero-length TXE1 payloads, which become four bytes, and 29 nonempty descriptions that already fill their aligned payload. EDTB and PPRM stay byte-identical. Full-patch round-trip fidelity is therefore a different property from effect-block fidelity.
Files 28–47 were rerun: their 121 effect blocks and 117/4 flag distribution reproduce the v4.1 totals, and their recorded sizes and section offsets match. Jimi's SHA-256 exactly matches the historical primary sample, and all six recorded parameter vectors match. AutoFunk's size, sections and PPRM bytes also match the prior record.
The new bit tables and reference code were additionally checked with 1,000 deterministic synthetic iterations against S1's Construct schemas. This verifies EDTB fields and preservation, PPRM fields and preservation, and named PRM2 field offsets. A one-hot PRM2 sample at bit 109 reproduces the duplicate byte13 rebuild loss. The section walker rejects six malformed synthetic inputs and correctly handles tag text inside a payload and external padding. S2's actual pack/unpack methods pass all lengths 0..100 and the documented packing example. These checks establish implementation agreement, not musical semantics or hardware acceptance.
The reproducible check is tools/validate_zptc_spec.py. With construct==2.10.70 installed, run from the workspace:
python tools/validate_zptc_spec.py
It reads the examples, extracts the reference Python snippets from this document, and compares them with the local source schemas. It does not connect to MIDI or alter input files. Optional --json-output PATH exports detailed results; --construct-path PATH supports an isolated dependency installation. No physical pedal import or upload was tested.
The 67 files named Empty are not all equivalent. Files 85–150 share one SHA-256 and contain seven slots with ID 0 and enabled bit 1. File 84 is different: six slots with IDs 0x04000010, 1, 0x0500002B, 0x05000018, 0, 0. Thus a patch named Empty is not proof of an empty effect chain, and the enabled bit alone does not imply an active audible effect. ID 0 should remain a raw value unless its device semantics are independently established.
Seven blocks contain both a nonzero unknown six-bit field and nonzero original bytes 21..23:
| Patch number | Zero-based slot | ID | Unknown six bits | Original bytes 21..23 |
|---|---|---|---|---|
| 8 | 5 | 0x02000050 |
16 | 01 04 00 |
| 14 | 4 | 0x02000060 |
16 | 01 00 00 |
| 16 | 2 | 0x02000053 |
24 | 60 40 01 |
| 28 | 3 | 0x02000053 |
24 | 60 40 01 |
| 30 | 4 | 0x02000060 |
16 | 01 00 00 |
| 37 | 2 | 0x02000053 |
24 | 60 40 01 |
| 57 | 1 | 0x02000053 |
24 | 60 40 01 |
S1's conversion table labels these IDs as GEQ variants. That correlation does not establish the unknown bits' functions, but directly disproves any assumption that the opaque region can always be zeroed.
- Musical interpretation, scaling and allowed values for each effect's
param1..12. - Semantics of header bytes
0x14..0x19, the 30 unknown EDTB bits and unnamed global fields. - Device-specific
PRM2masks, slot ordering,rightPositionand relationships among edit-slot fields. - Valid version values beyond the historical
1and compatibility of S1's broad newer branch. - Hardware precedence and synchronization of the fixed short name and
NAME. - Which devices require fixed transfer padding and which metadata must change when slots move.
- Meaning of any upper three ID-table bits in families where they occur.
- Text encodings and maximum lengths beyond the historical corpus and source ASCII policy.
For semantic validation, compare patches differing in exactly one device action: one enabled toggle, one control value, one selected edit slot, one tempo/volume change, or one effect type. Record raw bytes, device and firmware version, installed effect version and a hash of each sample. Verify neighboring and unknown bits remain unchanged.
| Repository | Local commit | Commit date | Relevant role |
|---|---|---|---|
zoom-zt2 |
b1f63b0bee6d2d1bc9755958fc8cf15887efbdcf |
2026-06-02 | Direct PTCF parser/builder, effect catalog parsing and MIDI transfer |
Zoom-Firmware-Editor |
31539401b470af380100dcab726d26887e00a663 |
2019-05-13 | Extracts and repacks files inside firmware updater images |
Source references throughout this document refer to these local snapshots, not an assumed latest upstream release.
| Ref | Source | Relevant symbols or behavior |
|---|---|---|
| S1 | decode_preset.py | ZPTC, EDTB2, PPRM_rev, PPRM_v2_rev, NAME, convert, main |
| S2 | zoomzt2.py | ZD2, ZT2, Effect, Group, pack, unpack, patch_check, patch transfer methods |
| S3 | decode_effect.py | Effect metadata/XML extraction and effect-binary CRC |
| S4 | zoom-zt2 readme | Device scope, effect variants, CLI and MIDI observations |
| S5 | Firmware Editor README | Firmware/effect scope and storage notes |
| S6 | Patch.java | Firmware file-table metadata and effect-name heuristic |
| S7 | Firmware.java, FileTable.java | Block and table constants |
| S8 | FileTableService.java, PatchService.java | File reconstruction, extraction and block counts |
The Java project's class named Patch represents a file in a firmware image; it is not a PTCF preset decoder. No PTCF/ZPTC decoder was found in its source. Its README even lists “Rename: patch -> effect” as a TODO. Conversely, the comment at the top of decode_effect.py says ZPTC, but its implementation parses zoomzt2.ZD2. Use the actual signatures and schemas to distinguish these formats. [S1, S3, S5, S6]
This section retains the prior sample records and explicitly cross-checks the examples now available. Jimi, AutoFunk and files 28–47 were newly revalidated; v3 corpus totals remain historical claims unless explicitly included in the new measurements above.
Physical/declared size: 336 bytes
SHA-256: 5e305d838aa7f5c57584f9f2eaab71f32820daf84da04ab578f85c82864c11ed
Version @ 0x08: 1
Effect count: 6
Target @ 0x10: 4
Short name: Jimi
TXJ1: offset 0x003C, payload length 40
TXE1: offset 0x006C, payload length 48
EDTB: offset 0x00A4, payload length 144
First effect: 0x00AC
PPRM: offset 0x013C, payload length 12
All listed offsets, lengths and the SHA-256 were revalidated from the supplied Jimi file. PPRM decodes to raw patchVolume = 60, editSlot = 0. The target interpretation agrees with the user-reported G3Xn provenance.
Historical English description: “This uses MS 800 for a Jimi Hendrix style tone.”
| Slot (zero-based) | Offset | Decimal ID | Enabled interpretation | Raw params 1..12 |
|---|---|---|---|---|
| 0 | 0x00AC |
184549408 | on | 50, 50, 0, 80, 0, 0, 0, 0, 0, 0, 0, 0 |
| 1 | 0x00C4 |
67108880 | on | 1, 42, 50, 50, 30, 84, 70, 2, 1, 0, 0, 0 |
| 2 | 0x00DC |
1 | on | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| 3 | 0x00F4 |
83886096 | on | 0, 50, 50, 47, 0, 0, 0, 0, 0, 0, 0, 0 |
| 4 | 0x010C |
134217760 | on | 710, 25, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| 5 | 0x0124 |
150994992 | on | 48, 9, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
S1's conversion table identifies 67108880 == 0x04000010 as MS 800 and 134217760 == 0x08000020 as AnalogDly; ID 1 is its Bypass entry. Other names should come from a matching catalog.
Physical/declared size: 392 bytes (0x188)
Effect count: 7
Short name: AutoFunk
TXJ1: offset 0x0040, payload length 56
TXE1: offset 0x0080, payload length 60
EDTB: offset 0x00C4, payload length 168
PPRM: offset 0x0174, payload length 12
Complete PPRM section:
50 50 52 4D 0C 00 00 00 00 00 00 00 02 00 00 00 32 84 0C 00
Historical English description: “This funky auto-wah sound uses AutoWah and FD TWNR effects.” Earlier revisions reported 6/6 effect-block round-trips for Jimi and 7/7 for AutoFunk; both results were reproduced in v4.2. Both complete files also rebuild byte-for-byte with S1. Neither file was part of v4.1's direct 28–47 revalidation. The supplied AutoFunk filename contains two trailing spaces before .zptc; its SHA-256 is 27df55160771924e4920ff584c6a2feca369bf074978cc6d1204212cf0872f09.
The v3 document reported files 37–56, 20 files and 119 effect slots. The v4.1 direct revalidation instead reported files 28–47:
Files: 20
Effect blocks: 121
Byte-for-byte effect round-trips: 121 / 121
Header/effect ID matches: 121 / 121
Declared/physical size matches: 20 / 20
PPRM at EOF: 20 / 20
TXJ1 Shift-JIS-compatible decode: 20 / 20
TXE1 UTF-8/ASCII-compatible decode: 20 / 20
Enabled flag values: 117 ones, 4 zeros
Effect-count distribution: 8 files with 5; 3 with 6; 9 with 7
File-size range: 308..460 bytes
Those files reportedly had version == 1, target == 4, zero opaque header metadata, ten-byte space-padded names, TXJ1 -> TXE1 -> EDTB -> PPRM, aligned payload lengths, EDTB.length == 24*fx_count and PPRM.length == 12. Effect round-trips prove bit preservation, not independent parameter semantics.
Every size, count and offset in this retained table was reproduced in v4.2.
| File | Size | Effects | TXJ1 | TXE1 | EDTB | PPRM |
|---|---|---|---|---|---|---|
28 - Rhapsody .zptc |
444 | 7 | 0x0040 | 0x0094 | 0x00F8 | 0x01A8 |
29 - LatinSolo .zptc |
440 | 7 | 0x0040 | 0x008C | 0x00F4 | 0x01A4 |
30 - ZepDrive .zptc |
396 | 7 | 0x0040 | 0x0078 | 0x00C8 | 0x0178 |
31 - Eruption .zptc |
396 | 7 | 0x0040 | 0x007C | 0x00C8 | 0x0178 |
32 - Sweet Lead.zptc |
412 | 7 | 0x0040 | 0x0084 | 0x00D8 | 0x0188 |
33 - RC Clean .zptc |
352 | 6 | 0x003C | 0x007C | 0x00B4 | 0x014C |
34 - Blues .zptc |
360 | 5 | 0x0038 | 0x0080 | 0x00D4 | 0x0154 |
35 - EarlyBrits.zptc |
352 | 5 | 0x0038 | 0x0080 | 0x00CC | 0x014C |
36 - Surf .zptc |
352 | 5 | 0x0038 | 0x0080 | 0x00CC | 0x014C |
37 - TAcoustic .zptc |
364 | 5 | 0x0038 | 0x0084 | 0x00D8 | 0x0158 |
38 - UKxUS .zptc |
340 | 5 | 0x0038 | 0x0080 | 0x00C0 | 0x0140 |
39 - Brit Grit .zptc |
308 | 5 | 0x0038 | 0x006C | 0x00A0 | 0x0120 |
40 - Hot Twin .zptc |
336 | 5 | 0x0038 | 0x007C | 0x00BC | 0x013C |
41 - Fuunnkk .zptc |
356 | 6 | 0x003C | 0x0078 | 0x00B8 | 0x0150 |
42 - EchoContrl.zptc |
380 | 7 | 0x0040 | 0x0078 | 0x00B8 | 0x0168 |
43 - ChuckWah .zptc |
428 | 7 | 0x0040 | 0x0090 | 0x00E8 | 0x0198 |
44 - Pedal Mod .zptc |
404 | 7 | 0x0040 | 0x008C | 0x00D0 | 0x0180 |
45 - Crush PDL .zptc |
380 | 6 | 0x003C | 0x0084 | 0x00D0 | 0x0168 |
46 - Johnny .zptc |
460 | 7 | 0x0040 | 0x0098 | 0x0108 | 0x01B8 |
47 - SuperSonic.zptc |
340 | 5 | 0x0038 | 0x007C | 0x00C0 | 0x0140 |
| v4.1 statement | v4.2 finding |
|---|---|
0x08 semantics unknown |
S1 calls it version and branches on it. |
0x10 == 4 semantics unknown |
S1 defines a device target mask; bit 4 is labeled G3Xn. |
0x14 and 0x18 displayed separately |
S1 preserves six opaque bytes at 0x14. |
| Parameter partition only historically derived | S1 implements exactly the 12/8-bit partition; original-order bit offsets are now explicit. |
| Enabled semantics only historically inferred | S1 names/displays the bit as enabled; hardware reconfirmation remains separate. |
| PPRM payload entirely unknown | S1 names patchVolume and editSlot; other bits remain opaque. |
| Variants not mapped | S1 adds PRM2, optional NAME and a target map, with implementation caveats. |
| Physical size must equal declared size | Still valid for historical unpadded files; S1 explicitly supports external zero padding. |
| No checksum identified | No embedded PTCF checksum identified; S2 implements a separate MIDI transfer CRC. |
| Direct corpus limited to files 28–47 | New G3Xn corpus: 150 files, 993 effect blocks, including four-effect patches, Empty names and zero-length text sections. |
| Block round-trips emphasized | Full source rebuild also measured: 54 exact matches; 96 TXE1 expansions of four bytes. |
| Effect catalog wholly unmapped | S1 supplies explicit ID pairs; S2/S3 expose effect and installed-library metadata. |
| Tags located using string searches in an example | Replaced by a bounded, sequential section walker. |
The golden rule remains: preserve unknown bytes and modify only understood fields. The added source evidence makes implementation more concrete without turning untested device behavior into confirmed facts.
The v4.2 material is organized into five chapters: overview, binary format, implementation, related formats, and validation. Source snapshots, historical samples and revision history are grouped in appendices. This editorial pass preserves the technical data and executable examples.