trummerschlunk / master_me

automatic mastering plugin for live streaming, podcasts and internet radio.
GNU General Public License v3.0
545 stars 22 forks source link

sync level meters output with GUI drawing #16

Open falkTX opened 2 years ago

falkTX commented 2 years ago

this is an issue as a form of question, because I dont know faust enough to understand how this is typically done.

we want to have level meters on the gui, but in order to make these meters actually useful the dsp level-meter side needs to keep going until notified by the ui that it has read the meter values.

the amateur approach is typically to calculate RMS and other values within a process cycle, every cycle, and set the passive / output meter value to that. this is not a good approach, as the meter values would then be block-size dependent. also there could be several meter values calculated and reset before the GUI even shows a single one.. in short, level meters should run as commanded by the UI.

the flow is typically:

  1. plugin is running, no UI visible yet. plugin has internal values for level metering but we skip them to save cpu load
  2. UI is open, and tells dsp to start level meter stuff
  3. dsp now has level metering active, and keep setting the level-meter value(s) across several audio cycles (that is, the maximum or average value keeps being calculated until told otherwise)
  4. UI repaint happens, it reads the last level-meter value(s) and tells the dsp to start again
  5. dsp sets its internal maximum/average to 0 and continues with the level-metering, same as step 3
  6. another UI repaint, same as step 4
  7. steps 5 and 6 keep going while UI is open
  8. UI is closed, it tells the dsp side it can stop the level metering

There are possible deviations to this, we can keep level metering active with a fixed time (in ms) if we want for example lv2 control outputs to have useful values even while UI is not open.

now my question is... how do we deal with this with faust? can we specify a time-window for the metering? can it run "forever" (not bound to time) and wait for a signal to reset its internal values?

jkbd commented 2 years ago

I have no experience with this. But I guess, the way is to embed the FAUST-generated class mydsp into something, that properly manages the GUI. (To look at this class, just run faust soundsgood.dsp.)

Instead of patching the generated code to the GUI needs, I assume, one can make FAUST generate what one needs by using "architecture files". But I am just guessing.

sletz commented 2 years ago

This kind of control outputs is typically coded in Faust usinghbargraph/vbargraph primitives (often along with attach) which can deliver one value per block. Then the GUI C++ code can possibly interpret those values the way it needs.

sletz commented 2 years ago

@jkbd BTW I guess https://github.com/trummerschlunk/soundsgood/blob/master/lib/ebur128.dsp would be a nice contribution to the Faust libraries... ((-;

jkbd commented 2 years ago

@sletz I take this as compliment! I have some details in mind that will need a bit of my work first. I'll get back to you within two weeks, I think.

x42 commented 2 years ago

For LV2 plugins I use an Atom port. As soon as the GUI opens, it ask the plugin to send values via Atom messages. Usually limited at 25Hz intervals (via a sample-counter in the DSP code), and when the UI is unmapped (closed, hidden or obscured). An Atom message asks the DSP to stop sending those. Peak hold etc is all on the DSP side.

Though this is likely not easily done in FAUST.

falkTX commented 2 years ago

Though this is likely not easily done in FAUST.

this is my fear too. faust seems to be doing exactly what I was hoping it would not, calculating these values on an audio block basis, which for one leads to inconsistent results depending on the audio buffer/block size the user has setup.

sletz commented 2 years ago

the flow is typically:

Any simple example of C++ code that does that?

falkTX commented 2 years ago

the flow is typically:

Any simple example of C++ code that does that?

Can even write something quick here.

on dsp side we have:

struct dsp {
    float fOutLeft = 0.f;
    float fOutRight = 0.f;
    std::atomic<bool> fNeedsReset { false };

    void run(const float** buffer, uint32_t frames) override
    {
        float tmp;
        float tmpLeft  = 0.f;
        float tmpRight = 0.f;

        for (uint32_t i=0; i<frames; ++i)
        {
            // left
            tmp = std::abs(buffer[0][i]);

            if (tmp > tmpLeft)
                tmpLeft = tmp;

            // right
            tmp = std::abs(buffer[1][i]);

            if (tmp > tmpRight)
                tmpRight = tmp;
        }

        if (tmpLeft > 1.0f)
            tmpLeft = 1.0f;
        if (tmpRight > 1.0f)
            tmpRight = 1.0f;

        if (fNeedsReset.getAndSwap(false))
        {
            fOutLeft  = tmpLeft;
            fOutRight = tmpRight;
        }
        else
        {
            if (tmpLeft > fOutLeft)
                fOutLeft = tmpLeft;
            if (tmpRight > fOutRight)
                fOutRight = tmpRight;
        }
    }
};

See how fOutLeft and fOutRight are kept across several audio block cycles, and only reset when fNeedsReset was set. This makes them accumulate the max value until told otherwise.

Also there are no log10 or similar calls to convert into dB scale, this part can be done by the UI later.

sletz commented 2 years ago

So the DSP loop computes tmpLeft and tmpRight on a given block size (= frames ), then the outside code use them and possibly update fOutLeft and fOutRight.

In Faust model you would use bargraph to compute and produce the equivalent of tmpLeft and tmpRight (each block), then have some external C++ in the architecture file to use them. So I don't see real blocking issue here.

falkTX commented 2 years ago

The "blocking" issue is just doing it, because we do not at the moment. My faust knowledge is very limited, close to zero, so I am unable to do this part at the moment.

jkbd commented 2 years ago

Perhaps I should read this sooner than later: https://faustdoc.grame.fr/manual/architectures/.

falkTX commented 2 years ago

I already have an architecture file. So custom code can be used. Seems like for this it is just a matter of doing the 2nd part of the code above on the architecture file, as @sletz describes. How would the C++ code that fetches these values from the dsp look like?

sletz commented 2 years ago

Your UI architecture would have to implement addHorizontalBargraph/addVerticalBargraph so that to keep (= register) theFAUSTFLOAT* zone parameter. Assuming hbargraph/vbargraph primitives are used properly in the Faust DSP code, those zones are written at each audio block. Then the outside C++ code can read and use them.

falkTX commented 2 years ago

Might be easier to handle it separately and then integrate it in soundsgood. Is there an example faust file that includes this?

sletz commented 2 years ago

Some "heavy" architectures files here:

falkTX commented 2 years ago

hah i meant faust example files. the architecture stuff I can do pretty easily, not so much the faust stuff.

sletz commented 2 years ago

Some analysis DSP here: https://faustdoc.grame.fr/examples/analysis/

x42 commented 2 years ago

Unless you use shared memory between UI and DSP, you can only send new values to the UI at the end of each process cycle.

So in this case it is up to the architecture (LV2 wrapper) to read meter values from the DSP, cache them as needed, send then on demand to the GUI, and also handle peak-hold reset requests from the GUI.