Replace a ringbuffer with a multi-reader circular table

a-Inline Spectrogram used a ringbuffer to send mixed down audio data
from the DSP thread to the inline display thread. The problem is that
several inline display threads can coexist (one for the channel strip in
the editor, one for the channel strip in the mixer, and soon one for an
inline display in the generic plugin UI). A ringbuffer is single-writer
single-reader so each display only got part of the data, and all were
competing for it.

Replace it with a circular table, where the DSP sets a write pointer,
and every (inline display) user keeps its own read pointer and checks it
is not so far in the past as to be overtaken by the DSP write pointer.
This commit is contained in:
Julien "_FrnchFrgg_" RIVAUD 2017-07-24 14:54:10 +02:00
parent f1632fcfd2
commit a9d2955f83

View file

@ -40,6 +40,11 @@ function dsp_params ()
}
end
-- symbolic names for shmem offsets
local SHMEM_RATE = 0
local SHMEM_WRITEPTR = 1
local SHMEM_AUDIO = 2
-- a C memory area.
-- It needs to be in global scope.
-- When the variable is set to nil, the allocated memory is free()ed.
@ -53,36 +58,34 @@ function dsp_init (rate)
dpy_hz = rate / 25
dpy_wr = 0
-- create a ringbuffer to hold (float) audio-data
-- http://manual.ardour.org/lua-scripting/class_reference/#PBD:RingBufferF
rb = PBD.RingBufferF (2 * rate)
-- create a shared memory area to hold the sample rate, the write_pointer,
-- and (float) audio-data. Make it big enough to store 2s of audio which
-- should be enough. If not, the DSP will overwrite the oldest data anyway.
self:shmem ():allocate(2 + 2 * rate)
self:shmem ():clear()
self:shmem ():atomic_set_int (SHMEM_RATE, rate)
self:shmem ():atomic_set_int (SHMEM_WRITEPTR, 0)
-- allocate memory, local mix buffer
cmem = ARDOUR.DSP.DspShm (8192)
-- create a table of objects to share with the GUI
local tbl = {}
tbl['rb'] = rb;
tbl['samplerate'] = rate
-- "self" is a special DSP variable referring
-- to the plugin instance itself.
--
-- "table()" is-a http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR.LuaTableRef
-- which allows to store/retrieve lua-tables to share them other interpreters
self:table ():set (tbl);
end
-- "dsp_runmap" uses Ardour's internal processor API, eqivalent to
-- 'connect_and_run()". There is no overhead (mapping, translating buffers).
-- The lua implementation is responsible to map all the buffers directly.
function dsp_runmap (bufs, in_map, out_map, n_samples, offset)
-- here we sum all audio input channels channels and then copy the data to a ringbuffer
-- for the GUI to process later
-- here we sum all audio input channels and then copy the data to a
-- custom-made circular table for the GUIs to process later
local audio_ins = in_map:count (): n_audio () -- number of audio input buffers
local ccnt = 0 -- processed channel count
local mem = cmem:to_float(0) -- a "FloatArray", float* for direct C API usage from the previously allocated buffer
local rate = self:shmem ():atomic_get_int (SHMEM_RATE)
local write_ptr = self:shmem ():atomic_get_int (SHMEM_WRITEPTR)
local ringsize = 2 * rate
local ptr_wrap = math.floor(2^50 / ringsize) * ringsize
for c = 1,audio_ins do
-- see http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:ChanMapping
-- Note: lua starts counting at 1, ardour's ChanMapping::get() at 0
@ -131,9 +134,15 @@ function dsp_runmap (bufs, in_map, out_map, n_samples, offset)
ARDOUR.DSP.memset (mem, 0, n_samples)
end
-- write data to the ringbuffer
-- http://manual.ardour.org/lua-scripting/class_reference/#PBD:RingBufferF
rb:write (mem, n_samples)
-- write data to the circular table
if (write_ptr % ringsize + n_samples < ringsize) then
ARDOUR.DSP.copy_vector (self:shmem ():to_float (SHMEM_AUDIO + write_ptr % ringsize), mem, n_samples)
else
local chunk = ringsize - write_ptr % ringsize
ARDOUR.DSP.copy_vector (self:shmem ():to_float (SHMEM_AUDIO + write_ptr % ringsize), mem, chunk)
ARDOUR.DSP.copy_vector (self:shmem ():to_float (SHMEM_AUDIO), cmem:to_float (chunk), n_samples - chunk)
end
self:shmem ():atomic_set_int (SHMEM_WRITEPTR, (write_ptr + n_samples) % ptr_wrap)
-- emit QueueDraw every FPS
-- TODO: call every FFT window-size worth of samples, at most every FPS
@ -154,10 +163,10 @@ local img = nil
local fft_size = 0
local last_log = false
function render_inline (ctx, w, max_h)
local ctrl = CtrlPorts:array () -- get control port array (read/write)
local tbl = self:table ():get () -- get shared memory table
local rate = tbl['samplerate']
local rate = self:shmem ():atomic_get_int (SHMEM_RATE)
if not cmem then
cmem = ARDOUR.DSP.DspShm (0)
end
@ -231,12 +240,36 @@ function render_inline (ctx, w, max_h)
local f_b = w / math.log (fft_size / 2) -- inverse log-scale base
local f_l = math.log (fft_size / rate) * f_b -- inverse logscale lower-bound
local rb = tbl['rb'];
local mem = cmem:to_float (0)
while (rb:read_space() >= fft_size) do
-- process one line / buffer
rb:read (mem, fft_size)
local ringsize = 2 * rate
local ptr_wrap = math.floor(2^50 / ringsize) * ringsize
local write_ptr
function read_space()
write_ptr = self:shmem ():atomic_get_int (SHMEM_WRITEPTR)
local space = (write_ptr - read_ptr + ptr_wrap) % ptr_wrap
if space > ringsize then
-- the GUI lagged too much and unread data was overwritten
-- jump to the oldest audio still present in the ringtable
read_ptr = write_ptr - ringsize
space = ringsize
end
return space
end
while (read_space() >= fft_size) do
-- read one window from the circular table
if (read_ptr % ringsize + fft_size < ringsize) then
ARDOUR.DSP.copy_vector (mem, self:shmem ():to_float (SHMEM_AUDIO + read_ptr % ringsize), fft_size)
else
local chunk = ringsize - read_ptr % ringsize
ARDOUR.DSP.copy_vector (mem, self:shmem ():to_float (SHMEM_AUDIO + read_ptr % ringsize), chunk)
ARDOUR.DSP.copy_vector (cmem:to_float(chunk), self:shmem ():to_float (SHMEM_AUDIO), fft_size - chunk)
end
read_ptr = (read_ptr + fft_size) % ptr_wrap
-- process one line
fft:set_data_hann (mem, fft_size, 0)
fft:execute ()