1.1. Morse por Tonos - codec y deco con Python



1. Morse - Tonos

Un generador de tonos permite "escuchar" la señal morse de forma similar a la tradicional del telégrafo.

telegrafooperadora

Un punto '.' es la referencia o marca para el sonido morse, teniendo que :

Para identificar los tonos que representan puntos, rayas o espacios, se intercala una pausa con la misma duración que una marca.

morse codificadortono 01

Para el ejemplo, un punto con una duración de 0.1 segundos se expresa con una señal senoidal de 440Hz (una marca, nota musical "La") realizado con muestreo de 11025 veces por segundo.

El resultado buscado para un mensaje completo en código Morse es por ejemplo:

mensaje = 'ESPOL impulsando la sociedad del conocimiento'
traducido = '. ... .--. --- .-.. .. -- .--. ..- .-.. ... .- -. -.. --- .-.. .- ... --- -.-. .. . -.. .- -.. -.. . .-.. -.-. --- -. --- -.-. .. -- .. . -. - --- '

sonido del mensaje morse: morsetonoESPOL.wav



2. Morse un tono - Codec Algoritmo en Python

Un tono para un punto '.', una raya '-', o un espacio ' ', puede ser generado con una función en python.

El tono se realiza con los valores muestreados del tono, separado del próximo '.' o un '-', añadiendo una pausa al final.

El resultado se prepara para ser guardado en un archivo.wav, haciendo que los valores tengan el formato dtype='int16' que es necesario para la generación del archivo de sonido.

# Código Morse -  Generador de tonos
# propuesta: edelros@espol.edu.ec
import numpy as np
import scipy.io.wavfile as waves

def morsetono(simbolo, duracion=0.1 , fs=440, muestreo=11025):
    # duracion=0.1   # segundos de '.' ó ' '
    # fs=440         # Hz del tono
    # muestreo=11025 #en .wav:44100,22050,11025

    marca = 2        # un punto y pausa
    if (simbolo=='-'):
        marca = 4    # raya y pausa
        
    tonodura = marca*duracion     # en segundos
    dt = 1/muestreo 
    t  = np.arange(0,tonodura,dt)  # marcas/tiempo
    tono = np.zeros(len(t), dtype='int16') #tono vacio
    
    volumen = 0.8      # rango [0,1)
    amplitud = int((2**15)*volumen)  #wav 16 bits
    w = 2*np.pi*fs     #frecuencia en radianes
    suena = int((marca-1)*duracion*muestreo)
      
    if (simbolo=='.' or simbolo=='-'):
        for i in range(0,suena):
            tono[i] = amplitud*(np.sin(w*t[i]))
    
    return(muestreo, tono)

Para realizar una prueba a la función, se puede generar un tono '.', '-' y escuchar el resultado en un archivo.wav, similar a los mostrados al inicio para cada símbolo.

# Una prueba
simbolo = '.'
muestreo, sonido=morsetono(simbolo)

# Salida # Archivo de audio.wav
waves.write('morsetonopunto.wav', muestreo, sonido)

Para crear los tonos de un mensaje y escucharlos en un archivo.wav, se procesa el mensaje traducido en morse, símbolo a símbolo, para añadir un punto, una raya o un espacio. Los resultados de cada símbolo se acumulan en el vector sonido.

# traducido = input('Escriba el mensaje morse: ')
traducido = '. ... .--. --- .-..   .. -- .--. ..- .-.. ... .- -. -.. ---   .-.. .-   ... --- -.-. .. . -.. .- -..   -.. . .-..   -.-. --- -. --- -.-. .. -- .. . -. - --- '
# archivo = input('nombre del archivo a guardar: ')
archivo = 'morsetonotest.wav'

# PROCEDIMIENTO
muestreo = 11025  # para .wav: 44100, 22050, 11025
duracion = 0.04

# morse en sonido, como lista por tamaño desconocido
sonido=[]
for i in range(0,len(traducido)):
    muestreo, untono = morsetono(traducido[i], duracion)
    for j in range(0,len(untono)):
        sonido.append(untono[j])
# para .wav convierte en arreglo numpy de 'int16' 
sonido = np.asarray(sonido, dtype='int16')

# SALIDA archivo.wav
print('muetreo: ',muestreo)
print('muestras sonido: ', len(sonido))
print('.... archivo: ' + archivo +'  guardado ....')
waves.write(archivo, muestreo, sonido)

Se crea un archivo.wav con la frecuencia de muestreo y el vector sonido, que puede ser ejecutado con un programa como "windows media player".

muetreo:  11025
muestras sonido:  180810
.... archivo: morsetonotest.wav  guardado ....

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



3. Morse un tono - Deco de símbolos con Python

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)

Analisis de archivo.wav en python

Para analizar el sonido de un símbolo Morse, se inicia por recordar la duración de cada símbolo:

Para la detección de un tono, se revisa la frecuencia activa en varios intervalos de tiempo o ventanas.

morse codificadortono 01

Al contar el número de intervalos (marcas) que aparece un tono vs. el número de intervalos de pausa, se podrá determinar si el sonido corresponde a un punto, una raya o un espacio. Es el proceso contrario a lo realizado para crear el tono, contando las marcas, la relación entre marcas y pausas indicará el símbolo recibido.

El sonido para el análisis se obtiene de un archivo.wav, creado con el generador de tonos para un punto '.' o raya '-'. La lectura del archivo proporciona los datos de sonido, y frecuencia de muestreo.

Para el análisis, se supondrá que la ventana tiene la mitad de la duración de un punto, para el ejercicio es 0.04 segundos. Así, una ventana tiene la mitad de muestras que una marca '.' o pausa, obteniendo al menos dos ventanas por cada punto o pausa y tres ventanas por cada raya. Se cuentan las ventanas que tienen sonido o pausa y se comparan para determinar el símbolo.

Que existan al menos dos ventanas por cada punto tienen relación con el muestreo de Nyquist.

Lo expuesto se prueba primero analizando solo una ventana:

# Código Morse -  Determina un simbolo a partir de UN sonido
# 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

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

# PROCEDIMIENTO
duracion = 0.04   # segundos de un punto
partes   = 2        # divisiones de un punto

# Extraer ventanas para espectro por partes
tventana = duracion/partes
mventana = int(muestreo * tventana) # muestras de una ventana

# Observacion intermedia
unaventana = sonido[0:mventana]

# SALIDA  GRAFICA
print('muestreo:', muestreo)
print('muestras en sonido', len(sonido))
print('intervalo de ventana: ', tventana)
print('muestras por ventana: ', mventana)

plt.figure(1)
plt.subplot(211)
plt.plot(sonido, label='sonido')
plt.xlabel('muestras')
plt.ylabel('sonido y ventanas')
k = 1
for i in range(0,int(len(sonido)/mventana)):
    plt.axvline(x=k*mventana, color='r', linestyle='--')
    k = k + 1
plt.subplot(212)
plt.plot(unaventana)
plt.xlabel('muestras')
plt.ylabel('unaventana')
plt.margins(0)
plt.show()
muestreo: 11025
muestras en sonido 1764
intervalo de ventana: 0.02
muestras por ventana: 220


4. Análisis de una ventana

Para determinar si existe un tono en una ventana, se analiza la existencia de una señal observada en el dominio de la frecuencia.

En el dominio de la frecuencia se requiere usar la "Transformada de Fourier" de la señal en la ventana. La Transformada rápida de Fourier fft() se encuentra disponible en las librerías "scipy" y el rango de frecuencias para la gráfica se obtiene con fftfreq().

En el resultado se busca la frecuencia (np.argmax()) con mayor magnitud (np.abs()) y se determina si corresponde al "tono" morse esperado.

# Espectro de Fourier de una ventana [0:nventana]
xf = fourier.fft(unaventana)
xf = np.abs(xf)  # magnitud de xf
tono = np.argmax(xf)  # tono en donde

# frecuencias para eje
frq = fourier.fftfreq(mventana, 1/muestreo ) 

# SALIDA  GRAFICA /Observacion intermedia
print('frecuencia tono (Hz): ',frq[tono])

plt.figure(2)
plt.xlabel('Frecuencia Hz')
plt.ylabel('Magnitud')
plt.stem(frq,xf)
plt.show()
frecuencia tono (Hz):  451.022727273

Análisis del archivo.wav de un símbolo Morse

Para analizar todo el archivo de sonido, los datos se segmentan por "ventanas" en una matriz, cada fila corresponde a las muestras de una ventana de tiempo.

La matriz debe ser rectangular, por lo que es necesario confirmar que el número de muestras por ventana sean iguales. En caso de no ser así, se realiza un ajuste del ancho del sonido para que el numero de muestras en el archivo sea múltiplo del número de muestras por ventana.

Observar el resultado de crear varias ventanas de tiempo, es semejante a tener un cuaderno en el que cada hoja tiene una imagen con los valores de la señal en cada ventana.

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

# SALIDA
print('muestras en sonido: ', antes)
print('muestras recortadas a: ', anchosonido)
print('ventanas: ')
print(ventanas)
muestras en sonido:  1764
muestras recortadas a:  1760
ventanas: 
[[     0   6504  12602 ..., -22161 -24942 -26163]
 [-25748 -23722 -20212 ...,   9594   3241  -3315]
 [ -9663 -15408 -20188 ...,  25762  26158  24919]
 ..., 
 [-15377  -9629  -3278 ...,  -9733 -15468 -20236]
 [-23738 -25755 -26161 ...,      0      0      0]
 [     0      0      0 ...,      0      0      0]]

Para observar el resultado se usa una gráfica 3D de superficie.

# Observacion intermedia
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

nfilas,ncolumnas = np.shape(ventanas)
x = np.arange(0,ncolumnas) # valores para cada eje
y = np.arange(0,nfilas)
X, Y = np.meshgrid(x, y) # valores para punto.
Z = ventanas             # Matriz

# Salida, gráfico 3d
fig  = plt.figure(3,figsize=plt.figaspect(0.5))
ax   = fig.add_subplot(1, 2, 1, projection='3d')
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
                       cmap=cm.coolwarm, linewidth=0,
                       antialiased=False)
plt.xlabel('muestras')
plt.ylabel('ventana')
plt.show()

El proceso de análisis se repite para cada ventana (una fila) en la matriz, guardando cada resultado en un vector "marcas".

# Analiza con FFT todas las ventanas del sonido
filas, columnas = np.shape(ventanas)
marcas = np.zeros(filas,dtype=float)

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

Observamos el resultado en una gráfica 2D.

# SALIDA  GRAFICA  /Observacion intermedia
print(marcas)
plt.figure(4)
plt.stem(marcas)
plt.xlabel('número de marca')
plt.ylabel('frecuencia del tono mas fuerte.')
plt.show()
[ 451.02272727  451.02272727  451.02272727  451.02272727  451.02272727  451.02272727    0.            0.        ]

Para determinar si es punto '.', raya'-' o espacio ' ', se cuentan los noceros y los ceros del arreglo de marcas, la relación entre ellos determina el símbolo que representa el sonido del archivo.wav.

# determina el simbolo
noceros = np.count_nonzero(marcas)
ceros  = len(marcas)-noceros
relacion = int(noceros/ceros)
if (relacion==3):
    simbolo = '-'
if (relacion==1):
    simbolo = '.'
if (relacion==0):
    simbolo = 'espacio'

# SALIDA
print('relacion noceros/ceros:', relacion)
print('simbolo morse detectado: ', simbolo)
relacion noceros/ceros: 3
simbolo morse detectado:  -

Nota: Observe que cualquier frecuencia en una ventana marca un punto '.'. El detalle es tratado en "Morse - Decodificador del sonido de un mensaje" en : # Busca frecuencia del tono punto.



5. Morse por tonos - Algoritmo con Python

El resultado del programa resumido se muestra a continuación:

# Código Morse -  Determina un simbolo a partir de UN sonido
# 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

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

# PROCEDIMIENTO
duracion = 0.04   # segundos de un punto
partes   = 2      # 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
antes = len(sonido)
anchosonido = int(len(sonido)/mventana)*mventana 
sonido   = np.resize(sonido,anchosonido)
ventanas = np.reshape(sonido,(-1,mventana))  
# -1 indica que calcule las filas

# Analiza con FFT todas las ventanas del sonido
filas, columnas = np.shape(ventanas)
marcas = np.zeros(filas,dtype=float)

# Espectro de Fourier de cada ventana
# frecuencias para eje
frq = fourier.fftfreq(mventana, 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] 

# determina el simbolo
noceros  = np.count_nonzero(marcas)
ceros    = len(marcas)-noceros
relacion = int(noceros/ceros)
if (relacion==3):
    simbolo = '-'
if (relacion==1):
    simbolo = '.'
if (relacion==0):
    simbolo = 'espacio'

# SALIDA
print('relacion de marcas:', relacion)
print('simbolo morse detectado: ', simbolo)
relacion de marcas: 3
simbolo morse detectado:  -