Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pad wavetables #54

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

JohannesLorenz
Copy link
Contributor

@JohannesLorenz JohannesLorenz commented Apr 4, 2020

This is a draft and basic approach to use a wheel in PAD synth to cycle through waves with different basewave parameters in realtime (those waves are what I call "table" here). So, e.g., by turning the wheel, you can smoothly turn a saw into a power wave.

Tester notes:

  1. There is no patch for zyn fusion yet. It can be tested in FLTK.
  2. The computation is slow (128 as usual), so using tiny buffers can be good for testing.

Review hints:

  1. The four commits can be reviewed independently.

There are some issues:

  1. The implementation is far from complete. In the end, I hope we can use modulators to cycle through the wavetable (though in some cases, a wheel might be preferable). But it already works right now.
  2. There are many minor TODOs (e.g. proper interpolation between OscilGen's integer parameters).
  3. The current implementation is just a copy of the "discrete" mode (with very few exceptions). However, the tables can also be generated for bandwidth mode (it just may take longer because you may want to use larger buffers in bandwidth mode). So, instead of adding "WaveTable" as fourth PAD synth mode, I'd rather make it a checkbox that can be applied for all current modes - If that makes sense.

@JohannesLorenz JohannesLorenz force-pushed the pad-wavetables branch 2 times, most recently from 0732fd5 to 5224e81 Compare April 8, 2020 19:46
No functional change, just cascading of `Sample` access. This is done
because it makes reading the next commit more easy.
@fundamental
Copy link
Member

There's a lot of changes towards std::vector (or similar style containers). I'd recommend against this as it does complicate looking at buffer memory placement (i.e. alignment for SSE ops), masks any reallocation, and doesn't really buy us much IMO.

I'm guessing it will be easier to read the individual commits as recommended. (I should follow up on the additional readthrough fairly soon).

@JohannesLorenz
Copy link
Contributor Author

There's a lot of changes towards std::vector

Do you mean those in OscilGen or the new Sample::smp in PADnoteParameters?

i.e. alignment for SSE ops

Do you plan to use SSE and vector will make problems because it restricts memory loading? In this case, I guess we could just replace vector by our own custom container sse_vector that has the same interface as vector?

I'm guessing it will be easier to read the individual commits as recommended

Yes, that should be really be easier.

@JohannesLorenz
Copy link
Contributor Author

Btw, the main reason for using std::vector in OscilGen was to not need to wrtie a copy ctor for OscilGen.

p->sample[n].size = rtosc_argument(m,0).i;
p->sample[n].basefreq = rtosc_argument(m,1).f;
p->setCurSampleSize(n, rtosc_argument(m,0).i);
p->setCurBaseFreq(n, rtosc_argument(m,1).f);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole setting a sample operation is (in my mind at least) a single atomic action. As such do you think it would make sense to have a single setter, which would keep the update somewhat more atomic (not atomic in the multi-threading sense though).

// ...take sample buffer
p->sample[n].smp[idx] = ((float**)blob.data)[idx];
}
//XXX TODO memory management (deallocation of current smp buffer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend against large copies on the realtime side. Just take the pointer and send the old one back to be freed.

{
float harmonics[synth.oscilsize];
memset(spectrum, 0, sizeof(float) * size);
memset(harmonics, 0, sizeof(float) * synth.oscilsize);

//get the harmonic structure from the oscillator (I am using the frequency amplitudes, only)
oscilgen->get(harmonics, basefreq, false);
oscilgen_ptr->get(harmonics, basefreq, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unclear why this change is needed.

assert(minIdx <= oscilgen_ptr->Pbasefuncpar*Sample::num_smps_per_key + 1.0f);
assert(maxIdx >= oscilgen_ptr->Pbasefuncpar*Sample::num_smps_per_key - 1.0f);
#else
std::size_t minIdx, maxIdx;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the idea with Pad Synth is to create a 2D matrix where dimension one is time, dimension two is expected sampling frequency. There's some randomization so typically you can't trivially walk between the different sampling freq rows.

I would have expected wavetable synthesis to turn this into a 3D tensor where you have timeXnote-freqXnote-parameter. It looks like instead there's just timeXnote-parameter here.

Not sure if that's a misreading or not though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this commit, we had Sample sample[PAD_MAX_SAMPLES] (freqs), and each Sample had one float* smp (array for time). Now, each Sample contains std::vector<float*> smp instead of float* smp - each vector element for one wavetable-parameter. So it's indeed 3D now.

// copying is deleted at all
Sample(const Sample& source) = delete;
Sample& operator=(const Sample& source) = delete;
Sample& operator=(Sample&& source) = default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the copy constructors are correctly handled, then I guess this is alright, though in my experience it's still slightly more error prone than a simple struct with raw pointers (and a bit harder to debug unless you have the gdb scripts to understand std::vectors all working and those tend to break at any non-trivial levels of optimization).

@@ -170,10 +199,10 @@ class OscilGen:public Presets
oldmodulationpar3;


fft_t *basefuncFFTfreqs; //Base Function Frequencies
std::vector<fft_t> basefuncFFTfreqs; //Base Function Frequencies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to attach a comment to a whole commit in this interface, but IMO this commit seems unnecessary. For the later commits, all that is needed is harmonic content given wave parameter=x, right?

Would it make sense to have a simple get_harmonics_with_param_equal_x(x)? Perhaps I'm oversimplying things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it's required because the PADnoteParameters::sample vectors can be calculated by different threads for the different frequencies (PAD synth multithreading - "If one thread wants to use wave parameter x, the other one can't use wave parameter y"). Most parts of OscilGen::get look easy to make thread-safe, but a call to prepare seems always needed due to the new basefunc parameter (needPrepare returns true if that one is changed), and I guess making prepare thread safe is as bad as copying the whole class?

operator const FFTwrapper*() const { return fft; }
FFTwrapper* operator->() { return fft; }
const FFTwrapper* operator->() const { return fft; }
} fft;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be really wary about this functionality. We can't have full smart pointers due to the whole multi-threaded situation and this copy would allow for a "weak pointer", which is a complexity that can bite back in unexpected ways. Also, with regards to FFTW, there are modes which assume that the library knows the address of the pointer passed to it. I don't think we're currently using one, but we may end up doing so in the future and this would complicate things as the pointer address of some of the fft_t* references would change.

void setCurSampleSize(std::size_t idx, int val) {
sample[idx].size = val;
}
float curBaseFreq(std::size_t idx) const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we might want to force this one to inline.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could ask the compiler to inline, but FWIR most compilers do their own thing anyways. I think in the end, some runtime tests should be done to see if there's any significant slowdown.

@JohannesLorenz
Copy link
Contributor Author

For general information, this PR is likely superseded by #61.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants