Experiments in PSK-31 Synthesis
PSK-31 is a narrow-bandwidth digital mode which encodes text as an audio tone that varies phase at a known rate. To learn more about this digital mode and solve a challenging programming problem, I’m going to write a PSK-31 encoder and decoder from scratch using the C# programming language. All code created for this project is open-source, available from my PSK Experiments GitHub Repository, and released under the permissive MIT license. This page documents my progress and notes things I learn along the way.
Encoding Bits as Phase Shifts
-
PSK31 messages have a continuous carrier tone
-
Symbols are represented by “symbols”, each 1/31.25 seconds long
-
If a symbol changes phase from its previous symbol it is a
0
, otherwise it is a1
Amplitude Modulation Silences Phase Transitions
Although a continuous phase-shifting tone of constant amplitude can successfully transmit PSK31 data, the abrupt phase transitions will cause splatter. If you transmit this you will be heard, but those trying to communicate using adjacent frequencies will be highly disappointed.
Hard phase transitions (splatter) | Soft phase transitions (cleaner) |
---|---|
![]() |
![]() |
To reduce spectral artifacts that result from abruptly changing phase, phase transitions are silenced by shaping the waveform envelope as a sine wave so it is silent at the transition. This way the maximum rate of phase shifts is a sine wave with a period of half the baud rate. This is why the opening of a PSK31 message (a series of logical 0
bits) sounds like two tones: It’s the carrier sine wave with an envelope shaped like a sine wave with a period of 31.25/2 Hz. These two tones separated by approximately 15 Hz are visible in the spectrogram (waterfall).
Encoding Text as Bits
Unlike ASCII (8 bits per character) and RTTY (5 bits per character), BPSK uses Varicode (1-10 bits per character) to encode text. Consecutive zeros 00
separate characters, so character codes must not contain 00
and they must start ane end with a 1
bit. Messages are flanked by a preamble (repeated 0
bits) and a postamble (repeated 1
bits).
NUL 1010101011
SOH 1011011011 STX 1011101101 ETX 1101110111 EOT 1011101011 ENQ 1101011111 ACK 1011101111 BEL 1011111101 BS 1011111111 HT 11101111 LF 11101 VT 1101101111 FF 1011011101 CR 11111 SO 1101110101 SI 1110101011 DLE 1011110111 DC1 1011110101 DC2 1110101101 DC3 1110101111 DC4 1101011011 NAK 1101101011 SYN 1101101101 ETB 1101010111 CAN 1101111011 EM 1101111101 SUB 1110110111 ESC 1101010101 FS 1101011101 GS 1110111011 RS 1011111011 US 1101111111 SP 1 ! 111111111 " 101011111 # 111110101 $ 111011011 % 1011010101 & 1010111011 ' 101111111 ( 11111011 ) 11110111 * 101101111 |
+ 111011111
, 1110101 - 110101 . 1010111 / 110101111 0 10110111 1 10111101 2 11101101 3 11111111 4 101110111 5 101011011 6 101101011 7 110101101 8 110101011 9 110110111 : 11110101 ; 110111101 < 111101101 = 1010101 > 111010111 ? 1010101111 @ 1010111101 A 1111101 B 11101011 C 10101101 D 10110101 E 1110111 F 11011011 G 11111101 H 101010101 I 1111111 J 111111101 K 101111101 L 11010111 M 10111011 N 11011101 O 10101011 P 11010101 Q 111011101 R 10101111 S 1101111 T 1101101 U 101010111 |
V 110110101
X 101011101 Y 101110101 Z 101111011 [ 1010101101 \ 111110111 ] 111101111 ^ 111111011 _ 1010111111 . 101101101 / 1011011111 a 1011 b 1011111 c 101111 d 101101 e 11 f 111101 g 1011011 h 101011 i 1101 j 111101011 k 10111111 l 11011 m 111011 n 1111 o 111 p 111111 q 110111111 r 10101 s 10111 t 101 u 110111 v 1111011 w 1101011 x 11011111 y 1011101 z 111010101 { 1010110111 | 110111011 } 1010110101 ~ 1011010111 DEL 1110110101 |
How to Generate a PSK Waveform
Now that we’ve covered the major steps of PSK31 message composition and modulation, let’s go through the steps ot generate a PSK31 message in code.
Step 1: Convert a Message to Varicode
Here’s the gist of how I store my varicode table in code. Note that the struct
has an additional Description
field which is useful for decoding and debugging.
public struct VaricodeSymbol
{
public readonly string Symbol;
public string BitString;
public int[] Bits;
public readonly string Description;
public VaricodeSymbol(string symbol, string bitString, string? description = null)
{
Symbol = symbol;
BitString = bitString;
Bits = bitString.ToCharArray().Select(x => x == '1' ? 1 : 0).ToArray();
Description = description ?? string.Empty;
}
}
static VaricodeSymbol[] GetAllSymbols() => new VaricodeSymbol[]
{
new("NUL", "1010101011", "Null character"),
new("LF", "11101", "Line feed"),
new("CR", "11111", "Carriage return"),
new("SP", "1", "Space"),
new("a", "1011"),
new("b", "1011111"),
new("c", "101111"),
// etc...
};
I won’t show how I do the message-to-varicode lookup, but it’s trivial. Here’s the final function I use to generate Varicode bits from a string:
static int[] GetVaricodeBits(string message)
{
List<int> bits = new();
// add a preamble of repeated zeros
for (int i=0; i<20; i++)
bits.Add(0);
// encode each character of a message
foreach (char character in message)
{
VaricodeSymbol symbol = Lookup(character);
bits.AddRange(symbol.Bits);
bits.AddRange(CharacterSeparator);
}
// add a postamble of repeated ones
for (int i=0; i<20; i++)
bits.Add(1);
return bits.ToArray();
}
Step 2: Determine Phase Shifts
Now that we have our Varicode bits, we need to generate an array to indicate phase transitions. A transition occurs every time a bit changes value form the previous bit. This code returns phase as an array of double
given the bits from a Varicode message.
public static double[] GetPhaseShifts(int[] bits, double phase1 = 0, double phase2 = Math.PI)
{
double[] phases = new double[bits.Length];
for (int i = 0; i < bits.Length; i++)
{
double previousPhase = i > 0 ? phases[i - 1] : phase1;
double oppositePhase = previousPhase == phase1 ? phase2 : phase1;
phases[i] = bits[i] == 1 ? previousPhase : oppositePhase;
}
return phases;
}
Step 3: Generate the Waveform
These constants will be used to define the shape of the waveform:
public const int SampleRate = 8000;
public const double Frequency = 1000;
public const double BaudRate = 31.25;
This minimal code generates a decipherable PSK-31 message, but it does not silence the phase transitions so it produces a lot of splatter. This function must be refined to shape the waveform such that phase transitions are silenced.
public double[] GetWaveformBPSK(double[] phases)
{
int totalSamples = (int)(phases.Length * SampleRate / BaudRate);
double[] wave = new double[totalSamples];
for (int i = 0; i < wave.Length; i++)
{
double time = (double)i / SampleRate;
int frame = (int)(time * BaudRate);
double phaseShift = phases[frame];
wave[i] = Math.Cos(2 * Math.PI * Frequency * time + phaseShift);
}
return wave;
}
Step 4: Generate the Waveform with Amplitude Modulation
This is the same function as above, but with extra logic for amplitude-modulating the waveform in the shape of a sine wave to silence phase transitions.
public double[] GetWaveformBPSK(double[] phases)
{
int baudSamples = (int)(SampleRate / BaudRate);
double samplesPerBit = SampleRate / BaudRate;
int totalSamples = (int)(phases.Length * SampleRate / BaudRate);
double[] wave = new double[totalSamples];
// create the amplitude envelope sized for a single bit
double[] envelope = new double[(int)samplesPerBit];
for (int i = 0; i < envelope.Length; i++)
envelope[i] = Math.Sin((i + .5) * Math.PI / envelope.Length);
for (int i = 0; i < wave.Length; i++)
{
// phase modulated carrier
double time = (double)i / SampleRate;
int frame = (int)(time * BaudRate);
double phaseShift = phases[frame];
wave[i] = Math.Cos(2 * Math.PI * Frequency * time + phaseShift);
// envelope at phase transitions
int firstSample = (int)(frame * SampleRate / BaudRate);
int distanceFromFrameStart = i - firstSample;
int distanceFromFrameEnd = baudSamples - distanceFromFrameStart + 1;
bool isFirstHalfOfFrame = distanceFromFrameStart < distanceFromFrameEnd;
bool samePhaseAsLast = frame == 0 ? false : phases[frame - 1] == phases[frame];
bool samePhaseAsNext = frame == phases.Length - 1 ? false : phases[frame + 1] == phases[frame];
bool rampUp = isFirstHalfOfFrame && !samePhaseAsLast;
bool rampDown = !isFirstHalfOfFrame && !samePhaseAsNext;
if (rampUp)
wave[i] *= envelope[distanceFromFrameStart];
if (rampDown)
wave[i] *= envelope[distanceFromFrameEnd];
}
return wave;
}
PSK31 Encoder Program
I wrapped the functionality above in a Windows Forms GUI that allows the user to type a message, specify frequency, baud rate, and whether or not to refine the envelope to reduce splatter, then either play or save the result. An interactive ScottPlot Chart allows the user to inspect the waveform.
-
Download PSK31 Encoder: PSK31-encoder.zip
-
PSK31 Encoder Source Code: PSK Experiments on GitHub
Sample PSK-31 Transmissions
These audio files encode the text The Quick Brown Fox Jumped Over The Lazy Dog 1234567890 Times! in 1kHz BPSK at various baud rates.
- PSK-31: dog31.wav
- PSK-63: dog63.wav
- PSK-125: dog125.wav
- PSK-256: dog256.wav
Non-Standard Baud Rates
Let’s see what PSK-3 sounds like. This mode encodes data at a rate of 3 bits per second. Note that Varicode characters may require up to ten bits, so this is pretty slow. On the other hand the side tones are closer to the carrier and the total bandwidth is much smaller. The message here has been shortened to just my callsign, AJ4VD.
- PSK-3: psk3.wav
Encode PSK-31 In Your Browser
After implementing the C# encoder described above I created a JavaScript version (as per Atwood’s Law).
- Try it on your phone or computer! Launch PskJS
Decoding PSK-31
Considering all the steps for encoding PSK-31 transmissions are already described on this page, it doesn’t require too much additional effort to create a decoder using basic software techniques. Once symbol phases are detected it’s easy to work backwards: detect phase transitions (logical 0s) or repeats (logical 1s), treat consecutive zeros as a character separator, then look-up characters according to the Varicode table. The tricky bit is analyzing the source audio to generate the array of phase offsets.
The simplest way to decode PSK-31 transmissions leans on the fact that we already know the baud rate: 31.25 symbols per second, or one symbol every 256 samples at 8kHz sample rate. The audio signal can be segregated into many 256-sample bins, and processed by FFT. Once the center frequency is determined, the FFT power at this frequency can be calculated for each bin. Signal offset can be adjusted to minimize the imaginary component of the FFTs at the carrier frequency, then the real component will be strongly positive or negative, allowing phase transitions to be easily detected.
There are more advanced techniques to improve BPSK decoding, such as continuously adjusting frequency and phase alignment (synchronization). A Costas loop can help lock onto the carrier frequency while preserving its phase. Visit Simple BPSK31 Decoding with Python for an excellent demonstration of how to decode BPSK31 using these advanced techniques.
A crude C# implementation of a BPSK decoded is available on GitHub in the PSK Experiments repository
Encoding PSK-31 in Hardware
Since BPSK is just a carrier that applies periodic 180º phase-shifts, it’s easy to generate in hardware by directly modulating the signal source. A good example of this is KA7OEI’s PSK31 transmitter which feeds the output of an oscillator through an even or odd number of NAND gates (from a 74HC00) to produce two signals of opposite phase.
Quadrature Phase Shift Keying (QPSK)
Unlike the 0º and 180º phases of binary phase shift keying (BPSK), quadrature phase shift keying (QPSK) encodes extra data into each symbol by uses a larger number of phases. When QPSK-31 is used in amateur radio these extra bits aren’t used to send messages faster but instead send them more reliably using convolutional coding and error correction. These additional features come at a cost (an extra 3 dB SNR is required), and in practice QPSK is not used as much by amateur radio operators.
QPSK encoding/decoding and convolutional encoding/decoding are outside the scope of this page, but excellent information exists on the Wikipedia: QPSK and in the US Naval Academy’s EC314 Lesson 23: Digital Modulation document.
PSK-31 in 2022
After all that, it turns out PSK-31 isn’t that popular anymore. These days it seems the FT-8 digital mode with WSJT-X software is orders of magnitude more popular ??
Resources
-
PSK Experiments (GitHub) - Source code for the project shown on this page
-
Software: digipan - A Freeware Program for PSK31 and PSK63
-
Software: fldigi - Supports PSK31 and other digital modes
-
Software: WinPSK - open source PSK31 software for Windows
-
Software: PSKCore DLL - A Windows DLL that can be included in other software to add support for PSK31
-
Software: jacobwgillespie/psk31 - Example PSK31 message generate using JavaScript
-
Digital Modulation (US Naval Academy, EC314 Lesson 23) - A good description of quadrature PSK and higher order phase-shift encoding.
-
PSK-31 Specification (ARRL) - theory, varicode table, and convolutional code table.
-
PSK31 Description by G3PLX is the original / official description of the digital mode.
-
PSK31: A New Radio-Teletype Mode (1999) by Peter Martinez, G3PLX
-
PSK31 The Easy Way (1999) by Alan Gibbs, VK6PG
-
Wikipedia: Varicode includes a table of all symbols
-
PSK31 Fundamentals and PSK31 Setup by Peter Martinez, G3PLX
-
Varicode - West Virginia State University CS240
-
Introduction to PSK31 by engineering students at Walla Walla University
-
GNURadio PSK31 Decoder by VA7STH
-
Simple BPSK31 - a fantastic Jupyter notebook demonstrating BPSK decoding with Python
-
PySDR: Digital Modulation - a summary of signal modulation types
-
A PIC-Based PSK31 exciter using a Balanced Modulator by Clint Turner, KA7OEI