Go and read his description but it's quite a sophisticated decoder, rather like a CW skimmer it can decode several morse signals in the audio passband. The PICO does the audio sampling with one of its A/D pins with just a simple circuit on the input.
"It combines adaptive signal processing, automatic speed estimation, probabilistic decoding, dictionary-based correction, and multi-channel decoding, all while keeping memory and CPU usage firmly under control."
I grabbed the source and built it in the Arduino IDE targeting the PICO. All very smooth. Unfortunately I don't have the display he uses, the ili9341, so I've ordered some. The code seems to run but of course I can't see the morse.
So. I pointed Claude Code at the Arduino files and simply asked it to create a C++ command line program that could read a WAV file with morse audio in it and print out the decoded morse. It wrote wav_decoder.cpp and a Makefile. Jonathan includes some sample WAV files so I tried some of these. Here's how that goes:
CQ SP3VT SP3VT TEST E4EQ SP3VE E3VT TEST PA3A IN PA3AT 5NN E 27 MIKEEE EAT/ HI TU S E3VT CQ SP3VT SE 3VT TEST CZ G3VT SPSE UT TEST CQ SW3VT SP EVE TEST CQ SP3VT SAME3VT TEST DK3T DA3T 5NT
I would add that the decode all happens in a fraction of a second.
I'm not sure if I'm more impressed with Jonathan's contribution or Claude Code's capabilities. Here's the file it produced and the Makefile.
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <string>
#include "cw_dsp.h"
#include "fft.h"
// WAV file header structure
struct WavHeader {
char riff[4]; // "RIFF"
uint32_t file_size; // File size - 8
char wave[4]; // "WAVE"
char fmt[4]; // "fmt "
uint32_t fmt_size; // Format chunk size
uint16_t audio_format; // 1 = PCM
uint16_t num_channels; // Number of channels
uint32_t sample_rate; // Sample rate
uint32_t byte_rate; // Bytes per second
uint16_t block_align; // Bytes per sample * channels
uint16_t bits_per_sample; // Bits per sample
};
// Derived DSP class that outputs decoded text to stdout
class c_wav_decoder : public c_cw_dsp {
private:
std::string last_text[NUM_CHANNELS];
protected:
void decode(uint16_t channel, std::string text, std::string partial) override {
if (!text.empty() && text != last_text[channel]) {
printf("%s", text.c_str());
fflush(stdout);
last_text[channel] = text;
}
}
public:
void print_final() {
// Flush any remaining text
flush();
printf("\n");
}
};
// Simple linear interpolation resampler
class Resampler {
private:
double ratio;
double position;
int16_t last_sample;
bool first_sample;
public:
Resampler(uint32_t input_rate, uint32_t output_rate)
: ratio((double)input_rate / output_rate)
, position(0.0)
, last_sample(0)
, first_sample(true) {}
// Process one input sample, may produce 0 or more output samples
// Returns number of output samples produced
int process(int16_t input, int16_t* output, int max_output) {
int count = 0;
if (first_sample) {
last_sample = input;
first_sample = false;
}
while (position < 1.0 && count < max_output) {
// Linear interpolation
double frac = position;
int32_t interpolated = (int32_t)((1.0 - frac) * last_sample + frac * input);
output[count++] = (int16_t)interpolated;
position += ratio;
}
position -= 1.0;
last_sample = input;
return count;
}
};
void print_usage(const char* program_name) {
fprintf(stderr, "Usage: %s <wav_file> [channel]\n", program_name);
fprintf(stderr, "\n");
fprintf(stderr, "Decodes Morse code (CW) from a WAV audio file.\n");
fprintf(stderr, "\n");
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " wav_file Path to the input WAV file (mono or stereo, any sample rate)\n");
fprintf(stderr, " channel Optional: frequency channel 0-%d (default: all channels)\n", NUM_CHANNELS - 1);
fprintf(stderr, "\n");
fprintf(stderr, "The decoder uses %d frequency channels spanning 0-%.0f Hz.\n",
NUM_CHANNELS, NUM_CHANNELS * CHANNEL_SIZE * (SAMPLE_FREQUENCY / 2.0) / (FRAME_SIZE / 2));
}
int main(int argc, char* argv[]) {
if (argc < 2 || argc > 3) {
print_usage(argv[0]);
return 1;
}
const char* filename = argv[1];
int selected_channel = -1; // -1 means all channels
if (argc == 3) {
selected_channel = atoi(argv[2]);
if (selected_channel < 0 || selected_channel >= NUM_CHANNELS) {
fprintf(stderr, "Error: channel must be between 0 and %d\n", NUM_CHANNELS - 1);
return 1;
}
}
// Open the WAV file
FILE* file = fopen(filename, "rb");
if (!file) {
fprintf(stderr, "Error: Cannot open file '%s'\n", filename);
return 1;
}
// Read WAV header
WavHeader header;
if (fread(&header, sizeof(WavHeader), 1, file) != 1) {
fprintf(stderr, "Error: Cannot read WAV header\n");
fclose(file);
return 1;
}
// Validate WAV format
if (strncmp(header.riff, "RIFF", 4) != 0 || strncmp(header.wave, "WAVE", 4) != 0) {
fprintf(stderr, "Error: Not a valid WAV file\n");
fclose(file);
return 1;
}
if (header.audio_format != 1) {
fprintf(stderr, "Error: Only PCM WAV files are supported (format=%d)\n", header.audio_format);
fclose(file);
return 1;
}
if (header.bits_per_sample != 16 && header.bits_per_sample != 8) {
fprintf(stderr, "Error: Only 8-bit or 16-bit WAV files are supported\n");
fclose(file);
return 1;
}
fprintf(stderr, "WAV file: %s\n", filename);
fprintf(stderr, " Sample rate: %u Hz\n", header.sample_rate);
fprintf(stderr, " Channels: %u\n", header.num_channels);
fprintf(stderr, " Bits per sample: %u\n", header.bits_per_sample);
fprintf(stderr, " Target sample rate: %.0f Hz\n", SAMPLE_FREQUENCY);
fprintf(stderr, "\n");
// Skip to data chunk
// The fmt chunk might be larger than our struct, and there might be other chunks
fseek(file, 12, SEEK_SET); // Skip RIFF header
char chunk_id[4];
uint32_t chunk_size;
while (fread(chunk_id, 4, 1, file) == 1) {
if (fread(&chunk_size, 4, 1, file) != 1) {
fprintf(stderr, "Error: Malformed WAV file\n");
fclose(file);
return 1;
}
if (strncmp(chunk_id, "data", 4) == 0) {
break; // Found data chunk
}
// Skip this chunk
fseek(file, chunk_size, SEEK_CUR);
}
if (strncmp(chunk_id, "data", 4) != 0) {
fprintf(stderr, "Error: Cannot find data chunk in WAV file\n");
fclose(file);
return 1;
}
// Initialize FFT and DSP
fft_initialise();
c_wav_decoder decoder;
// Create resampler
Resampler resampler(header.sample_rate, (uint32_t)SAMPLE_FREQUENCY);
// Process audio data
const int BUFFER_SIZE = 1024;
uint8_t buffer[BUFFER_SIZE * 4]; // Max size for stereo 16-bit
int16_t output_buffer[16]; // Resampler output buffer
int bytes_per_sample = header.bits_per_sample / 8;
int bytes_per_frame = bytes_per_sample * header.num_channels;
int samples_per_read = BUFFER_SIZE;
uint32_t total_samples = chunk_size / bytes_per_frame;
uint32_t samples_processed = 0;
while (samples_processed < total_samples) {
int samples_to_read = samples_per_read;
if (samples_processed + samples_to_read > total_samples) {
samples_to_read = total_samples - samples_processed;
}
size_t bytes_read = fread(buffer, bytes_per_frame, samples_to_read, file);
if (bytes_read == 0) break;
for (size_t i = 0; i < bytes_read; i++) {
int16_t sample;
if (header.bits_per_sample == 16) {
// 16-bit sample
int16_t* samples = (int16_t*)(buffer + i * bytes_per_frame);
if (header.num_channels == 1) {
sample = samples[0];
} else {
// Mix stereo to mono
sample = (samples[0] + samples[1]) / 2;
}
} else {
// 8-bit sample (unsigned)
uint8_t* samples = buffer + i * bytes_per_frame;
if (header.num_channels == 1) {
sample = ((int16_t)samples[0] - 128) * 256;
} else {
// Mix stereo to mono
sample = (((int16_t)samples[0] - 128) + ((int16_t)samples[1] - 128)) * 128;
}
}
// Resample to target rate
int num_outputs = resampler.process(sample, output_buffer, 16);
// Feed resampled samples to decoder
for (int j = 0; j < num_outputs; j++) {
decoder.process_sample(output_buffer[j]);
}
}
samples_processed += bytes_read;
}
// Flush remaining decoded text
decoder.print_final();
fclose(file);
return 0;
}