1.2 Morse - Decodificador de sonido a mensaje con Python

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 símbolos se separan por una pausa con un intervalo de una marca.

Se divide el sonido del archivo.wav en partes o ventanas, usando la función 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 símbolos 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)