AGW Logo

Source code for peakmeter

# --------------------------------------------------------------------------------- #
# PEAKMETERCTRL wxPython IMPLEMENTATION
#
# Andrea Gavana, @ 07 October 2008
# Latest Revision: 17 Aug 2011, 15.00 GMT
#
#
# TODO List
#
# 1) Falloff effect for vertical bands;
#
# 2) Possibly some nicer drawing of bands and leds (using GraphicsContext).
#
#
# For all kind of problems, requests of enhancements and bug reports, please
# write to me at:
#
# andrea.gavana@gmail.com
# andrea.gavana@maerskoil.com
#
# Or, obviously, to the wxPython mailing list!!!
#
#
# End Of Comments
# --------------------------------------------------------------------------------- #

"""
L{PeakMeterCtrl} mimics the behaviour of equalizers that are usually found in stereos
and MP3 players.


Description
===========

L{PeakMeterCtrl} mimics the behaviour of equalizers that are usually found in stereos
and MP3 players. This widgets supports:

* Vertical and horizontal led bands;
* Settings number of bands and leds per band;
* Possibility to change the colour for low/medium/high band frequencies;
* Falloff effects;
* Showing a background grid for the bands.

And a lot more. Check the demo for an almost complete review of the functionalities.


Usage
=====

Usage example::

    import wx
    import random
    
    import wx.lib.agw.peakmeter as PM

    class MyFrame(wx.Frame):

        def __init__(self, parent):
        
            wx.Frame.__init__(self, parent, -1, "PeakMeterCtrl Demo")

            panel = wx.Panel(self)
        
            # Initialize Peak Meter control 1
            self.vertPeak = PM.PeakMeterCtrl(panel, -1, style=wx.SIMPLE_BORDER, agwStyle=PM.PM_VERTICAL)
            # Initialize Peak Meter control 2
            self.horzPeak = PM.PeakMeterCtrl(panel, -1, style=wx.SUNKEN_BORDER, agwStyle=PM.PM_HORIZONTAL)

            self.vertPeak.SetMeterBands(10, 15)
            self.horzPeak.SetMeterBands(10, 15)

            # Layout the two PeakMeterCtrl            
            mainSizer = wx.BoxSizer(wx.HORIZONTAL)
            mainSizer.Add(self.vertPeak, 0, wx.EXPAND|wx.ALL, 15)
            mainSizer.Add(self.horzPeak, 0, wx.EXPAND|wx.ALL, 15)

            panel.SetSizer(mainSizer)
            mainSizer.Layout()

            self.timer = wx.Timer(self)
            self.Bind(wx.EVT_TIMER, self.OnTimer)

            wx.CallLater(500, self.Start)


        def Start(self):
            ''' Starts the PeakMeterCtrl. '''

            self.timer.Start(1000/2)            # 2 fps
            
            self.vertPeak.Start(1000/18)        # 18 fps
            self.horzPeak.Start(1000/20)        # 20 fps


        def OnTimer(self, event):
            '''
            Handles the ``wx.EVT_TIMER`` event for L{PeakMeterCtrl}.

            :param `event`: a `wx.TimerEvent` event to be processed.
            '''

            # Generate 15 random number and set them as data for the meter
            nElements = 15
            arrayData = []
            
            for i in xrange(nElements):
                nRandom = random.randint(0, 100)
                arrayData.append(nRandom)

            self.vertPeak.SetData(arrayData, 0, nElements)
            self.horzPeak.SetData(arrayData, 0, nElements)


    # our normal wxApp-derived class, as usual

    app = wx.PySimpleApp()

    frame = MyFrame(None)
    app.SetTopWindow(frame)
    frame.Show()

    app.MainLoop()



Supported Platforms
===================

L{PeakMeterCtrl} has been tested on the following platforms:
  * Windows (Windows XP).


Window Styles
=============

This class supports the following window styles:

================= =========== ==================================================
Window Styles     Hex Value   Description
================= =========== ==================================================
``PM_HORIZONTAL``         0x0 Shows horizontal bands in L{PeakMeterCtrl}.
``PM_VERTICAL``           0x1 Shows vertical bands in L{PeakMeterCtrl}.
================= =========== ==================================================


Events Processing
=================

`No custom events are available for this class.`


License And Version
===================

L{PeakMeterCtrl} is distributed under the wxPython license.

Latest Revision: Andrea Gavana @ 17 Aug 2011, 15.00 GMT

Version 0.3

"""

import wx

# Horizontal or vertical PeakMeterCtrl
PM_HORIZONTAL = 0
""" Shows horizontal bands in L{PeakMeterCtrl}. """
PM_VERTICAL = 1
""" Shows vertical bands in L{PeakMeterCtrl}. """

# Some useful constants...
BAND_DEFAULT = 8
LEDS_DEFAULT = 8
BAND_PERCENT = 10       # 10% of Max Range (Auto Decrease)
GRID_INCREASEBY = 15    # Increase Grid colour based on Background colour
FALL_INCREASEBY = 60    # Increase Falloff colour based on Background
DEFAULT_SPEED = 10


[docs]def InRange(val, valMin, valMax): """ Returns whether the value `val` is between `valMin` and `valMax`. :param `val`: the value to test; :param `valMin`: the minimum range value; :param `valMax`: the maximum range value. """ return val >= valMin and val <= valMax
[docs]def LightenColour(crColour, byIncreaseVal): """ Lightens a colour. :param `crColour`: a valid `wx.Colour` object; :param `byIncreaseVal`: an integer specifying the amount for which the input colour should be brightened. """ byRed = crColour.Red() byGreen = crColour.Green() byBlue = crColour.Blue() byRed = (byRed + byIncreaseVal <= 255 and [byRed + byIncreaseVal] or [255])[0] byGreen = (byGreen + byIncreaseVal <= 255 and [byGreen + byIncreaseVal] or [255])[0] byBlue = (byBlue + byIncreaseVal <= 255 and [byBlue + byIncreaseVal] or [255])[0] return wx.Colour(byRed, byGreen, byBlue)
[docs]def DarkenColour(crColour, byReduceVal): """ Darkens a colour. :param `crColour`: a valid `wx.Colour` object; :param `byReduceVal`: an integer specifying the amount for which the input colour should be darkened. """ byRed = crColour.Red() byGreen = crColour.Green() byBlue = crColour.Blue() byRed = (byRed >= byReduceVal and [byRed - byReduceVal] or [0])[0] byGreen = (byGreen >= byReduceVal and [byGreen - byReduceVal] or [0])[0] byBlue = (byBlue >= byReduceVal and [byBlue - byReduceVal] or [0])[0] return wx.Colour(byRed, byGreen, byBlue)
[docs]class PeakMeterData(object): """ A simple class which holds data for our L{PeakMeterCtrl}. """
[docs] def __init__(self, value=0, falloff=0, peak=0): """ Default class constructor. :param `value`: the current L{PeakMeterCtrl} value; :param `falloff`: the falloff effect. ``True`` to enable it, ``False`` to disable it; :param `peak`: the peak value. """ self._value = value self._falloff = falloff self._peak = peak
[docs] def IsEqual(self, pm): """ Returns whether 2 instances of L{PeakMeterData} are the same. :param `pm`: another instance of L{PeakMeterData}. """ return self._value == pm._value
[docs] def IsGreater(self, pm): """ Returns whether one L{PeakMeterData} is greater than another. :param `pm`: another instance of L{PeakMeterData}. """ return self._value > pm._value
[docs] def IsLower(self, pm): """ Returns whether one L{PeakMeterData} is smaller than another. :param `pm`: another instance of L{PeakMeterData}. """ return self._value < pm._value
[docs]class PeakMeterCtrl(wx.PyControl): """ The main L{PeakMeterCtrl} implementation. """
[docs] def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, agwStyle=PM_VERTICAL): """ Default class constructor. :param parent: the L{PeakMeterCtrl} parent. Must not be ``None`` :param `id`: window identifier. A value of -1 indicates a default value; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; :param `style`: the underlying `wx.PyControl` window style; :param `agwStyle`: the AGW-specific window style, which can be one of the following bits: ================= =========== ================================================== Window Styles Hex Value Description ================= =========== ================================================== ``PM_HORIZONTAL`` 0x0 Shows horizontal bands in L{PeakMeterCtrl}. ``PM_VERTICAL`` 0x1 Shows vertical bands in L{PeakMeterCtrl}. ================= =========== ================================================== """ wx.PyControl.__init__(self, parent, id, pos, size, style) self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) self._agwStyle = agwStyle # Initializes all data self.InitData() self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_TIMER, self.OnTimer)
[docs] def InitData(self): """ Initializes the control. """ colLime = wx.Colour(0, 255, 0) colRed = wx.Colour(255, 0, 0) colYellow = wx.Colour(255, 255, 0) self._showGrid = False self._showFalloff = True self._delay = 10 self._minValue = 60 # Min Range 0-60 self._medValue = 80 # Med Range 60-80 self._maxValue = 100 # Max Range 80-100 self._numBands = BAND_DEFAULT self._ledBands = LEDS_DEFAULT self._clrBackground = self.GetBackgroundColour() self._clrNormal = colLime self._clrMedium = colYellow self._clrHigh = colRed self._speed = DEFAULT_SPEED self._timer = wx.Timer(self) # clear vector data self._meterData = []
[docs] def SetAGWWindowStyleFlag(self, agwStyle): """ Sets the L{PeakMeterCtrl} window style flags. :param `agwStyle`: the AGW-specific window style. This can be a combination of the following bits: ================= =========== ================================================== Window Styles Hex Value Description ================= =========== ================================================== ``PM_HORIZONTAL`` 0x0 Shows horizontal bands in L{PeakMeterCtrl}. ``PM_VERTICAL`` 0x1 Shows vertical bands in L{PeakMeterCtrl}. ================= =========== ================================================== """ self._agwStyle = agwStyle self.Refresh()
[docs] def GetAGWWindowStyleFlag(self): """ Returns the L{PeakMeterCtrl} window style. :see: L{SetAGWWindowStyleFlag} for a list of possible window style flags. """ return self._agwStyle
[docs] def ResetControl(self): """ Resets the L{PeakMeterCtrl}. """ # Initialize vector for i in xrange(self._numBands): pm = PeakMeterData(self._maxValue, self._maxValue, self._speed) self._meterData.append(pm) self.Refresh()
[docs] def SetBackgroundColour(self, colourBgnd): """ Changes the background colour of L{PeakMeterCtrl}. :param `colourBgnd`: the colour to be used as the background colour, pass `wx.NullColour` to reset to the default colour. :note: The background colour is usually painted by the default `wx.EraseEvent` event handler function under Windows and automatically under GTK. :note: Setting the background colour does not cause an immediate refresh, so you may wish to call `wx.Window.ClearBackground` or `wx.Window.Refresh` after calling this function. :note: Overridden from `wx.PyControl`. """ wx.PyControl.SetBackgroundColour(self, colourBgnd) self._clrBackground = colourBgnd self.Refresh()
[docs] def SetBandsColour(self, colourNormal, colourMedium, colourHigh): """ Set bands colour for L{PeakMeterCtrl}. :param `colourNormal`: the colour for normal (low) bands, a valid `wx.Colour` object; :param `colourMedium`: the colour for medium bands, a valid `wx.Colour` object; :param `colourHigh`: the colour for high bands, a valid `wx.Colour` object. """ self._clrNormal = colourNormal self._clrMedium = colourMedium self._clrHigh = colourHigh self.Refresh()
[docs] def SetMeterBands(self, numBands, ledBands): """ Set number of vertical or horizontal bands to display. :param `numBands`: number of bands to display (either vertical or horizontal); :param `ledBands`: the number of leds per band. :note: You can obtain a smooth effect by setting `nHorz` or `nVert` to "1", these cannot be 0. """ assert (numBands > 0 and ledBands > 0) self._numBands = numBands self._ledBands = ledBands # Reset vector self.ResetControl()
[docs] def SetRangeValue(self, minVal, medVal, maxVal): """ Sets the ranges for low, medium and high bands. :param `minVal`: the value for low bands; :param `medVal`: the value for medium bands; :param `maxVal`: the value for high bands. :note: The conditions to be satisfied are: Min: [0 - nMin[, Med: [nMin - nMed[, Max: [nMed - nMax] """ assert (maxVal > medVal and medVal > minVal and minVal > 0) self._minValue = minVal self._medValue = medVal self._maxValue = maxVal
[docs] def GetRangeValue(self): """ Get range value of L{PeakMeterCtrl}. """ return self._minValue, self._medValue, self._maxValue
[docs] def SetFalloffDelay(self, speed): """ Set peak value speed before falling off. :param `speed`: the speed at which the falloff happens. """ self._speed = speed
[docs] def SetFalloffEffect(self, falloffEffect): """ Set falloff effect flag. :param `falloffEffect`: ``True`` to enable the falloff effect, ``False`` to disable it. """ if self._showFalloff != falloffEffect: self._showFalloff = falloffEffect self.Refresh()
[docs] def GetFalloffEffect(self): """ Returns the falloff effect flag. """ return self._showFalloff
[docs] def ShowGrid(self, showGrid): """ Request to have gridlines visible or not. :param `showGrid`: ``True`` to show grid lines, ``False`` otherwise. """ if self._showGrid != showGrid: self._showGrid = showGrid self.Refresh()
[docs] def IsGridVisible(self): """ Returns if gridlines are visible. """ return self._showGrid
[docs] def SetData(self, arrayValue, offset, size): """ Change data value. Use this function to change only a set of values. All bands can be changed or only 1 band, depending on the application. :param `arrayValue`: a Python list containing the L{PeakMeterData} values; :param `offset`: the (optional) offset where to start applying the new data; :param `size`: the size of the input data. """ assert (offset >= 0 and arrayValue != []) isRunning = self.IsStarted() # Stop timer if Animation is active if isRunning: self.Stop() maxSize = offset + size for i in xrange(offset, maxSize): if i < len(self._meterData): pm = self._meterData[i] pm._value = arrayValue[i] if pm._falloff < pm._value: pm._falloff = pm._value pm._peak = self._speed self._meterData[i] = pm # Auto-restart if isRunning: return self.Start(self._delay) self.Refresh() return True
[docs] def IsStarted(self): """ Check if animation is active. """ return self._timer.IsRunning()
[docs] def Start(self, delay): """ Start the timer and animation effect. :param `delay`: the animation effect delay, in milliseconds. """ if not self.IsStarted(): self._delay = delay self._timer.Start(self._delay) else: return False return True
[docs] def Stop(self): """ Stop the timer and animation effect. """ if self.IsStarted(): self._timer.Stop() return True return False
[docs] def DoTimerProcessing(self): """ L{PeakMeterCtrl} animation, does the ``wx.EVT_TIMER`` processing. """ self.Refresh() decValue = self._maxValue/self._ledBands noChange = True for pm in self._meterData: if pm._value > 0: pm._value -= (self._ledBands > 1 and [decValue] or [self._maxValue*BAND_PERCENT/100])[0] if pm._value < 0: pm._value = 0 noChange = False if pm._peak > 0: pm._peak -= 1 noChange = False if pm._peak == 0 and pm._falloff > 0: pm._falloff -= (self._ledBands > 1 and [decValue >> 1] or [5])[0] if pm._falloff < 0: pm._falloff = 0 noChange = False if noChange: # Stop timer if no more data self.Stop()
[docs] def DoGetBestSize(self): """ Gets the size which best suits the window: for a control, it would be the minimal size which doesn't truncate the control, for a panel - the same size as it would have after a call to `Fit()`. :note: Overridden from `wx.PyControl`. """ # something is better than nothing... return wx.Size(200, 150)
[docs] def OnPaint(self, event): """ Handles the ``wx.EVT_PAINT`` event for L{PeakMeterCtrl}. :param `event`: a `wx.PaintEvent` event to be processed. """ dc = wx.AutoBufferedPaintDC(self) self._clrBackground = self.GetBackgroundColour() dc.SetBackground(wx.Brush(self._clrBackground)) dc.Clear() rc = self.GetClientRect() pen = wx.Pen(self._clrBackground) dc.SetPen(pen) if self.GetAGWWindowStyleFlag() & PM_VERTICAL: self.DrawVertBand(dc, rc) else: self.DrawHorzBand(dc, rc)
[docs] def OnEraseBackground(self, event): """ Handles the ``wx.EVT_ERASE_BACKGROUND`` event for L{PeakMeterCtrl}. :param `event`: a `wx.EraseEvent` event to be processed. :note: This method is intentionally empty to reduce flicker. """ # This is intentionally empty, to reduce flicker pass
[docs] def OnSize(self, event): """ Handles the ``wx.EVT_SIZE`` event for L{PeakMeterCtrl}. :param `event`: a `wx.SizeEvent` event to be processed. """ self.Refresh() event.Skip()
[docs] def OnTimer(self, event): """ Handles the ``wx.EVT_TIMER`` event for L{PeakMeterCtrl}. :param `event`: a `wx.TimerEvent` event to be processed. """ self.DoTimerProcessing()
[docs] def DrawHorzBand(self, dc, rect): """ Draws horizontal bands. :param `dc`: an instance of `wx.DC`; :param `rect`: the horizontal bands client rectangle. :todo: Implement falloff effect for horizontal bands. """ horzBands = (self._ledBands > 1 and [self._ledBands] or [self._maxValue*BAND_PERCENT/100])[0] minHorzLimit = self._minValue*horzBands/self._maxValue medHorzLimit = self._medValue*horzBands/self._maxValue maxHorzLimit = horzBands size = wx.Size(rect.width/horzBands, rect.height/self._numBands) rectBand = wx.RectPS(rect.GetTopLeft(), size) # Draw band from top rectBand.OffsetXY(0, rect.height-size.y*self._numBands) xDecal = (self._ledBands > 1 and [1] or [0])[0] yDecal = (self._numBands > 1 and [1] or [0])[0] for vert in xrange(self._numBands): self._value = self._meterData[vert]._value horzLimit = self._value*horzBands/self._maxValue for horz in xrange(horzBands): rectBand.Deflate(0, yDecal) # Find colour based on range value colourRect = self._clrBackground if self._showGrid: colourRect = DarkenColour(self._clrBackground, GRID_INCREASEBY) if self._showGrid and (horz == minHorzLimit or horz == (horzBands-1)): points = [wx.Point() for i in xrange(2)] points[0].x = rectBand.GetTopLeft().x + (rectBand.width >> 1) points[0].y = rectBand.GetTopLeft().y - yDecal points[1].x = points[0].x points[1].y = rectBand.GetBottomRight().y + yDecal dc.DrawLinePoint(points[0], points[1]) if horz < horzLimit: if InRange(horz, 0, minHorzLimit-1): colourRect = self._clrNormal elif InRange(horz, minHorzLimit, medHorzLimit-1): colourRect = self._clrMedium elif InRange(horz, medHorzLimit, maxHorzLimit): colourRect = self._clrHigh dc.SetBrush(wx.Brush(colourRect)) dc.DrawRectangleRect(rectBand) rectBand.Inflate(0, yDecal) rectBand.OffsetXY(size.x, 0) # Move to Next Vertical band rectBand.OffsetXY(-size.x*horzBands, size.y)
[docs] def DrawVertBand(self, dc, rect): """ Draws vertical bands. :param `dc`: an instance of `wx.DC`; :param `rect`: the vertical bands client rectangle. """ vertBands = (self._ledBands > 1 and [self._ledBands] or [self._maxValue*BAND_PERCENT/100])[0] minVertLimit = self._minValue*vertBands/self._maxValue medVertLimit = self._medValue*vertBands/self._maxValue maxVertLimit = vertBands size = wx.Size(rect.width/self._numBands, rect.height/vertBands) rectBand = wx.RectPS(rect.GetTopLeft(), size) # Draw band from bottom rectBand.OffsetXY(0, rect.bottom-size.y) xDecal = (self._numBands > 1 and [1] or [0])[0] yDecal = (self._ledBands > 1 and [1] or [0])[0] for horz in xrange(self._numBands): self._value = self._meterData[horz]._value vertLimit = self._value*vertBands/self._maxValue rectPrev = wx.Rect(*rectBand) for vert in xrange(vertBands): rectBand.Deflate(xDecal, 0) # Find colour based on range value colourRect = self._clrBackground if self._showGrid: colourRect = DarkenColour(self._clrBackground, GRID_INCREASEBY) # Draw grid line (level) bar if self._showGrid and (vert == minVertLimit or vert == (vertBands-1)): points = [wx.Point() for i in xrange(2)] points[0].x = rectBand.GetTopLeft().x - xDecal points[0].y = rectBand.GetTopLeft().y + (rectBand.height >> 1) points[1].x = rectBand.GetBottomRight().x + xDecal points[1].y = points[0].y dc.DrawLinePoint(points[0], points[1]) if vert < vertLimit: if InRange(vert, 0, minVertLimit-1): colourRect = self._clrNormal elif InRange(vert, minVertLimit, medVertLimit-1): colourRect = self._clrMedium elif InRange(vert, medVertLimit, maxVertLimit): colourRect = self._clrHigh dc.SetBrush(wx.Brush(colourRect)) dc.DrawRectangleRect(rectBand) rectBand.Inflate(xDecal, 0) rectBand.OffsetXY(0, -size.y) # Draw falloff effect if self._showFalloff: oldPen = dc.GetPen() pen = wx.Pen(DarkenColour(self._clrBackground, FALL_INCREASEBY)) maxHeight = size.y*vertBands points = [wx.Point() for i in xrange(2)] points[0].x = rectPrev.GetTopLeft().x + xDecal points[0].y = rectPrev.GetBottomRight().y - self._meterData[horz]._falloff*maxHeight/self._maxValue points[1].x = rectPrev.GetBottomRight().x - xDecal points[1].y = points[0].y dc.SetPen(pen) dc.DrawLinePoint(points[0], points[1]) dc.SetPen(oldPen) # Move to Next Horizontal band rectBand.OffsetXY(size.x, size.y*vertBands)