Homemade Morse code audio parser

Category: Tag:

The main purpose of writing this article is to design a system to automatically decode Morse code of audio. But I do not have a ready-made Morse code audio file, so I had to design a Morse code audio generator first, and then a Morse code audio parser.

0x01 required materials
Materials needed for Morse code audio generator

1. Raspberry Pi

2.USB speaker

Materials needed for Morse code audio parser

1. Raspberry Pi

2.USB microphone

0x03 Morse code audio generator
Morse code audio generator is very simple to implement.

First make two audio files: short.wav and long.wav. Among them, short.wav will emit 700HZ and 100 milliseconds of audio, representing a “.” tone, and long.wav will emit 700HZ of 300 milliseconds of audio, representing a “-” tone.

Then through coding to control the playing time interval of each tone, so that text, words, and sentences can be sent according to the rules of Morse code table, and finally communication can be realized.

The picture below shows the Morse code table.

Pronunciation rules:

Di=1t, Da=3t, between DI DA=1t, between characters=3t, between words=7t

The core code of the Morse code audio generator is as follows (Note: If you need the complete source code, please leave a message in the comment area, leave your email address, and I will send it one by one):

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Description
1 for -
0 for .

Rules: Di=1t, Da=3t, between ticks=1t, between characters=3t, between words=7t
"""
from __future__ import print_function
import re, time, datetime, os, sys
import pygame

# Morse dictionnairy.
from morse_dict import *

T = 100 #millisecond

#Unit: milliseconds
def my_sleep(sleeptime):
    begin = datetime.datetime.now()
    
    while True:
        end = datetime.datetime.now()
        k = end - begin

        if (k.total_seconds()*1000) > sleeptime:
            break

def get_user_text():
    user_text = raw_input("Please enter the message:")
    user_text = user_text.lower()
    word_list = list(user_text)
    return word_list

def play_sound(path):
    pygame.mixer.music.load(path)
    pygame.mixer.music.play()
    while 1:
        if not pygame.mixer.music.get_busy():
            break

def long_pulse(is_sleep):
    play_sound("./audio/long.wav")

    if is_sleep == True:
        my_sleep(100)


def short_pulse(is_sleep):
    play_sound("./audio/short.wav")

    if is_sleep == True:
        my_sleep(100)

def gap_1t():
    #time.sleep(0.1)
    my_sleep(100)

def gap_3t():
    print("   ",end="") #short gap
    #time.sleep(0.3)
    my_sleep(300)

def gap_7t():
    print("       ",end="\n") #long gap
    #time.sleep(0.7)   
    my_sleep(700)

def play_morse_code(morse_code):
    length = len(morse_code)
    for i in range(len(morse_code)):
        if morse_code[i] == '1':
            if i != length-1:
                long_pulse(True)
            else:
                long_pulse(False)

        elif morse_code[i] == '0':
            if i != length-1:
                short_pulse(True)
            else:
                short_pulse(False)

def play_text(alpha_text):
    print("\n===================\nPlaying\n===================\n")
    alpha_text = alpha_text.lower()
    for letter in alpha_text:
        if letter in morse_dict.keys():
            morse_code = morse_dict[letter]
            play_morse_code(morse_code)
            gap_3t()
        elif letter == " ":
            gap_7t()
        else:
            print("?",end="")
            sys.stdout.flush()
            gap_3t()
    
    print("\n")

def test1():
    while True:
        play_sound("./audio/long.wav")
        my_sleep(100)
        play_sound("./audio/long.wav")
        my_sleep(100)
        play_sound("./audio/long.wav")
        my_sleep(100)
        play_sound("./audio/long.wav")
        my_sleep(100)
        play_sound("./audio/long.wav")
        my_sleep(3000)

        text = "11111"
        play_morse_code(text)
        my_sleep(3000)

def test2():
    while  True:
        text = "I LOVE YOU"
        play_text(text)
        my_sleep(3000)

if __name__ == '__main__':
    pygame.init()
    pygame.display.set_mode([300,300])
    pygame.mixer.init()
    pygame.time.delay(1000)   #Wait 1 second for the mixer to complete initialization

    #test1()
    #test2()

    text = get_user_text()
    play_text(text)

0x04 Morse code audio parser
Morse code audio parser is relatively difficult to implement. It requires some knowledge of signal processing, and the focus is on FFT transformation.

Knowledge needed:

How to set the sampling frequency?

Sampling is the conversion of a signal (that is, a continuous function in time or space) into a numerical sequence (that is, a discrete function in time or space). Shannon sampling theorem states that the sampling frequency must be higher than twice the signal frequency. Only in this way can the original continuous signal be completely reconstructed from the sampled samples.

The audio frequency generated by the Morse code audio generator in this experiment is 700HZ, so the sampling frequency of the Morse code audio parser should be slightly larger than the original audio frequency twice, the sampling frequency of this article is 1600HZ.

What is windowing?

When doing signal processing, it is often necessary to convert time domain signals into frequency domain signals. In order to enhance the clarity of the signal and suppress the spectrum leakage, it needs to be realized by adding windows. The window function used in this article is the Blackman Window (Blackman Window).

What is FFT?

FFT (Fast Fourier Transform, Fast Fourier Transform) is a fast algorithm for Discrete Fourier Transform. FFT can convert digital signals in the time domain into frequency domain signals.

The frequency calculation formula of single frequency signal?

It can be seen from the nature of fast Fourier change: when the sampling frequency (sampling_rate) is determined, when fft_size data in the waveform is taken for FFT transformation, if the fft_size data contains an integer number of cycles, the result calculated by FFT is accurate of. That is, when the sampled frequency f satisfies the following formula, the FFT calculation result is accurate.

After performing FFT transformation on the time domain signal, the frequency spectrum of fft_size/2 (fft_size is the signal length for your FFT) will appear; due to the size of the fft_size value, the sampling_rate/fft_size resolution cannot be reached, and there will be more than one larger The magnitude of the spectrum. When the fft_size value is relatively large, for a single-frequency signal, the frequency value corresponding to the largest amplitude value is the frequency of your single-frequency signal.

The core code of Morse code audio parser is as follows (Note: If you need the complete source code, please leave a message in the comment area, leave your email address, and I will send it one by one):

#!/usr/bin/Python
# -*- coding: UTF-8 -*-
from __future__ import print_function
from sys import byteorder
from array import array
from struct import pack

import pyaudio
import wave
import time
import struct
import sys
import numpy as np
import wiringpi

"""
Rules: Di=1T, Da=3T, between ticks=1T, between characters=3T, between words=7T
"""
"""
Development experience:
1. Due to external interference in the actual environment, the blank period of the character interval is polluted, and it is difficult to guarantee 3T.
2. Due to external interference in the actual environment, the da (-) may be cut off and be recognized as two tones.
"""
T = 100   #Unit: milliseconds
T3 = 3*T  #Unit: milliseconds

THRESHOLD = 300  #Threshold
CHUNK = 160
FORMAT = pyaudio.paInt16
RATE = 16000    #Sampling Rate
window = np.blackman(CHUNK) # blackman window
FREQ = 700      #700HZ
HzVARIANCE = 40
SCAMPLE_TIME_ONE_TIME = CHUNK*1000/RATE   #Each sampling time calculation result: 10 milliseconds
CHAR_INTERVAL = 250    # Character space = 3T

letter_to_morse = {
    "A" : ".-",     "B" : "-...",    "C" : "-.-.",
    "D" : "-..",    "E" : ".",       "F" : "..-.",
    "G" : "--.",    "H" : "....",    "I" : "..",
    "J" : ".---",   "K" : "-.-",     "L" : ".-..",
    "M" : "--",     "N" : "-.",      "O" : "---",
    "P" : ".--.",   "Q" : "--.-",    "R" : ".-.",
    "S" : "...",    "T" : "-",       "U" : "..-",
    "V" : "...-",   "W" : ".--",     "X" : "-..-",
    "Y" : "-.--",   "Z" : "--..",    "1" : ".----",
    "2" : "..---",  "3" : "...--",   "4" : "....-",
    "5" : ".....",  "6" : "-....",   "7" : "--...",
    "8" : "---..",  "9" : "----.",   "0" : "-----",
    " " : "/"}

def is_silent(sound_data):
    "Returns 'True' if below the 'silent' threshold"
    return max(sound_data) < THRESHOLD

# important
# Characters under normal circumstances
def encode(raw_data):
    listascii = ""
    maximum = 0
    icount = 0

    # Filter non-telegram tones
    for i in range(len(raw_data)):
        if raw_data[i] == '1':
            icount += 1
            maximum = max(maximum,icount)
        elif raw_data[i] == '0':
            icount = 0

    if maximum < 5:
        print("\n--------throw it--------\n")
        return

    # Print raw data
    #print("\n-------raw data-------\n")
    #print(raw_data);
    #print("\n-------raw data-------\n")

    # Eliminate noise (1/2): Eliminate interference '1'
    i = 0
    j = 0
    temp_list = list(raw_data)
    while i < len(temp_list):
        if temp_list[i] == '0':
            i += 1
            continue

        for j in range(i,len(temp_list)):
            if temp_list[j] != temp_list[i]:
                break

	    #
        if j-i <= 5:
            for k in range(i,j):
                temp_list[k] = '0'
        else:
            i = j

        i += 1

    raw_data=''.join(temp_list)
    #print("\n-------clean jam(1/2)-------\n")
    #print(raw_data);
    #print("\n-------clean jam(1/2)-------\n")

    # Eliminate noise (1/2): Eliminate interference '0'


    temp_raw_data = raw_data[0]
    last_number = raw_data[0]
    for i in range(1,len(raw_data)):
        if raw_data[i] != last_number:
            temp_raw_data += '#'
            last_number = raw_data[i]
        temp_raw_data += raw_data[i]

    #print(temp_raw_data)

    list1 = temp_raw_data.split("#")

    #print("\n-------modified data-------\n")
    #print(list1)
    #print("\n-------modified data-------\n")

    # Generate DI DA sequence
    for i in range(len(list1)):
        line = list1[i]
        if line[0] == '1':
            if len(list1[i]) >= 20 and len(list1[i]) < 100:    #200-1000 ms dah, throws values > 100
                listascii += "-"
            elif len(list1[i]) < 20 and len(list1[i]) > 5:     #50-200ms is dit
                listascii += "."

        if line[0] == '0':
            if len(list1[i]) >= 20 and len(list1[i]) < 60:    #200-600 ms Character interval
                listascii += "#"

    listascii = listascii.split("#")
    listascii = [i for i in listascii if(len(str(i))!=0)]
    #print("\n-------dida data-------\n")
    #print(listascii)
    #print("\n-------dida data-------\n")

    stringout=""

    for i in range(len(listascii)):
        bFind = False
        for letter,morse in letter_to_morse.items():
            if listascii[i] == morse:
                stringout += letter
                bFind = True
        if bFind == False:
            stringout += '?'

        if listascii[i] == "":
            stringout += " "

    if stringout != " ":
        print(stringout,end="")
        sys.stdout.flush()

def record():
    num_silent = 0
    snd_started = False
    oncount = 0
    offcount = 0
    status = 0
    timelist = ""

    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT,
                    channels=1,
                    rate=RATE,
                    input=True,
                    frames_per_buffer=CHUNK)

    print("##############START##############")

    while True:
        sound_data = stream.read(CHUNK, exception_on_overflow = False)

        if byteorder == 'big':
            sound_data.byteswap()

        #r.extend(sound_data)
        sample_width = p.get_sample_size(FORMAT)

        #find frequency of each chunk
        indata = np.array(wave.struct.unpack("%dh"%(CHUNK), sound_data))*window

        #take fft and square each value
        fftData = abs(np.fft.rfft(indata))**2

        # find the maximum
        which = fftData[1:].argmax() + 1
        silent = is_silent(indata)

        # signal frequency
        if silent:
            thefreq = 0
        elif which != len(fftData)-1:
            y0,y1,y2 = np.log(fftData[which-1:which+2:])
            x1 = (y2 - y0) * .5 / (2 * y1 - y2 - y0)
            # find the frequency and output it
            thefreq = (which+x1)*RATE/CHUNK
        else:
            thefreq = which*RATE/CHUNK
        #print(thefreq)

        #check frequency
        if thefreq > (FREQ-HzVARIANCE) and thefreq < (FREQ+HzVARIANCE):
            timelist += "1"
            num_silent = 0
            #print("1")
        else:
            timelist += "0"
            num_silent += 1
            #print("0")

        if num_silent*SCAMPLE_TIME_ONE_TIME > CHAR_INTERVAL and "1" in timelist:
            encode(timelist)
            timelist = ""

        # No sound within 10 seconds, reset
        if num_silent*SCAMPLE_TIME_ONE_TIME > 10*1000:
            print("reset")
            num_silent =0
            timelist = ""

    #print (timelist)
    print("##############END##############")
    #print(num_silent)
    p.terminate()

if __name__ == '__main__':
    #Raise priority
    #Note: need to run with root privileges
    wiringpi.piHiPri(1)


    record()

Note: It needs to be carried out in a room without external sound interference, otherwise the analysis process may be inaccurate.

0x05 End
Putting the Morse code audio generator and the Morse code audio parser together, listening to the ticking sound in the ear, watching the text output on the audio parser screen, at that moment, it seems that time has stopped, and I can’t help but sigh wonderful.

 

Reviews

There are no reviews yet.

Be the first to review “Homemade Morse code audio parser”

Your email address will not be published. Required fields are marked *