Open Source · Linux · Go

CW Trainer

A Linux terminal Morse code trainer for iambic paddles.
Real-time audio · adaptive timing · Koch method · live TUI.

⬇ Download binary View on GitHub
S O S
Go 1.21+ Linux only ALSA / PulseAudio MIT License

Features

🎛️

Iambic Keyer

Mode A and Mode B, squeeze keying, auto-repeating dit/dah from VBand and compatible paddles.

🔊

Real-time Audio

Precise sine wave via ALSA/PulseAudio. 5ms fade envelope eliminates clicks on fast keying.

Adaptive Timing

Automatically tracks your sending speed via rolling 8-symbol average. No manual WPM tuning needed.

📈

Koch Trainer

Structured learning from 2 characters at full speed. Advance at 90% accuracy. 40-character curriculum.

💾

Progress Persistence

Koch level and per-character stats saved to ~/.cw-trainer/progress.json between sessions.

🖥️

Live TUI

Decoded text, paddle activity indicator, WPM meter, and session timer in a clean terminal UI.

Installation

Download a binary

Pre-built binaries for linux/amd64 and linux/arm64 are on the Releases page.

tar xzf cw-trainer_*.tar.gz
sudo mv cw-trainer /usr/local/bin/

Build from source

Requires Go 1.21+ and libasound2-dev.

sudo apt install libasound2-dev

git clone https://github.com/shaposhnikoff/cw-trainer
cd cw-trainer
go build -o cw-trainer ./cmd/cw-trainer

Install with go install

go install github.com/shaposhnikoff/cw-trainer/cmd/cw-trainer@latest

Device permissions

/dev/input/event* requires the input group. Add your user once:

sudo usermod -aG input $USER
newgrp input   # apply without re-login

Find your paddle device

# List all input devices
ls /dev/input/event*

# Find your paddle by name
cat /proc/bus/input/devices | grep -A5 -i "vband\|cw\|morse"

# Monitor raw events
sudo evtest /dev/input/event4

VBand CW Trainer protocol: KEY_LEFTBRACE (code 26) → dit · KEY_RIGHTBRACE (code 27) → dah

Usage

# Default device /dev/input/event4
./cw-trainer

# Specify device explicitly
./cw-trainer --device /dev/input/event3

# Set speed and tone frequency
./cw-trainer --wpm 20 --freq 650

# Iambic Mode B
./cw-trainer --mode iambic-b

# Koch Trainer mode
./cw-trainer --koch --wpm 20

# Debug mode — prints decoded symbols, no TUI
./cw-trainer --debug --wpm 20

All flags

Flag Default Description
--device/dev/input/event4evdev device path
--wpm15Initial speed in WPM
--freq700Tone frequency in Hz
--modeiambic-aKeyer mode: iambic-a, iambic-b
--letter-space4.0Letter space threshold (× dit duration)
--kochfalseKoch Trainer mode
--debugfalseDebug mode: print symbols, no TUI

TUI keyboard shortcuts

KeyAction
Q / Ctrl+CQuit
+ / =Increase tone frequency (+10 Hz)
-Decrease tone frequency (−10 Hz)
RReset decoded text and session stats

Koch Trainer Method

The Koch method is the fastest way to learn Morse code. You practice at full speed from day one — no slow copying. Start with just two characters and advance only when you're accurate.

  1. Start with K and M — the trainer plays one character at a time and waits for your paddle response.

  2. Reach 90% accuracy over 50 symbols to unlock the next character.

  3. A new character is introduced and played 5 times so you can hear its pattern clearly.

  4. Repeat until all 40 characters are learned.

./cw-trainer --koch --wpm 20

Progress is saved automatically to ~/.cw-trainer/progress.json.

Character order

The 40-character Koch sequence, starting with the two highest-contrast characters:

K M R S U A P T L O W I . N J E F 0 Y V , G 5 / Q 9 Z H 3 8 B ? 4 2 7 C 1 D 6 X

Architecture

All timing-sensitive work runs in dedicated goroutines connected by channels. The iambic FSM is a single-goroutine timer-driven loop — no spawned goroutines per element, no races.

evdev goroutine chan KeyEvent iambic FSM goroutine (timer-driven, single select loop) onElement(toneMs, gapMs) onSymbol(sym, durationMs) audio goroutine timing decoder io.Pipe → oto player adaptive ditMs (rolling 8-avg) exact PCM per element letter / word space detection onChar(rune) TUI goroutine (tea.Program)

Key design: audio via io.Pipe

Instead of a streaming reader with an on/off flag — which causes oto to pre-buffer audio and ignore Stop() calls — each element writes an exact PCM block (tone samples + silence samples) to an io.Pipe. oto reads at hardware rate, so timing is precise with zero buffering artifacts.

Project structure

cw-trainer/
├── cmd/cw-trainer/main.go        # entry point, CLI flags
├── internal/
│   ├── input/evdev.go            # evdev reader → KeyEvent channel
│   ├── audio/tone.go             # PCM sine wave via oto/v2, io.Pipe
│   ├── decoder/
│   │   ├── iambic.go             # iambic keyer FSM (Mode A/B)
│   │   ├── timing.go             # adaptive timing decoder
│   │   ├── morse_table.go        # Morse code table A-Z, 0-9, punctuation
│   │   └── *_test.go             # 13 tests
│   ├── koch/
│   │   ├── session.go            # Koch session logic
│   │   ├── progress.go           # JSON progress persistence
│   │   └── morse_map.go          # symbol→pattern lookup
│   └── ui/
│       ├── tui.go                # main TUI (bubbletea + lipgloss)
│       └── koch_tui.go           # Koch Trainer TUI screen
├── go.mod
└── go.sum

Running tests

go test ./...
go test -v ./internal/decoder/