Codebase list multimon-ng / debian/1.1.6+dfsg-1 demod_morse.c
debian/1.1.6+dfsg-1

Tree @debian/1.1.6+dfsg-1 (Download .tar.gz)

demod_morse.c @debian/1.1.6+dfsg-1raw · history · blame

/*-----------------------------------------------------------------------
 *
 *      demod_morse.c -- Morse/CW decoder
 *
 *      Written and placed into the public domain by
 *      Elias Önal <morse@eliasoenal.com>
 *
 *-----------------------------------------------------------------------*/
#include "multimon.h"
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#define FREQ_SAMP  22050

#define SMOOTHING_MAGNITUDE 9
#define GAIN 1
#define SQUELCH 500
#define HOLDOFF_MS 10               // Compensate for ringing
#define AUTO_THRESHOLD_MULT 2/3
//#define AUTO_THRESHOLD_MULT 1/2

#define DEBUG 0
#define SHOW_FAILED_DECODES 1
#define SPAM_SAMPLES 0
#define SPAM_STATE 0

#define LOW 0
#define HIGH 1
#define DIT 0x1 //0b01 .
#define DAH 0x2 //0b10 -
#define NUM_ELEMENTS(x) (sizeof(x)/sizeof(x[0]))

// Threshold between:   DITs and DAHs is 2*cw_dit_length
//                      GAPS and EOC  is 2*cw_gap_length
//                      EOC  and EOW  is 5*cw_gap_length
int cw_dit_length = 50;
int cw_gap_length = 50;
int cw_threshold = 500;
bool cw_disable_auto_threshold = false;
bool cw_disable_auto_timing = false;

static const struct morse_codes
{
    const uint64_t sequence;
    const char* character;
} morse_codes[] =
{
    // DITs are 0b01 and DAHs are 0b10
    // List is sorted for binary search
    // HexValue       Character     BinValue                 MORSE/CW
    {0x0000,            "<NULL>"},  // 0b0                      
    {0x0001,            "E"},       // 0b01                     .
    {0x0002,            "T"},       // 0b10                     -
    {0x0005,            "I"},       // 0b0101                   ..
    {0x0006,            "A"},       // 0b0110                   .-
    {0x0009,            "N"},       // 0b1001                   -.
    {0x000A,            "M"},       // 0b1010                   --
    {0x0015,            "S"},       // 0b010101                 ...
    {0x0016,            "U"},       // 0b010110                 ..-
    {0x0019,            "R"},       // 0b011001                 .-.
    {0x001A,            "W"},       // 0b011010                 .--
    {0x0025,            "D"},       // 0b100101                 -..
    {0x0026,            "K"},       // 0b100110                 -.-
    {0x0029,            "G"},       // 0b101001                 --.
    {0x002A,            "O"},       // 0b101010                 ---
    {0x0055,            "H"},       // 0b01010101               ....
    {0x0056,            "V"},       // 0b01010110               ...-
    {0x0059,            "F"},       // 0b01011001               ..-.
    {0x005A,            "Ü"},       // 0b01011010               ..-- wrong? *?
    {0x0065,            "L"},       // 0b01100101               .-..
    {0x0066,            "Ä"},       // 0b01100110               .-.-
    {0x0069,            "P"},       // 0b01101001               .--.
    {0x006A,            "J"},       // 0b01101010               .---
    {0x0095,            "B"},       // 0b10010101               -...
    {0x0096,            "X"},       // 0b10010110               -..-
    {0x0099,            "C"},       // 0b10011001               -.-.
    {0x009A,            "Y"},       // 0b10011010               -.--
    {0x00A5,            "Z"},       // 0b10100101               --..
    {0x00A6,            "Q"},       // 0b10100110               --.-
    {0x00A9,            "Ö"},       // 0b10101001               ---.
    {0x00AA,            "CH"},      // 0b10101010               ----
    {0x0155,            "5"},       // 0b0101010101             .....
    {0x0156,            "4"},       // 0b0101010110             ....-
    {0x0159,            "<SN>"},    // 0b0101011001             ...-.
    {0x015A,            "3"},       // 0b0101011010             ...--
    {0x0166,            "/"},       // 0b0101100110             ..-.-
    {0x016A,            "2"},       // 0b0101101010             ..---
    {0x0195,            "&"},       // 0b0110010101             .-...
    {0x0199,            "+"},       // 0b0110011001             .-.-.
    {0x01AA,            "1"},       // 0b0110101010             .----
    {0x0255,            "6"},       // 0b1001010101             -....
    {0x0256,            "="},       // 0b1001010110             -...-
    {0x0266,            "<CT>"},    // 0b1001100110             -.-.-
    {0x0259,            "/"},       // 0b1001011001             -..-. wrong?
    {0x0269,            "("},       // 0b1001101001             -.--.
    {0x0295,            "7"},       // 0b1010010101             --...
    {0x02A5,            "8"},       // 0b1010100101             ---..
    {0x02A9,            "9"},       // 0b1010101001             ----.
    {0x02AA,            "0"},       // 0b1010101010             -----
    {0x0555,            "<ERR_6>"}, // 0b010101010101           ......
    {0x0566,            "<SK>"},    // 0b010101100110           ...-.-
    {0x05A5,            "?"},       // 0b010110100101           ..--..
    {0x05A6,            "_"},       // 0b010110100110           ..--.-
    {0x0659,            "\""},      // 0b011001011001           .-..-.
    {0x0666,            "."},       // 0b011001100110           .-.-.-
    {0x0699,            "@"},       // 0b011010011001           .--.-.
    {0x06A9,            "'"},       // 0b011010101001           .----.
    {0x0956,            "-"},       // 0b100101010110           -....-
    {0x096A,            "<DO>"},    // 0b100101101010           -..---
    {0x0999,            ";"},       // 0b100110011001           -.-.-.
    {0x099A,            "!"},       // 0b100110011010           -.-.--
    {0x09A6,            ")"},       // 0b100110100110           -.--.-
    {0x0A5A,            ","},       // 0b101001011010           --..--
    {0x0A95,            ":"},       // 0b101010010101           ---...
    {0x1555,            "<ERR_7>"}, // 0b01010101010101         .......
    {0x1596,            "$"},       // 0b01010110010110         ...-..-
    {0x2566,            "<BK>"},    // 0b10010101100110         -...-.-
    {0x5555,            "<ERR_8>"}, // 0b0101010101010101       ........
    {0x9965,            "<CL>"},    // 0b1001100101100101       -.-..-..
    {0x15555,           "<ERR_9>"}, // 0b010101010101010101     .........
    {0x15A95,           "<SOS>"},   // 0b010101101010010101     ...---...
    {0x55555,           "<ERR_10>"},// 0b01010101010101010101   ..........
    {0x155555,          "<ERR_11>"},// 0b0101010101010101010101 ...........
    {0x555555,          "<ERR_12>"},// 0b0101010101010101(...)  ......(...)
    {0x1555555,         "<ERR_13>"},// 0b0101010101010101(...)  ......(...)
    {0x5555555,         "<ERR_14>"},// 0b0101010101010101(...)  ......(...)
    {0x15555555,        "<ERR_15>"},// 0b0101010101010101(...)  ......(...)
    {0x55555555,        "<ERR_16>"},// 0b0101010101010101(...)  ......(...)
    {0x155555555,       "<ERR_17>"},// 0b0101010101010101(...)  ......(...)
    {0x555555555,       "<ERR_18>"},// 0b0101010101010101(...)  ......(...)
    {0x1555555555,      "<ERR_19>"},// 0b0101010101010101(...)  ......(...)
    {0x5555555555,      "<ERR_20>"},// 0b0101010101010101(...)  ......(...)
    {0x15555555555,     "<ERR_21>"},// 0b0101010101010101(...)  ......(...)
    {0x55555555555,     "<ERR_22>"},// 0b0101010101010101(...)  ......(...)
    {0x155555555555,    "<ERR_23>"},// 0b0101010101010101(...)  ......(...)
    {0x555555555555,    "<ERR_24>"},// 0b0101010101010101(...)  ......(...)
    {0x1555555555555,   "<ERR_25>"},// 0b0101010101010101(...)  ......(...)
    {0x5555555555555,   "<ERR_26>"},// 0b0101010101010101(...)  ......(...)
    {0x15555555555555,  "<ERR_27>"},// 0b0101010101010101(...)  ......(...)
    {0x55555555555555,  "<ERR_28>"},// 0b0101010101010101(...)  ......(...)
    {0x155555555555555, "<ERR_29>"},// 0b0101010101010101(...)  ......(...)
    {0x555555555555555, "<ERR_30>"},// 0b0101010101010101(...)  ......(...)
    {0x1555555555555555,"<ERR_31>"},// 0b0101010101010101(...)  ......(...)
    {0x5555555555555555,"<ERR_32>"},// 0b0101010101010101(...)  ......(...)
    // Sequences longer than 32 DITs will overflow and show up as <ERR_32> as well.
};

typedef struct dec_ret_t
{
    char* restrict string_ptr;
    char string[32 + 3]; //32 dit/dahs + 2 brackets and 0
    bool status;
} dec_ret_t;

// Binary search is crazy fast! 0.01% of our CPU time. O(log n) ftw!
static inline dec_ret_t decode_character(const struct demod_state * restrict const s)
{
    int_fast16_t min = 0;
    int_fast16_t max = NUM_ELEMENTS(morse_codes) - 1;
    uint64_t sequence = s->l1.morse.current_sequence;
    
    while (max >= min) // Search
    {
        int_fast16_t mid = (min + max) / 2;
        if(morse_codes[mid].sequence > sequence) max = mid - 1;
        else if(morse_codes[mid].sequence < sequence) min = mid + 1;
        else{dec_ret_t rtn = {(char*)morse_codes[mid].character,"",true};
            return rtn;}
    }
    
    // Build ASCII art from sequence
    dec_ret_t rtn = {rtn.string,"",false};
    *(rtn.string_ptr++) = '<';
    char symbol;
    for(int i = 0; i < (int)sizeof(sequence) * 8; i+=2)
        if((symbol = (sequence >> (sizeof(sequence) - 2 - i)) & (0x3)))
            *(rtn.string_ptr++) = (symbol == DIT)?'.':'_';
    *(rtn.string_ptr++) = '>';
    *rtn.string_ptr = '\0';
    rtn.string_ptr = rtn.string;
    return rtn;
}

// Low pass filter - eats 19.4% of the CPU time according to profiling
// It's a basic IIR filter optimized for integer operation while
// minimizing the rounding error.
static inline int_fast32_t low_pass(const int_fast32_t last_filtered,
                                    const int_fast32_t new_sample,
                                    const int_fast16_t strength)
{
    return ((last_filtered << strength) + 
            new_sample - last_filtered) >> strength;
}

// Probably a lot room for improvements here
// Another 19.5% of our precious CPU time. It's optional though.
static inline void auto_threshold(struct demod_state * restrict const s)
{
    // Hackish solution in order to have the threshold adjust.
    // The highest known amplitude bleeds 20 times 0.1%, per second.
    s->l1.morse.threshold_ctr = (s->l1.morse.threshold_ctr+1) % (FREQ_SAMP / 20);
    if(!s->l1.morse.threshold_ctr && s->l1.morse.signal_max > 0)
    {
        s->l1.morse.signal_max = s->l1.morse.signal_max * 999 / 1000;
        s->l1.morse.detection_threshold = s->l1.morse.signal_max*AUTO_THRESHOLD_MULT;
    }
    
    // Check for a higher upper limit
    if(s->l1.morse.filtered > s->l1.morse.signal_max)
    {
        s->l1.morse.signal_max = s->l1.morse.filtered;
        s->l1.morse.detection_threshold = s->l1.morse.signal_max*AUTO_THRESHOLD_MULT;
    }
    
    // Prevent threshold from dropping below SQUELCH
    if(s->l1.morse.detection_threshold < SQUELCH)
        s->l1.morse.detection_threshold = SQUELCH;
}

// TODO: Come up with a more fancy solution!
static inline void auto_timing(const bool state, struct demod_state * restrict const s)
{
    if(s->l1.morse.samples_since_change < FREQ_SAMP / (1000 / 120)) //120ms
    {
        if(state == LOW)
        {
            if(s->l1.morse.time_unit_gaps_samples > s->l1.morse.samples_since_change)
                s->l1.morse.time_unit_gaps_samples -= 50;
            else s->l1.morse.time_unit_gaps_samples += 50;
        }
        else
        {
            if(s->l1.morse.time_unit_dit_dah_samples > s->l1.morse.samples_since_change)
                s->l1.morse.time_unit_dit_dah_samples -= 50;
            else s->l1.morse.time_unit_dit_dah_samples += 50;
        }
    }
}

// This one gets about 60% of our cycles.
static void morse_demod(struct demod_state * restrict const s,
                        const buffer_t buffer, const int length)
{
    for(int i = 0; i < length; i++)
    {
        // A low-pass is nice, though we could add a high-pass in order to get a band-pass :)
        // abs() when combined with the low-pass works as a peak detector
        s->l1.morse.filtered = low_pass(s->l1.morse.filtered, (int_fast32_t)abs(buffer.sbuffer[i]) * GAIN,
                                       s->l1.morse.lowpass_strength);
        
        // Don't count too far
        if(s->l1.morse.samples_since_change < INT_FAST32_MAX/1000)
            s->l1.morse.samples_since_change++;
        
        if(!cw_disable_auto_threshold) auto_threshold(s);
        
        int_fast8_t oldstate = s->l1.morse.current_state;
        
        // Reject change for holdoff period
        if(s->l1.morse.samples_since_change > s->l1.morse.holdoff_samples)
            s->l1.morse.current_state = s->l1.morse.filtered > s->l1.morse.detection_threshold;
        
        if(SPAM_SAMPLES) verbprintf(0, " %d", s->l1.morse.filtered);
        if(SPAM_STATE) verbprintf(0, " %s", s->l1.morse.current_state?"#":".");
        
        int_fast8_t statechange = oldstate != s->l1.morse.current_state;
        int_fast8_t timeout = s->l1.morse.samples_since_change == 5*s->l1.morse.time_unit_gaps_samples;
        
        // Enter on state transition or timeout
        if(statechange || timeout)
        {
            // Ignore glitches only lasting the holdoff period
            if(s->l1.morse.samples_since_change == s->l1.morse.holdoff_samples+1)
            {
                if(DEBUG) verbprintf(0, "<GLITCH %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
                s->l1.morse.glitches++;
                goto reset_samples;
            }
            
            if(oldstate == LOW)
            {
                // Check whether it was just a inter DIT/DAH gap, else decode
                if(s->l1.morse.samples_since_change >= 2*s->l1.morse.time_unit_gaps_samples)
                {
                    dec_ret_t rtn = {NULL,"",false};
                    if(s->l1.morse.current_sequence)
                    {
                        rtn = decode_character(s);
                        if(SHOW_FAILED_DECODES) verbprintf(0, "%s", rtn.string_ptr);
                        else if(rtn.status) verbprintf(0, "%s", rtn.string_ptr);
                        
                        if(rtn.status) s->l1.morse.decoded_chars++;
                        else s->l1.morse.erroneous_chars++;
                        s->l1.morse.current_sequence = 0; // Start a new sequence
                    }
                    
                    if(s->l1.morse.samples_since_change < 5*s->l1.morse.time_unit_gaps_samples) // End of Char
                    {
                        if(DEBUG) verbprintf(0, "<EOC %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
                    }
                    else if(timeout) // End of word - timeout
                    {
                        if(rtn.status)
                            verbprintf(0," "); //Don't print additional spaces if last character failed to decode
                        if(DEBUG) verbprintf(0, "<EOW %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
                        goto end; // Don't reset samples, since there wasn't any change in state.
                    }
                } // It was just a inter DIT/DAH gap
                else if(DEBUG) verbprintf(0, "<GAP %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
            }
            else // Last state was either DIT or DAH
            {
                if(s->l1.morse.samples_since_change < 2*s->l1.morse.time_unit_dit_dah_samples)
                {
                    s->l1.morse.current_sequence = (s->l1.morse.current_sequence << 2) | DIT;
                    if(DEBUG) verbprintf(0, "<DIT %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
                }
                else 
                {
                    s->l1.morse.current_sequence = (s->l1.morse.current_sequence << 2) | DAH;
                    if(DEBUG) verbprintf(0, "<DAH %dms>", s->l1.morse.samples_since_change * 1000 / FREQ_SAMP);
                }
            }
            
            if(!cw_disable_auto_timing) auto_timing(oldstate, s);
reset_samples:
            s->l1.morse.samples_since_change = 0; // State has changed, restart counting
end:;
        }
    }
}

static void morse_init(struct demod_state * restrict s)
{
    memset(&s->l1.morse, 0, sizeof(s->l1.morse));
    s->l1.morse.time_unit_dit_dah_samples = FREQ_SAMP / (1000 / cw_dit_length);
    s->l1.morse.time_unit_gaps_samples = FREQ_SAMP / (1000 / cw_gap_length);
    s->l1.morse.detection_threshold = cw_threshold;
    s->l1.morse.lowpass_strength = SMOOTHING_MAGNITUDE;
    if(HOLDOFF_MS) s->l1.morse.holdoff_samples = FREQ_SAMP / (1000 / HOLDOFF_MS);
    
    s->l1.morse.signal_max = SQUELCH;
}

static void morse_deinit(struct demod_state * const restrict s)
{
    verbprintf(1, "\nMAX: %d THRESHOLD: %d GLITCHES: %d FAILED: %d "
               "DECODED: %d TIMING_GAP: %d TIMING_DIT: %d",
               s->l1.morse.signal_max,
               s->l1.morse.detection_threshold,
               s->l1.morse.glitches,
               s->l1.morse.erroneous_chars,
               s->l1.morse.decoded_chars,
               s->l1.morse.time_unit_gaps_samples * 1000 / FREQ_SAMP,
               s->l1.morse.time_unit_dit_dah_samples * 1000 / FREQ_SAMP);
    verbprintf(0, "\n");
}

const struct demod_param demod_morse = {
    "MORSE_CW", false, FREQ_SAMP, 0, morse_init, morse_demod, morse_deinit
};