Skip to content

Grain FX#363

Merged
jamiefaye merged 2 commits intoSynthstromAudible:communityfrom
alter-alter:grainfx-pr
Aug 20, 2023
Merged

Grain FX#363
jamiefaye merged 2 commits intoSynthstromAudible:communityfrom
alter-alter:grainfx-pr

Conversation

@alter-alter
Copy link
Copy Markdown
Contributor

Added Grain FX to Mod FX
https://youtu.be/9W70SZYMKXo

Parameters:
Mod Depth: Controls Grain Volume / Dry Wet Mix
Mod Offset: Adjusts Grain Size
Mod Rate: Sets Grain Rate
Mod Feedback: Selects Grain Type

Note:
This effect is somewhat resource-intensive, so please use only one instance per song.

@sichtbeton
Copy link
Copy Markdown
Contributor

Tested it with a simple field recording. Instant blade runner.

@sichtbeton
Copy link
Copy Markdown
Contributor

Some options can be modulated, some not. Why is that? Cpu?

@alter-alter
Copy link
Copy Markdown
Contributor Author

@sichtbeton

Some options can be modulated, some not. Why is that? Cpu?

Currently, it is following the standard behavior of Deluge. Changing that might be a bit challenging at the moment.

Copy link
Copy Markdown
Collaborator

@m-m-adams m-m-adams left a comment

Choose a reason for hiding this comment

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

So first off this sounds great! Awesome job and thanks for contributing

Testing this it looks like there's 2 performance issues. Using a test song that can sustain 50 voices, I lose 5 for each clip I add a grain fx too and they stay lost until the modfx is turned off (not just away from grain). E.g. going from off->stereochorus will get 48 voices, going from grain to stereochorus will get 45. This is just caused by the memory for the grain buffer. A first fix is to deallocate that buffer when it's not needed. A potential further fix could be to allocate a single global grain buffer and treat it as a pseudo send fx. Each voice could pull from it and add to it with it's own depth and rate. This is more work to implement so I haven't tested it but I think it would work and still sound cool, if a bit less 'traditional' granular.

Secondly the DSP code is more intensive than the other modfx. I think this is improvable, I've left some comments on easy wins but the bigger one is probably vectorizing the grain calculations. They're fully parallelizable so should be able to get on NEON for a 2-3x speedup

Third I think it is nicer to have the feedback scale with the mix level. There is a pulsing effect that becomes audible on sustained notes and it is alleviated with lower feedback settings. I've left a comment with the calculation I used for testing and for me it produces a more usable range of depth settings

grainRate = std::max<int32_t>(1, grainRate);
grainRate = (kSampleRate << 1) / grainRate;
// Preset 0=default
grainPitchType = (int8_t)(multiply_32x32_rshift32_rounded(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The goal here is to quantize to 5 levels? It could use a comment

// Preset 0=default
grainPitchType = (int8_t)(multiply_32x32_rshift32_rounded(
unpatchedParams->getValue(Param::Unpatched::MOD_FX_FEEDBACK), 5));
grainPitchType = std::max<int8_t>(-2, (std::min<int8_t>(2, grainPitchType)));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clamp instead

int32_t pos =
(grains[i].startPoint + delta + kModFXGrainBufferSize) & kModFXGrainBufferIndexMask;

grains_l = add_saturation(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Multiply and accumulate to save a cycle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

multiply_accumulate_32x32_rshift32_rounded (smmlar) does not seem to take saturation operations into account.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It doesn't, but if this is overflowing then that should be fixed instead. The saturation in saturating arithmetic is hard clipping in audio terms and not really desirable

& kModFXGrainBufferIndexMask;
grains[i].counter = 0;
grains[i].rev = (getRandom255() < 76);
if (grainPitchType == -2) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you use a switch for readability?

}
else if (grainPitchType == 2) { // temopo sync
grains[i].pitch = (getRandom255() < 25) ? 512
: (getRandom255() < 153) ? 2048
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can skip a call by storing the previous random value and comparing to 176 instead


int32_t grains_l = 0;
int32_t grains_r = 0;
for (int32_t i = 0; i < 8; i++) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You should be able to render this in a vector to speed it up. Not critical to merge but look at the oscillator render function for an example

}
}
}
grains[i].volScale = (2147483647 / (grains[i].length >> 1));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Stick this in the previous if statement, division is expensive and there's no point in calculating it if grain length is 0

grainShift = 44 * 300; //(kSampleRate / 1000) * 300;
// Size
grainSize = 44 * ((((unpatchedParams->getValue(Param::Unpatched::MOD_FX_OFFSET) >> 1) + 1073741824) >> 21));
grainSize = std::max<int32_t>(440, std::min<int32_t>(35280, grainSize)); //10ms - 800ms
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

std::Clamp instead (applies in a bunch of places)

if (!grainInitialized && modFXGrainBufferWriteIndex >= 65536) {
grainInitialized = true;
}
*postFXVolume = multiply_32x32_rshift32(*postFXVolume, 1518500250) << 1; // Divide by sqrt(2)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

define a constant for root 2 in fixedpoint.h (ONE_OVER_SQRT2_Q31) and use that

(multiply_32x32_rshift32_rounded(multiply_32x32_rshift32_rounded(grainVol, grainVol), grainVol) << 2)
+ 2147483648; //Cubic
grainVol = std::max<int32_t>(0, std::min<int32_t>(2147483647, grainVol));
grainDryVol =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be nice to have feedback scale with the mix level - grainFeedbackVol = grainVol >> 3; works well in my experimentation. The current feedback makes a pulsing sound with sustained notes and I'd like to be able to have lower mixes without it

@m-m-adams
Copy link
Copy Markdown
Collaborator

It also might be good to swap the feedback and depth pads

- Fixing buffer dealloc timing
- Using MAC
- std::clamp
- grainFeedbackVol = grainVol >> 3
@alter-alter
Copy link
Copy Markdown
Contributor Author

@m-m-adams

It also might be good to swap the feedback and depth pads

Since the modulatable parameters are only Depth and Rate, I believe Depth would be suitable for controlling the volume of the grain.

@m-m-adams
Copy link
Copy Markdown
Collaborator

You can just make it modulatable by changing it from a Param::Unpatched to a Param::Global. Automating it works fine with the current modfx so modulating should be fine as well

@alter-alter
Copy link
Copy Markdown
Contributor Author

@m-m-adams

You can just make it modulatable by changing it from a Param::Unpatched to a Param::Global. Automating it works fine with the current modfx so modulating should be fine as well

Thank you! However, the change seems to be static and permanent, and I don't want it to affect the entire system. Is there a way to dynamically change the patch type only when ModFxType is set to Grain?

Copy link
Copy Markdown
Collaborator

@m-m-adams m-m-adams left a comment

Choose a reason for hiding this comment

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

Good to go, the lshift by 0 should be removed but it's optimized out anyway

<< 2,
grains_r);
grains_l = multiply_accumulate_32x32_rshift32_rounded(
grains_l, multiply_32x32_rshift32(modFXGrainBuffer[pos].l, vol) << 0, grains[i].panVolL);
Copy link
Copy Markdown
Collaborator

@m-m-adams m-m-adams Aug 20, 2023

Choose a reason for hiding this comment

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

<< 0 is a nop but it's optimized out anyway

@m-m-adams
Copy link
Copy Markdown
Collaborator

No problem - it can always be made patched in the future

@jamiefaye jamiefaye added this pull request to the merge queue Aug 20, 2023
Merged via the queue into SynthstromAudible:community with commit 246804a Aug 20, 2023
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.

4 participants