1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Digital Peak Meter, a custom Qt widget
5# Copyright (C) 2011-2019 Filipe Coelho <falktx@falktx.com>
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License as
9# published by the Free Software Foundation; either version 2 of
10# the License, or any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# For a full copy of the GNU General Public License see the doc/GPL.txt file.
18
19# ------------------------------------------------------------------------------------------------------------
20# Imports (Global)
21
22from math import sqrt
23
24from PyQt5.QtCore import qCritical, Qt, QTimer, QSize
25from PyQt5.QtGui import QColor, QLinearGradient, QPainter, QPen, QPixmap
26from PyQt5.QtWidgets import QWidget
27
28# ------------------------------------------------------------------------------------------------------------
29# Widget Class
30
31class DigitalPeakMeter(QWidget):
32    # enum Color
33    COLOR_GREEN = 1
34    COLOR_BLUE  = 2
35
36    # enum Orientation
37    HORIZONTAL = 1
38    VERTICAL   = 2
39
40    # enum Style
41    STYLE_DEFAULT = 1
42    STYLE_OPENAV  = 2
43    STYLE_RNCBC   = 3
44    STYLE_CALF    = 4
45
46    # --------------------------------------------------------------------------------------------------------
47
48    def __init__(self, parent):
49        QWidget.__init__(self, parent)
50        self.setAttribute(Qt.WA_OpaquePaintEvent)
51
52        # defaults are VERTICAL, COLOR_GREEN, STYLE_DEFAULT
53
54        self.fChannelCount    = 0
55        self.fChannelData     = []
56        self.fLastChannelData = []
57
58        self.fMeterColor        = self.COLOR_GREEN
59        self.fMeterColorBase    = QColor(93, 231, 61)
60        self.fMeterColorBaseAlt = QColor(15, 110, 15, 100)
61
62        self.fMeterLinesEnabled = True
63        self.fMeterOrientation  = self.VERTICAL
64        self.fMeterStyle        = self.STYLE_DEFAULT
65
66        self.fMeterBackground = QColor("#070707")
67        self.fMeterGradient   = QLinearGradient(0, 0, 0, 0)
68        self.fMeterPixmaps    = ()
69
70        self.fSmoothMultiplier = 2
71
72        self.updateGrandient()
73
74    # --------------------------------------------------------------------------------------------------------
75
76    def channelCount(self):
77        return self.fChannelCount
78
79    def setChannelCount(self, count):
80        if self.fChannelCount == count:
81            return
82
83        if count < 0:
84            return qCritical("DigitalPeakMeter::setChannelCount(%i) - channel count must be a positive integer or zero" % count)
85
86        self.fChannelCount    = count
87        self.fChannelData     = []
88        self.fLastChannelData = []
89
90        for x in range(count):
91            self.fChannelData.append(0.0)
92            self.fLastChannelData.append(0.0)
93
94        if self.fMeterStyle == self.STYLE_CALF:
95            if self.fChannelCount > 0:
96                self.setFixedSize(100, 12*self.fChannelCount)
97            else:
98                self.setMinimumSize(0, 0)
99                self.setMaximumSize(9999, 9999)
100
101    # --------------------------------------------------------------------------------------------------------
102
103    def meterColor(self):
104        return self.fMeterColor
105
106    def setMeterColor(self, color):
107        if self.fMeterColor == color:
108            return
109
110        if color not in (self.COLOR_GREEN, self.COLOR_BLUE):
111            return qCritical("DigitalPeakMeter::setMeterColor(%i) - invalid color" % color)
112
113        if color == self.COLOR_GREEN:
114            self.fMeterColorBase    = QColor(93, 231, 61)
115            self.fMeterColorBaseAlt = QColor(15, 110, 15, 100)
116        elif color == self.COLOR_BLUE:
117            self.fMeterColorBase    = QColor(82, 238, 248)
118            self.fMeterColorBaseAlt = QColor(15, 15, 110, 100)
119
120        self.fMeterColor = color
121
122        self.updateGrandient()
123
124    # --------------------------------------------------------------------------------------------------------
125
126    def meterLinesEnabled(self):
127        return self.fMeterLinesEnabled
128
129    def setMeterLinesEnabled(self, yesNo):
130        if self.fMeterLinesEnabled == yesNo:
131            return
132
133        self.fMeterLinesEnabled = yesNo
134
135    # --------------------------------------------------------------------------------------------------------
136
137    def meterOrientation(self):
138        return self.fMeterOrientation
139
140    def setMeterOrientation(self, orientation):
141        if self.fMeterOrientation == orientation:
142            return
143
144        if orientation not in (self.HORIZONTAL, self.VERTICAL):
145            return qCritical("DigitalPeakMeter::setMeterOrientation(%i) - invalid orientation" % orientation)
146
147        self.fMeterOrientation = orientation
148
149        self.updateGrandient()
150
151    # --------------------------------------------------------------------------------------------------------
152
153    def meterStyle(self):
154        return self.fMeterStyle
155
156    def setMeterStyle(self, style):
157        if self.fMeterStyle == style:
158            return
159
160        if style not in (self.STYLE_DEFAULT, self.STYLE_OPENAV, self.STYLE_RNCBC, self.STYLE_CALF):
161            return qCritical("DigitalPeakMeter::setMeterStyle(%i) - invalid style" % style)
162
163        if style == self.STYLE_DEFAULT:
164            self.fMeterBackground = QColor("#070707")
165        elif style == self.STYLE_OPENAV:
166            self.fMeterBackground = QColor("#1A1A1A")
167        elif style == self.STYLE_RNCBC:
168            self.fMeterBackground = QColor("#070707")
169        elif style == self.STYLE_CALF:
170            self.fMeterBackground = QColor("#000")
171
172        if style == self.STYLE_CALF:
173            self.fMeterPixmaps = (QPixmap(":/bitmaps/meter_calf_off.png"), QPixmap(":/bitmaps/meter_calf_on.png"))
174            if self.fChannelCount > 0:
175                self.setFixedSize(100, 12*self.fChannelCount)
176        else:
177            self.fMeterPixmaps = ()
178            self.setMinimumSize(0, 0)
179            self.setMaximumSize(9999, 9999)
180
181        self.fMeterStyle = style
182
183        self.updateGrandient()
184
185    # --------------------------------------------------------------------------------------------------------
186
187    def smoothMultiplier(self):
188        return self.fSmoothMultiplier
189
190    def setSmoothMultiplier(self, value):
191        if self.fSmoothMultiplier == value:
192            return
193
194        if not isinstance(value, int):
195            return qCritical("DigitalPeakMeter::setSmoothMultiplier() - value must be an integer")
196        if value < 0:
197            return qCritical("DigitalPeakMeter::setSmoothMultiplier(%i) - value must be >= 0" % value)
198        if value > 5:
199            return qCritical("DigitalPeakMeter::setSmoothMultiplier(%i) - value must be < 5" % value)
200
201        self.fSmoothMultiplier = value
202
203    # --------------------------------------------------------------------------------------------------------
204
205    def displayMeter(self, meter, level, forced = False):
206        if not isinstance(meter, int):
207            return qCritical("DigitalPeakMeter::displayMeter(,) - meter value must be an integer")
208        if not isinstance(level, float):
209            return qCritical("DigitalPeakMeter::displayMeter(%i,) - level value must be a float" % (meter,))
210        if meter <= 0 or meter > self.fChannelCount:
211            return qCritical("DigitalPeakMeter::displayMeter(%i, %f) - invalid meter number" % (meter, level))
212
213        i = meter - 1
214
215        if self.fSmoothMultiplier > 0 and not forced:
216            level = (self.fLastChannelData[i] * float(self.fSmoothMultiplier) + level) / float(self.fSmoothMultiplier + 1)
217
218        if level < 0.001:
219            level = 0.0
220        elif level > 0.999:
221            level = 1.0
222
223        if self.fChannelData[i] != level:
224            self.fChannelData[i] = level
225            self.update()
226
227        self.fLastChannelData[i] = level
228
229    # --------------------------------------------------------------------------------------------------------
230
231    def updateGrandient(self):
232        self.fMeterGradient = QLinearGradient(0, 0, 1, 1)
233
234        if self.fMeterStyle == self.STYLE_DEFAULT:
235            if self.fMeterOrientation == self.HORIZONTAL:
236                self.fMeterGradient.setColorAt(0.0, self.fMeterColorBase)
237                self.fMeterGradient.setColorAt(0.2, self.fMeterColorBase)
238                self.fMeterGradient.setColorAt(0.4, self.fMeterColorBase)
239                self.fMeterGradient.setColorAt(0.6, self.fMeterColorBase)
240                self.fMeterGradient.setColorAt(0.8, Qt.yellow)
241                self.fMeterGradient.setColorAt(1.0, Qt.red)
242
243            elif self.fMeterOrientation == self.VERTICAL:
244                self.fMeterGradient.setColorAt(0.0, Qt.red)
245                self.fMeterGradient.setColorAt(0.2, Qt.yellow)
246                self.fMeterGradient.setColorAt(0.4, self.fMeterColorBase)
247                self.fMeterGradient.setColorAt(0.6, self.fMeterColorBase)
248                self.fMeterGradient.setColorAt(0.8, self.fMeterColorBase)
249                self.fMeterGradient.setColorAt(1.0, self.fMeterColorBase)
250
251        elif self.fMeterStyle == self.STYLE_RNCBC:
252            if self.fMeterColor == self.COLOR_BLUE:
253                c1 = QColor(40,160,160)
254                c2 = QColor(60,220,160)
255            elif self.fMeterColor == self.COLOR_GREEN:
256                c1 = QColor( 40,160,40)
257                c2 = QColor(160,220,20)
258
259            if self.fMeterOrientation == self.HORIZONTAL:
260                self.fMeterGradient.setColorAt(0.0, c1)
261                self.fMeterGradient.setColorAt(0.2, c1)
262                self.fMeterGradient.setColorAt(0.6, c2)
263                self.fMeterGradient.setColorAt(0.7, QColor(220,220, 20))
264                self.fMeterGradient.setColorAt(0.8, QColor(240,160, 20))
265                self.fMeterGradient.setColorAt(0.9, QColor(240,  0, 20))
266                self.fMeterGradient.setColorAt(1.0, QColor(240,  0, 20))
267
268            elif self.fMeterOrientation == self.VERTICAL:
269                self.fMeterGradient.setColorAt(0.0, QColor(240,  0, 20))
270                self.fMeterGradient.setColorAt(0.1, QColor(240,  0, 20))
271                self.fMeterGradient.setColorAt(0.2, QColor(240,160, 20))
272                self.fMeterGradient.setColorAt(0.3, QColor(220,220, 20))
273                self.fMeterGradient.setColorAt(0.4, c2)
274                self.fMeterGradient.setColorAt(0.8, c1)
275                self.fMeterGradient.setColorAt(1.0, c1)
276
277        elif self.fMeterStyle in (self.STYLE_OPENAV, self.STYLE_CALF):
278            self.fMeterGradient.setColorAt(0.0, self.fMeterColorBase)
279            self.fMeterGradient.setColorAt(1.0, self.fMeterColorBase)
280
281        self.updateGrandientFinalStop()
282
283    def updateGrandientFinalStop(self):
284        if self.fMeterOrientation == self.HORIZONTAL:
285            self.fMeterGradient.setFinalStop(self.width(), 0)
286
287        elif self.fMeterOrientation == self.VERTICAL:
288            self.fMeterGradient.setFinalStop(0, self.height())
289
290    # --------------------------------------------------------------------------------------------------------
291
292    def minimumSizeHint(self):
293        return QSize(10, 10)
294
295    def sizeHint(self):
296        return QSize(self.width(), self.height())
297
298    # --------------------------------------------------------------------------------------------------------
299
300    def drawCalf(self, event):
301        painter = QPainter(self)
302        event.accept()
303
304        # no channels, draw black
305        if self.fChannelCount == 0:
306            painter.setPen(QPen(Qt.black, 2))
307            painter.setBrush(Qt.black)
308            painter.drawRect(0, 0, self.width(), self.height())
309            return
310
311        for i in range(self.fChannelCount):
312            painter.drawPixmap(0, 12*i, self.fMeterPixmaps[0])
313
314        meterPos  = 4
315        meterSize = 12
316
317        # draw levels
318        for level in self.fChannelData:
319            if level != 0.0:
320                blevel = int(sqrt(level)*26.0)*3
321                painter.drawPixmap(5, meterPos, blevel, 4, self.fMeterPixmaps[1], 0, 0, blevel, 4)
322            meterPos += meterSize
323
324    def paintEvent(self, event):
325        if self.fMeterStyle == self.STYLE_CALF:
326            return self.drawCalf(event)
327
328        painter = QPainter(self)
329        event.accept()
330
331        width  = self.width()
332        height = self.height()
333
334        # draw background
335        painter.setPen(QPen(self.fMeterBackground, 2))
336        painter.setBrush(self.fMeterBackground)
337        painter.drawRect(0, 0, width, height)
338
339        if self.fChannelCount == 0:
340            return
341
342        meterPad  = 0
343        meterPos  = 0
344        meterSize = (height if self.fMeterOrientation == self.HORIZONTAL else width)/self.fChannelCount
345
346        # set pen/brush for levels
347        if self.fMeterStyle == self.STYLE_OPENAV:
348            colorTrans = QColor(self.fMeterColorBase)
349            colorTrans.setAlphaF(0.5)
350            painter.setBrush(colorTrans)
351            painter.setPen(QPen(self.fMeterColorBase, 1))
352            del colorTrans
353            meterPad  += 2
354            meterSize -= 2
355
356        else:
357            painter.setPen(QPen(self.fMeterBackground, 0))
358            painter.setBrush(self.fMeterGradient)
359
360        # draw levels
361        for level in self.fChannelData:
362            if level == 0.0:
363                pass
364            elif self.fMeterOrientation == self.HORIZONTAL:
365                painter.drawRect(0, meterPos, int(sqrt(level) * float(width)), meterSize)
366            elif self.fMeterOrientation == self.VERTICAL:
367                painter.drawRect(meterPos, height - int(sqrt(level) * float(height)), meterSize, height)
368
369            meterPos += meterSize+meterPad
370
371        if not self.fMeterLinesEnabled:
372            return
373
374        # draw lines
375        if self.fMeterOrientation == self.HORIZONTAL:
376            # Variables
377            lsmall = float(width)
378            lfull  = float(height - 1)
379
380            if self.fMeterStyle == self.STYLE_OPENAV:
381                painter.setPen(QColor(37, 37, 37, 100))
382                painter.drawLine(lsmall * 0.25, 2, lsmall * 0.25, lfull-2.0)
383                painter.drawLine(lsmall * 0.50, 2, lsmall * 0.50, lfull-2.0)
384                painter.drawLine(lsmall * 0.75, 2, lsmall * 0.75, lfull-2.0)
385
386                if self.fChannelCount > 1:
387                    painter.drawLine(1, lfull/2-1, lsmall-1, lfull/2-1)
388
389            else:
390                # Base
391                painter.setBrush(Qt.black)
392                painter.setPen(QPen(self.fMeterColorBaseAlt, 1))
393                painter.drawLine(lsmall * 0.25, 2, lsmall * 0.25, lfull-2.0)
394                painter.drawLine(lsmall * 0.50, 2, lsmall * 0.50, lfull-2.0)
395
396                # Yellow
397                painter.setPen(QColor(110, 110, 15, 100))
398                painter.drawLine(lsmall * 0.70, 2, lsmall * 0.70, lfull-2.0)
399                painter.drawLine(lsmall * 0.83, 2, lsmall * 0.83, lfull-2.0)
400
401                # Orange
402                painter.setPen(QColor(180, 110, 15, 100))
403                painter.drawLine(lsmall * 0.90, 2, lsmall * 0.90, lfull-2.0)
404
405                # Red
406                painter.setPen(QColor(110, 15, 15, 100))
407                painter.drawLine(lsmall * 0.96, 2, lsmall * 0.96, lfull-2.0)
408
409        elif self.fMeterOrientation == self.VERTICAL:
410            # Variables
411            lsmall = float(height)
412            lfull  = float(width - 1)
413
414            if self.fMeterStyle == self.STYLE_OPENAV:
415                painter.setPen(QColor(37, 37, 37, 100))
416                painter.drawLine(2, lsmall - (lsmall * 0.25), lfull-2.0, lsmall - (lsmall * 0.25))
417                painter.drawLine(2, lsmall - (lsmall * 0.50), lfull-2.0, lsmall - (lsmall * 0.50))
418                painter.drawLine(2, lsmall - (lsmall * 0.75), lfull-2.0, lsmall - (lsmall * 0.75))
419
420                if self.fChannelCount > 1:
421                    painter.drawLine(lfull/2-1, 1, lfull/2-1, lsmall-1)
422
423            else:
424                # Base
425                painter.setBrush(Qt.black)
426                painter.setPen(QPen(self.fMeterColorBaseAlt, 1))
427                painter.drawLine(2, lsmall - (lsmall * 0.25), lfull-2.0, lsmall - (lsmall * 0.25))
428                painter.drawLine(2, lsmall - (lsmall * 0.50), lfull-2.0, lsmall - (lsmall * 0.50))
429
430                # Yellow
431                painter.setPen(QColor(110, 110, 15, 100))
432                painter.drawLine(2, lsmall - (lsmall * 0.70), lfull-2.0, lsmall - (lsmall * 0.70))
433                painter.drawLine(2, lsmall - (lsmall * 0.82), lfull-2.0, lsmall - (lsmall * 0.82))
434
435                # Orange
436                painter.setPen(QColor(180, 110, 15, 100))
437                painter.drawLine(2, lsmall - (lsmall * 0.90), lfull-2.0, lsmall - (lsmall * 0.90))
438
439                # Red
440                painter.setPen(QColor(110, 15, 15, 100))
441                painter.drawLine(2, lsmall - (lsmall * 0.96), lfull-2.0, lsmall - (lsmall * 0.96))
442
443    # --------------------------------------------------------------------------------------------------------
444
445    def resizeEvent(self, event):
446        QWidget.resizeEvent(self, event)
447        self.updateGrandientFinalStop()
448
449# ------------------------------------------------------------------------------------------------------------
450# Main Testing
451
452if __name__ == '__main__':
453    import sys
454    import resources_rc
455    from PyQt5.QtWidgets import QApplication
456
457    app = QApplication(sys.argv)
458
459    gui = DigitalPeakMeter(None)
460    gui.setChannelCount(2)
461    #gui.setMeterOrientation(DigitalPeakMeter.HORIZONTAL)
462    gui.setMeterStyle(DigitalPeakMeter.STYLE_RNCBC)
463    gui.displayMeter(1, 0.5)
464    gui.displayMeter(2, 0.8)
465    gui.show()
466
467    sys.exit(app.exec_())
468
469# ------------------------------------------------------------------------------------------------------------
470