4. Morse Decodificador de sonido

Morse – Decodificador de un mensaje Morse desde el sonido en archivo.wav

Para analizar el sonido de un mensaje Morse, se procede de forma semejante al decodificador de un tono morse, revisando la duración de cada símbolo: punto o espacio tienen duración de una marca, la raya tiene una duración de 3 marcas, los simbolos se separan por una pausa con un intervalo de una marca.

telegrafomarca

Se divide el sonido del archivo.wav en partes o ventanas, usando la funcion separaventanas().

ventanas de sonido

Cada ventana se analiza con la transformada de Fourier para encontrar la frecuencia del sonido mas fuerte o marcas de puntos '.', '-', ó ' '. El resultado de análisis de las ventanas es un vector con la marca de frecuencia más fuerte para cada una de las ventanas. (función marcasdeventanas())

La frecuencia o ‘tono frecuente’ se usa como una referencia para diferenciar si existe una señal para un punto o raya Morse. El tono frecuente se obtiene mediante la «moda» en el vector marcas usando scipy.stats.itemfreq() y usando la fila donde donde está la mayor cuenta usando np.argmax().

Para facilitar el análisis de un tono en una marca, se realiza una conversión a una secuencia binaria, usando '1' ó ,'0' si se encuentra el ‘tono frecuente’ en cada ventana.

Para el ejemplo, las ventanas se convierten al vector:
[1 1 1 1 1 1 0 0]

Al contar la duracion de cada intevalo, se determina si el sonido corresponde a un punto, una raya o una espacio:

Ejemplo:
. ... .--. --- .-..

Algoritmo en python

El sonido se obtiene de un archivo.wav del generador de tonos para un punto '.' o raya '-', del cual se leen los datos de sonido, y frecuencia de muestreo.

Para el ejemplo, el sonido del mensaje morse: morsetonoESPOL.wav

Las funciones para cada uno de los procesos descritos se encuentran a continuación

# Código Morse -  Determina un mensaje desde sonido.wav
# propuesta: edelros@espol.edu.ec
import numpy as np
import matplotlib.pyplot as plt
import scipy.io.wavfile as waves
import scipy.fftpack as fourier
import scipy.stats as stats

def separaventanas(sonido, muestreo, duracion = 0.04, partes = 8 ):
    # Divide el sonido en partes o ventanas para estudio
    # duracion = 0.04   # segundos de un punto
    # partes = 8        # divisiones de un punto
    # Extraer ventanas para espectro por partes
    tventana = duracion/partes
    mventana = int(muestreo * tventana) # muestras de una ventana

    # Ajuste de muestras de ventanas para matriz
    anchosonido  = int(len(sonido)/mventana)*mventana 
    sonidoajuste = np.resize(sonido,anchosonido)
    ventanas = np.reshape(sonidoajuste,(-1,mventana))  
    # -1 indica que calcule las filas
    return(ventanas)

def marcasdeventanas(ventanas, muestreo):
    # Analiza con FFT todas las ventanas del sonido
    filas, columnas = np.shape(ventanas)
    marcas = np.zeros(filas,dtype=int)

    # Espectro de Fourier de cada ventana
    # frecuencias para eje frq = fourier.fftfreq(mventana, 1/muestreo) 
    frq = fourier.fftfreq(columnas, 1/muestreo)  
    for f in range(0,filas,1):
        xf = fourier.fft(ventanas[f])
        xf = np.abs(xf)        # magnitud de xf
        tono = np.argmax(xf) # tono, frecuencia mayor 
        marcas[f] = frq[tono]
    return(marcas)

def secuenciabinaria(marcas): 
    # Busca el tono frecuente mayor que cero
    tonosventana = stats.itemfreq(marcas)
    donde = np.argmax(tonosventana[1:,1])
    tonopunto = tonosventana[donde+1,0]

    # Convierte los tonos a secuencia binaria
    secuencia = ''
    for valor in marcas:
        if (valor==tonopunto):
            secuencia = secuencia + '1'
        else:
            secuencia = secuencia + '0'
    return(secuencia)

def duracionmarcas(secuencia):
    # Duración de cada marca
    conteo = []
    caracter = '1'
    if (len(secuencia)>0):
        caracter = secuencia[0]
    i = 0
    k = 0
    while (i<len(secuencia)):
        if (secuencia[i]==caracter):
            k = k + 1
        else:
            conteo.append([int(caracter),k])
            if (caracter=='1'):
                caracter = '0'
            else:
                caracter = '1'
            k = 1
        i = i+1
    conteo = np.array(conteo)
    return(conteo)

def marcasmorse(conteo):
    # Determina la base de un tono
    veces = stats.itemfreq(conteo[:,1])
    donde = np.argmax(veces[:,1])
    base  = veces[donde,0]

    # Genera el código Morse
    tolera = 0.2 # Tolerancia en relacion
    bajo   = 1-tolera
    alto   = 1+tolera
    morse  = ''
    for j in range(0,len(conteo)):
        relacion = conteo[j,1]/base
        simbolo  = conteo[j,0]
        if (simbolo==1):
            if (relacion>(1*bajo) and relacion<(1*alto)):
                morse = morse + '.'
            if  (relacion>(3*bajo) and relacion<(3*alto)):
                morse = morse + '-'
        if  simbolo==0 :
            if (relacion>(3*bajo) and relacion<(3*alto)):
                morse = morse + ' '
            if (relacion>(7*bajo) and relacion<(7*alto)):
                morse=morse+'   '
    return(morse)

# INGRESO 
# archivo = input('nombre del archivo: ')
archivo = 'morsetonoESPOL.wav'
muestreo, sonido = waves.read(archivo)

# PROCEDIMIENTO

ventanas  = separaventanas(sonido, muestreo)
marcas    = marcasdeventanas(ventanas, muestreo)
secuencia = secuenciabinaria(marcas)
conteo    = duracionmarcas(secuencia)
morse     = marcasmorse(conteo)

# SALIDA
print('codigo en morse: ')
print(morse)
codigo en morse: 
. ... .--. --- .-..   .. -- .--. ..- .-.. ... .- -. -.. ---   .-.. .-   ... --- -.-. .. . -.. .- -..   -.. . .-..   -.-. --- -. --- -.-. .. -- .. . -. - ---

Revisión de valores en el procedimiento

Se presentan algunos de los valores intermedios para observar el proceso de detección de símbolos Morse.

# Salida /Observación intermedia
print('ventanas analizadas: ',len(marcas))
print('marcas: ', marcas)
ventanas analizadas:  3287
marcas:  [400 400 400 ...,   0   0   0]

Para facilitar el proceso de detección morse se convierten las marcas a una secuencia binaria, usando como referencia la frecuencia del tono de un punto determinada en la sección anterior.

# Salida /Observación intermedia
if len(secuencia)<100:
    print(secuencia)
else:
    print(secuencia[0:200] + ' ... ')
11111111000000000000000000000000111111110000000011111111000000001111111110000000000000000000000011111111000000001111111111111111111111111000000011111111111111111111111110000000111111111000000000000000 ... 

Cambiar la secuencia a simbolos morse, consiste en determinar la relación entre las veces que aparecen los ‘1’s y ‘0’s, el resultado se puede guardar en un arreglo.

La base de cuántas marcas corresponden a un punto de estima como la «moda» de las veces en que se repite un uno o un cero.

print('simbolo, cuenta:')
print(conteo)
simbolo, cuenta:
[[ 1  8]
 [ 0 24]
 [ 1  8]
 [ 0  8]
 [ 1  8]
 [ 0  8]
 [ 1  9]
 [ 0 23]
 [ 1  8]
 [ 0  8]
 [ 1 25]
 [ 0  7]
 [ 1 25]
...
# Salida
print('codigo en morse: ')
print(morse)
codigo en morse: 
. ... .--. --- .-..   .. -- .--. ..- .-.. ... .- -. -.. ---   .-.. .-   ... --- -.-. .. . -.. .- -..   -.. . .-..   -.-. --- -. --- -.-. .. -- .. . -. - ---

Con el resultado puede usar el decodificador morse de los temas anteriores.

Referencia: Código Morse Wikipedia, Recommendation ITU-R M.1677-1 (10/2009) International Morse code, Leon-Couch, 5–9 Señalización Pasabanda Modulada Binaria (OOK)