diff options
author | Samuel A. Wirajaya <samuel@wira.plus> | 2023-12-12 22:36:32 +0700 |
---|---|---|
committer | Samuel A. Wirajaya <samuel@wira.plus> | 2023-12-12 22:36:32 +0700 |
commit | 6b3b13632abb77a919a8c90b5b3f54ad30853b11 (patch) | |
tree | 106dcb7c3aa09ba67644391ee33039922c9d66cd |
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | Makefile | 49 | ||||
-rw-r--r-- | README | 61 | ||||
-rw-r--r-- | cwsnd.1 | 144 | ||||
-rw-r--r-- | cwsnd.c | 480 | ||||
-rw-r--r-- | kochgen.1 | 101 | ||||
-rw-r--r-- | kochgen.c | 148 |
7 files changed, 997 insertions, 0 deletions
@@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2023 Samuel Wirajaya + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0fa4298 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +CC?= cc +CFLAGS?= +PREFIX?=/usr/local + +all: cwsnd kochgen + +install: install-cwsnd install-kochgen + +installdoc: installdoc-cwsnd installdoc-kochgen + +uninstall: uninstall-cwsnd uninstall-kochgen + +cwsnd: cwsnd.c + $(CC) -Wall -Werror -O2 $(CFLAGS) \ + -o cwsnd cwsnd.c -lm -lsndio + +kochgen: kochgen.c + $(CC) -Wall -Werror -O2 $(CFLAGS) \ + -o kochgen kochgen.c + +install-cwsnd: cwsnd + install -s cwsnd $(PREFIX)/bin/cwsnd + +installdoc-cwsnd: + install -d $(PREFIX)/man/man1 + install -m 0644 cwsnd.1 $(PREFIX)/man/man1/cwsnd.1 + +install-kochgen: kochgen + install -s kochgen $(PREFIX)/bin/kochgen + +installdoc-kochgen: + install -d $(PREFIX)/man/man1 + install -m 0644 kochgen.1 $(PREFIX)/man/man1/kochgen.1 + +uninstall-cwsnd: + -rm $(PREFIX)/bin/cwsnd + -rm $(PREFIX)/man/man1/cwsnd.1 + +uninstall-kochgen: + -rm $(PREFIX)/bin/kochgen + -rm $(PREFIX)/man/man1/kochgen.1 + +clean: + -rm cwsnd + -rm kochgen + +.PHONY: all clean install install-cwsnd install-kochgen \ + installdoc-cwsnd installdoc-kochgen \ + uninstall uninstall-cwsnd uninstall-kochgen @@ -0,0 +1,61 @@ +cwsnd & kochgen +version 0.9.0 + +CW Training Suite for OpenBSD, following the UNIX philosophy. + + +ABOUD CWSND +----------- +cwsnd is a CW/Morse code sounder written for OpenBSD using the sndio +API. The primary use case for this program is for CW ham study +purposes, although it can be used for computer keying with suitable +hardware, similar to FLDigi Tx using sound card audio output. + +More information can be found in the manpage (man ./cwsnd.1) + +(Currently only OpenBSD is supported. At the present moment, the +author does not want to deal with the Linux audio hellscape.) + + +ABOUT KOCHGEN +------------- +kochgen is a simple utility to generate a word list based on a +character set. It can be used for Koch or Koch--Farnsworth training +when piped into cwsnd. + +More information can be found in the manpage (man ./kochgen.1) + + +INSTALLATION +------------ +To compile: + + $ make + +To install the binaries as root: + + # make install + +Use the PREFIX variable to install somewhere other than /usr/local: + + $ PREFIX=/home/me/.local make install + +Run this as root to install the manpages: + + # make installdoc + +Other install targets: install-cwsnd install-kochgen installdoc-cwsnd +installdoc-kochgen uninstall uninstall-cwsnd uninstall-kochgen + + +AUTHOR +------ +Samuel Wirajaya + + +DISCLAIMER +---------- +This software is provided "as is" with no warranty from the author. +If you do use this program for actual radio transmission, bear in mind +that you are ultimately responsible for whatever is coming out of your +antenna. See the LICENSE file for details. @@ -0,0 +1,144 @@ +.Dd CWSND 1 +.Os +.Sh NAME +.Nm cwsnd +.Nd cw sounder +.Sh SYNOPSIS +.Nm cwsnd +.Op Fl h +.Op Fl E +.Op Fl P +.Op Fl f Ar effwpm +.Op Fl s Ar suffix +.Op Fl t Ar tonefrq +.Op Fl w Ar wpm +.Op Ar file +.Sh DESCRIPTION +.Nm +is a CW/Morse code sounder written for OpenBSD using the sndio API. +The primary use case for this program is for CW ham study purposes, +although it can be used for computer keying with suitable hardware, +similar to FLDigi Tx using sound card audio output. +.Pp +Input is taken from the standard input by default, unless specified +in the +.Ar file +argument. +Prosigns are prefixed with the dollar sign ($), for example $AR, $SK. +.Pp +The input is translated into code, and then into sinusoidal +audio-frequency tones outputted by the sound card, +which can be used as-is for ear training, +or further modulated by external hardware for transmission. +The output device can be selected with the +.Ev AUDIODEVICE +environment variable (see +.Xr sndio 7 +for details). +.Pp +There are three modes of operation: Default, +Echo (see the +.Fl E +option) and Prompt (see the +.Fl P +option). +In the Default mode, lines of text to be transmitted can be entered +into the standard input. +Lines are processed in a line-by-line basis, i.e. nothing gets sent +unless Enter/Return is inputted. +There will be no outputs on the standard output, +except one single newline upon the input EOF. +.Pp +If running interactively in a terminal, +Ctrl+D (end-of-file) ends the session. +.Pp +The options are: +.Bl -tag -width Ds +.It Fl h +Show help message. +.It Fl E +Starts a session in the Echo mode. +In this mode, the input will be obtained from the standard input (or +.Ar file +if supplied). +Characters are echoed into the standard output as they are sent. +Useful for making a transcript of what is actually sent. +Not recommended for interactive use. +.It Fl P +Starts a session in the Prompt mode. +The user types into an asterisk prompt before sending the entire line +by inputting Enter/Return. +The sent characters will be echoed into the standard output, +and the prompt will appear again after the transmission is finished. +.It Fl f Ar effwpm +Sets the Farnsworth speed (effective wpm) to +.Ar effwpm +words per minute. +When not specified, +.Ar effwpm +is assumed to be equal to the actual +.Ar wpm . +.It Fl s Ar suffix +To send +.Ar suffix +when end-of-file is reached. +Default empty. +.It Fl t Ar tonefrq +Sets the output tone frequency to +.Ar tonefrq +cycles per second. +Defaults to 600 cycles/sec. +.It Fl w Ar wpm +Sets the character speed (wpm) to +.Ar wpm +words per minute, using the PARIS standard word. +Defaults to 20 words/min. +.Sh ENVIRONMENT +.Bl -tag -width "AUDIODEVICEXXX" -compact +.It Ev AUDIODEVICE +Name of audio device to use. +.Sh EXAMPLES +Start a Prompt session. +.Pp +.Dl "$ cwsnd -P" +.Dl "* TEST TEST" +.Dl "TEST TEST" +.Pp +Press Ctrl+D to end the session. +.Pp +The Default mode works better for real-time conversations +since it allows the operator to buffer the input +while transmitting at the same time. +.Pp +.Dl "$ cwsnd" +.Dl "s2yz de s1abc" +.Dl "gm dr om [ these lines can be typed while ]" +.Dl "ur rst 5nn 5nn = [ the prev. lines are still sending ]" +.Dl "op is john john =" +.Dl "qth andalusia =" +.Dl "hw?" +.Dl "s2yz de s1abc" +.Dl "$kn" +.Pp +Sending an automatic report in a text file at 30 words per minute. +The transmission ends with the specified suffix. +.Pp +.Dl "$ cwsnd -w 30 -s '= 73 de s2yz $sk' report.txt" +.Pp +Random +.Xr fortune 1 +cookie with transcript via Echo mode. +.Pp +.Dl "$ fortune -s | cwsnd -E" +.Pp +Koch--Farnsworth traning practice, 25 wpm code +spaced out to 15 effective words per minute (requires +.Xr kochgen 1 ) +.Pp +.Dl "$ kochgen -n 30 KMRSUAPTLO | cwsnd -w 25 -f 15" +.Sh SEE ALSO +.Xr kochgen 1 , +.Xr morse 6 , +.Xr sndio 7 +.Sh AUTHORS +.An Samuel Wirajaya @@ -0,0 +1,480 @@ +/* + +BSD Zero Clause License + +Copyright (c) 2023 Samuel Wirajaya + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +================================================================================ + + C W S N D + +================================================================================ + +*/ + +#include <ctype.h> /* toupper */ +#include <limits.h> /* strtoul */ +#include <math.h> /* sin */ +#include <stdint.h> /* int16_t */ +#include <sndio.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#define VERSION "0.9.0" + +typedef int16_t sample_t; +#define SAMPLE_MAX INT16_MAX + +struct sio_hdl *hdl; +struct sio_par par; + +/* options */ +char *progname; +char *suffix; +int eflg; +int pflg; +int tonefrq; +int effwpm; +int wpm; + +/* audio data buffer layout: + [~-~~~-------] + ^dot ^space + ^dash + */ +sample_t *buf; +size_t dotsz; +size_t cspsz; +size_t wspsz; +sample_t *buf_dot; +sample_t *buf_dash; +sample_t *buf_space; + +/* buffer for stuff */ +#define TXBUFSZ 1024 +char txbuf[TXBUFSZ]; + + + + + + +void +calcsz(void) { + /* To calculate size (in # of elements) of the dot sound. + + "PARIS " -> 50 dot-durations + [. --- --- . . --- . --- . . . . . . ] + 12345678901234567890123456789012345678901234567890 + + dot duration in seconds: + dotdur = (60 s/min) / (50 dots/word * wpm in words/min) + + Let s = rate * pchan + + dot size in # of elements: + dotsz = s * dotdur + = s * 6 / (5 * wpm) + */ + + size_t s = par.rate * par.pchan; + dotsz = (s * 6) / (5 * wpm); + + /* To calculate size (# of elements) of the spaces. + + "$PARIS" as "prosign" without space -> 35 dot-durations + [. --- --- . . --- . --- . . . . . .] + 12345678901234567890123456789012345 + + 50 - 35 = 15 + -> 15 dot-duration units (spdotdur) that can be stretched to + match Farnsworth speed + + The string "PARIS PARIS PARIS ..." (repeated effwpm times + with trailing space) should take 60 seconds: + effwpm * (15 * spdotdur + 35 * dotdur) == 60 seconds + + spacing-dot duration solved from above eq: + (60 / effwpm) - (35 * dotdur) + spdotdur = ----------------------------- + 15 + + spacing-dot size from spdotdur and simplifying: + spdotsz = s * spdotdur + (s * 12 / effwpm) - (7 * dotsz) + = ------------------------------- + 3 + + character space size cspsz = spdotsz * 2 + word space size wspsz = spdotsz * 4 + */ + + cspsz = ( + (s * 24 / effwpm) - (14 * dotsz) + /*--------------------------------*/ + ) / 3; + wspsz = ( + (s * 48 / effwpm) - (28 * dotsz) + /*--------------------------------*/ + ) / 3; +} + +void +mksin(sample_t *ptr, size_t n) { + unsigned int i = 0, j; + double fi = 0., val; + /* TODO try windowing */ + while (1) { + val = sin(6.283185307 * tonefrq * (fi / par.rate)); + for (j = 0; j < par.pchan; ++j) { + ptr[i] = (sample_t)round(SAMPLE_MAX * val); + if (++i == n) { + return; + } + } + fi += 1.; + } +} + +int +mkbuf(void) { + /* audio data buffer layout: + [~-~~~-------] + ^dot ^space + ^dash + */ + buf = (sample_t *)calloc(dotsz * (2 + 4) + wspsz, sizeof(sample_t)); + if (!buf) { + perror("can't allocate buffer"); + return 0; + } + + mksin(buf_dot = buf, dotsz); + mksin(buf_dash = &buf[dotsz * 2], dotsz * 3); + buf_space = &buf[dotsz * 6]; + return 1; +} + +size_t +dot(void) { + return sio_write(hdl, buf_dot, + /* tone blank */ + sizeof(sample_t) * dotsz * 2); +} + +size_t +dash(void) { + return sio_write(hdl, buf_dash, + /* tone tone tone blank */ + sizeof(sample_t) * dotsz * 4); +} + +size_t +csp(void) { + return sio_write(hdl, buf_space, sizeof(sample_t) * cspsz); +} + +size_t +wsp(void) { + return sio_write(hdl, buf_space, sizeof(sample_t) * wspsz); +} + +void +usage(void) { + printf( + "CW Sounder for OpenBSD (version " VERSION ")\n" + "usage: %s [-EPh] [-f EFFWPM] [-w WPM] [-s SUFFIX] [-t TONEFRQ] " + "[file]\n" + " -E: echo mode\n" + " -P: prompt mode\n" + " -h: displays this message\n" + " -f: sets the Farnsworth (effective) speed in words per minute\n" + " -w: sets the speed in words (=\"PARIS \") per minute [%d]\n" + " -t: sets the output tone frequency in cycles per second [%d]\n" + , progname, wpm, tonefrq); +} + + + +/* ---------------------------------------------------------------------------- + * T H E M A I N F U N C T I O N + * ---------------------------------------------------------------------------- + */ + + + +int +main(int argc, char **argv) { + FILE *f; + char *p; + int opt, noprint, prosgn, contd; + int retval = 1; + +#define _CLEAN_EXIT(label, exitcode) retval=(exitcode);goto label; + + /* desired sndio params */ + struct sio_par req = { + .bits = 16, + .bps = 2, + .le = SIO_LE_NATIVE, + .sig = 1, + .pchan = 1, + .rate = 44100, + }; + + /* parse options */ + progname = argv[0]; + suffix = NULL; + eflg = 0; + pflg = 0; + tonefrq = 600; + effwpm = -1; + wpm = 20; + f = stdin; + + while ((opt = getopt(argc, argv, "EPf:hs:t:w:")) != -1) { + switch (opt) { + case 'E': + eflg = 1; + break; + case 'P': + pflg = 1; + eflg = 1; + break; + case 'h': + usage(); + _CLEAN_EXIT(end, 0); + case 'f': + effwpm = (int)strtoul(optarg, NULL, 10); + break; + case 's': + suffix = optarg; + break; + case 't': + tonefrq = (int)strtoul(optarg, NULL, 10); + break; + case 'w': + wpm = (int)strtoul(optarg, NULL, 10); + break; + case '?': + default: + usage(); + _CLEAN_EXIT(end, 1); + } + } + argc -= optind; + argv += optind; + if (argc == 1) { + f = fopen(argv[0], "r"); + } else if (argc > 1) { + usage(); + _CLEAN_EXIT(end, 1); + } + + /* validate options */ + if (!f) { + perror("error opening file"); + _CLEAN_EXIT(end, 1); + } + if ((tonefrq < 20) || (tonefrq > 22000)) { + fprintf(stderr, "error: tonefrq out of range (20--22000)\n"); + usage(); + _CLEAN_EXIT(after_fopen, 1); + } + if ((wpm < 5) || (wpm > tonefrq / 4)) { + fprintf(stderr, "error: wpm out of range (5--%d)\n", tonefrq / 4); + usage(); + _CLEAN_EXIT(after_fopen, 1); + } + if (effwpm == -1) { + effwpm = wpm; + } + if ((effwpm < 1) || (effwpm > wpm)) { + fprintf(stderr, "error: Farnsworth speed out of range (1--%d)\n", wpm); + usage(); + _CLEAN_EXIT(after_fopen, 1); + } + + /* get sndio handle for audio, with desired params if possible */ + hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0); + if (!hdl) { + fprintf(stderr, "sio_open\n"); + _CLEAN_EXIT(after_fopen, 1); + } + + sio_initpar(&par); + par.bits = req.bits; + par.bps = req.bps; + par.le = req.le; + par.sig = req.sig; + par.pchan = req.pchan; + par.rate = req.rate; + if (!sio_getpar(hdl, &par)) { + fprintf(stderr, "failed to get sio parameters\n"); + _CLEAN_EXIT(after_sio_open, 1); + } +#define _CHECK_PAR(p) \ + if (par.p != req.p) { \ + fprintf(stderr, \ + "sio_getpar unexpected " #p " (requested %u, got %u)\n", \ + req.p, par.p); \ + _CLEAN_EXIT(after_sio_open, 1); \ + } + _CHECK_PAR(bits) _CHECK_PAR(bps) _CHECK_PAR(le) _CHECK_PAR(sig) + + /* calculate size and prepare audio data buffer */ + calcsz(); + if (!mkbuf()) { + _CLEAN_EXIT(after_sio_open, 1); + } + +#define _PLAYBACK_ERROR(fmt, ...) \ + { \ + fprintf(stderr, "error playing back: " fmt "\n" __VA_ARGS__); \ + _CLEAN_EXIT(after_mkbuf, 1); \ + } +#define _DOT if (!dot()) _PLAYBACK_ERROR("sio_write dot"); +#define _DASH if (!dash()) _PLAYBACK_ERROR("sio_write dash"); +#define _CSP if (!csp()) _PLAYBACK_ERROR("sio_write csp"); +#define _WSP if (!wsp()) _PLAYBACK_ERROR("sio_write wsp"); + + if (!sio_start(hdl)) + _PLAYBACK_ERROR("sio_start"); + + /* main loop */ + contd = 1; + while (contd) { + /* prompt */ + if (pflg) { + printf("* "); + } + if (!fgets(txbuf, TXBUFSZ, f)) { + /* ending, send space then suffix (if any) */ + if (suffix) { + contd = 0; + _CSP; + _WSP; + p = suffix; + } else { + break; + } + } else { + /* business as usual */ + p = txbuf; + } + /* not the most code-efficient way to deal with Morse + encoding, but it works. */ + prosgn = 0; + for (; *p; ++p) { + *p = toupper(*p); + noprint = 0; + switch (*p) { +#define _BRK if (!prosgn) { _CSP } break; + case 'A': _DOT _DASH _BRK + case 'B': _DASH _DOT _DOT _DOT _BRK + case 'C': _DASH _DOT _DASH _DOT _BRK + case 'D': _DASH _DOT _DOT _BRK + case 'E': _DOT _BRK + case 'F': _DOT _DOT _DASH _DOT _BRK + case 'G': _DASH _DASH _DOT _BRK + case 'H': _DOT _DOT _DOT _DOT _BRK + case 'I': _DOT _DOT _BRK + case 'J': _DOT _DASH _DASH _DASH _BRK + case 'K': _DASH _DOT _DASH _BRK + case 'L': _DOT _DASH _DOT _DOT _BRK + case 'M': _DASH _DASH _BRK + case 'N': _DASH _DOT _BRK + case 'O': _DASH _DASH _DASH _BRK + case 'P': _DOT _DASH _DASH _DOT _BRK + case 'Q': _DASH _DASH _DOT _DASH _BRK + case 'R': _DOT _DASH _DOT _BRK + case 'S': _DOT _DOT _DOT _BRK + case 'T': _DASH _BRK + case 'U': _DOT _DOT _DASH _BRK + case 'V': _DOT _DOT _DOT _DASH _BRK + case 'W': _DOT _DASH _DASH _BRK + case 'X': _DASH _DOT _DOT _DASH _BRK + case 'Y': _DASH _DOT _DASH _DASH _BRK + case 'Z': _DASH _DASH _DOT _DOT _BRK + case '1': _DOT _DASH _DASH _DASH _DASH _BRK + case '2': _DOT _DOT _DASH _DASH _DASH _BRK + case '3': _DOT _DOT _DOT _DASH _DASH _BRK + case '4': _DOT _DOT _DOT _DOT _DASH _BRK + case '5': _DOT _DOT _DOT _DOT _DOT _BRK + case '6': _DASH _DOT _DOT _DOT _DOT _BRK + case '7': _DASH _DASH _DOT _DOT _DOT _BRK + case '8': _DASH _DASH _DASH _DOT _DOT _BRK + case '9': _DASH _DASH _DASH _DASH _DOT _BRK + case '0': _DASH _DASH _DASH _DASH _DASH _BRK + + case '?': + prosgn = 0; /* punctuation marks break prosigns */ + _DOT _DOT _DASH _DASH _DOT _DOT _BRK + case '=': + prosgn = 0; + _DASH _DOT _DOT _DOT _DASH _BRK + case '/': + prosgn = 0; + _DASH _DOT _DOT _DASH _DOT _BRK + case ',': + prosgn = 0; + _DASH _DASH _DOT _DOT _DASH _DASH _BRK + case '.': + prosgn = 0; + _DOT _DASH _DOT _DASH _DOT _DASH _BRK + + case '$': + prosgn = 1; + break; + + case ' ': + case '\n': + prosgn = 0; + _WSP; + break; + + default: + noprint = 1; + } + /* echo */ + if (eflg && !noprint) { + fputc(*p, stdout); + fflush(stdout); + } + } + } + + if (!sio_stop(hdl)) + _PLAYBACK_ERROR("sio_stop"); + + printf("\n"); + sleep(1); + + retval = 0; + +after_mkbuf: + free(buf); + +after_sio_open: + sio_close(hdl); + +after_fopen: + if (f && f != stdin) { + fclose(f); + } + +end: + return retval; +} diff --git a/kochgen.1 b/kochgen.1 new file mode 100644 index 0000000..7cafaec --- /dev/null +++ b/kochgen.1 @@ -0,0 +1,101 @@ +.Dd KOCHGEN 1 +.Os +.Sh NAME +.Nm kochgen +.Nd Koch training generator +.Sh SYNOPSIS +.Nm +.Op Fl h +.Op Fl M Ar wrlenmax +.Op Fl m Ar wrlenmin +.Op Fl n Ar nwrs +.Op Fl o Ar ofile +.Ar charset +.Sh DESCRIPTION +.Nm +generates a list of words suitable for CW operator training using +the Koch method. +The intended use case of this program is for its output to be piped +to +.Xr cwsnd 1 +for Koch method practice. +.Pp +A number of words will be generated with the supplied +.Ar charset . +The list will be separated by newlines. +.Pp +.Ar charset +is a string parameter consisting of all characters in the desired +character set. For example, for the traditional first lesson in Koch +training, +.Ar charset +can be set to 'km'. Characters can be given more weight by +duplicating the letter in the +.Ar charset +string: for example, 'kmrss' would favor the letter 's' in the word +generation. +.Pp +The +.Ar charset +parameter is mandatory and must be the last parameter supplied in the +command line. +.Pp +The available options are: +.Pp +.Bl -tag -width Ds +.It Fl h +Show help message. +.It Fl M Ar wrlenmax +Sets the maximum number of letters in a word to +.Ar wrlenmax . +Defaults to 5. +.It Fl m Ar wrlenmin +Sets the minimum number of letters in a word to +.Ar wrlenmin . +Defaults to 5. +.It Fl n Ar nwrs +Sets the number of words to be generated. Defaults to 50. +(To roughly set the duration of practice, +set it to the duration in minutes times the effective/Farnsworth wpm.) +.It Fl o Ar ofile +Writes a copy of the output to +.Ar ofile . +.Sh EXAMPLES +First lesson: student to copy 50 five-letter words with only +K's and M's. +.Pp +.Dl "$ kochgen km | cwsnd" +.Pp +Second lesson: to add 'r' to the character set. +Also generates the answer key. +.Pp +.Dl "$ kochgen -o anskey.txt kmr | cwsnd" +.Pp +Variant of the first lesson: vary the word lengths between 2 and 6 +characters. +.Pp +.Dl "$ kochgen -m 2 -M 6 km | cwsnd" +.Sh ABOUT KOCH METHOD +Many CW operators swear by Koch method; that is, if they can relearn +code from scratch. Apparently it works well in developing neural +pathways required for decoding code at speeds above 15 words per +minute. +.Pp +Koch training works by exposing CW trainees to a subset of the +standard Morse character set, played at full speed right from the +start of the training. The training starts with only two characters +(traditionally 'M' and 'K') in the character set. Words are generated +from the characters in the set, then played at full speed. When the +student can reliably copy at 90 percent accuracy or above, one +character is added to the set. The training progresses until the +student learns the entire character set. +.Pp +For self-paced study, especially when aiming to become conversational +in Morse code, mental copying (i.e. no typing/writing) is also a skill +worth practicing. In this case, the accuracy assessment must be done +on the basis of honesty. +.Sh SEE ALSO +.Xr morse 6 , +.Xr cwsnd 1 +.Sh AUTHORS +.An Samuel Wirajaya diff --git a/kochgen.c b/kochgen.c new file mode 100644 index 0000000..2f823ec --- /dev/null +++ b/kochgen.c @@ -0,0 +1,148 @@ +/* + +BSD Zero Clause License + +Copyright (c) 2023 Samuel Wirajaya + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +================================================================================ + + K O C H G E N + +================================================================================ + +*/ + +#include <ctype.h> /* toupper */ +#include <limits.h> /* strtoul */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> /* strnlen */ +#include <time.h> +#include <unistd.h> + +#define VERSION "0.9.0" + +char *progname; +int wrlenmax; +int wrlenmin; +int nwrs; +char *charset; +FILE *ofile; + +void +usage(void) { + printf( + "Koch Training Generator (version " VERSION ")\n" + "usage: %s [-h] [-M WRLENMAX] [-m WRLENMIN] [-n NWRS] [-o OFILE] " + "CHARSET\n" + "CHARSET is a string containing all letters to be trained on\n" + "e.g. 'km' for the first lesson, 'kmr' for the second lesson,\n" + "etc.\n" + " -h: displays this message\n" + " -M: sets the maximum n. of letters in a word [%d]\n" + " -m: sets the minimum n. of letters in a word [%d]\n" + " -n: sets the number of words to be generated [%d]\n" + " -o: writes a copy of the output to OFILE\n" + , progname, wrlenmax, wrlenmin, nwrs); +} + +void +teeputc(int c) { + fputc(c, stdout); + if (ofile) { + fputc(c, ofile); + } +} + +int +main(int argc, char **argv) { + int i, j, wrlen; + int k, setsz; + int opt; + + srand(time(NULL)); + + progname = argv[0]; + wrlenmax = 5; + wrlenmin = 5; + nwrs = 50; + ofile = NULL; + + while ((opt = getopt(argc, argv, "M:hm:n:o:")) != -1) { + switch (opt) { + case 'M': + wrlenmax = (int)strtoul(optarg, NULL, 10); + break; + case 'h': + usage(); + return 0; + case 'm': + wrlenmin = (int)strtoul(optarg, NULL, 10); + break; + case 'n': + nwrs = (int)strtoul(optarg, NULL, 10); + break; + case 'o': + ofile = fopen(optarg, "w"); + if (!ofile) { + perror("warning: cannot open output file"); + } + break; + case '?': + default: + usage(); + return 1; + } + } + argc -= optind; + argv += optind; + if (argc == 1) { + charset = argv[0]; + } else { + fprintf(stderr, "error: one charset must be supplied\n"); + usage(); + return 1; + } + + if ((wrlenmax < 1) || (wrlenmax > 1023)) { + fprintf(stderr, "error: wrlenmax out of range (1--1023)\n"); + usage(); + return 1; + } + if ((wrlenmin < 1) || (wrlenmin > wrlenmax)) { + fprintf(stderr, "error: wrlenmin out of range (1--wrlenmax)\n"); + usage(); + return 1; + } + + setsz = (int)strnlen(charset, 127); + for (k = 0; k < setsz; ++k) { + charset[k] = toupper(charset[k]); + } + + for (i = 0; i < nwrs; ++i) { + if (wrlenmin == wrlenmax) { + wrlen = wrlenmin; + } else { + wrlen = wrlenmin + rand() % (1 + wrlenmax - wrlenmin); + } + for (j = 0; j < wrlen; ++j) { + k = rand() % setsz; + teeputc(charset[k]); + } + teeputc('\n'); + } + + return 0; +} |