#!/usr/bin/python

##  VU metering taken from: www.blinkenlight.net
##                          Copyright 2011 Udo Klein
##  Tiertex coding: Djerk Geurts https://djerk.nl
##
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program. If not, see http://www.gnu.org/licenses/

# TXT
import serial, time, dbus
# VU
import alsaaudio, audioop, sys, math
# CPU/MEM (etc)
import os, psutil

#GLOBAL
path = "/dev/ttyACM0"
device = serial.Serial(port=path, baudrate=9600)
## #v=nn Set Brightness nn = 00 to 03
# 00 = 25%, 01 = 50%, 02 = 75%, 03 = 100%
brightness = "#v=03"
asc = 0
saver_t = 0
saving = 0

#SAVER
saver_Xoffset = 32 # Initial offset, width of matrix
XX = saver_Xoffset
saver_Yoffset = 0  # Initial offset
YY = saver_Yoffset
SCROLL = "LIN-AMP" # Text to scroll

#TXT
## #f=nn Set the current font. PIXOS has 4 fonts.
#fontsize = "#f=00" # 00 == 5x3
#fontsize = "#f=01" # 01 == 7x3
fontsize = "#f=02" # 02 == 9x5
#fontsize = "#f=03" # 03 == 12x5
## #xnn Set X printing position nn =-99 to 99
xnn = "#x00"
## #ynn Set Y printing position nn =-99 to 99
yn1 = "#y00"
yn2 = "#y09"
## #w Clears the display. 
# #Wxxyywwhh Clears an area defined by coordinates xxyy and width height wwhh.
## Alignment
# '' Left justifies the current string.
# #c Centres the current string. 
# #rnn Right justifies current string. The string is printed ending at coordinate nn.
# 32x16 >> nn =< 32
align = ''
## Lines in text file
lines = 1 
## Max chars in string
txt_l = 16

# CPU & memory load (psutil)
PROCNAME_1 = "guitarix"  # jackdbus
PROCNAME_2 = "rakarrack" # rakarrack
p1_proc = ''
p2_proc = ''
test_proc = 90
# Left aligned : range(14, -1, -1)
# Right aligned: range(17, 33,  1)
p1_line  = 11
p1_bar  = ["#p" + str(s).zfill(2) + str(p1_line) for s in range(14, -1, -1)]
p2_line  = p1_line +1
p2_bar  = ["#p" + str(s).zfill(2) + str(p2_line) for s in range(14, -1, -1)]
#p3_line  = p2_line +1
#p3_bar = ["#p" + str(s).zfill(2) + str(p3_line) for s in range(17, 33, 1)]
cpu_line = 11
cpu_bar = ["#p" + str(s).zfill(2) + str(cpu_line) for s in range(17, 33, 1)]
mem_line = cpu_line +1
mem_bar = ["#p" + str(s).zfill(2) + str(mem_line) for s in range(17, 33, 1)]

#VU
in_line  = 14
out_line = in_line +1
lin_bar  = ["#p" + str(s).zfill(2) + str(in_line) for s in range(14, -1, -1)]
rin_bar  = ["#p" + str(s).zfill(2) + str(in_line) for s in range(17, 33, 1)]
lout_bar = ["#p" + str(s).zfill(2) + str(out_line) for s in range(14, -1, -1)]
rout_bar = ["#p" + str(s).zfill(2) + str(out_line) for s in range(17, 33, 1)]

# Device(s)
#
snd_c_card = 'hw:4,0'
# Capture input
c_input = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, snd_c_card)
c_input.setchannels(2)                         # Stereo
c_input.setrate(48000)                         # 48000 Hz
c_input.setformat(alsaaudio.PCM_FORMAT_S16_LE) # Signed 16-bit little endian
c_input.setperiodsize(320)
#
snd_p_card = 'hw:4,1'
# Playback input
p_input = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, snd_p_card)
p_input.setchannels(2)                         # Stereo
p_input.setrate(48000)                         # 48000 Hz
p_input.setformat(alsaaudio.PCM_FORMAT_S16_LE) # Signed 16-bit little endian
p_input.setperiodsize(320)

lo = 160
hi = 32000
log_lo = math.log(lo)
log_hi = math.log(hi)

lin_max  = 0
rin_max  = 0
lout_max = 0
rout_max = 0
vu_fps = 40.0
lin_vu  = 0
rin_vu  = 0
lout_vu = 0
rout_vu = 0
lin_t  = 0
rin_t  = 0
lout_t = 0
rout_t = 0
lin_maxp  = ''
rin_maxp  = ''
lout_maxp = ''
rout_maxp = ''

# Convert log scale, calculate value & max
def vu_log(data,vu_max,max_t):
  # transform data to logarithmic scale
  vu = (math.log(float(max(audioop.max(data, 2),1)))-log_lo)/(log_hi-log_lo)
  # Calculate value
  vu = (min(max(int(vu*15),0),15))
  if vu >= vu_max:
    max_t = (3 * vu_fps) # keep max for 3 seconds
    vu_max = vu
  else:
    max_t = max(0, max_t-1) # Reduce max timer by 1 until 0
  return vu, vu_max, max_t

# Contruct max pixel string
def max_pix(vu_max,max_t,side,line):
  # Erase pixel if timer has expired
  if max_t == 0:
    maxp = ''
    vu_max = 0
  else:
    # Otherwise test if 2 seconds have passed
    if max_t < (1 * vu_fps):
      vu_max = int(vu_max*max_t/(1*vu_fps)) # Sweep max to 0 in the remaining second
    # Set pixel if value not too small
    if vu_max > 3:
      if side == "left":
        pix = str(15-vu_max)
      else: # Assume right if not left
        pix = str(16+vu_max)
      maxp = ("#p" + pix.zfill(2) + str(line))
    # Erase pixel if too small
    else:
      maxp = ''
  return maxp, vu_max

try:
  # ledmatrix settings
  device.write("#w"+fontsize+brightness+"\n")
  #print "cleared screen"

# Main routine
  while True:
    # Test if screen saver should run
    if saver_t >= (vu_fps * 300): # Wait 300 seconds (5 mins)
      saver_t = 15000
      # Set saver brightness to lowest setting
      device.write("#v=00#f=03\n") # "#v=00" == 25% brightness & "#f=03" == 12x5 pixel font
      if XX < 0:
        hor = str(XX).zfill(4)
      else:
        hor = str(XX).zfill(3)
      if YY < 0:
        ver = str(YY).zfill(3)
      else:
        ver = str(YY).zfill(2)
      device.write("#w#z"+hor+"#y"+ver+SCROLL+"\n")
      if XX == -(len(SCROLL)*6+100):
        XX = 32
        if YY == 15:
          YY = -8
        else:
          YY += 1
      else:
        XX -= 1
      saving = 1
    else:
      #device.write("#w"+fontsize+brightness+"\n")
      saving = 0

# Run 2 times a second
    if (asc == 0 or asc == (vu_fps/2)) and saving == 0: # Check text twice per second
# Fetch text from file
#      if saving == 0:
      try:
        with open ("LEDdisplay.txt", "r") as f_input:
          txt=f_input.readlines()

        txt = map(lambda s: s.strip(), txt)
        i = len(txt)
        if i <= 1:
          txt.append(' ')

        while i > 0:
          i -= 1
          if txt[i] == '':
            txt[i] == ' '

      except IOError:
        txt = ["DJERK's","LIN-AMP"]

# Limit string length
      shrt = [x[0:txt_l] for x in txt]
# Escape hashes with a hash (#)
      txt = [t.replace('#', '##') for t in shrt]
# Print text
      device.write("#W00003209"+xnn+yn1+align+txt[0]+"\n")
      #asc = int(vu_fps) # Track based on FPS
# PSUTIL
    # Check process id once in a while
    if asc == 0 and saving == 0: # Run once a 'second'
      if test_proc == 90: # Run once every 1.5 minutes
        test_proc = 0 # Reset counter
        for proc in psutil.process_iter():
          if proc.name == PROCNAME_1:
            p1_proc = proc.pid
            p1 = psutil.Process(p1_proc)
          elif proc.name == PROCNAME_2:
            p2_proc = proc.pid
            p2 = psutil.Process(p2_proc)
        # Set code in case process can't be found
        if p1_proc == '':
          p1_cpu = (''.join(p1_bar[12:14])) # Set 2 dots if process isn't found
        if p2_proc == '':
          p2_cpu = (''.join(p2_bar[12:14])) # Set 2 dots if process isn't found
      else:
        test_proc += 1
    # Fetch psutil stats at 4 intervals (vu_fps / 4)
    elif (asc == int(vu_fps/100*10) or asc == int(vu_fps/100*35) or asc == int(vu_fps/100*60) or asc == int(vu_fps/100*85)) and saving == 0:
      if p1_proc != '':
        p1_cpu = (''.join(p1_bar[0:int(min(15,round(p1.get_cpu_percent(0.0)/100*15)))]))
      if p2_proc != '':
        p2_cpu = (''.join(p2_bar[0:int(min(15,round(p2.get_cpu_percent(0.0)/100*15)))]))
      sys_cpu = int(min(15,round(psutil.cpu_percent(interval=0.0)/100*15)))
      sys_mem = int(round(psutil.virtual_memory().percent/100*15))
      #sys_mem = 6 # Ubuntu default psutil is broken
      # [sudo apt-get install python-dev && sudo pip install psutil --upgrade]
# Print psutil output
      device.write("#W00103203"+p1_cpu+p2_cpu+(''.join(cpu_bar[0:sys_cpu]))+(''.join(mem_bar[0:sys_mem]))+"#p0010#p0510#p1010#p1510#p1610#p2110#p2610#p3110\n")

# Fetch Capture input VU from ALSA
    l, data = c_input.read()
    if l*1 == -32 or l*1 == 0:
      # Try again if we failed the first time, pyaudio fails 50% :(
      l, data = c_input.read()
    if l > 0:
      l_data = audioop.tomono(data, 2, 1, 0) # 4bytes, Left*1, Right*0 (muted)
      r_data = audioop.tomono(data, 2, 0, 1) # 4bytes, Left*0 (muted), Right*1
      # Transform data to logarithmic scale and calculate value
      lin_vu, lin_max, lin_t = vu_log(l_data,lin_max,lin_t)
      rin_vu, rin_max, rin_t = vu_log(r_data,rin_max,rin_t)
      # Display max pixel
      lin_maxp, lin_max = max_pix(lin_max,lin_t,"left",in_line)
      rin_maxp, rin_max = max_pix(rin_max,rin_t,"right",in_line)
    #else:
    #  print "l != -32 and l < 0. This should not happen... :(", str(l)
# Fetch Playback output VU from ALSA
    l, data = p_input.read()
    if l*1 == -32:
      # Try again if we failed the first time, pyaudio fails 50% :(
      l, data = p_input.read()
    if l > 0:
      l_data = audioop.tomono(data, 2, 1, 0) # 4bytes, Left*1, Right*0 (muted)
      r_data = audioop.tomono(data, 2, 0, 1) # 4bytes, Left*0 (muted), Right*1
      # Transform data to logarithmic scale and calculate value
      lout_vu, lout_max, lout_t = vu_log(l_data,lout_max,lout_t)
      rout_vu, rout_max, rout_t = vu_log(r_data,rout_max,rout_t)
      # Display max pixel
      lout_maxp, lout_max = max_pix(lout_max,lout_t,"left",out_line)
      rout_maxp, rout_max = max_pix(rout_max,rout_t,"right",out_line)
    #else:
    #  print "l != -32 and l < 0. This should not happen... :(", str(l)

    if lin_vu == 0 and rin_vu == 0 and lout_vu == 0 and rout_vu == 0:
      saver_t +=1
    else:
      if saving == 1:
        # Reset font size & brightness
        device.write("#w"+fontsize+brightness+"\n")
      saver_t = 0
      saving = 0
      # Reset registers for next run of saver
      XX = saver_Xoffset # Initial offset, width of matrix
      YY = saver_Yoffset # Initial offset
      # Print 2* input & 2* output VU's
    if saving == 0:
      device.write("#W00133203"+(''.join(lin_bar[0:lin_vu]))+(''.join(rin_bar[0:rin_vu]))+lin_maxp+rin_maxp+(''.join(lout_bar[0:lout_vu]))+(''.join(rout_bar[0:rout_vu]))+lout_maxp+rout_maxp+"#p0013#p0513#p1013#p1513#p1613#p2113#p2613#p3113\n")

# Moderate speed
    time.sleep(1.0 / vu_fps) # 40 frames per second
    if asc == 0:
      asc = int(vu_fps)
    else:
      asc -= 1
except KeyboardInterrupt:
  # Clear the screen
  device.write("#w\n");
  # Close the device
  device.close()

