Skip to content

Instantly share code, notes, and snippets.

@koppi
Last active December 31, 2022 19:57
Show Gist options
  • Save koppi/e8bba3629c9bbab3478bb9e794b0d9de to your computer and use it in GitHub Desktop.
Save koppi/e8bba3629c9bbab3478bb9e794b0d9de to your computer and use it in GitHub Desktop.

Revisions

  1. koppi revised this gist Dec 31, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cwrx.c
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    /*
    * cwrx.c - ncurses morse decoder for Linux
    *
    * Copyright (c) 2011 - 2021 Jakob Flierl <jakob.flierl@gmail.com>
    * 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
  2. koppi created this gist Jul 31, 2021.
    607 changes: 607 additions & 0 deletions cwrx.c
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,607 @@
    /*
    * cwrx.c - ncurses morse decoder for Linux
    *
    * Copyright (c) 2011 - 2021 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);
    }