Answered
0
0

Greetings,

I’ve been pulling my hair on trying to find any reference on how to get the time domain data from a sound. I’ve seen several instance where channel::getWaveData claim to do just that but it seems to be non-existent from the latest API version.

Is there any example available that I could have missed? How does channel::getWaveData translate to FMOD 5?

Thank you,

  • You must to post comments
Best Answer
1
1

Here is a modification of the dsp_custom example that prints out some of the data in realtime

/*==============================================================================
Custom DSP Example
Copyright (c), Firelight Technologies Pty, Ltd 2004-2017.

This example shows how to add a user created DSP callback to process audio 
data. The read callback is executed at runtime, and can be added anywhere in
the DSP network.
==============================================================================*/
#include "fmod.hpp"
#include "common.h"

typedef struct 
{
    float *buffer;
    float volume_linear;
    int   length_samples;
    int   channels;
} mydsp_data_t;

FMOD_RESULT F_CALLBACK myDSPCallback(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels) 
{
    mydsp_data_t *data = (mydsp_data_t *)dsp_state->plugindata;

    /*
        This loop assumes inchannels = outchannels, which it will be if the DSP is created with '0' 
        as the number of channels in FMOD_DSP_DESCRIPTION.  
        Specifying an actual channel count will mean you have to take care of any number of channels coming in,
        but outputting the number of channels specified. Generally it is best to keep the channel 
        count at 0 for maximum compatibility.
    */
    for (unsigned int samp = 0; samp < length; samp++) 
    { 
        /*
            Feel free to unroll this.
        */
        for (int chan = 0; chan < *outchannels; chan++)
        {
            /* 
                This DSP filter just halves the volume! 
                Input is modified, and sent to output.
            */
            data->buffer[(samp * *outchannels) + chan] = outbuffer[(samp * inchannels) + chan] = inbuffer[(samp * inchannels) + chan] * data->volume_linear;
        }
    }

    data->channels = inchannels;

    return FMOD_OK; 
} 

/*
    Callback called when DSP is created.   This implementation creates a structure which is attached to the dsp state's 'plugindata' member.
*/
FMOD_RESULT F_CALLBACK myDSPCreateCallback(FMOD_DSP_STATE *dsp_state)
{
    unsigned int blocksize;
    FMOD_RESULT result;

    result = dsp_state->functions->getblocksize(dsp_state, &blocksize);
    ERRCHECK(result);

    mydsp_data_t *data = (mydsp_data_t *)calloc(sizeof(mydsp_data_t), 1);
    if (!data)
    {
        return FMOD_ERR_MEMORY;
    }
    dsp_state->plugindata = data;
    data->volume_linear = 1.0f;
    data->length_samples = blocksize;

    data->buffer = (float *)malloc(blocksize * 8 * sizeof(float));      // *8 = maximum size allowing room for 7.1.   Could ask dsp_state->functions->getspeakermode for the right speakermode to get real speaker count.
    if (!data->buffer)
    {
        return FMOD_ERR_MEMORY;
    }

    return FMOD_OK;
}

/*
    Callback called when DSP is destroyed.   The memory allocated in the create callback can be freed here.
*/
FMOD_RESULT F_CALLBACK myDSPReleaseCallback(FMOD_DSP_STATE *dsp_state)
{
    if (dsp_state->plugindata)
    {
        mydsp_data_t *data = (mydsp_data_t *)dsp_state->plugindata;

        if (data->buffer)
        {
            free(data->buffer);
        }

        free(data);
    }

    return FMOD_OK;
}

/*
    Callback called when DSP::getParameterData is called.   This returns a pointer to the raw floating point PCM data.
    We have set up 'parameter 0' to be the data parameter, so it checks to make sure the passed in index is 0, and nothing else.
*/
FMOD_RESULT F_CALLBACK myDSPGetParameterDataCallback(FMOD_DSP_STATE *dsp_state, int index, void **data, unsigned int *length, char *)
{
    if (index == 0)
    {
        unsigned int blocksize;
        FMOD_RESULT result;
        mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;

        result = dsp_state->functions->getblocksize(dsp_state, &blocksize);
        ERRCHECK(result);

        *data = (void *)mydata;
        *length = blocksize * 2 * sizeof(float);

        return FMOD_OK;
    }

    return FMOD_ERR_INVALID_PARAM;
}

/*
    Callback called when DSP::setParameterFloat is called.   This accepts a floating point 0 to 1 volume value, and stores it.
    We have set up 'parameter 1' to be the volume parameter, so it checks to make sure the passed in index is 1, and nothing else.
*/
FMOD_RESULT F_CALLBACK myDSPSetParameterFloatCallback(FMOD_DSP_STATE *dsp_state, int index, float value)
{
    if (index == 1)
    {
        mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;

        mydata->volume_linear = value;

        return FMOD_OK;
    }

    return FMOD_ERR_INVALID_PARAM;
}

/*
    Callback called when DSP::getParameterFloat is called.   This returns a floating point 0 to 1 volume value.
    We have set up 'parameter 1' to be the volume parameter, so it checks to make sure the passed in index is 1, and nothing else.
    An alternate way of displaying the data is provided, as a string, so the main app can use it.
*/
FMOD_RESULT F_CALLBACK myDSPGetParameterFloatCallback(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valstr)
{
    if (index == 1)
    {
        mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;

        *value = mydata->volume_linear;
        if (valstr)
        {
            sprintf(valstr, "%d", (int)((*value * 100.0f)+0.5f));
        }

        return FMOD_OK;
    }

    return FMOD_ERR_INVALID_PARAM;
}

int FMOD_Main()
{
    FMOD::System       *system;
    FMOD::Sound        *sound;
    FMOD::Channel      *channel;
    FMOD::DSP          *mydsp;
    FMOD::ChannelGroup *mastergroup;
    FMOD_RESULT         result;
    unsigned int        version;
    void               *extradriverdata = 0;

    Common_Init(&extradriverdata);

    /*
        Create a System object and initialize.
    */
    result = FMOD::System_Create(&system);
    ERRCHECK(result);

    result = system->getVersion(&version);
    ERRCHECK(result);

    if (version < FMOD_VERSION)
    {
        Common_Fatal("FMOD lib version %08x doesn't match header version %08x", version, FMOD_VERSION);
    }

    result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
    ERRCHECK(result);

    result = system->createSound(Common_MediaPath("stereo.ogg"), FMOD_LOOP_NORMAL, 0, &sound);
    ERRCHECK(result);

    result = system->playSound(sound, 0, false, &channel);
    ERRCHECK(result);

    /*
        Create the DSP effect.
    */  
    { 
        FMOD_DSP_DESCRIPTION dspdesc; 
        memset(&dspdesc, 0, sizeof(dspdesc));
        FMOD_DSP_PARAMETER_DESC wavedata_desc;
        FMOD_DSP_PARAMETER_DESC volume_desc;
        FMOD_DSP_PARAMETER_DESC *paramdesc[2] = 
        {
            &wavedata_desc,
            &volume_desc
        };

        FMOD_DSP_INIT_PARAMDESC_DATA(wavedata_desc, "wave data", "", "wave data", FMOD_DSP_PARAMETER_DATA_TYPE_USER);
        FMOD_DSP_INIT_PARAMDESC_FLOAT(volume_desc, "volume", "%", "linear volume in percent", 0, 1, 1);

        strncpy(dspdesc.name, "My first DSP unit", sizeof(dspdesc.name));
        dspdesc.version             = 0x00010000;
        dspdesc.numinputbuffers     = 1;
        dspdesc.numoutputbuffers    = 1;
        dspdesc.read                = myDSPCallback; 
        dspdesc.create              = myDSPCreateCallback;
        dspdesc.release             = myDSPReleaseCallback;
        dspdesc.getparameterdata    = myDSPGetParameterDataCallback;
        dspdesc.setparameterfloat   = myDSPSetParameterFloatCallback;
        dspdesc.getparameterfloat   = myDSPGetParameterFloatCallback;
        dspdesc.numparameters       = 2;
        dspdesc.paramdesc           = paramdesc;

        result = system->createDSP(&dspdesc, &mydsp); 
        ERRCHECK(result); 
    } 

    result = system->getMasterChannelGroup(&mastergroup);
    ERRCHECK(result);

    result = mastergroup->addDSP(0, mydsp);
    ERRCHECK(result);

    /*
        Main loop.
    */
    do
    {
        bool bypass;

        Common_Update();

        result = mydsp->getBypass(&bypass);
        ERRCHECK(result);

        if (Common_BtnPress(BTN_ACTION1))
        {
            bypass = !bypass;

            result = mydsp->setBypass(bypass);
            ERRCHECK(result);
        }        
        if (Common_BtnPress(BTN_ACTION2))
        {
            float vol;

            result = mydsp->getParameterFloat(1, &vol, 0, 0);
            ERRCHECK(result);

            if (vol > 0.0f)
            {
                vol -= 0.1f;
            }

            result = mydsp->setParameterFloat(1, vol);
            ERRCHECK(result);
        }
        if (Common_BtnPress(BTN_ACTION3))
        {
            float vol;

            result = mydsp->getParameterFloat(1, &vol, 0, 0);
            ERRCHECK(result);

            if (vol < 1.0f)
            {
                vol += 0.1f;
            }

            result = mydsp->setParameterFloat(1, vol);
            ERRCHECK(result);
        }

        result = system->update();
        ERRCHECK(result);

        {
            char                     volstr[32] = { 0 };
            FMOD_DSP_PARAMETER_DESC *desc;
            mydsp_data_t            *data;

            result = mydsp->getParameterInfo(1, &desc);
            ERRCHECK(result);
            result = mydsp->getParameterFloat(1, 0, volstr, 32);
            ERRCHECK(result);
            result = mydsp->getParameterData(0, (void **)&data, 0, 0, 0);
            ERRCHECK(result);

            Common_Draw("==================================================");
            Common_Draw("Custom DSP Example.");
            Common_Draw("Copyright (c) Firelight Technologies 2004-2017.");
            Common_Draw("==================================================");
            Common_Draw("");
            Common_Draw("Press %s to toggle filter bypass", Common_BtnStr(BTN_ACTION1));
            Common_Draw("Press %s to decrease volume 10%", Common_BtnStr(BTN_ACTION2));
            Common_Draw("Press %s to increase volume 10%", Common_BtnStr(BTN_ACTION3));
            Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
            Common_Draw("");
            Common_Draw("Filter is %s", bypass ? "inactive" : "active");
            Common_Draw("Volume is %s%s", volstr, desc->label);

            if (data->channels)
            {
                char display[80] = { 0 };
                int channel;

                for (channel = 0; channel < data->channels; channel++)
                {
                    int count,level;
                    float max = 0;

                    for (count = 0; count < data->length_samples; count++)
                    {
                        if (fabs(data->buffer[(count * data->channels) + channel]) > max)
                        {
                            max = fabs(data->buffer[(count * data->channels) + channel]);
                        }
                    }
                    level = max * 40.0f;

                    sprintf(display, "%2d ", channel);
                    for (count = 0; count < level; count++) display[count + 3] = '=';

                    Common_Draw(display);
                }
            }
        }

        Common_Sleep(50);
    } while (!Common_BtnPress(BTN_QUIT));

    /*
        Shut down
    */
    result = sound->release();
    ERRCHECK(result);

    result = mastergroup->removeDSP(mydsp);
    ERRCHECK(result);
    result = mydsp->release();
    ERRCHECK(result);

    result = system->close();
    ERRCHECK(result);
    result = system->release();
    ERRCHECK(result);

    Common_Close();

    return 0;
}
  • neosettler

    Thank you Brett,

    I took some time to digest all of this and I’ve never been closer to visualizing wave data with FMOD 5. There is one thing that remains ambiguous though. The length_samples return 1024 in my case. If my understanding is correct, there is no way to change this, or at least, it is not recommended right?

    I’m trying to make the wave data to fit in a 512 float array. How would you suggest to proceed while keeping the most accurate result?

    Also, since it is one dimension array, I must down mix all channels into a one. Would this be solved by averaging all channels?

    Then again, if my understanding is correct, the out buffer channel is a ring buffer, should the 512 float array be reset before every updated?

  • Brett Paterson

    There is a way to change it, but if you want to affect your audio, you wouldn’t, and you don’t need to change it.

    If you want to go smaller, 512 actually is reasonable to change with setDSPBufferSize. I just wouldn’t go bigger than 1024 or smaller than 256. If you halve the block size, double the numbuffers. if you quarter the size, quadruple the numbuffers. This keeps the total buffer size the same at least.

    My new demo pasted above, uses max(). If you want 1 value out of a stereo signal, It would probably make more sense to use max, not average. If one side is silent and the other side is full volume, would you rather see a level that represents the max, or a level that is only half way?

    For arbitrary sizes, you should create a ringbuffer yourself (ie data->buffer), and keep a cursor position of where the last 1024 block was written to. then you can read as much or as little as you like from the cursor position backwards. This is beyond the scope of something like this example that I have pasted above (i have edited it so that it can be included in the next public API release.

  • You must to post comments
0
0

Thanks again for your input Brett.

I assume you mean you want a variable size buffer, this is an exercise for you, you have to create a bigger buffer, and copy it into it in the form of a ringbuffer, instead of just overwriting the same buffer each time.

I mean, how do we set the block size? I need the wave data to fit inside a 512 Float array. Where is the dsp_state->callbacks->getblocksize comes from?

I don’t understand how you think printing more numbers helps you. The bulk of the code is there and the rest is for you to interpret it how you want (ie draw it on a screen as a waveform)””

I’d like to print ALL wave data collected within every channels. Would this be accurate?

for (Int channel = 0; channel < 8; channel++)
    for (UInt samples = 0; samples < length; samples++)
        Draw("chan %d = %f \n", channel, buffer[(samples * 8) + channel]);

Then again, how is the length being set? Could we set it to 512?

UInt length;
m_DspWave->getParameterData(0, (void**)&m_DataWave, &length, NULL, 0);

put a lowpass filter before the custom dsp.

When you say “filter” do you mean adding another DSP? When you say “before” do you mean inside the wave data callback? Would you mind elaborating on this?

  • Brett Paterson

    Please use a comment reply to keep the thread relevant.

    > I mean, how do we set the block size? I need the wave data to fit inside a 512 Float array. Where is the dsp_state->callbacks->getblocksize comes from?

    You dont set the block size. You change the size of your allocated buffer and turn it into a ring buffer, as I said in the original reply.
    System::setDSPBufferSize lets you change the buffer size, but ****dont use it****

    > I’d like to print ALL wave data collected within every channels. Would this be accurate?

    No, because you are printing ‘8’ instead of the correct number of channels that was passed into the read callback. You can store that somewhere.

    > Then again, how is the length being set? Could we set it to 512?

    Already answered above. Dont set fmod’s block size, change your own memory buffer block size, or the amount you consume in one go.

    > When you say “filter” do you mean adding another DSP? When you say “before” do you mean inside the wave data callback? Would you mind elaborating on this?

    Yes adding another DSP, but I dont recommend doing this anyway (because the sound will come out muffled), you would be better off filtering it yourself.

  • neosettler

    My apologies but the comments doesn’t behave well with code. The posts seems to be reordering themselves so yeah, it’s difficult to keep the flow.

    Regarding to your example. The wave data buffer returns (8 * ~32k) values. 8 is the max number of speakers… wish still a mystery to me. What we really want is the number of channel in the audio file, no? Why are we carrying unused data?

    >Dont set fmod’s block size, change your own memory buffer block size, or the amount you consume in one go.

    I was expecting to set it like we do with the spectrum FMOD_DSP_FFT_WINDOWSIZE but apparently the block size is determined differently.

    So I guess my question is, how do we crunch the wave data buffer to a range from 0 to 512?

    float *waveDataToDraw = new float[512];

    for (UInt sample = 0; sample < length; sample++)
    {
    for (Int channel = 0; channel < 8; channel++)
    {
    waveDataToDraw[???] += waveData[(sample * 8) + channel];
    }
    }

    for (UInt i = 0; i < 512; i++)
    waveDataToDraw[i] /= ???;

    How would you get the ???.

  • neosettler

    >No, because you are printing ‘8’ instead of the correct number of channels that was passed into the read callback. You can store that somewhere.

    I’m using 8 because that is the number in the example you provided. How would you set the current channel count to the call back, and retrieve it in the main thread?

  • Brett Paterson

    you would remember what you got passed in the read callback when it comes to channels.

    I’ve updated the original example code post to display info per channel and a bunch of other stuff.

  • You must to post comments
0
0

check the dsp_custom example in the FMOD Studio low level examples. You can capture PCM wavedata yourself through a callback.
getWaveData only exists in FMOD Ex.
This is described in the documentation http://www.fmod.org/documentation/#content/generated/overview/transitioning.html

  • You must to post comments
0
0

Ok, the custom DSP is in place would you mind elaborate on how do we get the time domain of the playing sound exactly?

Should we be concern about retrieving spectral data (FMOD_DSP_FFT_SPECTRUMDATA) in on loop and the wave data in another?

  • You must to post comments
0
0

I’ve been trying to figure how to get the wave data but I’m in the dark.

I guess my first question would be, why do we have to make a custom DPS in the first place? What about having the same mechanic as FMOD_DSP_FFT_SPECTRUMDATA like FMOD_DSP_FFT_WAVEDATA?!

When using a custom DSP, how do we get the callback data from the main thread?

Is there any documentation or example that could be expected in a near future on how to retrieve wave data with FMOD 5?

  • You must to post comments
0
0

Hi Brett, thank you for your input.

I hear you but there is a lot of figuring out without a written example. While this might sound very clear to most, writing PCM data is already ambiguous to me. Do you mean the in/out buffer of a custom DSP? What is the in/out buffer return anyways? How exactly do you share the memory with the custom DSP and the main thread? DSP::setParameterData and DSP::getParameterData ? I’ve looked at all the examples and none of them seems to shared a buffer in memory.

  • Brett Paterson

    The custom dsp code is pretty much all you need to look at.

    You can see it passes input signal (inbuffer) to the output signal (outbuffer). You always have to do that if you want to hear sound. In custom_dsp’s case it is scaling the input. Just change that to no scaling! (remove the multiply)

    Next , you just want to copy the same input buffer to a global memory buffer. You dont need to complicate it by using setParameter/getParameter, just allocate a buffer, write to it from the callback, and read from it in your drawing thread (ie main thread).

    This assumes that you know how to allocate heap memory as a global variable.
    It also assumes you know how to parse interleaved PCM data (ie is the incoming signal stereo? Then you have to parse it as such).

    http://www.fmod.org/documentation/#content/generated/overview/terminology.html has more information on this.

  • You must to post comments
0
0

Hi Brett, thank you for your support in this endeavor. Global variables would be on very last resort. I’m expecting to have several sounds with their own custom DSP simultaneously. I’d be interested in using setParameter/getParameter you referred to, would you mind to elaborate? I don’t have any problem using mutex lock/unlock as well if needed. At this point, I just need to know how to pass the data, for a lack a of better word, elegantly. What is the custom DSP out buffer UNITS exactly? Is it decibels?

  • Brett Paterson

    Hi,
    You can use setUserData on a DSP and getUserData inside the dsp read callback, which is easier, or you’ll have to define callbacks for setParameterData/getParameterData if you want to do that. The implementation of that is varied, you could set and get the address of a buffer, or just simply allocate the buffer in an init callback and use getParameterData to get it (from the app) if you wanted.

    the buffer is just linear floating point data, from -1.0 to +1.0.

  • You must to post comments
0
0

I still don’t get how to pass the out buffer to the DSP, here is some pseudo code that could help clear this out:

FMOD_RESULT F_CALLBACK TimeDomainCallback(
    FMOD_DSP_STATE *dsp_state,
    Float *in_buffer,
    Float *out_buffer,
    UInt in_length, Int in_channels, Int *out_channels)
{
    memcpy(out_buffer, in_buffer, in_length * in_channels * sizeof(Float));

    ???->setUserData(out_buffer);

    return FMOD_OK;
}

void ZFmodSoundSpectrum::Init()
{
FMOD_DSP_DESCRIPTION l_desc;
memset(&l_desc, 0, sizeof(l_desc));
l_desc.version = 0x00010000;
l_desc.numinputbuffers = 1;
l_desc.numoutputbuffers = 1;
l_desc.read = TimeDomainCallback;
ZFmod::GetSystem()->createDSP(&l_desc, &m_DspTime);
}

void ZFmodSoundSpectrum::Update()
{
    Float l_buffer;
    m_DspTime->getUserData(l_buffer);
}

What am I missing?

  • Brett Paterson

    You are setting the userdata as the address of a temporary variable. You can’t do that.
    Next if you dont know how to get the handle of the dsp, you should look at the state that was passed into your callback. https://www.fmod.org/docs/content/generated/FMOD_DSP_STATE.html

    the DSP instance is there.

    Also in your case you’d probably be more interested in FMOD_DSP_STATE::plugindata
    If you set up a ‘create’ callback, your create callback could allocate the secondary buffer, and the read callback could write to it, and your getParameterData callback could return it to the user for reading.
    If you set up a close callback as well, you can free the memory.

  • You must to post comments
0
0

What is the linear floating point represents?

  • You must to post comments
0
0

Thank you for your suggestion Brett but all I’m looking for is the replacement of channel::getWaveData. For all I could gather from your explanations, It seems overly complicated and there is no universe where I can figure this out without some piece of code.

As an example, here is how web audio is doing it:

https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getByteTimeDomainData

One line of code, analyser.getByteTimeDomainData(dataArray); a working example and 30 seconds later, you’re up and running.

I’m still hopping for a solution but it is mind boggling that FMOD removed getWaveData without any concrete alternative.

Thanks anyways,

  • Brett Paterson

    Hi,
    We can add some documentation on how to do it, but i’ve already pretty much given you the answer in the examples and posts above. getWaveData gives you a non continuous window of data, and a dsp callback is a function that gives you the same data, but in a continuous window. All you have to do is copy it from the callback’s inbuffer pointer to the same buffer you would have used as with getWaveData. There is barely extra work in doing this.

  • neosettler

    Hi Brett, thank you for your input. I see the value in this. If it is not too much to ask, would you mind sharing a code snippet on how to pass the out buffer from the call back to the main thread (not using global variables)?

  • You must to post comments
0
0

That is very handy. Thank you Brett.

dsp_state->functions seems to translate to dsp_state->callbacks but nonetheless, we’re getting wave data! We’re almost there as I have a few more questions.

Since the out buffer doesn’t need to be modified, (or does it?)

    memcpy(dsp_state->plugindata, in_buffer, in_length * in_channels * sizeof(Float));

Having only the wave data in mind, would this be copying the buffer correctly or I’m missing something?

The wave data value count should match the FMOD_DSP_FFT_SPECTRUMDATA buffer in this case. How should we set the equivalent of FMOD_DSP_FFT_WINDOWSIZE to the wave data buffer?

Common_Draw(“buffer = … seems to be cutting corners. For the sake of having a complete example and once again, if it is not too much too ask. How about adding the drawing of all values including every channels to the code?

The wave data values seems to be very small, is there a common way to amplify the result with FMOD 5? Could the values be scaled linearly?

The wave data values seems to be very edgy, is there a common way to smooth the result with FMOD 5?

  • Brett Paterson

    > Having only the wave data in mind, would this be copying the buffer correctly or I’m missing something?

    Yes this would work. Its up to you to interpret the data in the way you want after getting it in the main thread

    > The wave data value count should match the FMOD_DSP_FFT_SPECTRUMDATA buffer in this case. How should we set the equivalent of FMOD_DSP_FFT_WINDOWSIZE to the wave data buffer?

    I assume you mean you want a variable size buffer, this is an exercise for you, you have to create a bigger buffer, and copy it into it in the form of a ringbuffer, instead of just overwriting the same buffer each time.

    > How about adding the drawing of all values including every channels to the code?

    I don’t understand how you think printing more numbers helps you. The bulk of the code is there and the rest is for you to interpret it how you want (ie draw it on a screen as a waveform)

    > The wave data values seems to be very small, is there a common way to amplify the result with FMOD 5? Could the values be scaled linearly?

    The wave data is exactly as it comes in, unless it has been panned or volume scaled somewhere in the mix. Scale it linearly to make it ‘louder’

    > The wave data values seems to be very edgy, is there a common way to smooth the result with FMOD 5?

    Thats an interpretation issue. You can draw it the way it is to be correct. smoothing it makes it less correct but maybe more aesthetically pleasing. If you want the edges rounded off, put a lowpass filter before the custom dsp.

  • You must to post comments
Showing 11 results
Your Answer

Please first to submit.