summaryrefslogtreecommitdiff
path: root/voctogui/lib/audioleveldisplay.py
blob: 1a4be141336224a2695adfa0f24b5a3f332b6559 (plain)
  1. import logging, math
  2. from gi.repository import Gst, Gtk
  3. class AudioLevelDisplay(object):
  4. """ Displays a Level-Meter of another VideoDisplay into a GtkWidget """
  5. def __init__(self, drawing_area):
  6. self.log = logging.getLogger('AudioLevelDisplay[%s]' % drawing_area.get_name())
  7. self.drawing_area = drawing_area
  8. self.levelrms = []
  9. self.levelpeak = []
  10. self.leveldecay = []
  11. # register on_draw handler
  12. self.drawing_area.connect('draw', self.on_draw)
  13. def on_draw(self, widget, cr):
  14. # number of audio-channels
  15. channels = len(self.levelrms)
  16. if channels == 0:
  17. return
  18. width = self.drawing_area.get_allocated_width()
  19. height = self.drawing_area.get_allocated_height()
  20. # space between the channels in px
  21. margin = 2
  22. # 1 channel -> 0 margins, 2 channels -> 1 margin, 3 channels…
  23. channel_width = int( (width - (margin * (channels - 1))) / channels )
  24. # self.log.debug(
  25. # 'width: %upx filled with %u channels of each %upx '
  26. # 'and %ux margin of %upx',
  27. # width, channels, channel_width, channels-1, margin)
  28. # normalize db-value to 0…1 and multiply with the height
  29. rms_px = [ self.normalize_db(db) * height for db in self.levelrms ]
  30. peak_px = [ self.normalize_db(db) * height for db in self.levelpeak ]
  31. decay_px = [ self.normalize_db(db) * height for db in self.leveldecay ]
  32. # set the line-width >1, to get a nice overlap
  33. cr.set_line_width(2)
  34. # iterate over all pixels
  35. for y in range(0, height):
  36. # calculate our place in the color-gradient, clamp to 0…1
  37. # 0 -> green, 0.5 -> yellow, 1 -> red
  38. color = self.clamp(((y / height) - 0.6) / 0.42)
  39. for channel in range(0, channels):
  40. # start-coordinate for this channel
  41. x = (channel * channel_width) + (channel * margin)
  42. # calculate the brightness based on whether this line is in the
  43. # active region
  44. # default to 0.25, dark
  45. bright = 0.25
  46. if int(y - decay_px[channel]) in range(0, 2):
  47. # decay marker, 2px wide, extra bright
  48. bright = 1.5
  49. elif y < rms_px[channel]:
  50. # rms bar, full bright
  51. bright = 1
  52. elif y < peak_px[channel]:
  53. # peak bar, a little darker
  54. bright = 0.75
  55. # set the color with a little reduced green
  56. cr.set_source_rgb(
  57. color * bright,
  58. (1-color) * bright * 0.75,
  59. 0
  60. )
  61. # draw the marker
  62. cr.move_to(x, height-y)
  63. cr.line_to(x + channel_width, height-y)
  64. cr.stroke()
  65. # draw a black line for the margin
  66. cr.set_source_rgb(0,0,0)
  67. cr.move_to(x + channel_width, height-y)
  68. cr.line_to(x + channel_width + margin, height-y)
  69. cr.stroke()
  70. # draw db text-markers
  71. cr.set_source_rgb(1, 1, 1)
  72. for db in [-40, -20, -10, -5, -4, -3, -2, -1]:
  73. text = str(db)
  74. xbearing, ybearing, textwidth, textheight, xadvance, yadvance = (
  75. cr.text_extents(text))
  76. y = self.normalize_db(db) * height
  77. cr.move_to((width-textwidth) / 2, height - y - textheight)
  78. cr.show_text(text)
  79. return True
  80. def normalize_db(self, db):
  81. # -60db -> 1.00 (very quiet)
  82. # -30db -> 0.75
  83. # -15db -> 0.50
  84. # -5db -> 0.25
  85. # -0db -> 0.00 (very loud)
  86. logscale = 1 - math.log10(-0.15 * db + 1)
  87. return self.clamp(logscale)
  88. def clamp(self, value, min_value=0, max_value=1):
  89. return max(min(value, max_value), min_value)
  90. def level_callback(self, rms, peak, decay):
  91. self.levelrms = rms
  92. self.levelpeak = peak
  93. self.leveldecay = decay
  94. self.drawing_area.queue_draw()