f82d4fe4bb2f34bc1f0e4ecf6536ec9756ef9399
[ardour.git] / scripts / spectrogram.lua
1 ardour {
2         ["type"]    = "dsp",
3         name        = "Inline Spectrogram",
4         category    = "Visualization",
5         license     = "GPLv2",
6         author      = "Robin Gareus",
7         email       = "robin@gareus.org",
8         site        = "http://gareus.org",
9         description = [[An Example DSP Plugin to display a spectrom on the mixer strip]]
10 }
11
12 -- return possible i/o configurations
13 function dsp_ioconfig ()
14         -- -1, -1 = any number of channels as long as input and output count matches
15         return { [1] = { audio_in = -1, audio_out = -1}, }
16 end
17
18 function dsp_params ()
19         return
20         {
21                 { ["type"] = "input", name = "Logscale", min = 0, max = 1, default = 0, toggled = true },
22                 { ["type"] = "input", name = "1/f scale", min = 0, max = 1, default = 1, toggled = true },
23                 { ["type"] = "input", name = "FFT Size", min = 0, max = 4, default = 3, enum = true, scalepoints =
24                         {
25                                 ["512"]  = 0,
26                                 ["1024"] = 1,
27                                 ["2048"] = 2,
28                                 ["4096"] = 3,
29                                 ["8192"] = 4,
30                         }
31                 },
32                 { ["type"] = "input", name = "Height (Aspect)", min = 0, max = 3, default = 1, enum = true, scalepoints =
33                         {
34                                 ["Min"] = 0,
35                                 ["16:10"] = 1,
36                                 ["1:1"] = 2,
37                                 ["Max"] = 3
38                         }
39                 },
40                 { ["type"] = "input", name = "Range", min = 20, max = 160, default = 60, unit="dB"},
41                 { ["type"] = "input", name = "Offset", min = -40, max = 40, default = 0, unit="dB"},
42         }
43 end
44
45 function dsp_init (rate)
46         -- global variables (DSP part only)
47         samplerate = rate
48         bufsiz = 2 * rate
49         dpy_hz = rate / 25
50         dpy_wr = 0
51 end
52
53 function dsp_configure (ins, outs)
54         -- store configuration in global variable
55         audio_ins = ins:n_audio ()
56         -- allocate shared memory area, ringbuffer between DSP/GUI
57         self:shmem ():allocate (4 + bufsiz)
58         self:shmem ():clear ()
59         self:shmem ():atomic_set_int (0, 0)
60         local cfg = self:shmem ():to_int (1):array ()
61         cfg[1] = samplerate
62         cfg[2] = bufsiz
63 end
64
65 function dsp_runmap (bufs, in_map, out_map, n_samples, offset)
66         local shmem = self:shmem ()
67         local write_ptr = shmem:atomic_get_int (0)
68
69         -- sum channels, copy to ringbuffer
70         for c = 1,audio_ins do
71                 -- Note: lua starts counting at 1, ardour's ChanMapping::get() at 0
72                 local ib = in_map:get (ARDOUR.DataType ("audio"), c - 1) -- get id of mapped input buffer for given cannel
73                 local ob = out_map:get (ARDOUR.DataType ("audio"), c - 1) -- get id of mapped output buffer for given cannel
74                 if (ib ~= ARDOUR.ChanMapping.Invalid) then
75                         -- check ringbuffer wrap-around
76                         if (write_ptr + n_samples < bufsiz) then
77                                 if c == 1 then
78                                         ARDOUR.DSP.copy_vector (shmem:to_float (4 + write_ptr), bufs:get_audio (ib):data (offset), n_samples)
79                                 else
80                                         ARDOUR.DSP.mix_buffers_no_gain (shmem:to_float (4 + write_ptr), bufs:get_audio (ib):data (offset), n_samples)
81                                 end
82                         else
83                                 local w0 = bufsiz - write_ptr
84                                 if c == 1 then
85                                         ARDOUR.DSP.copy_vector (shmem:to_float (4 + write_ptr), bufs:get_audio (ib):data (offset), w0)
86                                         ARDOUR.DSP.copy_vector (shmem:to_float (4)            , bufs:get_audio (ib):data (offset + w0), n_samples - w0)
87                                 else
88                                         ARDOUR.DSP.mix_buffers_no_gain (shmem:to_float (4 + write_ptr), bufs:get_audio (ib):data (offset), w0)
89                                         ARDOUR.DSP.mix_buffers_no_gain (shmem:to_float (4)            , bufs:get_audio (ib):data (offset + w0), n_samples - w0)
90                                 end
91                         end
92                         -- copy data to output (if not processing in-place)
93                         if (ob ~= ARDOUR.ChanMapping.Invalid and ib ~= ob) then
94                                 ARDOUR.DSP.copy_vector (bufs:get_audio (ob):data (offset), bufs:get_audio (ib):data (offset), n_samples)
95                         end
96                 else
97                         -- invalid (unconnnected) input
98                         if (write_ptr + n_samples < bufsiz) then
99                                 ARDOUR.DSP.memset (shmem:to_float (4 + write_ptr), 0, n_samples)
100                         else
101                                 local w0 = bufsiz - write_ptr
102                                 ARDOUR.DSP.memset (shmem:to_float (4 + write_ptr), 0, w0)
103                                 ARDOUR.DSP.memset (shmem:to_float (4)            , 0, n_samples - w0)
104                         end
105                 end
106         end
107
108         -- normalize  1 / channel-count
109         if audio_ins > 1 then
110                 if (write_ptr + n_samples < bufsiz) then
111                         ARDOUR.DSP.apply_gain_to_buffer (shmem:to_float (4 + write_ptr), n_samples, 1 / audio_ins)
112                 else
113                         local w0 = bufsiz - write_ptr
114                         ARDOUR.DSP.apply_gain_to_buffer (shmem:to_float (4 + write_ptr), w0, 1 / audio_ins)
115                         ARDOUR.DSP.apply_gain_to_buffer (shmem:to_float (4)            , n_samples - w0, 1 / audio_ins)
116                 end
117         end
118
119         -- clear unconnected inplace buffers
120         for c = 1,audio_ins do
121                 local ib = in_map:get (ARDOUR.DataType ("audio"), c - 1) -- get id of mapped input buffer for given cannel
122                 local ob = out_map:get (ARDOUR.DataType ("audio"), c - 1) -- get id of mapped output buffer for given cannel
123                 if (ib == ARDOUR.ChanMapping.Invalid and ob ~= ARDOUR.ChanMapping.Invalid) then
124                         bufs:get_audio (ob):silence (n_samples, offset)
125                 end
126         end
127
128         write_ptr = (write_ptr + n_samples) % bufsiz
129         shmem:atomic_set_int (0, write_ptr)
130
131         -- emit QueueDraw every FPS
132         -- TODO: call every window-size worth of samples, at most every FPS
133         dpy_wr = dpy_wr + n_samples
134         if (dpy_wr > dpy_hz) then
135                 dpy_wr = dpy_wr % dpy_hz
136                 self:queue_draw ()
137         end
138 end
139
140 ----------------------------------------------------------------
141 -- GUI
142
143 local fft = nil
144 local read_ptr = 0
145 local line = 0
146 local img = nil
147 local fft_size = 0
148 local last_log = false
149
150 function render_inline (ctx, w, max_h)
151         local ctrl = CtrlPorts:array () -- get control port array (read/write)
152         local shmem = self:shmem () -- get shared memory region
153         local cfg = shmem:to_int (1):array () -- "cast" into lua-table
154         local rate = cfg[1]
155         local buf_size = cfg[2]
156
157         if buf_size == 0 then
158                 return
159         end
160
161         -- get settings
162         local logscale = ctrl[1] or 0; logscale = logscale > 0 -- x-axis logscale
163         local pink = ctrl[2] or 0; pink = pink > 0 -- 1/f scale
164         local fftsizeenum = ctrl[3] or 3 -- fft-size enum
165         local hmode = ctrl[4] or 1 -- height mode enum
166         local dbrange = ctrl[5] or 60
167         local gaindb = ctrl[6] or 0
168
169         local fftsize
170         if fftsizeenum == 0 then fftsize = 512
171         elseif fftsizeenum == 1 then fftsize = 1024
172         elseif fftsizeenum == 2 then fftsize = 2048
173         elseif fftsizeenum == 4 then fftsize = 8192
174         else fftsize = 4096
175         end
176
177         if fftsize ~= fft_size then
178                 fft_size = fftsize
179                 fft = nil
180         end
181
182         if dbrange < 20 then dbrange = 20; end
183         if dbrange > 160 then dbrange = 160; end
184         if gaindb < -40 then dbrange = -40; end
185         if gaindb >  40 then dbrange =  40; end
186
187
188         if not fft then
189                 fft = ARDOUR.DSP.FFTSpectrum (fft_size, rate)
190         end
191
192         if last_log ~= logscale then
193                 last_log = logscale
194                 img = nil
195                 line = 0
196         end
197
198         -- calc height
199         if hmode == 0 then
200                 h = math.ceil (w * 10 / 16)
201                 if (h > 44) then
202                         h = 44
203                 end
204         elseif (hmode == 2) then
205                 h = w
206         elseif (hmode == 3) then
207                 h = max_h
208         else
209                 h = math.ceil (w * 10 / 16)
210         end
211         if (h > max_h) then
212                 h = max_h
213         end
214
215         -- re-create image surface
216         if not img or img:get_width() ~= w or img:get_height () ~= h then
217                 img = Cairo.ImageSurface (Cairo.Format.ARGB32, w, h)
218         end
219         local ictx = img:context ()
220
221         local bins = fft_size / 2 - 1 -- fft bin count
222         local bpx = bins / w  -- bins per x-pixel (linear)
223         local fpb = rate / fft_size -- freq-step per bin
224         local f_e = rate / 2 / fpb -- log-scale exponent
225         local f_b = w / math.log (fft_size / 2) -- inverse log-scale base
226         local f_l = math.log (fft_size / rate) * f_b -- inverse logscale lower-bound
227
228         -- available samples in ring-buffer
229         local write_ptr = shmem:atomic_get_int (0)
230         local avail = (write_ptr + buf_size - read_ptr) % buf_size
231
232         while (avail >= fft_size) do
233                 -- process one line / buffer
234                 if read_ptr + fft_size < buf_size then
235                         fft:set_data_hann (shmem:to_float (read_ptr + 4), fft_size, 0)
236                 else
237                         local r0 = buf_size - read_ptr
238                         fft:set_data_hann (shmem:to_float (read_ptr + 4), r0, 0)
239                         fft:set_data_hann (shmem:to_float (4), fft_size - r0, r0)
240                 end
241
242                 fft:execute ()
243
244                 read_ptr = (read_ptr + fft_size) % buf_size
245                 avail = (write_ptr + buf_size - read_ptr ) % buf_size
246
247                 -- draw spectrum
248                 assert (bpx >= 1)
249
250                 -- scroll
251                 if line == 0 then line = h - 1; else line = line - 1; end
252
253                 -- clear this line
254                 ictx:set_source_rgba (0, 0, 0, 1)
255                 ictx:rectangle (0, line, w, 1)
256                 ictx:fill ()
257
258                 for x = 0, w - 1 do
259                         local pk = 0
260                         local b0, b1
261                         if logscale then
262                                 -- 20 .. 20k
263                                 b0 = math.floor (f_e ^ (x / w))
264                                 b1 = math.floor (f_e ^ ((x + 1) / w))
265                         else
266                                 b0 = math.floor (x * bpx)
267                                 b1 = math.floor ((x + 1) * bpx)
268                         end
269
270                         if b1 >= b0 and b1 <= bins and b0 >= 0 then
271                                 for i = b0, b1 do
272                                         local level = gaindb + fft:power_at_bin (i, pink and i or 1) -- pink ? i : 1
273                                         if level > -dbrange then
274                                                 local p = (dbrange + level) / dbrange
275                                                 if p > pk then pk = p; end
276                                         end
277                                 end
278                         end
279                         if pk > 0.0 then
280                                 if pk > 1.0 then pk = 1.0; end
281                                 ictx:set_source_rgba (ARDOUR.LuaAPI.hsla_to_rgba (.70 - .72 * pk, .9, .3 + pk * .4));
282                                 ictx:rectangle (x, line, 1, 1)
283                                 ictx:fill ()
284                         end
285                 end
286         end
287
288         -- copy image surface
289         if line == 0 then
290                 img:set_as_source (ctx, 0, 0)
291                 ctx:rectangle (0, 0, w, h)
292                 ctx:fill ()
293         else
294                 local yp = h - line - 1;
295                 img:set_as_source (ctx, 0, yp)
296                 ctx:rectangle (0, yp, w, line)
297                 ctx:fill ()
298
299                 img:set_as_source (ctx, 0, -line)
300                 ctx:rectangle (0, 0, w, yp)
301                 ctx:fill ()
302         end
303
304
305         -- draw grid on top
306         function x_at_freq (f)
307                 if logscale then
308                         return f_l + f_b * math.log (f)
309                 else
310                         return 2 * w * f / rate;
311                 end
312         end
313
314         function grid_freq (f)
315                 -- draw vertical grid line
316                 local x = .5 + math.floor (x_at_freq (f))
317                 ctx:move_to (x, 0)
318                 ctx:line_to (x, h)
319                 ctx:stroke ()
320         end
321
322         -- draw grid on top
323         local dash3 = C.DoubleVector ()
324         dash3:add ({1, 3})
325         ctx:set_line_width (1.0)
326         ctx:set_dash (dash3, 2) -- dotted line
327         ctx:set_source_rgba (.5, .5, .5, .8)
328         grid_freq (100)
329         grid_freq (1000)
330         grid_freq (10000)
331         ctx:unset_dash ()
332
333         return {w, h}
334 end