Skip to content

Instantly share code, notes, and snippets.

@koppi
Last active December 31, 2022 19:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save koppi/e8bba3629c9bbab3478bb9e794b0d9de to your computer and use it in GitHub Desktop.
Save koppi/e8bba3629c9bbab3478bb9e794b0d9de to your computer and use it in GitHub Desktop.
ncurses morse decoder for Linux.
/*
* cwrx.c - ncurses morse decoder for Linux
*
* Copyright (c) 2011 - 2023 Jakob Flierl <jakob.flierl@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Compile and run with:
*
* make CFLAGS="-std=gnu11" LDLIBS="-lasound -lncursesw -lm" cwrx && \
* ./cwrx -f 800 hw:0
*/
#define PACKAGE "cwrx"
#define VERSION "0.1.0"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define NCURSES_WIDECHAR 1
#include <curses.h>
#include <stdint.h>
#include <limits.h>
#include <getopt.h>
#include <math.h>
#include <time.h>
#include <signal.h>
#include <locale.h>
#include <wchar.h>
#include <alsa/asoundlib.h>
#include <termios.h>
/* ALSA. */
#define N 256
int16_t buffer[N];
const uint32_t SAMPLING_FREQUENCY = 44100;
/* Morse decoder. */
#define MORSE_PRINT(x) (print(x))
#define MORSE_TIMER() (msec())
#define MORSE_DELAY(x) (msleep(x))
const uint8_t morse_code[]
= "##TEMNAIOGKDWRUS##QZYCXBJP#L#FVH09#8###7#(###/-61######&2###3#45";
#define MORSE_TIME_BOUNCE 1
static uint8_t dit_or_dah;
static uint8_t character_done;
static uint8_t printed_space;
static uint64_t time_down;
static uint64_t time_up;
static uint64_t start_down_time;
static uint64_t start_up_time;
static uint32_t dit_time;
static uint32_t avg_dah_time;
static uint8_t current_char;
/* Goertzel filter. */
static long frequency;
const uint32_t default_frequency = 800;
static double SAMPLING_RATE;
static double coeff;
static double Q1;
static double Q2;
static double sine;
static double cosine;
/* Debug output. */
// " ▁▂▃▄▅▆▇█"
wchar_t block[] =
L" \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
static uint8_t dbm_level = 0;
static double dbm = 0;
static double dbm_min = INT_MAX;
static double dbm_cen = 0;
static double dbm_max = INT_MIN;
static void print_level(uint8_t l) {
dbm_level = l;
}
/* ncursesw */
int max_x = 0, max_y = 0;
#define RED 1
#define GREEN 2
#define YELLOW 3
#define BLUE 4
#define CYAN 5
#define MAGENTA 6
#define WHITE 7
static WINDOW *wMain;
static WINDOW *wEmpty;
static WINDOW *wStatus;
static WINDOW *wTerm;
static void finish(int sig) {
endwin();
exit(EXIT_SUCCESS);
}
void status(void) {
wchar_t strStatus[max_x];
int cnt = swprintf(strStatus, max_x,
L" %lc signal: %2.0lf"
" (min,cen,max: "
"%2.0lf,%2.0lf,%2.0lf) dBm, "
"at: %5.2lf Hz. ",
block[dbm_level], dbm,
dbm_min, dbm_cen, dbm_max,
(double)frequency);
wclear(wStatus);
wattrset(wStatus, COLOR_PAIR(WHITE));
waddnwstr(wStatus, strStatus, -1);
// pad status line
for (int j = 0; j < max_x - cnt; j++) {
waddnwstr(wStatus, L"-", -1);
}
copywin(wStatus, wMain, 0, 0, max_y-1,
0, max_y-1, max_x-1, 0);
}
void handle_timer() {
//wclear(wMain);
status();
copywin(wTerm, wMain, 0, 0, 0, 0, max_y-2, max_x-1, 0);
redrawwin(wMain);
wrefresh(wMain);
}
void nonblock(int state) {
struct termios ttystate;
tcgetattr(STDIN_FILENO, &ttystate);
if (state == 1) {
ttystate.c_lflag &= ~ICANON;
ttystate.c_cc[VMIN] = 1;
} else if (state == 0) {
ttystate.c_lflag |= ICANON;
}
tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
}
bool kbhit() {
struct timeval tv;
fd_set fds;
tv.tv_sec = 0;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
return (FD_ISSET(0, &fds));
}
void win_init(void) {
if (wEmpty) delwin(wEmpty);
if (wMain) delwin(wMain);
if (wStatus) delwin(wStatus);
if (wTerm) delwin(wTerm);
wEmpty = newpad(max_y, max_x);
wclear(wEmpty);
wMain = newwin(max_y, max_x, 0, 0);
wclear(wMain);
mvwin(wMain, 0, 0);
wStatus = newpad(1, max_x);
wclear(wStatus);
wTerm = newwin(max_y - 1, max_x, 0, 0);
scrollok(wTerm, true);
//wclear(wTerm);
mvwin(wTerm, 0, 0);
copywin(wEmpty,wMain, 0, 0, 0, 0, max_y-1, max_x-1, 0);
}
void sig_handle_resize(int sig) {
//endwin();
//refresh();
getmaxyx(stdscr, max_y, max_x);
win_init();
}
/* Call this before every "block" (size=N) of samples. */
static void goertzel_reset(void) {
Q2 = 0;
Q1 = 0;
}
static void goertzel_init(double TARGET_FREQUENCY,
double SAMPLING_FREQ) {
SAMPLING_RATE = SAMPLING_FREQ;
int k;
double omega;
k = (int) (N*(TARGET_FREQUENCY/SAMPLING_RATE) + 0.93);
omega = (2.0 * M_PI * k) / N;
sine = sin(omega);
cosine = cos(omega);
coeff = 2.0 * cosine;
goertzel_reset();
}
static double goertzel_detect() {
int index;
double magnitudeSquared;
double magnitude;
double real;
double imag;
for (index = 0; index < N; index++) {
double Q0;
Q0 = coeff * Q1 - Q2 + (double) buffer[index];
Q2 = Q1;
Q1 = Q0;
}
real = (Q1 - Q2 * cosine);
imag = (Q2 * sine);
magnitudeSquared = real * real + imag * imag;
magnitude = sqrt(magnitudeSquared) / 2;
goertzel_reset();
return magnitude;
}
/* Print given string and flush stdout. */
static void print(const char *s) {
wchar_t buf[10];
swprintf(buf, 10, L"%s", s);
waddnwstr(wTerm, buf, -1);
}
/* Return current time in milliseconds. */
uint64_t msec(void) {
struct timespec _t = {0};
clock_gettime(CLOCK_REALTIME, &_t);
return _t.tv_sec * 1000 + lround((_t.tv_nsec) / 1e6);
}
/* Sleep millisec time. */
/*
static void msleep(unsigned long milisec) {
struct timespec req = {0};
time_t sec = (int)(milisec / 1000);
milisec = milisec - (sec * 1000);
req.tv_sec = sec;
req.tv_nsec = milisec * 1000000L;
while (nanosleep(&req, &req) == -1)
continue;
}*/
/* Initialize morse decoder. */
static void morse_init(void) {
dit_or_dah = 1;
character_done = 1;
printed_space = 1;
time_down = 0;
time_up = 0;
start_down_time = 0;
start_up_time = 0;
dit_time = 10;
avg_dah_time = 240; //3 * dit_time; // ?
current_char = 0;
}
static void morse_shift_bits(void) {
if (time_down < dit_time / 3)
return;
current_char <<= 1;
dit_or_dah = 1;
if (time_down < dit_time) {
current_char++;
//fprintf(stderr, "-");
} else {
avg_dah_time = (time_down + avg_dah_time) / 2;
dit_time = (avg_dah_time / 3) * 2;
//fprintf(stderr, ".");
}
}
static void morse_print_character(void) {
printed_space = 0;
if (current_char > 63) {
switch (current_char) {
case 0x47: MORSE_PRINT(":"); break;
case 0x4c: MORSE_PRINT(","); break;
case 0x52: MORSE_PRINT(")"); break;
case 0x54: MORSE_PRINT("!"); break;
case 0x55: MORSE_PRINT(";"); break;
case 0x5e: MORSE_PRINT("-"); break;
case 0x61: MORSE_PRINT("\""); break;
case 0x65: MORSE_PRINT("@"); break;
case 0x6a: MORSE_PRINT("."); break;
case 0x73: MORSE_PRINT("?"); break;
case 0x7a: MORSE_PRINT("SK"); break;
case 0xf6: MORSE_PRINT("$"); break;
default: MORSE_PRINT("<#>"); break;
}
} else if (current_char == 0x35) {
MORSE_PRINT("AR");
} else {
char buf[2];
buf[0] = morse_code[current_char];
buf[1] = 0;
MORSE_PRINT(buf);
}
}
static void morse_print_space(void) {
if (printed_space)
return;
printed_space = 1;
MORSE_PRINT(" ");
}
/* Process morse code. */
static void morse_process(uint8_t audio_present) {
if (audio_present == 1) {
start_up_time = 0;
if (start_down_time == 0) {
start_down_time = MORSE_TIMER();
}
character_done = 0;
dit_or_dah = 0;
// MORSE_DELAY(MORSE_TIME_BOUNCE);
if (current_char == 0) {
current_char = 1;
}
} else {
if (start_up_time == 0) {
start_up_time = MORSE_TIMER();
}
time_up = MORSE_TIMER() - start_up_time;
if (time_up < 10) { // 200
return;
}
if (time_up > avg_dah_time * 2) {
morse_print_space();
}
if (start_down_time > 0) {
time_down = MORSE_TIMER() - start_down_time;
start_down_time = 0;
}
if (!dit_or_dah) {
morse_shift_bits();
}
if (!character_done) {
if (time_up > dit_time) {
morse_print_character();
character_done = 1;
current_char = 0;
}
time_down = 0;
}
}
}
/* Prints an error message to stderr, and dies. */
static void fatal(const char *msg, ...) {
va_list ap;
endwin();
va_start(ap, msg);
vfprintf(stderr, msg, ap);
va_end(ap);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
/* Error handling for ALSA functions. */
static void check_snd(const char *operation, int err) {
if (err < 0) {
fatal("Cannot %s - %s", operation,
snd_strerror(err));
}
}
static void usage(const char *argv0) {
fprintf(stderr,
"Usage: %s <OPTIONS> [ALSA PCM capture device] ...\n\n"
"Options:\n"
" -f, --frequency=[int] the signal's center frequency (default: 800 Hz)\n"
" -h, --help this help\n"
" -V, --version print current version\n"
"\n", argv0);
}
static void print_version(void) {
printf("%s %s\n", PACKAGE, VERSION);
}
static snd_pcm_t *pcm;
void bye(void) {
if (pcm) {
snd_pcm_close(pcm);
}
finish(0);
}
static volatile bool quit = false;
static void sighandler(int sig) {
quit = true;
}
int main(int argc, char *argv[]) {
static char short_options[] = "hVdf:";
static struct option long_options[] = {
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'V'},
{"frequency", 1, NULL, 'f'},
{NULL, 0, NULL, 0}
};
int c; long err; snd_pcm_hw_params_t *pcm_hw_params;
frequency = default_frequency;
while ((c = getopt_long(argc, argv, short_options,
long_options, NULL)) != -1) {
switch (c) {
case 'h':
usage(argv[0]);
exit(EXIT_SUCCESS);
break;
case 'V':
print_version();
exit(EXIT_SUCCESS);
break;
case 'f':
frequency = atol(optarg);
break;
default:
usage(argv[0]);
exit(EXIT_FAILURE);
break;
}
}
if (argc == 1 || !argv[optind]) {
usage(argv[0]);
exit(EXIT_FAILURE);
}
setlocale(LC_ALL, "");
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
//XXX window resizing does not work yet
//signal(SIGWINCH, sig_handle_resize);
(void) initscr();
raw();
noecho();
keypad(stdscr, TRUE);
meta(stdscr, TRUE);
nodelay(stdscr, TRUE);
set_escdelay(25);
notimeout(stdscr, TRUE);
(void) nonl();
intrflush(stdscr, FALSE);
curs_set(0);
(void) cbreak();
(void) noecho();
scrollok(stdscr, TRUE);
start_color();
init_pair(RED, COLOR_RED, COLOR_BLACK);
init_pair(GREEN, COLOR_GREEN, COLOR_BLACK);
init_pair(YELLOW, COLOR_YELLOW, COLOR_BLACK);
init_pair(BLUE, COLOR_BLUE, COLOR_BLACK);
init_pair(CYAN, COLOR_CYAN, COLOR_BLACK);
init_pair(MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
init_pair(WHITE, COLOR_BLACK, COLOR_WHITE);
nonblock(1);
assume_default_colors(COLOR_WHITE, COLOR_BLACK);
getmaxyx(stdscr, max_y, max_x);
win_init();
err = atexit(bye);
if (err != 0) {
fatal("Cannot set exit function");
}
err = snd_pcm_open(&pcm, argv[optind],
SND_PCM_STREAM_CAPTURE, 0);
check_snd("open audio device", err);
err = snd_pcm_hw_params_malloc(&pcm_hw_params);
check_snd("allocate hw parameter structure",err);
err = snd_pcm_hw_params_any(pcm, pcm_hw_params);
check_snd("initialize hw parameter structure", err);
err = snd_pcm_hw_params_set_access(pcm, pcm_hw_params,
SND_PCM_ACCESS_RW_INTERLEAVED);
check_snd("set access type", err);
err = snd_pcm_hw_params_set_format(pcm, pcm_hw_params,
SND_PCM_FORMAT_S16_LE);
check_snd("set sample format", err);
err = snd_pcm_hw_params(pcm, pcm_hw_params);
check_snd("set parameters", err);
snd_pcm_hw_params_free(pcm_hw_params);
err = snd_pcm_prepare(pcm);
check_snd("prepare audio interface for use", err);
morse_init();
while (!quit) {
err = snd_pcm_readi(pcm, buffer, N) != N ? -1 : 0;
//check_snd("read from audio interface", err);
if (err == -1) continue;
goertzel_init(frequency, SAMPLING_FREQUENCY);
double d = goertzel_detect();
dbm = 10 * log10(2 * d * 1000 / 600.0);
if (dbm_min > dbm) dbm_min = dbm;
if (dbm_max < dbm) dbm_max = dbm;
dbm_cen = dbm_min + (dbm_max - dbm_min) / 2.0;
double dm = d / 2404281.0;
print_level(dm * 8.0);
char ch;
if (kbhit() != 0) {
ch = fgetc(stdin);
//fprintf(stderr, "stdin: %c\n", ch);
if (ch == 'q') {
quit = true;
}
}
//XXX make (..>=4) a variable
morse_process(dbm_level >= 4);
handle_timer();
}
finish(0);
exit(EXIT_SUCCESS);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment