A Linux terminal Morse code trainer for iambic paddles.
Real-time audio · adaptive timing · Koch method · live TUI.
Mode A and Mode B, squeeze keying, auto-repeating dit/dah from VBand and compatible paddles.
Precise sine wave via ALSA/PulseAudio. 5ms fade envelope eliminates clicks on fast keying.
Automatically tracks your sending speed via rolling 8-symbol average. No manual WPM tuning needed.
Structured learning from 2 characters at full speed. Advance at 90% accuracy. 40-character curriculum.
Koch level and per-character stats saved to ~/.cw-trainer/progress.json between sessions.
Decoded text, paddle activity indicator, WPM meter, and session timer in a clean terminal UI.
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/
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
go installgo install github.com/shaposhnikoff/cw-trainer/cmd/cw-trainer@latest
/dev/input/event* requires the input group. Add your user once:
sudo usermod -aG input $USER
newgrp input # apply without re-login
# 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
# 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
| Flag | Default | Description |
|---|---|---|
--device | /dev/input/event4 | evdev device path |
--wpm | 15 | Initial speed in WPM |
--freq | 700 | Tone frequency in Hz |
--mode | iambic-a | Keyer mode: iambic-a, iambic-b |
--letter-space | 4.0 | Letter space threshold (× dit duration) |
--koch | false | Koch Trainer mode |
--debug | false | Debug mode: print symbols, no TUI |
| Key | Action |
|---|---|
Q / Ctrl+C | Quit |
+ / = | Increase tone frequency (+10 Hz) |
- | Decrease tone frequency (−10 Hz) |
R | Reset decoded text and session stats |
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.
Start with K and M — the trainer plays one character at a time and waits for your paddle response.
Reach 90% accuracy over 50 symbols to unlock the next character.
A new character is introduced and played 5 times so you can hear its pattern clearly.
Repeat until all 40 characters are learned.
./cw-trainer --koch --wpm 20
Progress is saved automatically to ~/.cw-trainer/progress.json.
The 40-character Koch sequence, starting with the two highest-contrast characters:
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.
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.
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
go test ./...
go test -v ./internal/decoder/