Skip to content

Instantly share code, notes, and snippets.

@alexishida
Last active September 15, 2026 03:15
Show Gist options
  • Select an option

  • Save alexishida/99a5b241b999497102a3b107a50ec03b to your computer and use it in GitHub Desktop.

Select an option

Save alexishida/99a5b241b999497102a3b107a50ec03b to your computer and use it in GitHub Desktop.
ZPTC files used to store effect/amp-model patches for compatible ZOOM devices

Technical specification of the .zptc patch format

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.md so existing links continue to work.

Contents

1. Overview and reading conventions

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.

1.1 Quick reference

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

1.2 Evidence classifications

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

1.3 Notation and source references

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.

2. Binary format

Read these sections in file order: container and target header, tagged payloads, effect blocks, and global parameters.

2.1 Container structure and header

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.

2.1.1 Fixed header

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.

2.1.2 Version branches

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]

2.1.3 Container length and padding

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_length is truncation and must be rejected.
  • declared_length == physical_length describes the historical unpadded factory files.
  • declared_length < physical_length may 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]

2.2 Device target mask

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]

2.3 Tagged sections, text and names

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

2.3.1 Names

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]

2.3.2 Text length and termination

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]

2.4 EDTB effect blocks

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.

2.4.1 Equivalent layout in original byte order

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.

2.4.2 IDs, bypass and parameter interpretation

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]

2.5 PPRM global parameters

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.

2.6 PRM2 global parameters

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.

2.6.1 Source rebuild caveat

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]

3. Reading, editing and implementation

3.1 Inspection and profile validation

  1. Require at least 36 bytes, PTCF, and 36 <= declared_length <= physical_length.
  2. Read version, count, target and opaque metadata without normalizing them.
  3. Check fx_count <= (declared_length - 36) // 4 before reading the ID table.
  4. Walk section envelopes from 36 + 4*fx_count to exactly declared_length; reject truncated headers and out-of-bounds payloads.
  5. Preserve tag order, duplicate/unknown sections and any external suffix. Reject ambiguous duplicates of required sections before editing them.
  6. For the source profiles, require TXJ1, TXE1, EDTB, followed by PPRM or PRM2 as appropriate. Check EDTB.length == 24*fx_count, older PPRM.length == 12, and newer PRM2.length == 32.
  7. Inspect optional NAME in the newer profile; report unsupported versions rather than blindly assuming the > 1 source branch works for them.
  8. 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.

3.2 Template editing

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.

3.3 Recommended software model

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.

3.4 Reference code

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.

3.4.1 Bounds-checked container inspection

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.

3.4.2 Preserve unknown bits when editing a field

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.

3.5 Repository commands and limitations

3.5.1 Offline inspection commands

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.

3.5.2 Rebuild limitations to account for

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.

4. Effect catalogs and external formats

The following layers provide effect metadata, transport or firmware storage around the raw patch container.

4.1 Effect IDs and parameter catalogs

A .zptc stores effect references and control values, not DSP executable code, effect display names, icons or a complete schema of parameter labels.

4.1.1 Sources for a device-specific catalog

  • S2's .ZD2 schema starts with signature ZDLF and includes target, effect version, group, ID, name, description and optional parameter sections.
  • S2's FLST_SEQ.ZT2 schema 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) & 0xFF and is checked against the enclosing group.
  • Optional .ZD2 PRME holds English XML in S2; PRMJ is the Japanese/raw counterpart. S3 can export them using --xml and --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.

4.1.2 Explicit ID conversions in S1

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]

4.2 MIDI SysEx transport and CRC

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]

4.2.1 Packing

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]

4.2.2 Patch transfer commands implemented in S2

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.

4.2.3 CRC scope

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]

4.3 Firmware file storage

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]

5. Validation and open questions

5.1 v4.2 measured validation

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

5.1.1 Whole-file rebuild behavior

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.

5.1.2 Reproducing the checks

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.

5.2 Empty names and nonzero opaque bits

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.

5.3 Open questions

  • 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 PRM2 masks, slot ordering, rightPosition and relationships among edit-slot fields.
  • Valid version values beyond the historical 1 and 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.

Appendix A. Reviewed sources

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]

Appendix B. Historical samples and cross-checks

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.

B.1 Primary sample: 51 - Jimi .zptc

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.

B.2 Second historical sample: 58 - AutoFunk.zptc

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.

B.3 Prior corpus summaries

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.

B.4 Per-file summary retained from v4.1

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

Appendix C. Revision history

C.1 Technical changes from v4.1 to v4.2

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.

C.2 Editorial organization

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment