From 42b571e9dbb20f89333eb86dcbb907203a388edb Mon Sep 17 00:00:00 2001 From: cuu Date: Tue, 24 Jul 2018 11:38:32 +0800 Subject: [PATCH] replace music spectrum --- Menu/GameShell/Music Player/list_item.py | 3 +- .../Music Player/mpd_spectrum_page.py | 386 ++++++++++-------- Menu/GameShell/Music Player/play_list_page.py | 4 + .../sys.py/gameshell/icons/clockworkpi.png | Bin 0 -> 9792 bytes 4 files changed, 217 insertions(+), 176 deletions(-) create mode 100644 skin/default/sys.py/gameshell/icons/clockworkpi.png diff --git a/Menu/GameShell/Music Player/list_item.py b/Menu/GameShell/Music Player/list_item.py index f681fc3..c03ab5d 100644 --- a/Menu/GameShell/Music Player/list_item.py +++ b/Menu/GameShell/Music Player/list_item.py @@ -65,7 +65,7 @@ class ListItem(object): _PlayingProcess = 0 # 0 - 100 _Parent = None - + _Text = "" def __init__(self): self._Labels = {} self._Icons = {} @@ -75,6 +75,7 @@ class ListItem(object): def Init(self,text): #self._Fonts["normal"] = fonts["veramono12"] + self._Text = text l = ListItemLabel() l._PosX = 22 diff --git a/Menu/GameShell/Music Player/mpd_spectrum_page.py b/Menu/GameShell/Music Player/mpd_spectrum_page.py index d12bccf..4892a95 100644 --- a/Menu/GameShell/Music Player/mpd_spectrum_page.py +++ b/Menu/GameShell/Music Player/mpd_spectrum_page.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- +import os import time import pygame -from numpy import fromstring,ceil,abs,log10,isnan,isinf,int16,sqrt,mean -from numpy import fft as Fft +import numpy +import math import gobject @@ -20,141 +21,51 @@ from UI.keys_def import CurKeys from UI.icon_item import IconItem from UI.icon_pool import MyIconPool -from Queue import Queue, Empty from threading import Thread - - from list_item import ListItem import myvars class PIFI(object): _MPD_FIFO = "/tmp/mpd.fifo" - _SAMPLE_SIZE = 256 + _SAMPLE_SIZE = 1024 _SAMPLING_RATE = 44100 _FIRST_SELECTED_BIN = 5 - _NUMBER_OF_SELECTED_BINS = 10 - _SCALE_WIDTH = Height/2 - 20 + _NUMBER_OF_SELECTED_BINS = 1024 - count = 0 - average = 0 - - rmscount=0 - rmsaverage=0 def __init__(self): self.sampleSize = self._SAMPLE_SIZE self.samplingRate = self._SAMPLING_RATE - self.firstSelectedBin = self._FIRST_SELECTED_BIN - self.numberOfSelectedBins = self._NUMBER_OF_SELECTED_BINS - # Initialization : frequency bins - freq = Fft.fftfreq(self.sampleSize) * self.samplingRate - freqR = freq[:self.sampleSize/2] - self.bins = freqR[self.firstSelectedBin:self.firstSelectedBin+self.numberOfSelectedBins] - - self.resetSmoothing() + def GetSpectrum(self,fifoFile,trim_by=10,log_scale=False,div_by=100): + try: + rawSamples = os.read(fifoFile,self.sampleSize) # will return empty lines (non-blocking) + if len(rawSamples) < 1: +# print("Read error") + return rawSamples + except Exception,e: + return "" - def resetSmoothing(self): - self.count = 0 - self.average = 0 - self.rmscount = 0 - self.rmsaverage = 0 + data = numpy.fromstring(rawSamples, dtype=numpy.int16) - def rms_smoothOut(self, x): - self.rmscount += 1 - self.rmsaverage = (self.rmsaverage*self.rmscount + x) / (self.rmscount+1) - return self.rmsaverage + data = data * numpy.hanning(len(data)) + + left,right = numpy.split(numpy.abs(numpy.fft.fft(data)),2) + spec_y = numpy.add(left,right[::-1]) + + if log_scale: + spec_y=numpy.multiply(20,numpy.log10(spec_y)) + if trim_by: + i=int((self.sampleSize/2)/trim_by) + spec_y=spec_y[:i] + if div_by: + spec_y=spec_y/float(div_by) + + return spec_y - def smoothOut(self, x): - self.count += 1 - self.average = (self.average*self.count + x) / (self.count+1) - return self.average - def scaleList(self, _list): - for i,x in enumerate(_list): - if isnan(x) or isinf(x): - _list[i] = 0 - - # Compute a simple just-above 'moving average' of maximums - maximum = 1.1*self.smoothOut(max( _list )) - if maximum == 0: - scaleFactor = 0.0 - else: - scaleFactor = self._SCALE_WIDTH/float(maximum) - - # Compute the scaled list of values - scaledList = [int(x*scaleFactor) for x in _list ] - return scaledList - - - def computeSpectrum(self, fifoFile): - - # Read PCM samples from fifo - rawSamples = fifoFile.read(self.sampleSize) # will return empty lines (non-blocking) - if len(rawSamples) == 0: - print("computeSpectrum read zero") - return [],[] - else: - pass -## print("computeSpectrum %d " % len(rawSamples)) - - pcm = fromstring(rawSamples, dtype=int16) - - # Normalize [-1; +1] - pcm = pcm / (2.**15) - - # Compute RMS directly from signal - rms = sqrt(mean(pcm**2)) - # Compute a simple 'moving maximum' - maximum = 2*self.rms_smoothOut(rms) - if maximum == 0: - scaleFactor = 0.0 - else: - scaleFactor = self._SCALE_WIDTH/float(maximum) - - final_rms = int(rms*scaleFactor) - - # Compute FFT - N = pcm.size - fft = Fft.fft(pcm) - uniquePts = ceil((N+1)/2.0) - fft = fft[0:int(uniquePts)] - - # Compute amplitude spectrum - amplitudeSpectrum = abs(fft) / float(N) - - # Compute power spectrum - p = amplitudeSpectrum**2 - - # Multiply by two to keep same energy - # See explanation: - # https://web.archive.org/web/20120615002031/http://www.mathworks.com/support/tech-notes/1700/1702.html - if N % 2 > 0: - # odd number of points - # odd nfft excludes Nyquist point - p[1:len(p)] = p[1:len(p)] * 2 - else: - # even number of points - p[1:len(p) -1] = p[1:len(p) - 1] * 2 - - # Power in logarithmic scale (dB) - logPower = 10*log10(p) - - # Compute RMS from power - #rms = numpy.sqrt(numpy.sum(p)) - #print "RMS(power):", rms - - # Select a significant range in the spectrum - spectrum = logPower[self.firstSelectedBin:self.firstSelectedBin+self.numberOfSelectedBins] - - # Scale the spectrum - scaledSpectrum = self.scaleList(spectrum) - scaledSpectrum.append( final_rms) - return scaledSpectrum - - class MPDSpectrumPage(Page): _Icons = {} @@ -162,14 +73,15 @@ class MPDSpectrumPage(Page): _FootMsg = ["Nav","","","Back",""] _MyList = [] _ListFont = fonts["veramono12"] - + _SongFont = fonts["notosanscjk17"] _PIFI = None _FIFO = None _Color = pygame.Color(126,206,244) _GobjectIntervalId = -1 _Queue = None _KeepReading = True - + _ReadingThread = None + _BGpng = None _BGwidth = 320 _BGheight = 200 @@ -182,10 +94,23 @@ class MPDSpectrumPage(Page): _SheepBodyW = 105 _SheepBodyH = 81 + _RollCanvas = None + _RollW = 220 + _RollH = 68 + _freq_count = 0 _head_dir = 0 _Neighbor = None + + + _bby = [] + _bbs = [] + _capYPositionArray = [] + _frames = 0 + read_retry = 0 + _queue_data = [] + _vis_values = [] def __init__(self): Page.__init__(self) @@ -200,7 +125,8 @@ class MPDSpectrumPage(Page): self._Height = self._Screen._Height self._CanvasHWND = self._Screen._CanvasHWND - + self._RollCanvas = pygame.Surface(( self._RollW,self._RollH)) + """ self._BGpng = IconItem() self._BGpng._ImgSurf = MyIconPool._Icons["sheep_bg"] @@ -221,19 +147,35 @@ class MPDSpectrumPage(Page): self._SheepBody.Adjust(0,0,self._SheepBodyW,self._SheepBodyH,0) """ - self.Start() - self._GobjectIntervalId = gobject.timeout_add(50,self.Playing) + self._cwp_png = IconItem() + self._cwp_png._ImgSurf = MyIconPool._Icons["clockworkpi"] + self._cwp_png._MyType = ICON_TYPES["STAT"] + self._cwp_png._Parent = self + self._cwp_png.Adjust(0,0,79,79,0) + + + self._song_title = Label() + self._song_title.SetCanvasHWND(self._RollCanvas) + + self._song_title.Init("Untitled",self._SongFont,(255,255,255)) + + + + self.Start() + def Start(self): + + if self._Screen.CurPage() != self: + return try: - self._FIFO = open(self._PIFI._MPD_FIFO) - q = Queue() - self._Queue = q + self._FIFO = os.open(self._PIFI._MPD_FIFO, os.O_RDONLY | os.O_NONBLOCK) t = Thread(target=self.GetSpectrum) t.daemon = True # thread dies with the program t.start() + self._ReadingThread = t except IOError: print("open %s failed"%self._PIFI._MPD_FIFO) @@ -242,61 +184,79 @@ class MPDSpectrumPage(Page): def GetSpectrum(self): - if self._FIFO == None: - print("self._FIFO none") - return + while self._KeepReading and self._FIFO != None: + raw_samples = self._PIFI.GetSpectrum(self._FIFO) + if len(raw_samples) < 1: + #print("sleeping... 0.01") + time.sleep(0.01) + self.read_retry+=1 + if self.read_retry > 40: + os.close(self._FIFO) + self._FIFO = os.open(self._PIFI._MPD_FIFO, os.O_RDONLY | os.O_NONBLOCK) + self.read_retry = 0 + + self.Playing() + + else: + self.read_retry = 0 + self._queue_data = raw_samples + self.Playing() - scaledSpectrum = self._PIFI.computeSpectrum(self._FIFO) - self._Queue.put( scaledSpectrum ) - - self._KeepReading = False - - return ## Thread ends def Playing(self): - if self._Screen.CurPage() == self: - if self._KeepReading == False: - self._KeepReading = True - - t = Thread(target=self.GetSpectrum) - t.daemon=True - t.start() - - self._Screen.Draw() - self._Screen.SwapAndShow() - - else: - - return False - return True + self._Screen.Draw() + self._Screen.SwapAndShow() + + + def ClearCanvas(self): + self._CanvasHWND.fill((0,0,0)) + + def SgsSmooth(self): + passes = 1 + points = 3 + origs = self._bby[:] + for p in range(0,passes): + pivot = int(points/2.0) + + for i in range(0,pivot): + self._bby[i] = origs[i] + self._bby[ len(origs) -i -1 ] = origs[ len(origs) -i -1 ] + + smooth_constant = 1.0/(2.0*pivot+1.0) + for i in range(pivot, len(origs)-pivot): + _sum = 0.0 + for j in range(0,(2*pivot)+1): + _sum += (smooth_constant * origs[i+j-pivot]) +j -pivot + + self._bby[i] = _sum + + if p < (passes - 1): + origs = self._bby[:] def OnLoadCb(self): + if self._Neighbor != None: + pass + + if self._KeepReading == False: + self._KeepReading = True + if self._FIFO == None: self.Start() - if self._Queue != None: - with self._Queue.mutex: - self._Queue.queue.clear() - - try: - if self._GobjectIntervalId != -1: - gobject.source_remove(self._GobjectIntervalId) - except: - pass - - self._GobjectIntervalId = gobject.timeout_add(50,self.Playing) - def KeyDown(self,event): if event.key == CurKeys["Menu"] or event.key == CurKeys["A"]: - if self._FIFO != None and self._FIFO.closed == False: - try: - self._FIFO.close() - self._FIFO = None - except Exception, e: - print(e) + try: + os.close(self._FIFO) + self._FIFO = None + + except Exception, e: + print(e) + self._KeepReading = False + self._ReadingThread.join() + self._ReadingThread = None self.ReturnToUpLevelPage() self._Screen.Draw() @@ -308,25 +268,101 @@ class MPDSpectrumPage(Page): if event.key == CurKeys["Enter"]: pass + def Draw(self): self.ClearCanvas() bw = 10 + gap = 2 + margin_bottom = 100 + spects = None + meterNum = self._Width / float(bw +gap ) ## 320/12= 26 + meter_left = meterNum - int(meterNum) + meter_left = meter_left*int(bw+gap) + margin_left = meter_left / 2 + gap + meterNum = int(meterNum) + + self._cwp_png.NewCoord(43,149) + self._cwp_png.Draw() + + if self._Neighbor != None: + if self._Neighbor._CurSongName != "": + self._song_title.SetText(self._Neighbor._CurSongName) + + + + if self._RollCanvas != None: + self._RollCanvas.fill((0,0,0)) + + self._song_title.NewCoord(0,0) + self._song_title.Draw() + + self._CanvasHWND.blit(self._RollCanvas,(86,114,220,68)) + try: - spects = self._Queue.get_nowait() ## last element is rms -# print("get_nowait: " , spects) + spects = self._queue_data + if len(spects) == 0: + return +# print("spects:",spects) + step = int( round( len( spects ) / meterNum) ) + + self._bbs = [] + + for i in range(0,meterNum): + index = int(i*step) + total = 0 + + value = spects[index] + self._bbs.append(value) + + if len(self._bby) < len(self._bbs): + self._bby = self._bbs + elif len(self._bby) == len(self._bbs): + for i in range(0,len(self._bbs)): + self._bby[i] = (self._bby[i]+self._bbs[i])/2 + + self.SgsSmooth() + + for i in range(0,meterNum): + value = self._bby[ i ] + if math.isnan(value) or math.isinf(value): + value = 0 + + value = value/32768.0 + value = value * 100 + value = value % (self._Height-gap-margin_bottom) + + if len(self._vis_values) < len(self._bby): + self._vis_values.append(value) + elif len(self._vis_values) == len(self._bby): + if self._vis_values[i] < value: + self._vis_values[i] = value + + except Empty: return else: # got line - if len(spects) == 0: + if len(self._vis_values) == 0: return - w = self._Width / len( spects[0:-1] ) - left_margin = (w-bw)/2 - for i,v in enumerate(spects[0:-1]): - pygame.draw.rect(self._CanvasHWND,self._Color,(i*w+left_margin,self._Height-v,bw,v),0) - + + for i in range(0,meterNum): + value = self._vis_values[i] + + if len(self._capYPositionArray) < round(meterNum): + self._capYPositionArray.append(value) + + if value < self._capYPositionArray[i]: + self._capYPositionArray[i]-=0.5 + else: + self._capYPositionArray[i] = value + + pygame.draw.rect(self._CanvasHWND,(255,255,255),(i*(bw+gap)+margin_left,self._Height-gap-self._capYPositionArray[i]-margin_bottom,bw,gap),0) + + pygame.draw.rect(self._CanvasHWND,(255,255,255),(i*(bw+gap)+margin_left,self._Height-value-gap-margin_bottom,bw,value+gap),0) + + self._vis_values[i] -= 2 diff --git a/Menu/GameShell/Music Player/play_list_page.py b/Menu/GameShell/Music Player/play_list_page.py index 308a888..b5ae9bf 100644 --- a/Menu/GameShell/Music Player/play_list_page.py +++ b/Menu/GameShell/Music Player/play_list_page.py @@ -71,6 +71,8 @@ class PlayListPage(Page): _BGheight = 70 _Scrolled = 0 + + _CurSongName = "" def __init__(self): self._Icons = {} @@ -134,6 +136,8 @@ class PlayListPage(Page): if "song" in current_song: posid = int(current_song["song"]) if posid < len(self._MyList): # out of index + self._CurSongName = self._MyList[posid]._Text + if "state" in current_song: if current_song["state"] == "stop": self._MyList[posid]._Playing = False diff --git a/skin/default/sys.py/gameshell/icons/clockworkpi.png b/skin/default/sys.py/gameshell/icons/clockworkpi.png new file mode 100644 index 0000000000000000000000000000000000000000..b45aff9e661b5b8a61a168c0991f9b8d15a37811 GIT binary patch literal 9792 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 a@%|6%caFW>sGczZ0000