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