Wednesday, February 04, 2026

Tuesday, February 03, 2026

Very good morse decoder from 101 things

Prolific ham radio hacker Jonathan P Dawson has just shared a remarkable project he calls Hamfist that is a morse decode device running in the Arduino runtime for Pi PICO processors (including the original). 

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:

cw_decoder % ./cw_wav_decoder examples/cw2.wav

WAV file: examples/cw2.wav

  Sample rate: 12000 Hz

  Channels: 1

  Bits per sample: 16

  Target sample rate: 15000 Hz

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;
}

Makefile:

CXX = g++
CXXFLAGS = -std=c++17 -O2 -Wall

# Source files
SOURCES = wav_decoder.cpp \
cw_dsp.cpp \
cw_decode.cpp \
cw_classifier.cpp \
cw_data.cpp \
dictionary.cpp \
fft.cpp \
utils.cpp

# Object files
OBJECTS = $(SOURCES:.cpp=.o)

# Output binary
TARGET = cw_wav_decoder

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $@ $^

%.o: %.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<

clean:
rm -f $(OBJECTS) $(TARGET)

# Dependencies
wav_decoder.o: wav_decoder.cpp cw_dsp.h fft.h
cw_dsp.o: cw_dsp.cpp cw_dsp.h cw_decode.h fft.h utils.h
cw_decode.o: cw_decode.cpp cw_decode.h cw_classifier.h cw_data.h dictionary.h
cw_classifier.o: cw_classifier.cpp cw_classifier.h
cw_data.o: cw_data.cpp cw_data.h
dictionary.o: dictionary.cpp cw_data.h
fft.o: fft.cpp fft.h
utils.o: utils.cpp utils.h

Thursday, January 29, 2026

Apple's Knowledge Navigator - are we there yet?

Like many Apple users I'm keen to see how an upgraded Siri, rumoured to be coming this year, works. Right now Siri is way behind competing speech handling agents. It can turn lights on and off just fine but ask it anything more and it either searches the Internet or asks ChatGPT.

Back in 1987, Apple published a ground breaking vision for how human computer interaction might work in the future. It was commissioned by then CEO, John Sculley, and it not only predicted AI agents but even the Internet.


This morning I was chatting with SmartFriend™ Tony about Clawdbot/Moltbot and he mentioned Knowledge Navigator. We are now very close to this futuristic vision. Not quite there, but it is within reach. ChatGPT helped me create this feature list.

Achieved:

  • Voice-activated assistants
  • Retrieving and summarising information
  • Video calls and collaboration
  • Making and managing calls/messages
  • Touchscreens and tablets
  • Text-to-speech and speech-to-text
Not as envisaged:
  • We don't use an animated person's head as our assistant - probably for the best
Not quite here yet:
  • Deep contextual long-term assistant memory - Apple announced this two years ago but didn't ship
  • Fully autonomous, multi-step tasks with actions - Agents promise this.
What a remarkable vision of the future Sculley and Apple published almost 40 years ago. I hope the Siri team is taking a look.


Wednesday, January 28, 2026

CYD Internet radio

Recently I've been playing with the Cheap Yellow Display boards. They are an ESP-32 on the back of a 2.8 inch LCD display with touch. One of the most interesting projects is an Internet Radio. Source code is here.

The boards have quite a range of devices already in place such as an RGB LED and an audio amplifier, other I/O is nicely taken out to sockets on the edge. (I've upgraded my 3D case design to add holes for these). I've just got it working:


You'd think it would be possible to use the on-board audio DAC with the speaker driver but unfortunately the I/O is shared with one of the touch screen lines. An external audio DAC should be used.

I changed a few of the constants to use the pins available on my version of the board (The USB-C one) as follows:

// On the USB-C CYD
const int I2S_BCKL = GPIO_NUM_18; // BLU --> BCKL | // 26 |
const int I2S_WSEL = GPIO_NUM_19; // YEL --> WSEL |UDA1334A // 25 | when using spkr output
const int I2S_DOUT = GPIO_NUM_23; // RED --> DIN | // 27 |
// also GPIO_NUM_27 BLK
// 3.3V RED VIN
// GND GRN GND

Other changes I made included the screen rotation and time zone for my area.

It's now playing lovely classical music, although it's a bit glitchy for some reason. My touch screen doesn't seem to be working so I plan to figure that out next. Thanks to John VK2ASU for the prompt to work on this.

Tech Talk on ABC Radio - Artificial Intelligence is the wrong name

Do you think the term Artificial Intelligence is being misused for things which aren't intelligent at all?  

Have we come to trust the output too much, partly because of the name? Could we have fallen for a marketing trick? 

Prediction market odds are increasingly being reported as news, what are they and can they be gamed?

Communication failures during the recent Victorian fires, why the big ABC AM stations are an important backup in times of emergency.

Peter Marks, mobile software developer and technology commentator from Access Informatics, joined Philip Clark and listeners to Nightlife with the latest tech news. https://www.abc.net.au/listen/programs/nightlife/nightlife-tech-talk-with-peter-marks/106276236 

Monday, January 26, 2026

Starlink residential lite is enough for me

I've been very happy with Starlink after NBN dropped the ball here and drove me away. Until now I've been on the Residential Full plan at AU$139 / month. Most of my computers are connected over Wifi and here's the speed test I was getting:


I decided to try Residential Lite which is said to be a lower speed and lower priority in busy times. Here's what I get now:


Still pretty good. My guess is that the busy times when I might see reduced speed due to congestion would be in the evenings when people are streaming video but so far I haven't noticed it.

I do see a bit of buffering just after a YouTube video starts playing on the Apple TV box but I saw that before and assume it's some networking mis-match. Lengthy multi-party Zoom calls work as well as before.

Residential Lite is AU$99 / month so that's $40 a month freed up for ham radio bits.



Sunday, January 25, 2026

FreeDV net - 12 on frequency but poor conditions

A good rollup this morning but few stations could actually hear many people. Very warm here in south eastern Australia. Jack, VK5KVA said he had 42.7C yesterday (108.86F) and the only cooling was a ceiling fan. We're expecting about 34C today.

Stations on frequency: VK2AWJ, VK2GMH, VK3JCO, VK3KEZ, VK3PCC, VK3SRC, VK3XCI, VK5KVA, VK5MH, VK7DMH, VK4IKZ, VK5RA and me, VK3TPM.

Saturday, January 24, 2026

It's time to upgrade the home to Matter and Thread

We have some simple home automation here. Lights come on at sunset and go off again later. Some lights we turn on or off by calling out. Nothing fancy. Mostly these were done with Wifi connected switches via whatever app they need which is hooked in to Google home and Apple Home.

In the past it's been a struggle to configure new devices, often having to try over and over again. Some of them tend to drop of the Wifi network at times. Zigbee based mesh networking is an alternative but requires a hub to link to your network.

Happily Apple, Google, and the manufacturers have come together and agreed on a standard for setup, status and control called Matter and a mesh network called Thread. My local electrical store didn't have any Matter + Thread devices but, guess what, the old stock is on special! I ordered some bulbs on line.


If you're buying, make sure you see these little icons on the box:


Adding the "accessories" to my Home was the best experience I've ever had. Open the Google or Apple Home app, scan the 2D barcode on the device, plug in the device and in seconds it had been found and added. There is a link to a manufacturer's app but there's no need to install it or use it for control. 

You might think that a Wifi to Thread radio hub might be needed but happily Apple has been quietly building Thread in to many of their products for years. Thread is in Apple TV, HomePods, iPhone 15 and later, iPads (M4 Pro, M2 Air), MacBooks (M3 and later). I was pleased to learn that starting with iOS 18 newer iPhones can directly control Matter devices over Thread radio locally without needing Wifi or internet. This makes the experience fast and reliable.

Google has been doing a similar thing and Thread routers are in the Nest Hub (2nd gen), Nest Hub Max, Nest Wifi, Google TV Streamer (4K), and is expected to be in the Pixel 10 phones. Android version 15 or later supports Matter.