CSVF2 PROTOCOL

Complete specification of the CSVF2 radio modem, written so that anyone can build a client that sends and receives correctly and has every feature. Matches CSVF2 program version 3.6.3 (2026-10-17). ← CSVF2   download the reference implementation (zip)

Nothing here is secret, encrypted or obscured. Every tone, every table, every checksum, the error-correction code and the one pseudo-random sequence in the format (a published whitening sequence, section 7) are written out below, and the program's source code (radio_modem.py, radio_fec.py) is in the download and is the reference when this text and the code disagree. The tables in this page were generated from that code, not typed by hand. What goes on the air is plain text, files you chose to send, a station ID, and checksums. Section 13 has test vectors so an implementation can check itself.

Contents

1. Overview

CSVF2 ("client server via frequency") moves text and files over an analog voice radio channel as audio tones. A transmission is: a lead-in of silence, a steady key tone (so a VOX radio is already transmitting when the data starts), optionally the station's callsign, one or more bursts of tones, optionally a spoken website announcement, the callsign again, optionally an end-of-transmission tone for a server.

The tones carry frames. A frame is a run of symbols: a synchronisation pattern, a type, an optional path, a body, a checksum and an end symbol. A symbol is one short burst of one tone (single-tone profiles) or two simultaneous tones (two-tone profiles). Several speed profiles exist; they differ only in symbol length, frequencies and how many bits a body symbol carries. A receiver listens for every profile at once and needs no speed setting.

Layers, bottom to top: (1) audio samples; (2) symbols (section 4); (3) frames (5, 6, 7, 8); (4) transmission timeline with bursts, key tone and IDs (9, 10). Section 11 describes a reference receiver.

2. Audio and the tone alphabet

Symbols (number = character)
0=q 1=w 2=e 3=r 4=t 5=y 6=u 7=i 8=o 9=p
10=a 11=s 12=d 13=f 14=g 15=h 16=j 17=k 18=l 19=z
20=x 21=c 22=v 23=b 24=n 25=m 26=1 27=2 28=3 29=4
30=5 31=6 32=7 33=8 34=9 35=0 36=space 37=SHIFT 38=END 39=SYNC_A

SHIFT + digit: 1→! 2→@ 3→# 4→$ 5→% 6→^ 7→& 8→* 9→( 0→)
SHIFT SHIFT + digit: 1→, 2→. 3→/ 4→; 5→' 6→[ 7→] 8→\ 9→- 0→=

Text characters. A data symbol is its character. SHIFT followed by a letter gives the capital letter; SHIFT followed by a digit symbol gives 1 2 3 4 5 6 7 8 9 0 → ! @ # $ % ^ & * ( ); SHIFT SHIFT followed by a digit symbol gives 1 2 3 4 5 6 7 8 9 0 → , . / ; ' [ ] \ - =. SHIFT affects only the next symbol. A third SHIFT in a row, or SHIFT before a character with no shifted form, is invalid and is shown as ?. Characters that can be sent: letters (both cases), digits, space and ! @ # $ % ^ & * ( ) , . / ; ' [ ] \ - =. Line breaks and tabs are sent as a space; anything else is dropped by the sender.

File symbols. In a file body a symbol 0..31 is a 5-bit digit (base 32); see section 6.

3. Speed profiles and tone maps

Every profile has sym samples per symbol at 24000 Hz, a first tone f0 and a tone spacing df. Tone k is f0 + df·k Hz. The spacing is chosen so each tone has a whole number of cycles per symbol (df = 24000 / sym for every profile except the 50 Hz bank profile, which uses half of that).

profilesamples / symbolsymbol msf0 Hzdf Hzphysical tonestones / symbolbandsymbols / sfile bits / symbolfile bytes / ssymbols / 120 s burstSYNC symbols (+ / -)
fast160066.6755515401555..1140 Hz15.059.381800SYNC_A / SHIFT (39 / 37)
normal4800200.005505401550..745 Hz5.053.12600SYNC_A / SHIFT (39 / 37)
xfast60025.0056040152 (pair)560..1120 Hz40.0525.004800SYNC_A / SHIFT (39 / 37)
axfast60025.0056040152 (pair)560..1120 Hz40.0735.004800q / y (0 / 5)
xxfast24010.00600100152 (pair)600..2000 Hz100.0562.5012000SYNC_A / SHIFT (39 / 37)
xxxfast1606.67600150152 (pair)600..2700 Hz150.0593.7518000SYNC_A / SHIFT (39 / 37)
xxxfastn24010.0060050292 (pair)600..2000 Hz100.0675.0012000SYNC_A / SHIFT (39 / 37)
slow9600400.0012002.54011200..1298 Hz2.551.56300SYNC_A / SHIFT (39 / 37)
superslow19200800.0014001.254011400..1449 Hz1.250.78150SYNC_A / SHIFT (39 / 37)
slowest384001600.0016000.6254011600..1624 Hz0.650.3975SYNC_A / SHIFT (39 / 37)

Single-tone profiles (normal, fast, slow, superslow, slowest): symbol k is tone k, 40 tones.

Slow modes (v2.9). slow (400 ms symbols) and superslow (800 ms) are normal with longer symbols, on tones at 1200 and 1400 Hz so no other profile's decoder confuses them. They are for weak or noisy signals. The only difference in the receiver is the score of a tone (section 11): for these two profiles it is the tone's share of the energy of the profile's own 40 tones, instead of its share of all the audio in the window, so noise outside the 100 Hz or 50 Hz band no longer dilutes it. Everything else (frames, bursts, error correction, IDs, thresholds in symbols) is as for normal; the receiver gives up on a weak stretch after the same number of symbols (4 text, 25 file), not the same number of seconds.

Two-tone profiles: xfast, axfast, xxfast, xxxfast

All four share one pair map: 15 physical tones (index 0..14, f0 + df·i); symbol k is the pair (tone a, tone b) listed below, sounded together, each at half amplitude (so the peak equals a single tone's). The map is: all pairs (i, i+g) in the order g = 2 (i = 0..12), g = 3 (i = 0..11), g = 4 (i = 0..10), g = 5 (i = 0..9), taking the first 40; symbols 37, 38, 39 (SHIFT, END, SYNC_A) are (1,6), (2,7), (3,8).

xfast: tone index i → 560 + 40·i Hz (axfast = xfast's frequencies)

symchartone atone bHz aHz bsymchartone atone bHz aHz bsymchartone atone bHz aHz b
0q0256064014g1460072028337680840
1w1360068015h2564076029448720880
2e2464072016j3668080030559760920
3r3568076017k47720840316610800960
4t4672080018l587608803277118401000
5y5776084019z698009203388128801040
6u6880088020x7108409603499139201080
7i7984092021c811880100035010149601120
8o81088096022v912920104036space05560760
9p911920100023b1013960108037SHIFT16600800
10a1012960104024n11141000112038END27640840
11s11131000108025m0456072039SYNC_A38680880
12d12141040112026115600760
13f0356068027226640800

xxfast: tone index i → 600 + 100·i Hz

symchartone atone bHz aHz bsymchartone atone bHz aHz bsymchartone atone bHz aHz b
0q0260080014g147001000283379001300
1w1370090015h2580011002944810001400
2e24800100016j3690012003055911001500
3r35900110017k471000130031661012001600
4t461000120018l581100140032771113001700
5y571100130019z691200150033881214001800
6u681200140020x7101300160034991315001900
7i791300150021c81114001700350101416002000
8o8101400160022v9121500180036space056001100
9p9111500170023b10131600190037SHIFT167001200
10a10121600180024n11141700200038END278001300
11s11131700190025m04600100039SYNC_A389001400
12d121418002000261157001100
13f03600900272268001200

xxxfast: tone index i → 600 + 150·i Hz

symchartone atone bHz aHz bsymchartone atone bHz aHz bsymchartone atone bHz aHz b
0q0260090014g1475012002833710501650
1w13750105015h2590013502944812001800
2e24900120016j36105015003055913501950
3r351050135017k471200165031661015002100
4t461200150018l581350180032771116502250
5y571350165019z691500195033881218002400
6u681500180020x7101650210034991319502550
7i791650195021c81118002250350101421002700
8o8101800210022v9121950240036space056001350
9p9111950225023b10132100255037SHIFT167501500
10a10122100240024n11142250270038END279001650
11s11132250255025m04600120039SYNC_A3810501800
12d121424002700261157501350
13f036001050272269001500

Bank profile: xxxfastn ("Extra extra extra fast (new)")

29 physical tones, 600..2000 Hz in 50 Hz steps (index 0..28), 240 samples per symbol. Control symbols and text use the 40 pairs of the control table (symbols 0..31 are bank 0's pairs, 32..36 five more characters, 37..39 the specials). A file body symbol is a pair chosen from 64: bank 0 or 1 and a pair 0..31 within it; 6 bits = 5 bits for the pair index (low bits) and 1 bit for the bank (the high bit). The decoder treats every pair of either bank, and END, as candidates inside a file body.

Tone i is 600 + 50·i Hz, i = 0..28. Pairs (tone a, tone b):

Control table: symbols 0..39 (text, TYPE, PATH, SYNC, END)

symtone atone bHz aHz bsymtone atone bHz aHz bsymtone atone bHz aHz b
0 (q)48800100014 (g)616900140028 (3)6189001500
1 (w)12161200140015 (h)717950145029 (4)7199501550
2 (e)14181300150016 (j)8181000150030 (5)82010001600
3 (r)16201400160017 (k)9191050155031 (6)92110501650
4 (t)18221500170018 (l)10201100160032 (7)5238501750
5 (y)20241600180019 (z)11211150165033 (8)6249001800
6 (u)22261700190020 (x)12221200170034 (9)7259501850
7 (i)24281800200021 (c)14241300180035 (0)82610001900
8 (o)18241500180022 (v)16261400190036 (space)102811002000
9 (p)20261600190023 (b)18281500200037 (SHIFT)598501050
10 (a)22281700200024 (n)214700130038 (END)131712501450
11 (s)16241400180025 (m)315750135039 (SYNC_A)212516501850
12 (d)18261500190026 (1)4168001400
13 (f)20281600200027 (2)5178501450

File body, bank 0: pair index 0..31 (bank 0)

symtone atone bHz aHz bsymtone atone bHz aHz bsymtone atone bHz aHz b
04880010001116241400180022162614001900
11216120014001218261500190023182815002000
214181300150013202816002000242147001300
3162014001600146169001400253157501350
4182215001700157179501450264168001400
52024160018001681810001500275178501450
62226170019001791910501550286189001500
724281800200018102011001600297199501550
8182415001800191121115016503082010001600
9202616001900201222120017003192110501650
1022281700200021142413001800

File body, bank 1: pair index 0..31 (bank 1)

symtone atone bHz aHz bsymtone atone bHz aHz bsymtone atone bHz aHz b
0102211001700117219501650226229001700
11123115017501282210001700237239501750
212241200180013923105017502482410001800
3132512501850141024110018002592510501850
41426130019001511251150185026102611001900
51628140020001612261200190027122812002000
6216700140017142813002000281196501550
73177501450182187001500292207001600
84188001500193197501550303217501650
95198501550204208001600314228001700
106209001600215218501650

The control table's first 32 pairs are the same as bank 0's. The codebook was chosen by a deterministic greedy removal of the most look-alike pair (the program's _bank_codebook()); the lists above are the result, and are what matters.

Amplitude profile: axfast

Same tone pairs and 25 ms symbols as xfast, but with its own SYNC pattern (symbols 0 and 5 instead of 39 and 37) and, in a file body, two extra bits per symbol coded as the balance of the pair: the lower tone is louder or quieter than the upper by the amounts below. A file symbol is 7 bits: low 5 = pair index, high 2 = balance level. Text, TYPE, PATH and END use equal tones (balance -1: weight 1/2 each). Both tones together keep the same peak.

levellower tone vs upperweight of lower toneweight of upper tone
0+4.0 dB0.61310.3869
1+1.3 dB0.53730.4627
2-1.3 dB0.46270.5373
3-4.0 dB0.38690.6131

Every burst of an axfast transmission is: SYNC (11 symbols), then 40 calibration symbols (each of symbols 0..39 once, equal tones), then 8 more (symbol 6, the letter u, at balance level 0 twice, 1 twice, 2 twice, 3 twice), then the frame's remaining symbols. The receiver uses them to learn each pair's own balance offset and the level thresholds. axfast files were published as experimental: over the air, text worked and files failed on the one radio it was tried on.

4. Making a symbol (modulation)

symbol_audio[k][n] = mean over the symbol's tones t of  sin(2*pi * f_t * n / 24000),   n = 0 .. sym-1
(two-tone: the average of its two sines;  axfast file body: w_low*sin(f_low) + w_high*sin(f_high),
 r = 10^(LEVEL_DB[level]/20), w_low = r/(1+r), w_high = 1/(1+r))

5. Frames

plain frame:     SYNC(11)  TYPE  [PATH(8)]  body  [CRC]  END
extended frame:  SYNC(11)  x  TYPE  BURST  FLAG  [PATH(8)]  body  [CRC]  END

6. Bodies

Text (t, s)

One symbol per character (SHIFT forms as in section 2), then a CRC-8 as two base-37 digits (high digit first; each digit is a data symbol 0..36). CRC-8 polynomial 0x07, initial value 0, no reflection, no final XOR, computed with one "byte" per symbol value (0..37) over: TYPE, PATH symbols (if any) and the body symbols, in order. (In an extended frame the three header symbols after x are part of the checksummed head as well; see section 7.)

crc = 0
for v in values:           # symbol values, not characters
    crc ^= v
    repeat 8:  crc = ((crc << 1) ^ 0x07) & 0xFF  if crc & 0x80  else  (crc << 1) & 0xFF
send  crc // 37,  crc % 37

Files (f, d)

Payload bytes: [name length: 1 byte][name: UTF-8, at most 255 bytes][size: 4 bytes, big endian][content] [CRC-32: 4 bytes, big endian]. The file name is reduced to its base name with characters < > : " / \ | ? * and control characters replaced by _, leading/trailing spaces and dots removed, at most 150 characters. The CRC-32 is the standard zlib/PNG CRC-32 and covers [extended-header symbols and PATH symbols, as bytes] + name length + name + size + content. (For a plain non-server frame that prefix is empty.)

The payload is sent MSB first as 5-bit digits, one per symbol using symbols 0..31, the last digit zero-padded. The receiver knows the number of symbols from the size field, so no padding marker is needed: tones = ceil((9 + name_len + size) * 8 / bits). The 7-bit (axfast) and 6-bit (xxxfastn) profiles cut the stream into 7 or 6 bit groups; the low 5 bits of a group are the symbol 0..31 and the high 1 or 2 bits are the balance level or the bank.

There is no size limit other than the 4-byte size field (4 GB). Each profile has a usual maximum (10,000 bytes at normal and fast, 100,000 bytes for the others); larger files are still sent and received, with a warning, and receivers older than v2.3 refuse them.

Station-ID frame (s with path idstart, idend or idalone)

New in v2.7. An ordinary server text frame whose PATH is one of idstart (sent at the beginning of a transmission), idend (at the end) or idalone (an ID sent by itself), and whose text is the sender's callsign in lower case (letters, digits and /; at most 15 characters), optionally followed by one space and the sender's location as a grid square, the Maidenhead locator (below). A receiver shows "<CALLSIGN> is sending you a message" for idstart. Because it is an ordinary frame, every receiver since v1.4 decodes it as a message with that path; a v2.7 receiver recognises the path and treats it as a station ID instead of a message. See section 10.

Location. The text is callsign or callsign locator, e.g. ke9gdp fn31pr21. The locator is the amateur-radio Maidenhead grid square: 4 characters (FN31, about 100 x 150 km), 6 (FN31pr, about 5 x 7 km) or 8 (FN31pr21, about 0.5 x 0.7 km, "neighbourhood"; the default). Characters: field = two letters A..R (20° of longitude x 10° of latitude), square = two digits (2° x 1°), subsquare = two letters a..x (5' x 2.5'), extended square = two digits (30" x 15"). Longitude letter/digit first, then latitude. Sent in lower case; receivers read it case-insensitively. A position is never sent unless its owner has switched sending on and entered a location, and only the cell (never exact coordinates) is sent. The reference computation:

x = (lon + 180) / 360 * 18;  y = (lat + 90) / 180 * 18
field     = letter(A + int(x)), letter(A + int(y))
x = (x - int(x)) * 10;  y = (y - int(y)) * 10       square     = int(x), int(y)
x = (x - int(x)) * 24;  y = (y - int(y)) * 24       subsquare  = letter(a + int(x)), letter(a + int(y))
x = (x - int(x)) * 10;  y = (y - int(y)) * 10       extended   = int(x), int(y)

The centre of the cell is the position a receiver draws. A receiver ignores a second word that is not a valid 4-, 6- or 8-character locator.

Direct-message frame (s or d with path dm + address)

New in v2.8. An ordinary text or file frame, sent as a server-type frame (s text, d file, plain or extended, with or without error correction), whose PATH is the two characters dm followed by a 6-character address of the station it is meant for. The address is not a secret and the content is not encrypted or obscured in any way: it is an addressing label, like the target ID of a private call on other digital voice systems. Everyone can decode every frame; a receiver program simply does not display (or offer to save) a direct message that is not addressed to its own operator.

cs      = callsign, upper case, all white space removed (e.g. "KE9GDP", "W1AW/P")
n       = CRC-32(UTF-8 bytes of cs)  mod  36^6          (CRC-32 as in zlib / IEEE 802.3)
address = the 6 base-36 digits of n, most significant first, digit alphabet "0123456789abcdefghijklmnopqrstuvwxyz"
PATH    = "dm" + address

The match is exact: W1AW and W1AW/P are different addresses. A receiver compares the PATH with the address of its own callsign: equal = for this station (shown, saved), different = for somebody else (hidden; it may say that a direct message for somebody else was heard), no dm path = a public message. Receivers older than v2.8 treat the frame as a server message and show it with its path; nothing else about the frame differs.

Compressed message or file (f/d frame whose name ends in .csvz)

New in v3.0. Compression is an application-level convention inside an ordinary file frame: nothing in the frame, its checksums, the bursts or the error correction changes. A compressed file is sent under its real name plus the suffix .csvz (a compressed text message under the name m.csvz), and its content is

KIND   1 byte   0 = a file (the receiver removes ".csvz" from the name), 1 = a text message (ASCII, as it would
                have been sent as text)
METHOD 1 byte   0 stored, 1 deflate, 2 deflate with the dictionary below, 3 bzip2, 4 LZMA2
data            the compressed bytes

Deflate is the raw deflate stream (RFC 1951, no zlib header or checksum; the file frame's CRC-32 covers it). Method 2 is the same with the preset dictionary below (zlib zdict; 32 KiB window). Method 3 is a normal bzip2 stream (level 9). Method 4 is a raw LZMA2 stream (FORMAT_RAW, preset 9, dictionary size 1 MiB). A sender uses the container only when the frame becomes shorter; otherwise it sends the original. A receiver that gets a file whose CRC-32 is good and whose name ends in .csvz and whose content decodes as above gives the user the original file or message (and may say how many bytes were sent); if it does not decode, it is an ordinary file with that name. A receiver that does not know this convention (before v3.0) simply saves a file called name.csvz. The unpacked size is limited to 256 MiB.

Lossy modes are not part of the format: the sender replaces the text or file by a smaller simplified one of the same kind (for example a smaller image, or text without vowels) and then sends that, with or without the container above. The receiver cannot tell and needs nothing.

Method 2 dictionary (the bytes, ASCII, one line):

the and that have for not with you this but his from they say her she will one all would there their what out if about who get which go me when make can like time no just him know take people into year your good some could them see other than then now look only come its over think also back after use two how our work first well way even new want because any these give day most us is are was were been being has had do does did am can may might must shall should where why here today tonight tomorrow yesterday morning afternoon evening hello thanks thank you please sorry okay yes message file received sent signal report weather radio station antenna power watts frequency band mhz khz repeater simplex copy roger over out cq de qth qsl qso qrz qrm qrn qsb qrp qrt qrv qsy 73 88 best regards good morning evening night how are you doing fine rst 599 59 name location county state grid locator callsign operator rig contact contest net check in checking schedule meeting tonight at pm am utc local time date home portable mobile base tower hill mountain river lake road street city town north south east west left right up down high low strong weak clear noisy static fade fading rough conditions test testing this is a test of the message from to on in of it be as by or an at so we he my up do go no ke9gdp w1aw n0call 

7. Extended frames and scrambling

Sent only when needed: the radio's time-out is not the 180 s default (the bursts are then sized to fit it), the body has a run of more than 40 identical symbols (it is scrambled), or error correction is on. SYNC x TYPE BURST FLAG [PATH] body END:

Scrambling (whitening, not secrecy)

During a long run of one tone a receiver's clock tracker has nothing to lock on and drifts, so a body with more than 40 equal symbols in a row is whitened with a fixed sequence both ends know. Body symbol i (counting from the first body symbol) is replaced by (s + K[i]) mod M, M = 32 for files and error-corrected bodies, 38 for plain text (data symbols and SHIFT); symbols outside 0..M-1 (END, unreadable) are left alone. The receiver subtracts the same K[i]. K is xorshift32 with seed 0x2545F491:

x = 0x2545F491
for i in 0, 1, 2, ...:
    x ^= (x << 13) & 0xFFFFFFFF
    x ^= x >> 17
    x ^= (x << 5) & 0xFFFFFFFF
    K[i] = x >> 8

First values: K[0..15] = 14754998 9149044 6611379 5958 15904187 16701265 9081936 1209626 9852735 4647537 1299178 10335439 9310030 10372034 2525782 15196658

8. Error correction (Reed-Solomon)

Optional, per transmission; levels 0..4 (Off, Low, Medium, High, Maximum) = 0, 16, 32, 64, 128 parity bytes per block. A frame with level > 0 is always an extended frame with the level in FLAG. Older receivers (before v2.4) accept only 0 or 1 in FLAG and report such a frame as lost.

Cost for a long message: Low +7%, Medium +14%, High +34%, Maximum +101%; a short text pays the length codeword and one block's parity.

9. The transmission: bursts, key tone, IDs, end tone

A burst is the profile's SYNC (and, for axfast, the 48 calibration symbols) followed by up to every frame symbols; the last burst ends with END. A transmission, in order:

  1. Lead-in: silence, 0.5 s by default (the user's PTT delay; 0 to 5 s).
  2. KEY tone: a steady 400 Hz sine, 5 s by default (0 = none; 0..15 s), full level, 20 ms raised-cosine fades. It is below every data tone so it is never decoded; a VOX radio clips this instead of the SYNC.
  3. Station ID (if the mechanics call for one at the start, section 10), then 0.25 s of silence.
  4. Burst 1. Then for each further burst: silence (the pause, 6 s by default, 0..30 s, so the radio un-keys and its time-out timer resets), a KEY tone again, a spoken/Morse ID if one is due (below) and 0.25 s of silence, then the burst.
  5. After the last burst, if the website announcement is due: 0.25 s of silence then website.wav played 1.5x faster at the same pitch (WSOLA). Receivers ignore it, like all speech.
  6. 0.25 s of silence and the closing station ID (if the mechanics call for one at the end).
  7. For a client talking to a server: 0.25 s of silence then a steady 450 Hz tone, 2 s, full level (section 12).
  8. 0.3 s of silence (tail).

Time-out off (BURST code 37). The BURST tone may also be 37 (the SHIFT symbol, outside the 36 codes above): the sender's radio has no time-out, and bursts are 480 s of tones long (the longest that still leaves a pause for a callsign inside the ten-minute window). A receiver that does not know code 37 reports the frame as lost. Because that tone is part of the checksummed head, checksum code treats a header symbol of 37 as valid.

Bursts and the radio time-out. A plain frame is cut into 120 s bursts. With another time-out setting T (30..600 s) the sender uses the longest burst length (5..185 s in 5 s steps) for which the longest keyed stretch (everything between two pauses, from the start of the lead-in) is at most T - 5 seconds, and sends the length in the extended header. The receiver knows where pauses fall: after every every symbols with no END coming it stops reading, waits up to 90 s for the next SYNC (matching the same profile), and continues the same frame (symbol counting carries on).

Mid-transmission IDs. At a pause, an ID (spoken, or Morse if selected) is inserted when the time since the last ID plus the next burst (plus, for the last burst, the website clip and closing gaps) would exceed 540 s. A frame is never interrupted by a data ID frame: a second SYNC there would be taken for the next burst.

10. Station identification

The program offers two mechanics; which one a station uses is its operator's setting. Receivers ignore the choice and decode both.

New mechanics (default from v2.7)

A data ID frame (section 6) carrying the station's callsign is sent in the speed of the transmission at the beginning (idstart, after the KEY tone) and again at the end (idend, after the website clip, before the end tone) of every transmission. An ID sent on its own (the Send callsign button) is an idalone frame in the Fast profile. If more than 540 s would pass between IDs inside one long transmission, the callsign is also spoken (or sent in Morse) at the pause between bursts, as in the legacy mechanics. A client shows "<CALLSIGN> is sending you a message" when it hears an idstart and "has finished sending" at an idend. The callsign is up to 15 characters of letters, digits and /.

Location and the map. The program's Location setting (on by default; nothing is sent until a location has been entered) adds the sender's neighbourhood locator after the callsign in these frames. It goes out over the radio, so anyone who can receive the signal can read it; the program does not send it to the website. A receiver remembers stations heard with a location and its Map tab draws a red dot for the operator and a blue dot, with a line, for each of them. Who may use or share what they receive is up to them; the location is exactly what is in the frame, no more.

Legacy mechanics (v2.6 and earlier, still available)

The callsign is spoken (one pre-recorded clip per character: a.wav..z.wav are the phonetic words alpha..zulu, 0.wav..9.wav digits, 0.15 s between characters, silence trimmed) or sent as Morse code. In the desktop window the operator presses Send callsign (a 10-minute timer flashes from 9:30) and one goes out where waiting any longer would pass 10 minutes; the unattended servers speak it at the start and end of every reply. Morse: International Morse, PARIS timing, 20 words per minute (one unit = 1.2/20 s = 60 ms; dot 1 unit, dash 3, 1 between elements, 3 between characters), a sine at 2400 Hz, 5 ms fades. Table:

a .-   b -...   c -.-.   d -..   e .   f ..-.   g --.   h ....   i ..   j .---   k -.-   l .-..   m --   n -.   o ---   p .--.   q --.-   r .-.   s ...   t -   u ..-   v ...-   w .--   x -..-   y -.--   z --..   0 -----   1 .----   2 ..---   3 ...--   4 ....-   5 .....   6 -....   7 --...   8 ---..   9 ----.   / -..-.

11. Receiving

This is a description of the reference receiver; any receiver that makes the same decisions interoperates. The reference runs one decoder per profile on the same audio and merges what they report.

11.1 Features

For a profile with sym samples per symbol, every hop = sym / 40 samples compute, over the last sym samples x and for each physical tone t at frequency f_t:

c = sum x[n] cos(2 pi f_t n / 24000)      s = sum x[n] sin(2 pi f_t n / 24000)      e = sum x[n]^2
frac_t = (c^2 + s^2) / ( (sym/2) * e + 0.0001 )         # fraction of the window's energy in tone t; ~1 for a clean tone
score(symbol) = frac_t                                  # single-tone profiles
score(symbol) = 2 * sqrt(frac_a * frac_b)               # two-tone profiles (a, b the symbol's pair)

Loudness does not matter: only the energy fraction, so speech and noise score low. This gives, per hop, a vector of 40 scores (the bank profile has 40 control columns plus 32 for each extra bank).

11.2 Finding a frame

11.3 Deciding symbols

11.4 Clock tracking

The sampling grid is adjusted by one hop when the evidence adds up: for a clean symbol (score at least 0.6) that sits between two different symbols, accumulate score[next hop] - score[previous hop] of its tone; when the accumulated value reaches ±0.2, move the grid one hop (later if positive, earlier if negative) and clear the accumulator. That absorbs sound-card clock differences of at least ±1000 ppm; it is why long runs are scrambled (a steady tone has nothing to track on).

11.5 Burst pauses

When symbols_read is a multiple of the frame's every and the next symbol is not END (score of END under 0.5), the sender has paused: stop reading and search for the profile's SYNC again; on finding it, continue with the next symbol of the same frame. Give up (frame lost) after 90 s without it.

11.6 Frame end, checks and events

11.7 Body refinement (bank and XXXFAST profiles)

For xxxfast and xxxfastn file and error-corrected bodies, once the whole body has been read the receiver re-decides it: for each body symbol keep the three hop rows around the live sampling point; repeat 5 passes of "pick, per symbol, the (hop, pair) with the highest score divided by that pair's own current average; recompute each pair's average over the symbols assigned to it" (a pair needs at least 3 symbols to have its own average, otherwise the median is used); a symbol whose score is under 0.15 of its pair's average stays unreadable. It needs at least 300 body symbols per 32 pairs of candidates.

11.8 Amplitude profile

For axfast the receiver also measures each tone's amplitude through a Hann window (w = 0.5 - 0.5 cos(2 pi n / sym), scaled so a steady tone reads its true amplitude). Calibration: l0[s] = ln(amp_low / amp_high) for each of the 40 equal-tone symbols; the ladder gives the four level offsets d[j] = mean ln(amp_low/amp_high) at level j - l0[6]; require d0 > d1 > 0.05, d2 < -0.05 and d2 > d3, otherwise use the nominal LEVEL_DB·ln(10)/20. Threshold T = (d0 - d3 + d1 - d2)/4. A body symbol's level: x = ln(amp_low/amp_high) - l0[symbol]; x >= 0: level 0 if x > T else 1; x < 0: level 3 if -x > T else 2. The clock tracker uses the symbol's own amplitude in this mode. The "weak symbol" minimum is multiplied by 0.3.

11.9 Arbitration between profiles

Every profile's decoder sees every sample, and at a high sensitivity another speed's decoder can false-sync on a frame's data or on the speech around it. The reference therefore: treats as primary the receiving decoder with at least 16 non-weak symbols and the best average score (a real frame averages about 1); while one exists, passes on only a file whose CRC-32 passed, or a message whose CRC-8 passed, that is about as substantial and whose symbols averaged at least 0.25; drops any failed frame (lost, garbled, bad file) whose symbols averaged under 0.2 (speech or noise), or under 0.5 in the 30 s after an intact frame; and when a frame finishes intact, resets the other decoders' half-done frames. None of this is needed on a quiet channel at the default sensitivity.

11.10 End-of-transmission tone detector

Separately from the frames: every 0.1 s measure the energy fraction (same formula as 11.1) at 450 Hz over the last 0.5 s; after 12 consecutive windows at or over the sync threshold, report the tone once; re-arm when it stops.

12. Servers

A server is an unattended station that answers requests. A client sends a server text (s) or server file (d) frame whose PATH names what it wants (ping, document, chatgpt, ...), followed by the 450 Hz end tone. The server decodes the frame, waits for the end tone (it means "I have finished, your turn"; if none comes within 30 s the server answers anyway), waits about a second, and answers with an ordinary transmission (text or file, with its own IDs). A server ignores ID frames (idstart, idend, idalone) and plain messages without a path are answered only if they equal its configured trigger. A server must be half duplex: deaf while it transmits and for 1.5 s after. The built-in paths are examples; the path namespace and reply contents are up to each server.

Server 2.0 (v3.2): connections. A TCP-style transport between two stations, carried in ordinary server file frames (d) whose PATH is tc + the 6 base-36 characters of the destination callsign (computed exactly as for a direct message: CRC-32 of the upper-case callsign mod 366), file name t, and the file content a segment: [flags 1][conn 1][port 1][seq 4][ack 4][src len 1] [src callsign][payload] (numbers big-endian). Flags: SYN 1, ACK 2, FIN 4, RST 8. Sequence numbers count bytes (SYN and FIN each use one), are 32 bit, and the first one is random; ack is the next byte expected. Open: SYN, SYN+ACK, ACK. Data and acknowledgements ride together. Close: FIN, ACK in each direction. One segment is in flight at a time (stop and wait, because every transmission costs a key-up, an ID and a SYNC); an unacknowledged segment is repeated when its timer runs out (the airtime of the segment, plus the airtime of an acknowledgement, plus 20 s for the peer's end tone and key-up; doubled on each repeat up to 8 times), up to a set number of tries. The frame's CRC-32 is the checksum: a damaged frame is dropped and repaired by the repeat. A segment is sent with the end tone, and the peer answers after hearing it. A segment for another callsign is ignored. A station that is not accepting connections sends nothing. RST answers a SYN to a closed port or a segment of an unknown connection (only from a station that accepts connections or has one open). A station has one connection at a time, so a SYN for a new connection id (and port) from a station that still has an open connection with you replaces that old connection (drop it silently; v3.6.2: over the air a SYN+ACK was lost and the retry was refused); a SYN from a different station is refused with RST while a connection is open. The first transmission of a SYN+ACK is not a repeat. Test: tests/test_tcp.py. Status: simulation only; not yet tried over the air.

13. Test vectors

Symbols are shown as numbers (the alphabet table of section 2). SYNC is 39 39 39 37 37 37 39 37 37 39 37 in every frame below and is omitted from the lists. If your implementation produces these symbol sequences, and its audio decodes with another implementation, the framing is right.

Text "Hi!"

t frame: TYPE, 'H' (SHIFT h), i, '!' (SHIFT 1), CRC-8 as two base-37 digits, END.

4 37 15 7 37 26 1 27 38

CRC-8 over TYPE and body = 64.

Server text "ping" to path "ping"

s frame: TYPE, PATH ping (8 symbols), body, CRC-8, END.

11 9 7 24 14 36 36 36 36 9 7 24 14 0 13 38

Punctuation "a,b"

, is SHIFT SHIFT then the digit symbol for 1.

4 10 37 37 26 23 5 17 38

File "a.txt" containing "hello"

f frame: base-32 digits of 05 'a.txt' 00000005 'hello' CRC-32.

13 0 21 16 18 28 29 3 24 14 16 0 0 0 0 0 5 13 1 18 22 24 27 3 15 16 3 23 7 17 0 8 38

payload CRC-32 = 80EE7881

Station ID, start (callsign KE9GDP)

s frame with PATH idstart.

11 7 12 11 4 10 3 4 36 17 2 34 14 12 9 4 33 38

Station ID with a location (KE9GDP, locator FN31pr21)

s frame with PATH idstart; the text is ke9gdp fn31pr21 (the space is symbol 36).

11 7 12 11 4 10 3 4 36 17 2 34 14 12 9 36 13 24 28 26 9 3 27 26 1 12 38

41.714775, -72.727260 is in FN31pr21 (8 characters); in 6 characters FN31pr, in 4 FN31.

Station ID, end (callsign W1AW/P)

s frame with PATH idend; / is SHIFT SHIFT then the digit symbol for 3.

11 7 12 2 24 12 36 36 36 1 26 10 1 37 37 28 9 0 26 38

Direct message "hi" to KE9GDP

s frame with PATH dmiszlim (dm + the address of KE9GDP).

11 12 25 7 11 19 18 7 25 15 7 5 7 38

CRC-32("KE9GDP") = 43C67BAE, mod 36^6 = 1137081262.

Extended text frame, burst code 23 (120 s), no scramble, time-out 180 s

x TYPE BURST FLAG ... (flag 0).

20 4 23 0 37 15 7 37 26 0 14 38

Error corrected text (Low, level 1)

x t BURST FLAG with FLAG = 2, then the Reed-Solomon stream as base-32 digits.

20 4 23 2 0 0 0 0 0 2 5 11 11 2 25 10 26 10 24 1 1 27 0 9 9 28 15 5 13 13 25 7 26 18 18 3 9 21 20 4 15 11 17 11 7 21 25 28 9 15 13 9 4 5 7 22 21 11 21 12 18 8 38

Reed-Solomon vectors

encode_header(5, parity=8) = 00000005242708ee7423cf78
encode(b"CSVF2", nsym=16) = 4353564632c870bce93835d828155af21fcc2b3750 (21 bytes: the 5 data bytes then 16 parity bytes)
GF exp table alpha^0..alpha^15 = 1 2 4 8 16 32 64 128 29 58 116 232 205 135 19 38

The 0x11D field; alpha^8 = 29 because x^8 = x^4+x^3+x^2+1.

Sample counts

One fast symbol is 1600 samples (66.67 ms); a text frame of n symbols (SYNC and END included) lasts n * 1600 / 24000 s at fast, n * 4800 / 24000 s at normal, n * 600 / 24000 at xfast, n * 240 / 24000 at xxfast.

14. Versions and compatibility

Program versionWhat it added to the on-air format
v1.0Text and file frames, NORMAL and FAST, SHIFT, CRC-8/CRC-32, SYNC, bursts
v1.1-1.2120 s bursts with pauses, 400 Hz key tone
v1.4Server types s / d with an 8-symbol PATH, 450 Hz end tone
v1.9XFAST (two simultaneous tones per symbol)
v2.0Extended frames (burst code, scramble flag), radio time-out setting
v2.1Morse ID (at 1500 Hz; 2400 Hz since v2.3)
v2.3AXFAST (experimental for files), second shift layer, no hard file-size limit
v2.4Reed-Solomon error correction (flag 0..9), XXFAST
v2.6XXXFAST, XXXFAST (new, bank profile), receiver restart on a new SYNC
v2.7Station-ID frames (idstart/idend/idalone paths), "new" callsign mechanics, an optional Maidenhead locator after the callsign; no change to anything else
v2.8Direct messages (dm + 6-character address PATH on s / d frames); no change to the frame format itself
v2.9Profiles slow and superslow (band-scored tones); older receivers do not hear them
v3.0Profile slowest (1600 ms symbols); compressed messages and files (the .csvz container, section 6); the station ID of "Send callsign" follows the chosen speed
v3.1BURST code 37 = radio time-out off (480 s bursts); older receivers report such a frame as lost
v3.2Server 2.0: TCP-style connections (tc PATH, section 12); older receivers show a segment as a received file
v3.3No change to frames, speeds or paths: ke9gdp.com accounts, Feedback chat and email inbox are website features (see the privacy policy)

A receiver ignores audio it cannot decode (no output, never a false file: files carry a CRC-32). Older receivers report frames from newer versions as lost or not at all.

15. Constants

namevalue
FS24000
HOPS_PER_SYM40
BURST_S120
ID_MAX_GAP_S540
BREAK_TIMEOUT_S90
KEY_TONE_HZ400.0
END_TONE_HZ450.0
END_TONE_S2.0
END_TONE_GAP_S0.25
DEFAULT_KEY_S5.0
DEFAULT_PAUSE_S6.0
MAX_PAUSE_S30.0
DEFAULT_TOT_S180
MIN_TOT_S30
MAX_TOT_S600
TOT_MARGIN_S5
BURST_STEP_S5
BURST_CODES37
EXT_LEN3
RUN_LIMIT40
PATH_LEN8
FILE_TONES32
FORMAT_MAX_BYTES4294967295
WEBSITE_SPEED1.5
WEBSITE_EVERY10
WORD_GAP_S0.15
MORSE_TONE_HZ2400.0
MORSE_WPM20
VOICE_TO_TONES_S0.25
TONES_TO_VOICE_S0.25
TAIL_S0.3
ID_MAX_CHARS15
DM_PREFIXdm
DM_ADDR_LEN6
FEC_PARITY(0, 16, 32, 64, 128)
BARKER11(1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1)
Decoder.SYNC_THRESHOLD0.55
Decoder.SYMBOL_MIN0.2
Decoder.LOST_AFTER_S{'text': 0.8, 'file': 5.0}
Decoder.TRACK_MIN_PEAK / TRACK_THRESHOLD0.6 / 0.2
EndToneDetector WINDOW / HOP / NEED12000 / 2400 / 12
ID_PATHS{'idstart': 'start', 'idend': 'end', 'idalone': 'alone'}

This specification is generated from the program source by tools/build_protocol_doc.py for each release. If you find a difference between it and the program, the program is the reference; please report it using the Feedback button in the program.

← back to CSVF2   ← KE9GDP