Specification
wt3d: wavetable dimensions in WAV files
Version 1 Status: draft Public domain (CC0) Maintained by WaveStack
A wavetable WAV is a run of single-cycle frames. Tools agree on how long each frame is (the Serum
clm chunk), but not on how the frames are arranged: a 2D table of 8 × 8 frames and a 1D
table of 64 frames look identical. wt3d is a small RIFF chunk that records the arrangement, a grid
of columns × rows, optionally stacked in layers, so a table keeps its shape when it moves between tools.
Tools that don't know wt3d skip it, as RIFF requires, and read the file as an ordinary wavetable
with all frames in one row. Adding wt3d never breaks a file for anyone. For background on frames,
frame sizes and 2D tables, see How wavetables work.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as described in RFC 2119.
Terms
| Term | Meaning |
|---|---|
| Frame | One single-cycle waveform of frame size samples. |
| Column | Position along a row. Scanned by the X axis. |
| Row | One line of columns. Scanned by the Y axis. |
| Layer | One complete grid of rows. Scanned by the Z axis. |
| 1D table | rows = 1, layers = 1. |
| 2D table | rows > 1, layers = 1. |
| 3D table (volume) | layers > 1. |
The chunk
wt3d is a chunk inside a RIFF WAVE file, alongside fmt and data.
- Chunk ID: the four ASCII bytes
wt3d(77 74 33 64). - A file MUST NOT contain more than one
wt3dchunk. If it does, readers SHOULD use the first. -
The chunk MAY appear anywhere among the file's chunks. Writers SHOULD place it before
data, so streaming readers see it first. - As with every RIFF chunk, an odd-length body is followed by one pad byte that is not counted in the chunk size.
All integers are unsigned 32-bit little-endian.
| Offset | Size | Field | Rule |
|---|---|---|---|
| 0 | 4 | version | 1 for this specification. |
| 4 | 4 | frame size | Samples per frame. MUST be ≥ 1. |
| 8 | 4 | cols | Columns per row. MUST be ≥ 1. |
| 12 | 4 | rows | Rows per layer. MUST be ≥ 1. |
| 16 | 4 | layers | Layers. MUST be ≥ 1. |
| 20 | 4 | flags | Hints about the audio; see Flags. |
| 24 | 4 | names length | N, the byte length of the name block (may be 0). |
| 28 | N | name block | Layer names; see Layer names. |
The body is 28 + N bytes in version 1.
The audio
-
The
datachunk holds every frame back to back, and MUST contain exactlyframe size × cols × rows × layerssamples, no more and no fewer. - Audio SHOULD be mono. A reader that accepts multichannel files MUST count samples per channel and SHOULD mix the channels down.
- Any sample format WAV allows (PCM integer or IEEE float) MAY be used.
wt3ddoesn't constrain it.
Frame order and orientation
Frames are stored layer by layer, each layer row by row, each row column by column (column varies fastest).
With zero-based layer, row and col, a frame's index in the file is:
index = (layer × rows + row) × cols + col and its first sample is at index × frame size.
Orientation, for display and for mapping scan positions:
- Column 0 is the left. X increases to the right.
- Row 0 is the top. Y increases downward, the way images and spreadsheets are read.
- Layer 0 is the first (front) layer. Z increases away from it.
A tool that draws Y increasing upward MAY flip its display, but MUST NOT change the stored order.
Flags
Flags are hints. They describe processing the writer believes the audio already has, so a reader can skip redundant work. A reader MAY ignore any or all of them, and MUST NOT reject a file because of them. A wrong hint can at worst cause a reader to skip a step it would otherwise apply.
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x1 | Normalized: the table's peak level is at or near full scale. |
| 1 | 0x2 | DC removed: each frame averages to (about) zero. |
| 2 | 0x4 | Zero-aligned: each frame starts at or near a zero crossing. |
| 3–31 | Reserved. Writers MUST write 0; readers MUST ignore them. |
Layer names
The name block holds one name per layer, in layer order. Each name is UTF-8 followed by a NUL byte
(00), including the last.
- Names MAY be empty (just the NUL byte). An empty name keeps its place.
- A writer MUST write either a name for every layer or no names (
N = 0). -
A reader that finds a different number of names than
layersMUST ignore all of the names (it MUST still use the grid). - Readers SHOULD replace invalid UTF-8 rather than reject the file.
Reading
A reader MUST ignore the wt3d chunk, and read the file as if it were absent, when any of these is true:
- the body is shorter than 28 bytes;
versionis 0;frame size,cols,rowsorlayersis 0;-
frame size × cols × rows × layers(computed without overflow) differs from the number of samples indata.
When the chunk is valid:
- Its
frame sizetakes precedence over aclmorsrgeframe size that disagrees. -
A reader that supports fewer dimensions than the file has SHOULD keep every frame in its stored order by
folding the extra dimension into the next: a 2D-only reader SHOULD show a volume's layers as extra rows
(
rows × layersrows), and a 1D-only reader reads the whole table as one row. Such a reader SHOULD tell the user, and SHOULD NOT write the file back over the original, because the layers would be lost. - A reader MAY refuse tables larger than it supports. It SHOULD then open the file as if the chunk were absent rather than fail.
Writing
- Write
version1 and the body exactly as above. - Write
wt3dfor 2D and 3D tables. It MAY also be written for 1D tables. - Also write a Serum
clmchunk with the same frame size, so tools that only knowclmstill find the frames. - Write reserved flag bits as 0, and write a name for every layer or none.
Versioning
Later versions will only append fields after the version 1 fields and increase
version. They will not change the meaning of any version 1 field.
-
A reader MUST accept any
version≥ 1, read the fields it knows, and ignore any bytes after them. The name block is always found at offset 28 with lengthN; bytes after it belong to later versions. - Writers MUST write the lowest version that holds what they write.
Example
An 8-frame table of 64-sample frames arranged as 4 columns × 2 rows, no flags, no names. The chunk is 36 bytes:
77 74 33 64 "wt3d"
1C 00 00 00 body size 28
01 00 00 00 version 1
40 00 00 00 frame size 64
04 00 00 00 cols 4
02 00 00 00 rows 2
01 00 00 00 layers 1
00 00 00 00 flags (none)
00 00 00 00 names length 0
The frame in the second row's fourth column (row 1, col 3, zero-based) is index
(0 × 2 + 1) × 4 + 3 = 7, starting at sample 448.
Chunk builder
Enter a table's shape to see the exact bytes a writer should produce, and click any frame to see where it sits in the file. Layer names are written only when there's one for every layer.
Reference files
Small test files written by an independent script, not by WaveStack's own encoder. Every frame is a constant
level equal to its index ÷ 100 (frame 7 is all 0.07), so you can check where each frame lands. All files are
mono 32-bit float at 44.1 kHz and also carry a clm chunk.
| File | Contents | Expected result |
|---|---|---|
grid-4x2.wav | 8 frames, 4 × 2 × 1 | A 4 × 2 grid. Row 2, column 4 (1-based) is 0.07. |
volume-3x2x2.wav | 12 frames, 3 × 2 × 2, flags 0x5, names Glass and Reed | A 3 × 2 × 2 volume. Layer 2, row 2, column 3 is 0.11. |
future-version.wav | Version 2 with 8 extra bytes | Read as a 2 × 2 grid. |
invalid-count.wav | Claims 4 × 3 = 12 frames, holds 8 | Chunk ignored: one row of 8 frames. |
The script that writes them, wt3d_vectors.py, needs
only Python 3 and is a compact second reference for the byte layout.
Implementations
- WaveStack reads and writes
wt3d.
If your tool supports wt3d, let us know and
we'll add it here.
License
This specification is dedicated to the public domain under
CC0 1.0. Anyone
may implement, copy or adapt wt3d for any purpose, with no permission, fee or attribution
required. The reference files are dedicated the same way.
Changes
- Version 1 (2026-10): first version.