Last active
December 31, 2022 19:57
-
-
Save koppi/e8bba3629c9bbab3478bb9e794b0d9de to your computer and use it in GitHub Desktop.
ncurses morse decoder for Linux.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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