#
# Audio Visualizer RGB lights, v1.0
# Author: Jared Sanson
# Created: 11/01/2013
#
# LED strip will be a blue colour when the room is calm,
# and progressively turn to red as the room gets noisy.
# 
# The settings below should work fine,
# try adjusting the microphone volume/gain before you touch them!
#
# Connects to an arduino, which controls the RGB led strip.
#
# Required libraries:
# Python 2.7
# SciPy      : http://www.scipy.org/
# Console    : http://effbot.org/downloads/#console
# pySerial   : http://pyserial.sourceforge.net/
# pyAudio    : http://people.csail.mit.edu/hubert/pyaudio/
#

########## LIBRARIES ###########################################################

#import console as Console
import scipy.fftpack
import pyaudio
import time
import struct
import scipy
import math
from scipy import pi, signal
from scipy.fftpack import fft,rfft,rfftfreq,fftfreq

import serial
import struct
import time


########## VISUALIZATION SETTINGS ##############################################

# Loudness detect:
CHANNEL = 1     # frequency channel of the FFT to use (see console output to decide)
GAIN = 1.5       # audio gain (multiplier)
THRESHOLD = 0.15 # audio trigger threshold

#
ATTACK = 0.004  # amount of rowdz increase with loudness
DECAY = 0.003   # amount of rowdz decay

# Brightness:
MODULATION = 0.0        # amount of loudness flickering modulation
MIN_BRIGHTNESS = 0.5    # minimum brightness

# Hue mapping:
MIN_HUE = 200   # Aqua
MAX_HUE = 0     # Red
# Note that the hue mapping is actually a power function,
# so it will spend more time towards the MIN_HUE, and only a short time towards the MAX_HUE.

########## APPLICATION SETTINGS ################################################

#COM_PORT = 'COM3'   # COM port to use, or None to run without an arudino
COM_PORT = None

# Audio capture settings
SAMPLE_RATE = 44100
BUFFER_SIZE = 2**11     # Changing this will change the frequency response of the algorithm
#CUTOFF_FREQ = 20000     # LPF freq (Hz)


########## SUPPORT FUNCTIONS ###################################################

def tobyte(i):
    """
    Clip values that fall outside an unsigned byte
    """
    i = int(i)
    if i < 0: i = 0
    if i > 255: i = 255
    return i
    
def limit(val, vmin, vmax):
    """
    Clip values that fall outside vmin & vmax
    """
    if val < vmin: return vmin
    if val > vmax: return vmax
    return val

def mapval(val, minin, maxin, minout, maxout):
    """
    Linear value mapping between in and out
    """
    norm = (val-minin)/(maxin-minin)
    return norm*(maxout-minout) + minout
    
def thresh(val, threshold):
    """
    A bit hard to describe, but this will return 0 
    when val is below the threshold, and will
    linearly map val to anything higher than threshold.

    The effect being that above the threshold, louder
    signals will have more of an effect.
    """
    val -= threshold
    if val < 0: val = 0
    val *= (1.0/threshold)
    return val
    
def hsv2rgb(h, s, v):
    """
    Convert H,S,V to R,G,B
    H: 0.0 to 360.0, S&V: 0.0 to 1.0
    R,G,B: 0 to 255
    """
    h = float(h)
    s = float(s)
    v = float(v)
    h60 = h / 60.0
    h60f = math.floor(h60)
    hi = int(h60f) % 6
    f = h60 - h60f
    p = v * (1 - s)
    q = v * (1 - f * s)
    t = v * (1 - (1 - f) * s)
    r, g, b = 0, 0, 0
    if hi == 0: r, g, b = v, t, p
    elif hi == 1: r, g, b = q, v, p
    elif hi == 2: r, g, b = p, v, t
    elif hi == 3: r, g, b = p, q, v
    elif hi == 4: r, g, b = t, p, v
    elif hi == 5: r, g, b = v, p, q
    r, g, b = int(r * 255), int(g * 255), int(b * 255)    
    return r, g, b
    
def get_fft(data):
    """
    Run the sample through a FFT, and normalize
    """
    FFT = fft(data)
    freqs = fftfreq(BUFFER_SIZE*2, 1.0/SAMPLE_RATE)
    y = 20*scipy.log10(abs(FFT))/ 100

    y = abs(FFT[0:len(FFT)/2])/1000
    y = scipy.log(y) - 2
    return (freqs,y)
    
ffts=[]
def smoothMemory(ffty,degree=3):
    """
    Average samples. Taken from Python FFT tutorial
    """
    global ffts
    ffts = ffts+[ffty]
    if len(ffts) <=degree: return ffty
    ffts=ffts[1:]
    return scipy.average(scipy.array(ffts),0)
    

bar_len = 70
def update_bars(x,y):
    """
    Display a bar graph in the console
    """
    for i,_ in enumerate(y):
        a = int(min(max(y[i],0),1)*bar_len)

        label = str(x[i])[:5]
        label = ' '*(5-len(label)) + label

        text =  label +'[' + ('#'*a) + (' '*(bar_len-a)) + ']' + str(i)
        print(0, i+3, text)


########## SUPPORT CLASSES #####################################################

class RGBController(object):
    """
    Communicates with the Arduino
    """
    def __init__(self, port):
        self.ser = serial.Serial(port)
        time.sleep(1)

    def update(self, color):
        #print "Update color to (R:%d G:%d B:%d)" % (color[0], color[1], color[2])
        r = tobyte(color[0])
        g = tobyte(color[1])
        b = tobyte(color[2])
        packet = struct.pack('cBBB', 'X', r,g,b)
        #print packet
        self.ser.write(packet)


########## CODE ################################################################

TITLE = "Audio Visualizer"
INK = 0x1f

# Set up console
#console = Console.getconsole(0)
#console.title(TITLE)
#console.text(0, 0, chr(0xfe) + ' ' + TITLE + ' ' + chr(0xfe))
#console.text(0, 1, chr(0xcd) * 80)

# Create LPF filter
# norm_pass = 2*math.pi*CUTOFF_FREQ/SAMPLE_RATE
# norm_stop = 1.5*norm_pass
# (N, Wn) = signal.buttord(wp=norm_pass, ws=norm_stop, gpass=2, gstop=30, analog=0)
# (b, a) = signal.butter(N, Wn, btype='low', analog=0, output='ba')
# b *= 1e3


# Open Audio stream (uses default audio adapter)
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,channels=1,rate=SAMPLE_RATE,input=True,output=False,frames_per_buffer=BUFFER_SIZE)
        
# Open comms port to Arduino
if COM_PORT:
    RGB = RGBController(COM_PORT)


########## GLOBAL VARIABLES ####################################################

red = 0
green = 0
blue = 0

noisiness = 0       # Noisiness level

########## VISUALIZATION LOOP ##################################################

while True:
    ## Part 1: Sample Audio ##

    # Get audio sample
    buf = stream.read(BUFFER_SIZE)
    data = scipy.array(struct.unpack("%dh"%(BUFFER_SIZE),buf))

    ## Part 2: Perform FFT and Filtering ##

    # Filter incoming data
    #data = signal.lfilter(b,a,data)

    # Generate FFT
    freqs,y = get_fft(data)

    # Average the samples
    #y=smoothMemory(y,3)

    # Normalize
    y = y / 5

    # Average into chunks of N
    N = 25
    yy = [scipy.average(y[n:n+N]) for n in range(0, len(y), N)]
    yy = yy[:len(yy)/2] # Discard half of the samples, as they are mirrored


    ## Part 3: Algorithm ##

    # Loudness detection
    loudness = thresh(yy[CHANNEL] * GAIN, THRESHOLD)

    # Noisiness meter
    noisiness -= DECAY
    noisiness += loudness * ATTACK
    noisiness = limit(noisiness, 0.0, 1.0)

    # Brightness modulation
    modulation = MODULATION * limit(noisiness, 0.0, 1.0)
    brightness = limit(MIN_BRIGHTNESS + (loudness * modulation), 0.0, 1.0)

    # Hue modulation (power relationship)
    mapping = (10 ** limit(noisiness, 0.0, 1.0)) / 10.0
    mapping = mapping * 1.1 - 0.11
    hue = mapval(mapping, 0.0, 1.0, MIN_HUE, MAX_HUE)

    # Display colour
    red,green,blue = hsv2rgb(hue,1.0,brightness)
    if COM_PORT:
        RGB.update([int(red),int(green),int(blue)])

    # Debug information
    labels = list(yy)
    bars = list(yy)
    labels.extend(   ['-', 'loud','noise','map', 'brght', '-', 'hue','red','grn','blue'])
    bars.extend(     [0, loudness, noisiness, mapping, brightness, 0, hue/360.0, red/255.0,green/255.0,blue/255.0])

    update_bars(labels,bars)
    
########## #####################################################################