Extra "ports" of juce-based plugins using the distrho build system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
7.0KB

  1. -- pac freqgraph.lua
  2. --[[
  3. an interactive histogram-ish frequency graph
  4. used in "spectral filter" and "pitch distort"
  5. usage :
  6. local Freqgraph = require "include/pac/freqgraph"
  7. local fg = Freqgraph {
  8. -- required paramters :
  9. -- an array (Lua or C) containing the values to be read and modified by the interactive graph
  10. data = ;
  11. -- number of elements in the array
  12. dataSize = ;
  13. -- optional paramters :
  14. -- the position and size of the graph
  15. bounds = juce.Rectangle_int{15,0,480,330};
  16. title = "";
  17. yAxis = {
  18. name = "amplitude (%)";
  19. values = {
  20. [0] = "0";
  21. [0.5] = "50";
  22. [1] = "100";
  23. }
  24. }
  25. -- colours : pageBack, pageFore, graphBack, graphFore
  26. }
  27. -- public methods :
  28. -- paint the graph onto a pGraphics ctype
  29. fg:paint(g)
  30. -- change the graph's position
  31. fg:setPos(x, y)
  32. --]]
  33. local J = require "include/protojuce"
  34. -- class
  35. local M = {
  36. -- default values
  37. bounds = J.Rectangle_int{15,0,480,330};
  38. pageBack = J.Colour.black;
  39. pageFore = J.Colour.green;
  40. graphBack = J.Colour{r=0, g=0, b=160};
  41. graphFore = J.Colour.green;
  42. yAxis = {
  43. name = "amplitude (%)";
  44. values = {
  45. [0] = "0";
  46. [0.25] = "50";
  47. [0.5] = "100";
  48. [0.75] = "150";
  49. [1] = "200";
  50. }
  51. }
  52. }
  53. M.__index = M
  54. setmetatable(M, {
  55. -- constructor
  56. __call = function (_, arg)
  57. local self = setmetatable(arg, M)
  58. self.frame = J.Rectangle_int(4, 30, self.bounds.w-50, self.bounds.h-65)
  59. self.outFrame = J.Rectangle_int(2, 28, self.frame.w+4, self.frame.h+4)
  60. self.yAmp = 1/self.frame.h
  61. -- display-coord quarters in data coordinates
  62. self.q1 = self:x2bar(self.frame.w/4)
  63. self.q2 = self:x2bar(self.frame.w/2)
  64. self.bgFill = J.FillType(self.pageBack)
  65. self.bgFill2 = J.FillType(self.graphBack)
  66. self.fgFill = J.FillType(self.graphFore)
  67. self:InitBackBuffer()
  68. self.dirtyLeft, self.dirtyRight = 0, self.dataSize-1
  69. gui.addHandler("mouseDrag", function (event)
  70. self:mouseDrag(event)
  71. end)
  72. gui.addHandler("mouseUp", function (event)
  73. self:mouseUp(event)
  74. end)
  75. plugin.addHandler("prepareToPlay", function ()
  76. self:InitBackBuffer()
  77. end)
  78. return self
  79. end;
  80. })
  81. -- Coordinate system conversion between displayed bars <-> data
  82. -- cheap log-like scale made of 3 linear functions
  83. function M:x2bar(x)
  84. if x<self.frame.w/4 then
  85. return math.floor(x*(self.dataSize/(4*self.frame.w)))
  86. elseif x<self.frame.w/2 then
  87. return math.floor(x*((3*self.dataSize)/(4*self.frame.w))-(self.dataSize/8))
  88. else
  89. return math.floor(x*((3*self.dataSize)/(2*self.frame.w))-(self.dataSize/2))
  90. end
  91. end
  92. function M:bar2x(s)
  93. if s<self.q1 then
  94. return math.floor((4*self.frame.w*s)/self.dataSize)
  95. elseif s<self.q2 then
  96. return math.floor((self.frame.w*(self.dataSize+8*s))/(6*self.dataSize))
  97. else
  98. return math.floor((self.frame.w*(self.dataSize+2*s))/(3*self.dataSize))
  99. end
  100. end
  101. function M:y2amp(y) return 1 - y*self.yAmp end
  102. function M:amp2y(a) return math.floor(self.frame.h - a*self.frame.h) end
  103. -- on mouse drag, interpolate the movement, update the data, and mark the dirty graph area
  104. function M:mouseDrag(event)
  105. if not self.bounds:contains(event.mouseDownPos) then
  106. return
  107. end
  108. if self.bar ~= nil then
  109. self.oldbar = self.bar
  110. self.oldamp = self.amp
  111. end
  112. self.bar = self:x2bar(event.x-self.frame.x-self.bounds.x)
  113. self.amp = self:y2amp(event.y-self.frame.y-self.bounds.y)
  114. -- limit to bounds
  115. if self.bar<0 then self.bar=0 end
  116. if self.bar>self.dataSize-1 then self.bar=self.dataSize-1 end
  117. if self.amp<0 then self.amp=0 end
  118. if self.amp>1 then self.amp=1 end
  119. -- interpolate and set
  120. local x1, x2, y1, y2 = self.oldbar,self.bar,self.oldamp,self.amp
  121. if x1 == nil or x1==x2 then
  122. x1,y1 = x2,y2
  123. self.data[x2] = y2
  124. else
  125. if x1>x2 then
  126. x1, x2, y1, y2 = x2, x1, y2, y1
  127. end
  128. for i=x1,x2 do
  129. local x = (x2-i)/(x2-x1)
  130. self.data[i] = y1*x + y2*(1-x)
  131. end
  132. end
  133. -- create or enlarge the currently dirty area (ie. needing a repaint)
  134. if self.dirtyLeft then
  135. if x1<self.dirtyLeft then self.dirtyLeft = x1 end
  136. if x2>self.dirtyRight then self.dirtyRight = x2 end
  137. else
  138. self.dirtyLeft, self.dirtyRight = x1, x2
  139. end
  140. event.originalComponent:repaint()
  141. end
  142. function M:mouseUp(event)
  143. self.bar = nil
  144. self.oldbar = nil
  145. end
  146. -- create the backbuffer and draw all permanent parts of the graph
  147. function M:InitBackBuffer()
  148. self.backBuffer = J.Image(
  149. J.Image.PixelFormat.RGB,
  150. self.bounds.w,
  151. self.bounds.h, true)
  152. local g = J.Graphics(self.backBuffer)
  153. g:fillAll(self.pageBack)
  154. g:setColour(self.graphFore)
  155. g:drawRect(self.outFrame, 1)
  156. g:setColour(self.pageFore)
  157. if self.title then
  158. g:setFont(17)
  159. g:drawText("== "..self.title.." ==", self.frame.x, 5, self.frame.w, 20, J.Justification.centred)
  160. end
  161. -- draw the Y axis labels
  162. g:saveState()
  163. g:addTransform(J.AffineTransform(0, -1, self.bounds.w, 1, 0, self.frame.y))
  164. g:setFont(16)
  165. g:drawText(self.yAxis.name, 0, 0, self.frame.h, 20, J.Justification.centred)
  166. g:restoreState()
  167. g:setFont(14)
  168. for pos, label in pairs(self.yAxis.values) do
  169. local y = self.frame.y+((self.frame.h-10)*(1-pos))-6
  170. g:drawText(tostring(label), self.frame:getR()+3, y, 33, 20)
  171. end
  172. -- draw the X axis labels
  173. g:setFont(16)
  174. g:drawText("frequency (kHz)", self.frame.x, self.bounds.h-20, self.frame.w, 20, J.Justification.centred)
  175. g:setFillType(self.fgFill)
  176. local sr = 44100
  177. if plugin.isSampleRateKnown() then
  178. sr = plugin.getSampleRate()
  179. else
  180. -- if the samplerate is unknown, call this again when it becomes known
  181. plugin.addHandler('prepareToPlay', function() self:InitBackBuffer() end)
  182. end
  183. local f,fmax = 0, sr/2
  184. local function f2x(f)
  185. return self:bar2x(f/fmax*self.dataSize)+self.frame.x
  186. end
  187. g:setFont(14)
  188. while f<fmax do
  189. if f%10000==0 then
  190. g:fillRect(f2x(f), self.frame:getB(), 3, 8)
  191. elseif f%1000==0 then
  192. g:fillRect(f2x(f), self.frame:getB(), 2, 4)
  193. end
  194. f = f + 100
  195. end
  196. g:drawText("0", f2x(0)-2, self.frame:getB()+3, 20, 20)
  197. g:drawText("2", f2x(2000)-2, self.frame:getB()+3, 20, 20)
  198. g:drawText("10", f2x(10000)-2, self.frame:getB()+3, 20, 20)
  199. g:drawText("20", f2x(20000)-2, self.frame:getB()+3, 20, 20)
  200. g = nil
  201. -- draw the entire graph's contents
  202. self:DrawBars(0, self.dataSize-1)
  203. end
  204. -- update the graph between left and right
  205. function M:DrawBars(leftBar, rightBar)
  206. local g = J.Graphics(self.backBuffer)
  207. g:setFillType(self.bgFill2)
  208. local leftX, rightX = self:bar2x(leftBar), self:bar2x(rightBar+1)
  209. g:fillRect(leftX+self.frame.x, self.frame.y, rightX-leftX, self.frame.h)
  210. g:setFillType(self.fgFill)
  211. local lastX = -1
  212. for i = leftBar, rightBar do
  213. local x = self:bar2x(i)
  214. if x~=lastX then
  215. local width = math.max(1,self:bar2x(i+1)-x)
  216. local y = self:amp2y(self.data[i])
  217. local height = self.frame.h-y
  218. g:fillRect(J.Rectangle_int(x+self.frame.x, y+self.frame.y, width, height))
  219. end
  220. lastX = x
  221. end
  222. end
  223. -- on paint, update the backbuffer where necessary, and blit it
  224. function M:paint(g)
  225. if self.dirtyLeft then
  226. local l, r = self.dirtyLeft, self.dirtyRight
  227. self.dirtyLeft, self.dirtyRight = nil, nil
  228. self:DrawBars(l, r)
  229. end
  230. g:drawImageAt(self.backBuffer, self.bounds.x, self.bounds.y)
  231. end
  232. function M:setPos(x,y)
  233. self.bounds.x, self.bounds.y = x, y
  234. local guiComp = gui.getComponent()
  235. if guiComp ~= nil then
  236. guiComp:repaint()
  237. end
  238. end
  239. return M