mpd_spectrum_page.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. # -*- coding: utf-8 -*-
  2. import time
  3. import pygame
  4. from numpy import fromstring,ceil,abs,log10,isnan,isinf,int16
  5. from numpy import fft as Fft
  6. import gobject
  7. from beeprint import pp
  8. ## local UI import
  9. from UI.constants import Width,Height
  10. from UI.page import Page,PageSelector
  11. from UI.label import Label
  12. from UI.fonts import fonts
  13. from UI.util_funcs import midRect
  14. from UI.keys_def import CurKeys
  15. from Queue import Queue, Empty
  16. from threading import Thread
  17. from list_item import ListItem
  18. import myvars
  19. class PIFI(object):
  20. _MPD_FIFO = "/tmp/mpd.fifo"
  21. _SAMPLE_SIZE = 256
  22. _SAMPLING_RATE = 44100
  23. _FIRST_SELECTED_BIN = 5
  24. _NUMBER_OF_SELECTED_BINS = 10
  25. _SCALE_WIDTH = Height/2 - 20
  26. count = 0
  27. average = 0
  28. def __init__(self):
  29. self.sampleSize = self._SAMPLE_SIZE
  30. self.samplingRate = self._SAMPLING_RATE
  31. self.firstSelectedBin = self._FIRST_SELECTED_BIN
  32. self.numberOfSelectedBins = self._NUMBER_OF_SELECTED_BINS
  33. # Initialization : frequency bins
  34. freq = Fft.fftfreq(self.sampleSize) * self.samplingRate
  35. freqR = freq[:self.sampleSize/2]
  36. self.bins = freqR[self.firstSelectedBin:self.firstSelectedBin+self.numberOfSelectedBins]
  37. self.resetSmoothing()
  38. def resetSmoothing(self):
  39. self.count = 0
  40. self.average = 0
  41. def smoothOut(self, x):
  42. self.count += 1
  43. self.average = (self.average*self.count + x) / (self.count+1)
  44. return self.average
  45. def scaleList(self, _list):
  46. for i,x in enumerate(_list):
  47. if isnan(x) or isinf(x):
  48. _list[i] = 0
  49. # Compute a simple just-above 'moving average' of maximums
  50. maximum = 1.1*self.smoothOut(max( _list ))
  51. if maximum == 0:
  52. scaleFactor = 0.0
  53. else:
  54. scaleFactor = self._SCALE_WIDTH/float(maximum)
  55. # Compute the scaled list of values
  56. scaledList = [int(x*scaleFactor) for x in _list ]
  57. return scaledList
  58. def computeSpectrum(self, fifoFile):
  59. # Read PCM samples from fifo
  60. rawSamples = fifoFile.read(self.sampleSize) # will return empty lines (non-blocking)
  61. if len(rawSamples) == 0:
  62. print("computeSpectrum read zero")
  63. return [],[]
  64. else:
  65. pass
  66. ## print("computeSpectrum %d " % len(rawSamples))
  67. pcm = fromstring(rawSamples, dtype=int16)
  68. # Normalize [-1; +1]
  69. pcm = pcm / (2.**15)
  70. # Compute FFT
  71. N = pcm.size
  72. fft = Fft.fft(pcm)
  73. uniquePts = ceil((N+1)/2.0)
  74. fft = fft[0:int(uniquePts)]
  75. # Compute amplitude spectrum
  76. amplitudeSpectrum = abs(fft) / float(N)
  77. # Compute power spectrum
  78. p = amplitudeSpectrum**2
  79. # Multiply by two to keep same energy
  80. # See explanation:
  81. # https://web.archive.org/web/20120615002031/http://www.mathworks.com/support/tech-notes/1700/1702.html
  82. if N % 2 > 0:
  83. # odd number of points
  84. # odd nfft excludes Nyquist point
  85. p[1:len(p)] = p[1:len(p)] * 2
  86. else:
  87. # even number of points
  88. p[1:len(p) -1] = p[1:len(p) - 1] * 2
  89. # Power in logarithmic scale (dB)
  90. logPower = 10*log10(p)
  91. # Compute RMS from power
  92. #rms = numpy.sqrt(numpy.sum(p))
  93. #print "RMS(power):", rms
  94. # Select a significant range in the spectrum
  95. spectrum = logPower[self.firstSelectedBin:self.firstSelectedBin+self.numberOfSelectedBins]
  96. # Scale the spectrum
  97. scaledSpectrum = self.scaleList(spectrum)
  98. return (self.bins, scaledSpectrum)
  99. class MPDSpectrumPage(Page):
  100. _Icons = {}
  101. _Selector=None
  102. _FootMsg = ["Nav","","","Back",""]
  103. _MyList = []
  104. _ListFont = fonts["veramono12"]
  105. _PIFI = None
  106. _FiFo = None
  107. _Color = pygame.Color(126,206,244)
  108. _GobjectIntervalId = -1
  109. _Queue = None
  110. _KeepReading = True
  111. def __init__(self):
  112. Page.__init__(self)
  113. self._Icons = {}
  114. self._CanvasHWND = None
  115. self._MyList = []
  116. self._PIFI = PIFI()
  117. def Init(self):
  118. self._PosX = self._Index * self._Screen._Width
  119. self._Width = self._Screen._Width
  120. self._Height = self._Screen._Height
  121. self._CanvasHWND = self._Screen._CanvasHWND
  122. self.Start()
  123. self._GobjectIntervalId = gobject.timeout_add(50,self.Playing)
  124. def Start(self):
  125. try:
  126. self._FIFO = open(self._PIFI._MPD_FIFO)
  127. q = Queue()
  128. self._Queue = q
  129. t = Thread(target=self.GetSpectrum)
  130. t.daemon = True # thread dies with the program
  131. t.start()
  132. except IOError:
  133. print("open %s failed"%self._PIFI._MPD_FIFO)
  134. self._FIFO = None
  135. return
  136. def GetSpectrum(self):
  137. if self._FIFO == None:
  138. print("self._FIFO none")
  139. return
  140. (bins,scaledSpectrum) = self._PIFI.computeSpectrum(self._FIFO)
  141. self._Queue.put( scaledSpectrum )
  142. self._KeepReading = False
  143. return ## Thread ends
  144. def Playing(self):
  145. if self._Screen.CurPage() == self:
  146. if self._KeepReading == False:
  147. self._KeepReading = True
  148. t = Thread(target=self.GetSpectrum)
  149. t.daemon=True
  150. t.start()
  151. self._Screen.Draw()
  152. self._Screen.SwapAndShow()
  153. else:
  154. return False
  155. return True
  156. def OnLoadCb(self):
  157. if self._Queue != None:
  158. with self._Queue.mutex:
  159. self._Queue.queue.clear()
  160. try:
  161. if self._GobjectIntervalId != -1:
  162. gobject.source_remove(self._GobjectIntervalId)
  163. except:
  164. pass
  165. self._GobjectIntervalId = gobject.timeout_add(50,self.Playing)
  166. def KeyDown(self,event):
  167. if event.key == CurKeys["Menu"] or event.key == CurKeys["A"]:
  168. self.ReturnToUpLevelPage()
  169. self._Screen.Draw()
  170. self._Screen.SwapAndShow()
  171. if event.key == CurKeys["Start"]:
  172. self._Screen.Draw()
  173. self._Screen.SwapAndShow()
  174. if event.key == CurKeys["Enter"]:
  175. pass
  176. def Draw(self):
  177. self.ClearCanvas()
  178. bw = 10
  179. spects = None
  180. try:
  181. spects = self._Queue.get_nowait()
  182. # print("get_nowait: " , spects)
  183. except Empty:
  184. return
  185. else: # got line
  186. if len(spects) == 0:
  187. return
  188. w = self._Width / len(spects)
  189. left_margin = (w-bw)/2
  190. for i,v in enumerate(spects):
  191. pygame.draw.rect(self._CanvasHWND,self._Color,(i*w+left_margin,self._Height-v,bw,v),0)