miércoles, 27 de octubre de 2010

Como generar una onda de Audio para archivos WAV (PCM)

Como en su momento no encontré nada sobre este tema, aquí va una pequeña explicación de como generar audio modulado en PCM, lo que usa por ejemplo, el formato WAV.

Partiremos de algunos conceptos básicos que supongo que conoceréis:

Sobre las ondas de sonido:
  • Frecuencia de la onda: veces en un segundo que la onda se repite (o que pasa por el valor intermedio).
  • Amplitud de la onda: en este caso, la distancia entre el punto más alto y el más bajo, determinará el volumen.

Sobre su representación en el archivo:
  • Framerate: número de veces por segundo que se representa la onda (a más framerate, más cercano es a la onda original y mejor calidad).
  • Canales: número de salidas del audio (1: Mono, 2: Stereo).
  • Ancho de canal: bits que se dedican a cada frame de cada canal (suele ser de 16 bits).


Para la manipulación de los archivos WAV se puede usar la librería estándar de python wave, haciendo que el manejo del archivo sea muy simple, por lo que me centraré en la generación de la onda, que es lo que no implementa.

Supongamos, por ejemplo, que queremos generar una onda de 300Hz que dure 1 segundo, lo primero es generar una onda completa que nos servirá de base:

 - Si la frecuencia es cero, la onda es plana, así que se representaría como un solo valor, 0

Sino, tendremos para generar una onda completa, para ello se necesitan tantos frames como framerate/frecuencia, para que la onda esté completa, además, como la onda es sinusoidal, tomará valores de seno de entre 0 y 2 PI , así que por último hay que partir de la base b = (2 * pi) / frames y  multiplicarlo por cada frame, el código de esta parte quedaría así:


import math

def getBase(freq, framerate):

    # Si no es una onda plana
    if (freq != 0):
        base = []

        # Se obtiene el numero de frames a generar
        frames = int(framerate / abs(freq))

        # Y la base de los valores que tomaran
        p = (math.pi * 2) / frames

        # Por ultimo se genera la onda sinusoidal entre seno de  0 y 2*Pi
        for i in xrange(frames):
            base.append(math.sin(i * p))

    # Si es una onda plana
    else:
        base = [0]

    return base


El siguiente paso es añadir a la onda base el volumen y los canales, para añadir el volumen simplemente hay que multiplicar el valor de un frame por el volumen, el valor del volumen (usando PCM con signo, como en WAV) puede variar entre 0 y 2**(ancho de banda - 1) - 1, para 8 bits sería entre 0 y 127, y para 16 bits, entre 0 y 32767.
Por ultimo, solo hay que repetir el frame por cada canal (aunque si va a ser la misma onda no hay motivos para usar múltiples canales...)

# Fija el volumen y los canales
def setVolChan(src, vol, n_can):
    out = []

    for i in src:
        n = int(i * vol) # Se aplica el volumen
        out.append([n] * n_can) # Y se replica para otros canales

    return out



A continuación hay que extender la onda para todo el tiempo que cubra

# Extiende la base a todo el tiempo
def mkFrames(base, t, framerate):
        base_flen = len(base) / float(framerate) # Lo que dura la base

        n = int(round(t / base_flen)) # Las veces que hay que repetirla

        return base * n


Y solo queda convertir la onda a una cadena (es el formato que utiliza la librería wave), si los canales fueran de 8 bits se podría usar una funcion de array directamente:

cadena = array.array('b', original).tostring()


Sino, hay que hacerlo a mano


def arr2Stream(sample, width):
    stream = ""
    for i in sample: # Por cada frame
        for k in i:  # Y cada canal
            for j in xrange(width):
                stream += chr(k & 255 ) # Se extrae un byte
                k >>= 8

    return stream


Un código completo sería (los cambios se hacen en las variables al principio del codigo), entonces [audio_sample.py]:

#!/usr/bin/env python

canales = 2 # Stereo
framerate = 11025
ancho = 16
volumen = 10000 # Para un ancho de canal de 16 bits

frecuencia = 300 # Hz
duracion = 2 # Segundos

archivo = "salida.wav"

def arr2Stream(sample, width):
    stream = ""
    for i in sample: # Por cada frame
        for k in i:  # Y cada canal
            for j in xrange(width):
                stream += chr(k & 255 ) # Se extrae un byte
                k >>= 8

    return stream

def getBase(freq, framerate):

    # Si no es una onda plana
    if (freq != 0):
        base = []

        # Se obtiene el numero de frames a generar
        frames = int(framerate / abs(freq))

        # Y la base de los valores que tomaran
        p = (math.pi * 2) / frames

        # Por ultimo se genera la onda sinusoidal entre seno de  0 y 2*Pi
        for i in xrange(frames):
            base.append(math.sin(i * p))

    # Si es una onda plana
    else:
        base = [0]

    return base

# Fija el volumen y los canales
def setVolChan(src, vol, n_can):
    out = []

    for i in src:
        n = int(i * vol) # Se aplica el volumen
        out.append([n] * n_can) # Y se replica para otros canales

    return out

# Extiende la base a todo el tiempo
def mkFrames(base, t, framerate):
        base_flen = len(base) / float(framerate) # Lo que dura la base

        n = int(round(t / base_flen)) # Las veces que hay que repetirla

        return base * n

def genFrames(freq, t, framerate, vol, chan, width):
        s = getBase(freq, framerate)
        s = setVolChan(s, vol, chan)
        s = mkFrames( s, t, framerate)
        return arr2Stream(s, width)

import math, wave

w = wave.open(archivo, "wb")

w.setnchannels(canales)
w.setsampwidth(ancho / 8)
w.setframerate(framerate)

w.writeframes(genFrames(frecuencia, duracion, framerate, volumen, canales, ancho / 8))

w.close()


Ve con root =)

3 comentarios:

  1. hola, tengo una duda, a ver si me puedes alludar.

    estoy tratando de distorcionar una onda, que simplemente es cortarla despues de que pasa un rango, el concepto lo entiendo. lo que no entiendo es como puedo manipular la onda si esta representada como float en un rango de 1.0 y - 1.0 y no como un entero

    ResponderEliminar
  2. No estoy seguro de entender el problema, pero creo que puedes cortar la onda en la función "setVolChan" después de "n = int(i * vol)" con algo como:

        if n > loquesea:
            n=loquesea

    Saludos

    ResponderEliminar
  3. hola, gracias por responder, si al final el problema lo resolvi así

    if(in[i] > (max / 100) * por)
    in[i] = (max / 100) * por;
    else if(in[i] < (min / 100) * por)
    in[i] = (min / 100) * por;
    out[i] = in[i];

    lo que no comprendia era como usar ese rango 1.0 y -1.0 ja ja estaba acostumbrado a que era un array de ints, no de floats

    ResponderEliminar