summaryrefslogtreecommitdiff
path: root/cwsnd.c
diff options
context:
space:
mode:
Diffstat (limited to 'cwsnd.c')
-rw-r--r--cwsnd.c480
1 files changed, 480 insertions, 0 deletions
diff --git a/cwsnd.c b/cwsnd.c
new file mode 100644
index 0000000..7a91243
--- /dev/null
+++ b/cwsnd.c
@@ -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;
+}