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.
Update: A display board arrived.
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;
}
1 comment:
I’ve been using Claude a lot for Arduino development recently. My current project is an SI4732 radio. I found an example that uses seven buttons, displayed on a Nokia 5110 display and running on an Arduino Nano. I wanted it to run on an ESP32 and display on an ST7789 240×320 colour display using the LovyanGFX library.
I asked Claude to make the changes. It did, and it worked. It even wrote the configuration code for the display. Interestingly, when it handled the display code, the first items shown were in a large font, and it then gradually reduced the font size of subsequent items until the last one was tiny, just to make everything fit.
I’ve since cleaned it up and worked with Claude to make many other changes. Claude has done a great job, and I’ve learned a lot.
Post a Comment