[Tutorial] How to create a c++ custom node
-
How to create a custom node, as promised.
I apologise in advance for the awful quality video (quite dry and boring)! But the good news is that I have some nicer quality ones in the pipeline. However I thought I should get this out as soon as possible since some people were waiting on it.
Hope it helps.Windows only, I'll do a mac tutorial later.
Template v1.1:
// ==========================| NodeTemplate v1.0 : by Griffinboy |========================== #pragma once #include <JuceHeader.h> namespace project { using namespace juce; using namespace hise; using namespace scriptnode; template <int NV> // --- Replace with Name struct ExternalNodeTemplate : public data::base { // ------ Replace with Name SNEX_NODE(ExternalNodeTemplate); struct MetadataClass { // --------- Replace with "Name" SN_NODE_ID("ExternalNodeTemplate"); }; // ==========================| Node Properties |========================== static constexpr bool isModNode() { return false; } static constexpr bool isPolyphonic() { return NV > 1; } static constexpr bool hasTail() { return false; } static constexpr bool isSuspendedOnSilence() { return false; } static constexpr int getFixChannelAmount() { return 2; } static constexpr int NumTables = 0; static constexpr int NumSliderPacks = 0; static constexpr int NumAudioFiles = 0; static constexpr int NumFilters = 0; static constexpr int NumDisplayBuffers = 0; // ==========================| Global Variables |========================== // ==========================| Prepare |========================== // Called on init, and when sample rate changes void prepare(PrepareSpecs specs) { float sampleRate = specs.sampleRate; float numChannels = specs.numChannels; leftChannelEffect.prepare(sampleRate, 5.0); // 5ms smoothing on the gain parameter rightChannelEffect.prepare(sampleRate, 5.0); } // ==========================| Reset |========================== // Called when the plugin is reloaded void reset() {} // ==========================| Process |========================== // Blocks of audio enter the script here template <typename ProcessDataType> void process(ProcessDataType& data) { // Convert the audio data to a fixed-channel format for efficient processing auto& fixData = data.template as<ProcessData<getFixChannelAmount()>>(); // Get pointers to each channel's audio data // Changes to these variables will now directly modify the original audio buffer auto audioBlock = fixData.toAudioBlock(); auto* leftChannelData = audioBlock.getChannelPointer(0); auto* rightChannelData = audioBlock.getChannelPointer(1); // Get the number of samples (one channel) for this block int numSamples = data.getNumSamples(); // Pass each channel's audio to the appropriate AudioEffect class leftChannelEffect.process(leftChannelData, numSamples); rightChannelEffect.process(rightChannelData, numSamples); } // ==========================| AudioEffect Class |========================== class AudioEffect { public: AudioEffect(float initialGain = 1.0f) { smoothGain.set(initialGain); // Initialize sfloat with initial gain } void prepare(double sampleRate, double timeInMilliseconds) { smoothGain.prepare(sampleRate, timeInMilliseconds); } void process(float* samples, int numSamples) { for (int i = 0; i < numSamples; ++i) { samples[i] *= smoothGain.advance(); // Apply the gain using the smoothed parameter } } void updateGain(float newGain) { smoothGain.set(newGain); } private: sfloat smoothGain; // Declare smoothGain variable, using sfloat type for smoothing }; // ==========================| Set Parameter |========================== template <int P> void setParameter(double v) { if (P == 0) leftChannelEffect.updateGain(static_cast<float>(v)); // Update gain for left channel else if (P == 1) rightChannelEffect.updateGain(static_cast<float>(v)); // Update gain for right channel } // ==========================| Create Parameters |========================== void createParameters(ParameterDataList& data) { { // { min, max, step } { id } parameter::data p("Left Gain", { 0.1, 2.0, 0.01 }); registerCallback<0>(p); p.setDefaultValue(1.0); data.add(std::move(p)); } { parameter::data p("Right Gain", { 0.1, 2.0, 0.01 }); registerCallback<1>(p); p.setDefaultValue(1.0); data.add(std::move(p)); } } // ==========================| External Data |========================== void setExternalData(const ExternalData& data, int index) {} // ==========================| Handle HISE Event |========================== void handleHiseEvent(HiseEvent& e) {} // ==========================| Modulation Slot |========================== // ( first enable isModNode() at the top of script ) /* ModValue modValue; int handleModulation(double& value) { return modValue.getChangedValue(value); } // Creates callback so that altering the 'modValue' var elsewhere will now update the mod slot modValue.setModValue(0); */ // processFrame: Needed for compiler, does nothing template <typename FrameDataType> void processFrame(FrameDataType& data) {} private: AudioEffect leftChannelEffect; AudioEffect rightChannelEffect; }; }
-
@griffinboy Great stuff, a really clear introduction to the workflow that I've had in mind for advanced DSP stuff.
I've got 2 suggestions, one cosmetically and one critical error in your template:
- For the development compile-test cycle I would recommend using the hardcoded master FX and a saved .hip preset that you can recall with one click, then you don't have to perform so many actions to get the node running when you start debugging. Also the note icon next to the keyboard on the top plays a single note (right click to enable note toggle to simulate your sustain pedal usage of the virtual keyboard). Saves you a few clicks...
- The
blockSize
variable must not be taken from the processing specs. It's a common misconception that will blowup your plugin in some hosts (FL Studio being the most naughty one in this regard). There is a "maximum" number of samples that you can expect to get (which is theblockSize
from the prepare call and should be used to setup buffers and other data structures) and the actual samples that you have to process, which you can query from theProcessData
structure with thegetNumSamples()
method. That value can be lower on certain occasions (either because the DAW splits up the processing at loop points or because it's FL Studio and they do whatever they want), so if you assume a block size of the maximum for every process call you'll get a read access violation and a hard crash pretty reliably.
https://docs.hise.dev/scriptnode/snex_api/data_structures/preparespecs.html
https://docs.hise.dev/scriptnode/snex_api/data_structures/processdata.html -
Whoops!
Thank you for that!I shall amend this and be re-uploading the video anyway, because I found it to be very dry haha.
I shall have to get used to talking on video, doing VO is not something I have experience with! -
@griffinboy don't be too harsh on yourself, the presentation and pace is top notch and you're talking about a super nerdy subject here so being too dry is a non-issue.
-
-
@griffinboy Thanks for this useful and fluent tutorial, I'm sure it will be very helpful for lot's of developers here.
-
Update: Fixed the template for FL Studio buffer size compatibility
-
@griffinboy Thanks a lot for this tutorial! It's much appreciated!
-
@griffinboy well done - !!
-
@griffinboy Thank you I need more tutorials on this topic. It is another HISE learning Youtube Channel / Learning path for Audio Plugins developers.
-
The next video I release will be a reupload of the last one. I want to clean it up and include clearer explanations of how audio data travels through a plugin. I also have a general hise startup guide, but this is aimed at complete beginners.
What would you like to see next in terms of DSP though?
I said in the video that I would be covering filters, but some of that stuff is horribly complex. I'd rather cover another topic before analog modelling haha. -
@griffinboy Many thanks for that! Your tutorial is really very helpful. Somewhere in the forum you mentioned the Chow Tape integration. I would really appreciate a tutorial on this.
-
Okay I'll put that on the list.
Tape is fun.yes, what I actually ended up doing was creating new dsp, heavily based on Chow's work.
Essentially porting parts of his work to Scriptnode.I'll put that on the List.
External library integration is also on my Tutorial to-do list if that's what you were getting at. But I can certainly cover tape. Distortion was a topic I had thought of, but it's a very deep rabbit hole. -
@griffinboy said in [Tutorial] How to create a c++ custom node:
@DabDab
What would you like to see next in terms of DSP though?@griffinboy I am pretty much interested in Hyper SAW / Super Saw Oscillator design. HiSE default synthesizer Group and Waveform Generator SuperSaw / Unison is horrible. If I use Sampler in Synthesizer Group for HyperSaw/SuperSaw, it is weird flanged. Synthesizer group also the same. So I guess only C++ custom OSC design can be the remedy.
Videos for Good Osc , Hyper Saw and Good Filter with Envelop design.
https://youtu.be/-9bdn5S90j0?t=206
** However I didn't try FAUST and RNBO for Custom OSC SAW specially for Unisono. -
If I use Sampler in Synthesizer Group for HyperSaw/SuperSaw, it is weird flanged. Synthesizer group also the same.
It flanges because all unisono voices start at the same time. In order to fix this, you need to add a little bit of sample start modulation (a few thousand samples), then the unison sounds much better.
-
I'm not an expert in Synthesis.
I've only just started dipping into it.
I'm looking at all the different methods for saw generation (With low to no aliasing, in mind) I'm likely to look at this topic after covering some basic distortion stuff., I'm still doing my research.Thanks for the suggestion.
-
@Christoph-Hart Great. Will try it.
-
Nice! Following.
-
@griffinboy Well done! Thank you for your time and your work, it's much appreciated :)
I'll soon dive into custom nodes so this will help me a lot! -
@griffinboy said in [Tutorial] How to create a c++ custom node:
I also have a general hise startup guide, but this is aimed at complete beginners.
And there are such people here! For example, it is doubly difficult for me, not only am I not a programmer, I also do not know English. But even in this situation, intuitively, from the picture, I try and learn. Such visual lessons really help to join the HISE family. Many thanks to all of you for this!