3. Morse Decodificador de untono

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)

Morse – Decodificador de sonido de Un simbolo

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.

telegrafomarca

Al contar el número de intevalos (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, contanto las marcas, la relacion entre marcas y pausas indicará el símbolo recibido.

Algoritmo en python

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

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 Fourirer fft() se encuentra disponible en las librerias «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

Analisis 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 analisis 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.


Programa resumido

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:  -