-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Improved Oscillators #4397
Improved Oscillators #4397
Conversation
Prototype of using multiband wavetables in the Oscillator class. This implementation hijacks the original getsample functions, as such replaces the original waveforms of the square, triangle and saw waves. This is not intended to be merged in it’s current format. The additional code is manly a port from a separate GPL project I have developed and requires refactoring to LMMS coding conventions. https://github.com/curlymorphic/involve.git
Wavetables are held in in array of tables accessed s_waveTables[WaveShape][band][frame]
sine triange saw square implemented
I am aware it appears to fail to link the fftw3 library , and am investigating. |
One more issue. When default (wt is off) TripleOscillator/SEGuitar.xpf is played at the same time with simple beat made of kick01.ogg then LMMS always crashes very quickly. I can't crash LMMS when playing SEGuitar.xpf in solo. |
Thanks for testing this, @karmux! |
refactored the fft code
@karmux |
Thanks you @curlymorphic. Both issues are fixed now. |
Added the |
This with @SecondFlight 3xOSC randomization patches will be great. |
I feel this is ready for both testing and code review. The adding of the WT oscillators to additional internal plugins would be better handled in additional pull requests, once any issues are ironed out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The math looks sound. But there are a lot of missing delete
statements that need to be addressed (also, do you have to do something like fftw_free
a plan once you're done using it?) and most importantly, all of the code and variables used to set up the wavetables should be static.
include/Oscillator.h
Outdated
sample_t *m_generatedWaveTable; | ||
static float *s_waveTableBandFreqs; | ||
static int s_waveTablesPerWaveformCount; | ||
static sample_t ***s_waveTables; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing an element in s_waveTables
requires 3 levels of indirection. But we know the size of this array at compile time. I think we should declare it as something like:
static sample_t s_waveTables[WaveShapes::NumWaveShapes][32 /*s_waveTablesPerWaveformCount*/][WAVETABLE_LENGTH];
That way all of these are sitting next to each other in memory & the compiler can translate indices into memory addresses much more easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has the additional advantage of removing the untidy memory allocations for the generated tables, that were never correctly deleted.
src/core/Oscillator.cpp
Outdated
void Oscillator::generateSawWaveTable(int bands) | ||
{ | ||
float max = 0; | ||
m_generatedWaveTable = new sample_t[WAVETABLE_LENGTH]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These generate<X>WaveTable()
functions all leak the allocation associated with m_generatedWaveTable
(i.e. there's no delete
matching this new
statement). I would advise just returning a std::array<sample_t, WAVETABLE_LENGTH>
type rather than returning the data through a heap-allocated member variable. i.e.
std::array<sample_t, WAVETABLE_LENGTH> Oscillator::generateSawWaveTable(int bands)
{
std::array<sample_t, WAVETABLE_LENGTH> wave;
// populate wave[i] for each index i.
return wave;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more alternatives:
- Pass the address to write results
- Use arrays and/or some smart pointers for
s_waveTables
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think you may reduce duplications by using passing waveform type/pointer to a function that returns coefficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generation functions have been revised to take a pointer to the relevant location in s_waveTables
. This addresses the issue of the memory leakage, and removes unnecessarily verbose code.
include/Oscillator.h
Outdated
f1 + 1 : | ||
0; | ||
return linearInterpolate(s_waveTables[shape][waveTableBandFromFreq(m_freq)][f1], | ||
s_waveTables[shape][waveTableBandFromFreq(m_freq)][f2], fraction(frame)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, the compiler can't always see into waveTableBandFromFreq(m_freq)
since that's defined in the source file. So I'll bet it's going to actually call that function twice, since it won't be able to verify it has no side-effects. This might change if you move that function into the header, but I'd feel more comfortable if you assigned that result to a variable, e.g. something like
int band = waveTableBandFromFreq(m_freq);
return linearInterpolate(s_waveTables[shape][band][f1], s_waveTables[shape][band][f2], fraction(frame));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
On a side note do you have any references or key words to type into google, so I can do some reading on this matter please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@curlymorphic I don't know any specific resources to point you to regarding compiler optimizations. If you can read assembly code, you can figure out what kinds of things compilers are capable of by typing code into http://godbolt.org and then trying to figure out what the assembly code is doing. You can even select old compiler versions and see how the code they generate and the optimization they can do has improved over time. But that can be a lot of work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@curlymorphic The optimization @Wallacoloo is describing is called common subexpression elimination.
src/core/Oscillator.cpp
Outdated
@@ -88,6 +118,206 @@ void Oscillator::update( sampleFrame * _ab, const fpp_t _frames, | |||
} | |||
} | |||
|
|||
void Oscillator::generateSineWaveTable(int bands) | |||
{ | |||
bands = bands; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please use Q_UNUSED
instead ? Currently it's unclear why this statement exists. Also, some compilers will complain about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noted, seeing as a sinewave has no harmonics, as the generation is independent of the number of bands, bands
has been removed from the function signature.
include/Oscillator.h
Outdated
int waveTableBandFromFreq(float freq); | ||
void generateWaveTables(); | ||
void allocWaveTables(); | ||
void createFFtPlans(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat odd choice of cases, I guess either FFT or Fft would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revised
src/core/Oscillator.cpp
Outdated
void Oscillator::generateSawWaveTable(int bands) | ||
{ | ||
float max = 0; | ||
m_generatedWaveTable = new sample_t[WAVETABLE_LENGTH]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more alternatives:
- Pass the address to write results
- Use arrays and/or some smart pointers for
s_waveTables
src/core/Oscillator.cpp
Outdated
void Oscillator::generateSawWaveTable(int bands) | ||
{ | ||
float max = 0; | ||
m_generatedWaveTable = new sample_t[WAVETABLE_LENGTH]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think you may reduce duplications by using passing waveform type/pointer to a function that returns coefficient.
s_waveTables[WaveShapes::TriangleWave][i] = m_generatedWaveTable; | ||
} | ||
//generate moogSaw tables | ||
//generate signal buffer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's possible to generate moog saw/exponential(actually parabolic) without FFT. Though their Fourier decompositions are not known well, but I can calculate it if you want. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was intending to keep the fft, As I am intending to use the same functionality for the user wave shapes, However, I would prefer to get the current scope implemented correctly first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FFT analysis has remained, as the functionality has now been extended to the user waveforms
@Wallacoloo @PhysSong Thanks for the review, I shall work through the changes over the coming days |
It would appear that playing a note too high has some unintended side effects. If you're going to test this, turn your volume down. Steps to reproduce:
This produces a genuinely horrifying noise. I've gotten it to segfault by moving my mouse back and forth, but I can't seem to reproduce this consistently. |
Also, I remember noticing that the FFT on some notes doesn't extend all the way to the nyquist rate. How far up it would go varied per-note if I'm remembering correctly. It's possible that this isn't realistic to fix, and it doesn't sound audibly bad to me. Still would be nice to get that precision if it's not out of your way and doesn't have a huge performance impact. Or I might be remembering things wrong. I don't have an easy analysis setup on Ubuntu so I can't test it here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my recent comments on the PR, especially the first one.
I guess it's an out-of-range error. Current implementation seems to handle pitch within MIDI standard correctly, but it doesn't seem like for too high or low notes. |
@curlymorphic I'm sorry I didn't get to this sooner. Everything looks ready to merge, except for the two new comments I just left. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some minor issues which may be ignored. You can ignore this review and leave them as-is if you want.
@@ -79,6 +83,14 @@ class EXPORT Oscillator | |||
delete m_subOsc; | |||
} | |||
|
|||
static void waveTableInit(); | |||
static void destroyFFTPlans(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this line after createFFTPlans
for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The destroyFftPlans
is public so it can be called from Engine::destroy()
on clean up, createFftPlans
is private, I feel this is currently in the correct location.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay.
Downloads for this pull requestGenerated by the LMMS pull requests bot. |
Updated the handling of the wavetables for the userWaveShape, the buffer is now dynamically allocated only when used by @Wallacoloo, @PhysSong I feel I have addressed all outstanding comments |
|
||
void Oscillator::generateAntiAliasUserWaveTable(SampleBuffer *sampleBuffer) | ||
{ | ||
delete sampleBuffer->m_userAntiAliasWaveTable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible memory leaks: sampleBuffer->m_userAntiAliasWaveTable[i]
(0 <= i < WAVE_TABLES_PER_WAVEFORM_COUNT
) are not deleted.
I also think you can move allocation/deallocation code to SampleBuffer
instead. If so, you can share the deletion code with SampleBuffer
destructor.
Alternatively, you may use std::unique_ptr
as suggested by @Wallacoloo. I can provide an example code if you want.
@curlymorphic Do you have time to address the memory issue? I think that's the only remaining issue here. |
@PhysSong Yes I shall address, I am nearing the end of a busy project, and will have some time to finish this issue. |
@curlymorphic Are you still here? |
I Shall not be issuing any additional commits to this pull request, please feel free to make any amendments if any developer wishes |
Hi; I would be interested in getting this PR finished and merged, so I started to play around with it in some free moments. It seems to work fine, although it had to be rebased to master before it even compiled (some headers missing in Zyn, a missing Qt include somewhere else, ladspa libraries failing dynamic linking.. apparently this old version does not like my up-to-date system?). One thing I noticed though: it seems that the band-limited waveform is selected purely based on the pressed key, with no regard to detuning. In other words, the harmonic series of any (non-sine) tone played with the default triple-osc preset abruptly ends at 10 kHz for osc. 2 and at 5 kHz for osc. 3. This obviously makes a big difference to the final sound so I assume it was not intended? EDIT: To clarify, I meant the coarse-detune in triple-osc itself; the master pitch and instrument-wide pitch shift both work fine. It has also another effect: when the coarse-detune is turned up, the waveform used is no longer "band limited enough" and folds back, doing exactly what this PR meant to prevent. So I take it as a sign this behavior was not intentional and should be fixed if possible. Are there any other known issues I missed, apart from this and the memory leak? I also noticed the wave tables take some time to generate when LMMS starts, and only a single CPU core was utilized. So spawning a few threads to take care of it instead could be a good idea as well (although at this point I didn't even see the code yet so I guess I'll decide when it comes to that). |
Good find. That sounds like a bug to me. I don't remember seeing any other issues, but that doesn't mean there aren't any. |
So, I think I have the memory issues resolved (yes, plural -- apart from the leak, valgrind also found some uninitialized values used in conditions -- one in this code and one in my spectrum analyzer, so I fixed that as well). I tried to use Now, the question is: how do I get my changes here? The only way I can think of is making a new PR based on my modified branch; all the commits should remain visible, so I also found 2 more issues:
EDIT: Solved. The first issue happened because the IFFT result of an empty user waveform was normalized by dividing all samples by the maximum value, i.e. zero. This produced a buffer full of NaN-s, which then mixed with the lower-index oscillator and "poisoned" its signal with NaN-s as well. I replaced the max-search + divide with a normalization function from |
The detuning issue should be fixed now as well. All commits are available at: I also parallelized the initialization for platforms that properly support And I also reduced the cutoff threshold for the exponential wave (reasoning same as with the user wave). That should be about it, so far I don't know about any other issues that would block this PR. |
Thanks for finishing and fixing the bugs in this pull request. I can not speak for LMMS as I am no longer active, but as for the pull request, if it makes life easier and the commit logs cleaner I would not object if this was to be squashed into a single new commit and pull request, no need to worry about keeping my name in the logs. @he29-net You deserve the credit for this. @PhysSong would probably be best to ask on how to handle the pull request. |
Can anyone summarize what needs to be done next with this PR? |
Someone with write access should merge my branch from: And a final round of testing may be needed as well, I'm not sure how much testing was done in the original review. |
@he29-net Normally, you are allowed to push on other users' PRs (unless they explicitly forbid it). Did you try? Otherwise I'll merge it and push for you. |
@he29-net Does your branch make sense on its own? If yes, I'd suggest you make a PR against master and then we rebase that PR on master. Otherwise, I would cherry-pick it all from your branch, i.e. just copy the commits. |
@JohannesLorenz I was worried it could somehow mess up Dave's attribution, so I did not even try. But perhaps it won't be so bad; from what I saw in the mean time, the squash commits tend to credit multiple authors, so hopefully that includes all the commits in the PR? Anyway, Dave wrote earlier that we should not worry too much about keeping his name in the logs if it makes things easier, so I'll open a new PR for the fixed version as you suggest, since this seems to be the case that "makes things easier". :) |
This was not my suggestion, but it seems OK, too 😄 |
Wait, what? :D Why else would you suggest to make a PR using my branch then? I'm just running a test build to see if anything broke after I merged current master into the branch. If it works, I'll open the PR and see how it looks.. |
Oh, I just thought you could make
|
Well, "PR with only my fix against master" makes zero sense, since the code that I fixed does not exist in master. :) My fixed branch = Dave's branch + a few extra commits (my fixes). |
Continued in #5826, closing. |
LMMS used the same oscillators in many places for audio generation. This pull request provides a band limited option, so no aliasing, and a cleaner signal. The Implementation has been via TripleOscillator, will the addition of a new button per oscillator allowing switching between the classic sounds, and the new. Currently the sin, triangle, saw, square, moog saw and the exponential waveforms have a band limited version. The user wave shape is still WIP
The clearness of the waveforms can be clearly heard, and become more apparent when using FM or phase mode, backward compatibility is retained.
I have a wiki page with explanations images and audio files
https://github.com/curlymorphic/lmms/wiki/LMMS-Oscillator-Class