diff options
Diffstat (limited to 'cwsnd.c')
-rw-r--r-- | cwsnd.c | 480 |
1 files changed, 480 insertions, 0 deletions
@@ -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; +} |