/*----------------------------------------------------------------------- * * demod_morse.c -- Morse/CW decoder * * Written and placed into the public domain by * Elias Önal * *-----------------------------------------------------------------------*/ #include "multimon.h" #include #include #include #include #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, ""}, // 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, ""}, // 0b0101011001 ...-. {0x015A, "3"}, // 0b0101011010 ...-- {0x0166, "/"}, // 0b0101100110 ..-.- {0x016A, "2"}, // 0b0101101010 ..--- {0x0195, "&"}, // 0b0110010101 .-... {0x0199, "+"}, // 0b0110011001 .-.-. {0x01AA, "1"}, // 0b0110101010 .---- {0x0255, "6"}, // 0b1001010101 -.... {0x0256, "="}, // 0b1001010110 -...- {0x0266, ""}, // 0b1001100110 -.-.- {0x0259, "/"}, // 0b1001011001 -..-. wrong? {0x0269, "("}, // 0b1001101001 -.--. {0x0295, "7"}, // 0b1010010101 --... {0x02A5, "8"}, // 0b1010100101 ---.. {0x02A9, "9"}, // 0b1010101001 ----. {0x02AA, "0"}, // 0b1010101010 ----- {0x0555, ""}, // 0b010101010101 ...... {0x0566, ""}, // 0b010101100110 ...-.- {0x05A5, "?"}, // 0b010110100101 ..--.. {0x05A6, "_"}, // 0b010110100110 ..--.- {0x0659, "\""}, // 0b011001011001 .-..-. {0x0666, "."}, // 0b011001100110 .-.-.- {0x0699, "@"}, // 0b011010011001 .--.-. {0x06A9, "'"}, // 0b011010101001 .----. {0x0956, "-"}, // 0b100101010110 -....- {0x096A, ""}, // 0b100101101010 -..--- {0x0999, ";"}, // 0b100110011001 -.-.-. {0x099A, "!"}, // 0b100110011010 -.-.-- {0x09A6, ")"}, // 0b100110100110 -.--.- {0x0A5A, ","}, // 0b101001011010 --..-- {0x0A95, ":"}, // 0b101010010101 ---... {0x1555, ""}, // 0b01010101010101 ....... {0x1596, "$"}, // 0b01010110010110 ...-..- {0x2566, ""}, // 0b10010101100110 -...-.- {0x5555, ""}, // 0b0101010101010101 ........ {0x9965, ""}, // 0b1001100101100101 -.-..-.. {0x15555, ""}, // 0b010101010101010101 ......... {0x15A95, ""}, // 0b010101101010010101 ...---... {0x55555, ""},// 0b01010101010101010101 .......... {0x155555, ""},// 0b0101010101010101010101 ........... {0x555555, ""},// 0b0101010101010101(...) ......(...) {0x1555555, ""},// 0b0101010101010101(...) ......(...) {0x5555555, ""},// 0b0101010101010101(...) ......(...) {0x15555555, ""},// 0b0101010101010101(...) ......(...) {0x55555555, ""},// 0b0101010101010101(...) ......(...) {0x155555555, ""},// 0b0101010101010101(...) ......(...) {0x555555555, ""},// 0b0101010101010101(...) ......(...) {0x1555555555, ""},// 0b0101010101010101(...) ......(...) {0x5555555555, ""},// 0b0101010101010101(...) ......(...) {0x15555555555, ""},// 0b0101010101010101(...) ......(...) {0x55555555555, ""},// 0b0101010101010101(...) ......(...) {0x155555555555, ""},// 0b0101010101010101(...) ......(...) {0x555555555555, ""},// 0b0101010101010101(...) ......(...) {0x1555555555555, ""},// 0b0101010101010101(...) ......(...) {0x5555555555555, ""},// 0b0101010101010101(...) ......(...) {0x15555555555555, ""},// 0b0101010101010101(...) ......(...) {0x55555555555555, ""},// 0b0101010101010101(...) ......(...) {0x155555555555555, ""},// 0b0101010101010101(...) ......(...) {0x555555555555555, ""},// 0b0101010101010101(...) ......(...) {0x1555555555555555,""},// 0b0101010101010101(...) ......(...) {0x5555555555555555,""},// 0b0101010101010101(...) ......(...) // Sequences longer than 32 DITs will overflow and show up as 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, "", 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, "", 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, "", 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, "", 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, "", 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, "", 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 };