-
-
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
Fix instrument release fade-out #6908
Conversation
This fixes to bugs leading to clicks on instrument note-off in most instruments. The first bug was introduced as part of a refactoring done by Vesa [1] and caused each note play handle's last buffer being dropped because the play handles were deleted before being processed in AudioPort. Thanks to @lleroy for pointing this out. [2] The second bug / typo has always been there [3] and was a misplaced parenthesis in Instrument::applyRelease breaking the calculation of the note-off envelope's start frame, sometimes putting it outside of the buffer. Fixes #3086 [1] 857de8d and related commits [1] #3086 (comment) [2] 0243338
I've been testing a bug in the Sample Track where not the whole sample is rendered and it also seem to be fixed in this PR. I'm not done setting up a proper test case for this but a quick test gives me the following result. Original soundfile, 2 seconds 440 Hz sinewave. |
@lukas-w, is the lower TripleOsciallator waveform from after the fix? I am asking because it look like some form of DC offset is introduced towards the end, i.e. the waveform is not oscillating around the zero line. That computation you have mentioned looks complicated indeed. When looking at it I found it surprising that the |
I reacted to this too. It's just multiple sinewaves of different frequencies together though. We're good... this time. |
That's strange. I admit I didn't really take a closer look at the release ramp and how it's calculated. If the implementation is just flawed, I'm all for simply replacing those handful of lines with something that makes more sense to us. |
There are also artifacts that don't look like simple fade outs (this is a sine signal without the code changes described above!): Test data was repeated A1 notes with a TripleOsc set to one sine. Note lenghts were eights with a sixteenth of pause in between. Most notes look as follows and I am not sure if this is expected with a linear fade out. The curves at the end look strangely inverted: |
I think that looks like what I'd expected on a lower note and a fast decay. Try a higher note and see if you can get a decaying note over more than one wave cycle. |
The decay of the release ramp is linear and with a linear release ramp there shouldn't be any quick changes in direction like the one seen in the last image. I have tested this with the following Desmos graph: https://www.desmos.com/calculator/msc6zsiefc In the graph only the quadrants to the right of the y axis are relevant. The graph shows a sine signal that is attenuated linearly starting at x=0. The parameters do the following:
Edit: Added direct screenshot of the graph. |
You're right @zonkmachine. It looks like the release should be defined in milliseconds which is then converted into a number of samples based on the sample rate. I also wonder if this problem can be solved in the Perhaps a helper class that's used by
An instance of this class is then used by
When the number of release samples of the instrument have been reached the current value should be 0 or close to 0 for the exponential decay. Edit: more detailed description of the helper class and its usage. |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Excuse the noise, I was on the wrong branch. 😢 |
Ok, that's strange. I now also get the expected ramps from 1 to 0 for almost all notes if I apply the "output the release ramp"-patch, @zonkmachine. Except for the last note in my test file: Here's the test file that I have used with a short buffer size of 32: 6908-FixReleaseEnv-TestFile.zip. |
It's easier to follow the wave/'release ramp' if you output them side by side: buf[f][0] *= fac;
buf[f][1] = fac; |
The only explanation that I can find for the ramps shown towards the beginning of this thread is that I must accidentally have been on some other branch as well. Which is strange because I vividly remember that I noticed that the |
32 samples is what the idle 1.0 beginning part of the release ramp is and if I use a buffer size of 64 I get a 64 samples width. |
@michaelgregorius Can you test if this fix takes care of the problem? diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp
index fd729e3ab..1ff01535e 100644
--- a/src/core/Instrument.cpp
+++ b/src/core/Instrument.cpp
@@ -182,7 +182,7 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n )
const fpp_t frames = _n->framesLeftForCurrentPeriod();
const fpp_t fpp = Engine::audioEngine()->framesPerPeriod();
const f_cnt_t fl = _n->framesLeft();
- if( fl <= desiredReleaseFrames()+fpp )
+ if (fl < desiredReleaseFrames() + fpp)
{
for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ?
(std::max(fpp - desiredReleaseFrames(), 0) + |
I'm not 100% confident but I printed out the value of |
I tested @lleroy 's attempt at a smoother release presented here. Below is a mashup of ten releases of sinewaves of various frequencies. PS. In this test I had also forgotten that I had increased the release time to 256 frames in the Triple Oscillator. Upper - This PR with 256 frame release |
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 have read the code to the best of my abilities and I believe that this is correct. Testing has showed no new issues and the relief of the clicking on note off is noticeable. As shown in the discussion in this thread it's also visibly improved.
I am now definitively on the branch and I am still getting the hard changes of direction in the signal: To get some better insight I have improved the Desmos graph a bit: https://www.desmos.com/calculator/vzq5goyugd. It now shows the original signal, the attenuation curve and the resulting signal. The graph now also shows the signal before the attenuation. For certain settings the hard changes can be reproduced with the graph: In this case it is caused by an attenuation that does not even last half of a cycle. As @zonkmachine has noted the attenuation time should ideally be defined in milliseconds and not in a fixed number of samples. So I propose to choose a time that will take around one cycle of a lower note, e.g. 40 Hz. This would mean an attenuation time of 25 ms. At a sample rate of 44100 Hz this would mean that the attenuation is applied over 44100/40 = 1102,5 samples. Visually this would look as follows: |
@lukas-w @michaelgregorius This is already an improvement and I think it should be merged. The rest can be fixed up later. |
Hi @zonkmachine, I can test you fix. However, I am a bit out of the loop now about which specific problem it is supposed to fix. 😅 Can you please describe the expected difference if it is applied to the branch of this PR? |
This issue with the release ramp hanging on 1.0 for one buffer described here: #6908 (comment) |
Good news: I cannot reproduce the ramps hanging at 1.0 anymore. I have used this PRs branch with and without the patch. @zonkmachine, do you have a test file reproduces the problem reliably? |
I'll see if I can fix one for you. I think the issue basically is that we're one off and depending on the buffer size, so it may be that it's much easier to reproduce with smaller buffer sizes. Are you still on 32 ? |
I have been using a buffer size of 64 because it seemed that this would make the glitches more prominent (comment). However, now I have tried again with a size of 32 and was able to reproduce the problem without the patch but not when it is applied. 🥳 |
Here is a test project: sinearp.mmp.txt |
Thanks for the project! Same results here: the problem can be reproduced without the patch but it cannot be reproduced with the patch applied. Looks like the patch should go in. 👍 |
Nice job with those Desmos graphs! Very enlightening.
That sounds like a wee bit too much. Did you try the smoother, s-shape release linked to above? lleroy's code rebased on this PR below. When testing I also bumped the 3osc release to 256 and that combo greatly reduced the issue. diff --git a/src/core/Instrument.cpp b/src/core/Instrument.cpp
index fd729e3ab..7badee7db 100644
--- a/src/core/Instrument.cpp
+++ b/src/core/Instrument.cpp
@@ -182,14 +182,30 @@ void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n )
const fpp_t frames = _n->framesLeftForCurrentPeriod();
const fpp_t fpp = Engine::audioEngine()->framesPerPeriod();
const f_cnt_t fl = _n->framesLeft();
- if( fl <= desiredReleaseFrames()+fpp )
+
+ if( _n->totalFramesPlayed() == 0 )
+ {
+ const fpp_t nf = qMin( fpp, frames);
+ for( fpp_t f = 0 ; f < nf; ++f )
+ {
+ float fac = (float)( f) / nf;
+ if (fac<0.5) fac = 2*fac*fac; else fac = 1 - 2*(1-fac)*(1-fac);
+ for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch )
+ {
+ buf[f][ch] *= fac;
+ }
+ }
+ }
+
+ if( fl < desiredReleaseFrames()+fpp )
{
for( fpp_t f = (fpp_t)( ( fl > desiredReleaseFrames() ) ?
(std::max(fpp - desiredReleaseFrames(), 0) +
fl) % fpp : 0); f < frames; ++f)
{
- const float fac = (float)( fl-f-1 ) /
- desiredReleaseFrames();
+ float fac = (float)( fl-f-1 ) / desiredReleaseFrames();
+ if (fac<0.5) { fac = 2*fac*fac; }
+ else { fac = 1 - 2*(1-fac)*(1-fac); }
for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch )
{
buf[f][ch] *= fac;
(END)
|
As discovered by @michaelgregorious & @zonkmachine, applyRelease's condition to determine whether the release ramp starts within the current buffer was off by 1 frame, running the release code when the ramp should only start in frame 0 of the next buffer. This could cause the ramp to be miscalculated, starting at a value greater than 1.0 and and thus actually amplifying the signal.
Thanks @michaelgregorius & @zonkmachine for all your testing. :) I pushed the proposed "hanging" fix as bf5f4a7. Does anyone else find this function really hard to read though? It should be simple but my head hurts trying to understand it. If you don't mind, I'd like to push the rewrite seen below. It should be equivalent to the existing one, at least the void Instrument::applyRelease( sampleFrame * buf, const NotePlayHandle * _n )
{
const fpp_t fpp = Engine::audioEngine()->framesPerPeriod();
const auto releaseFrames = desiredReleaseFrames();
const auto endFrame = _n->framesLeft();
const auto startFrame = std::max(0, endFrame - releaseFrames);
for (auto f = startFrame; f < endFrame && f < fpp; f++)
{
const float fac = (float)(endFrame - f - 1) / (float)releaseFrames;
for (ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ch++)
{
buf[f][ch] *= fac;
}
}
}
Also, I propose dropping the
Here's a different signal with the change applied:
I know the difference is negligible but somehow it bugs me because I believe this incorrect shift by 1 frame to be the same logic error that lead to the release hanging bug. I'm happy to open separate pull requests for this if it makes things simpler. For the other proposed changes (sample-rate-independent release duration, different release shapes), I'd prefer to keep their discussion and implementation out of this PR to not bloat the scope. |
Thanks for the merge!
👍
No, it sounds reasonable to fix it here. I'm testing this right now.
Yes, I had my eyes on this one too but couldn't work it out. After the expose above I know why. Also a reasonable fix. |
Hi @lukas-w, the original method is definitively hard to read. I wanted to experiment with implementing an exponential decay but could not wrap my head around what is happening in that method and how to integrate the new code. I have to admit that I find the whole structure around playing notes, etc. rather weird and hard to understand. For example in
It looks as if first a fade in is applied to the buffer followed by an immediate release. I assume that this is not what's really happening and that both implementation will check the |
See discussion in #6908 (comment) and following comments.
@lukas-w I have now tested the reworked |
I don't know, this is one of the few places in LMMS's code that feel somewhat intuitive to me. 😅 I'd say it's an instrument's responsibility to produce a signal based on the information about the note to be played, i.e. start time, release time, frequency, intensity/volume etc. That information is provided in
Pushed, will merge assuming there are no further objections. |
Has tested it some more. Just works, please merge! |
I think the reason that it feels strange to me is that I am more used to the separation of concerns as it is defined by many plugin interfaces. Here it's also an instrument's responsibility to produce sound based on some information but in this case the information is already more "integrated". In most cases the instruments gets handed some buffers and information about the MIDI events as they occur with regards to that buffer (ideally with sample-accuracy). So an instrument for example receives information like "at sample 42 there's a 'note-on' event". As a consequence an instrument does not have to perform calculations using all kinds of information about frames. These calculations would be done outside of the instrument, i.e. in the DAW code that feeds the plugins with the integrated information.
Doesn't the |
It does have a |
This fixes #3086 by addressing two bugs leading to clicks on instrument note-off in most instruments.
The first bug was introduced as part of a refactoring done by Vesa via 857de8d and caused each note play handle's last buffer being dropped because the play handles were deleted before being processed in AudioPort as discovered by @lleroy in #3086 (comment).
The second bug looks like a typo that has always been there since the fade-out was introduced in 0243338 and was a misplaced parenthesis in Instrument::applyRelease breaking the calculation of the note-off envelope's start frame, sometimes putting it outside of the buffer. I'd love a second set of eyes on this one because I'm not sure I really understand the calculation. I think it could just be replaced by
_n->framesBeforeRelease()
and in my testsf
is always equal to that at the start of the loop, but maybe I'm missing some edge case here.Before/after examples copied from #3086 (comment):