surge-synthesizer / shortcircuit-xt

Will be a sampler when its done!
GNU General Public License v3.0
241 stars 27 forks source link

"Normalize sample" feature #971

Open Andreya-Autumn opened 2 months ago

Andreya-Autumn commented 2 months ago

Would be really useful for at least two reasons: We have a nice velocity feature which depends on samples being even level, and #947.

I imagine we'd want to use RMS, but some exploration needed there probably.

baconpaul commented 2 months ago

The key insight here is to not have normalize on load and store it and so on, but rather have a user action which lets the mapping or round robin level to the decibel change required to normalize to (whatever measure we want)

baconpaul commented 2 weeks ago

OK so here's the approach I would take laid out in quite a bit of detail. And I would do this in three steps.

Step one is add "sample analytics" to the codebase with a couple of very simple analytics (get peak and get rms) with a test perhaps

Step two is make it so the zone can use those analytics to normalize either one or all samples in its sample set

Step three is to make a UI gesture to hook that up.

Sample Analytics

peak and rms could go on sample.h but the project will eventually have lots of other sample analytics for things like transient detection and slicing and stuff, and those should not go on sample, since sample is really intended to be the store of raw data. So I propose

  1. Add src/dsp/sample_analytics.h and .cpp
  2. Populate it with free functions. For instance the header would look like
namespace scxt::dsp::sample_analytics
{
float computePeak(const std::shared_ptr<sample::Sample> &s);
float computeRMS(const std::shared_ptr<sample::Sample> &s);
}
  1. Should be able to add these to the regtest engine with a stored sample to test if you want

This approach means we could add things like 'find transients for slicing' here in the future.

Once those are implemented you are at merge point one!

Function in zone to call these

You want a function in zone.h to set the level. It probably has a signature like this

void setNormalizedSampleLevel(bool usePeak=false, int associatedSampleID=-1);

And then an implementation roughly like

void Zone::setNormalizedSampleLevel(bool usePeak, int associatedSampleID)
{
   auto startSample = (associatedSampleID < 0) ? 0 : associatedSampleID;
   auto endSample = (associatedSampleID < 0) ? maxSamplesPerZone : associatedSampleID;

   for (auto i=startSample; i < endSample; ++i)
   {
      if (sampleData.samples[i].active && samplePointers[i])
      {
         auto normVal = isPeak ? 
                     dsp::sample_analytics::computePeak(samplePointers[i]) : 
                     dsp::sample_analytics::computerRMS(samplePointers[i]);
         sampleData.samples[i].volume = (whatever calculation of norm val sets volume properly)
      }
   }
}

cool

Set up the UI

Go read the how the ui hangs together documentation some or ask on discord but the short version is

  1. Add a new c2s message
  2. make it so that c2s message looks up the lead zone and calls the function you added and then sends the mapping data back to the ui
  3. Hook that up to some random button or gesture in the ui to test

if you get to this point I'll add some more comments

Jengamon commented 2 weeks ago

Sample Analytics section is done

Jengamon commented 2 weeks ago

The proposal to implement is as follows (from my understanding):

The normalization factor (in dB) will be displayed once a sample has been normalized, something like "normalized: +11.58dB" (the method of normalization is not recorded, only its magnitude).

This normalization is kept separate from the mapping and is done per AssociatedSample, and is not user-editable (except by the means of selecting a different measure to normalize by, or resetting normalization).

This factor is implicitly shown in the waveform display, and zone amplitude would be an offset from this value.

baconpaul commented 2 weeks ago

Yup; and to do that you will add a new variable to associated sample called 'normalizationAmplitude' which streams etc... and the UI can trigger based on it not being exactly 0.

Then the paint method of MappingPane.cpp SampleDisplay can show the text. It is a 'HasEditor' so you have methods like

auto font = editor->themeApplier.interRegularFor(13);
 auto col = editor->themeColor(theme::ColorMap::generic_content_high);
g.drawText("Normalize", (rectangle), juce::Justification::centered);

While in there you might want to add the #973 fields also and set them up in streaming and stuff for them even if we don't add editors now.