@Chazrox
I don't have a "clean" example.
This is old code too, so not a good example of good c++ node practices or anything like that.
But here is a c++ node with a global cable(s) set up.
I've had no problems before, with updating existing c++ headers when adding more global cables.
If you make sure that the generated global cable code is up to date.
When you create more cables, you need to re-generate the global cable c++ code in Hise and update those parts in the c++
This generated stuff needs to be kept up to date with the cable names and IDs in your Hise project:
enum class GlobalCables { grainpos = 0 };
using cable_manager_t = routing::global_cable_cpp_manager<SN_GLOBAL_CABLE(94428153)>;
^ Here, only one global cable exists, however when you add more global cables to your Hise project, this code will need regenerating / updating with the new names and IDs of the cables in your Hise project.
C++ node example (using one global cable):
// FILE: Griffin_GrainOsc.h
/*
Granular OSC node: per-voice state & note-reactive pitch.
Each voice owns a Granulator instance via snex::PolyData so reset/prepare
are scoped to the active voice. Pitch maps so that MIDI note 60 plays the
sample at its original pitch / speed
*/
#pragma once
#include <JuceHeader.h>
#include <array>
#include <vector>
#include <deque>
#include <cmath>
#include <atomic>
#include "src/GriffinGrainOsc/GGO_GranularEngine.h"
namespace project
{
using namespace juce; using namespace hise; using namespace scriptnode;
enum ParamID
{
kGrainSizeMs, kDensityHz, kMainPos, kSpray, kPitchSt, kRandPitch, kRandSize,
kStereoSpread, kReverseChance, kEnvMode, kMaxGrains, kNumParams
};
enum class GlobalCables { grainpos = 0 };
using cable_manager_t = routing::global_cable_cpp_manager<SN_GLOBAL_CABLE(94428153)>;
template<int NV>
struct Griffin_GrainOsc : public data::base, public cable_manager_t
{
SNEX_NODE(Griffin_GrainOsc);
struct MetadataClass { SN_NODE_ID("Griffin_GrainOsc"); };
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, NumSliderPacks = 0,
NumAudioFiles = 1, NumFilters = 0,
NumDisplayBuffers = 0;
struct Voice
{
ggrain::Granulator<float> gran;
uint32_t lastGrainSm{ 0 };
double lastDensityHz{ -1.0 };
int note{ 60 };
double sr{ 44100.0 };
void prepare(double sampleRate)
{
sr = sampleRate;
gran.prepare(sr);
lastGrainSm = 0; lastDensityHz = -1.0;
}
void reset()
{
gran.prepare(sr); // keeps grains cleared, parameters intact
lastGrainSm = 0; lastDensityHz = -1.0;
}
void setSample(const float* mono, uint32_t num, double fileSr)
{
gran.loadSample(mono, num, fileSr);
}
};
template<typename PD>
void process(PD& d)
{
auto& fix = d.template as<snex::Types::ProcessData<2>>();
auto blk = fix.toAudioBlock();
float* L = blk.getChannelPointer(0);
float* R = blk.getChannelPointer(1);
auto& v = voices.get();
v.gran.process(L, R, d.getNumSamples());
flushSpawnEvents();
}
void prepare(snex::Types::PrepareSpecs s)
{
sr = s.sampleRate;
voices.prepare(s);
for (auto& v : voices)
{
v.prepare(sr);
v.gran.setSpawnCallback(&Griffin_GrainOsc::onSpawnThunk, this);
v.gran.setSpawnInfoCallback(&Griffin_GrainOsc::onSpawnInfoThunk, this);
}
// Leave existing param cache alone in runtime use; these are only defaults for first-time init.
params[kGrainSizeMs] = (params[kGrainSizeMs] == 0.0 ? 100.0 : params[kGrainSizeMs]);
params[kDensityHz] = (params[kDensityHz] == 0.0 ? 5.0 : params[kDensityHz]);
params[kMainPos] = (params[kMainPos] == 0.0 ? 0.5 : params[kMainPos]);
params[kSpray] = (params[kSpray] == 0.0 ? 0.0 : params[kSpray]);
params[kPitchSt] = params[kPitchSt]; // keep
params[kRandPitch] = params[kRandPitch];
params[kRandSize] = params[kRandSize];
params[kStereoSpread] = params[kStereoSpread];
params[kReverseChance] = params[kReverseChance];
params[kEnvMode] = (params[kEnvMode] == 0.0 ? 2.0 : params[kEnvMode]);
for (auto& v : voices)
params[kMaxGrains] = (double)v.gran.capacity();
applyParamsAll();
qCount.store(0, std::memory_order_relaxed);
seq.store(0, std::memory_order_relaxed);
}
template<int P>
void setParameter(double v)
{
static_assert(P < kNumParams);
params[P] = v;
for (auto& voice : voices)
applyParamsForVoice(voice);
}
void createParameters(ParameterDataList& data)
{
using PD = parameter::data;
PD pSize("GrainSizeMs", { 1.0, 2000.0, 0.01 }); pSize.setDefaultValue(100.0); pSize.setSkewForCentre(100.0);
registerCallback<kGrainSizeMs>(pSize); data.add(pSize);
PD pDen("DensityHz", { 0.1, 200.0, 0.0001 }); pDen.setDefaultValue(5.0); pDen.setSkewForCentre(10.0);
registerCallback<kDensityHz>(pDen); data.add(pDen);
PD pPos("Position", { 0.0, 1.0, 0.0001 }); pPos.setDefaultValue(0.0);
registerCallback<kMainPos>(pPos); data.add(pPos);
PD pSpr("Spray", { 0.0, 1.0, 0.0 }); pSpr.setDefaultValue(0.0); pSpr.setSkewForCentre(0.2);
registerCallback<kSpray>(pSpr); data.add(pSpr);
PD pPst("PitchSt", { -36.0, 36.0, 0.001 }); pPst.setDefaultValue(0.0);
registerCallback<kPitchSt>(pPst); data.add(pPst);
PD pRP("RandPitch", { 0.0, 1.0, 0.0 }); pRP.setDefaultValue(0.0); pRP.setSkewForCentre(0.08);
registerCallback<kRandPitch>(pRP); data.add(pRP);
PD pRS("RandSize", { 0.0, 1.0, 0.0001 }); pRS.setDefaultValue(0.0); pRS.setSkewForCentre(0.4);
registerCallback<kRandSize>(pRS); data.add(pRS);
PD pSpread("RandPan", { 0.0, 1.0, 0.0001 }); pSpread.setDefaultValue(0.0); pSpread.setSkewForCentre(0.35);
registerCallback<kStereoSpread>(pSpread); data.add(pSpread);
PD pRev("ReverseChance", { 0.0, 1.0, 0.0001 }); pRev.setDefaultValue(0.0); pRev.setSkewForCentre(0.4);
registerCallback<kReverseChance>(pRev); data.add(pRev);
PD pEnv("EnvelopeShape", { 0.0, 2.0, 1.0 }); pEnv.setDefaultValue(2.0);
registerCallback<kEnvMode>(pEnv); data.add(pEnv);
PD pCap("MaxVoiceGrains", { 1.0, (double)128.0, 1.0 }); pCap.setDefaultValue((double)128.0);
registerCallback<kMaxGrains>(pCap); data.add(pCap);
}
/*
Safe sample load:
- Push newest buffer.
- Point all voices at newest buffer (Granulator snapshots pointer/size).
- Compact pool keeping newest and any buffers still used by active grains.
*/
void setExternalData(const snex::ExternalData& ed, int) override
{
using DT = snex::ExternalData::DataType;
if (ed.dataType != DT::AudioFile) return;
if (ed.isXYZ()) return;
if (ed.isEmpty()) return;
if (ed.numChannels <= 0 || ed.numChannels > 2) return;
if (ed.numSamples <= 0 || ed.sampleRate <= 0.0) return;
auto buf = ed.toAudioSampleBuffer();
const int nc = buf.getNumChannels();
const int ns = buf.getNumSamples();
if (ns <= 0 || (nc != 1 && nc != 2)) return;
const double maxSec = 600.0;
const int cap = juce::jmin(ns, (int)juce::roundToInt(ed.sampleRate * maxSec));
SampleBuf newest;
newest.data.resize((size_t)cap);
if (nc == 1)
{
const float* s0 = buf.getReadPointer(0);
for (int i = 0; i < cap; ++i)
{
float x = s0[i];
if (!std::isfinite(x)) x = 0.0f;
newest.data[(size_t)i] = juce::jlimit(-1.0f, 1.0f, x);
}
}
else
{
const float* L = buf.getReadPointer(0);
const float* R = buf.getReadPointer(1);
for (int i = 0; i < cap; ++i)
{
float x = 0.5f * (L[i] + R[i]);
if (!std::isfinite(x)) x = 0.0f;
newest.data[(size_t)i] = juce::jlimit(-1.0f, 1.0f, x);
}
}
newest.srcRate = ed.sampleRate;
pool.push_back(std::move(newest));
const size_t newestIdx = pool.size() - 1;
const float* newestPtr = pool[newestIdx].data.data();
const uint32_t newestSz = (uint32_t)pool[newestIdx].data.size();
const double newestSR = pool[newestIdx].srcRate;
for (auto& v : voices)
v.setSample(newestPtr, newestSz, newestSR);
if (pool.size() <= 1)
return;
size_t write = 0;
for (size_t read = 0; read < pool.size(); ++read)
{
const bool keepNewest = (read == newestIdx);
bool keep = keepNewest;
if (!keepNewest)
{
const float* p = pool[read].data.data();
for (auto& v : voices)
{
if (v.gran.isSamplePointerInUse(p)) { keep = true; break; }
}
}
if (keep)
{
if (write != read)
pool[write] = std::move(pool[read]);
++write;
}
}
pool.resize(write);
}
void reset()
{
for (auto& v : voices)
{
v.reset(); // clear grains; keep parameters/seeds/buffers
applyParamsForVoice(v);
}
qCount.store(0, std::memory_order_relaxed);
}
void handleHiseEvent(HiseEvent& e)
{
if (e.isNoteOn())
{
auto& v = voices.get();
v.gran.flush(true);
v.note = e.getNoteNumberIncludingTransposeAmount();
applyParamsForVoice(v);
}
}
SN_EMPTY_PROCESS_FRAME;
void connectToRuntimeTarget(bool addConnection, const runtime_target::connection& c) override
{
cable_manager_t::connectToRuntimeTarget(addConnection, c);
}
private:
// GUI spawn-event sender
juce::Array<juce::var> packed;
void flushSpawnEvents()
{
const int N = qCount.exchange(0, std::memory_order_acq_rel);
if (N <= 0)
{
packed.clearQuick();
return;
}
packed.clearQuick();
packed.ensureStorageAllocated(N * 3);
for (int i = 0; i < N; ++i)
{
packed.add(juce::var((double)qP0[(size_t)i]));
packed.add(juce::var((double)qVel[(size_t)i]));
packed.add(juce::var((double)qDurMs[(size_t)i]));
}
this->sendDataToGlobalCable<GlobalCables::grainpos>(juce::var(packed));
}
static void onSpawnThunk(void*, float) {}
static void onSpawnInfoThunk(void* user, float p0, float v01ps, float durMs)
{
auto* self = static_cast<Griffin_GrainOsc*>(user);
if (!self) return;
const int idx = self->qCount.load(std::memory_order_relaxed);
if ((unsigned)idx >= (unsigned)kQueueCap) return;
self->qP0[(size_t)idx] = p0;
self->qVel[(size_t)idx] = v01ps;
self->qDurMs[(size_t)idx] = durMs;
self->qCount.store(idx + 1, std::memory_order_release);
}
void applyParamsAll()
{
for (auto& v : voices)
applyParamsForVoice(v);
}
void applyParamsForVoice(Voice& v)
{
const double grainMs = juce::jlimit(1.0, 2000.0, params[kGrainSizeMs]);
const double densityHz = juce::jlimit(0.1, 200.0, params[kDensityHz]);
uint32_t grainSm = (uint32_t)juce::roundToInt((grainMs * 0.001) * v.sr);
if (grainSm == 0) grainSm = 1;
const bool lenChanged = (grainSm != v.lastGrainSm);
const bool denChanged = (std::abs(densityHz - v.lastDensityHz) > 1e-12);
if (lenChanged || denChanged)
{
v.gran.setParameters(grainSm, densityHz, 0.0);
v.lastGrainSm = grainSm;
v.lastDensityHz = densityHz;
}
ggrain::SpawnParams sp{};
sp.mainPos01 = juce::jlimit(0.0, 1.0, params[kMainPos]);
sp.spray01 = juce::jlimit(0.0, 1.0, params[kSpray]);
sp.sprayMode = ggrain::SprayMode::Gaussian;
sp.baseLenSm = grainSm;
sp.sizeRand01 = juce::jlimit(0.0, 1.0, params[kRandSize]);
// Honor requested pitch exactly (no limiting). UI control is still clamped, but note+UI sum is not.
const double uiPitchSt = juce::jlimit(-36.0, 36.0, params[kPitchSt]);
const double noteSemis = (double)v.note - 60.0;
sp.pitchSemitones = uiPitchSt + noteSemis;
sp.pitchRand01 = juce::jlimit(0.0, 1.0, params[kRandPitch]);
sp.reverseChance01 = juce::jlimit(0.0, 1.0, params[kReverseChance]);
sp.stereoSpread01 = juce::jlimit(0.0, 1.0, params[kStereoSpread]);
const int envIdx = (int)juce::jlimit(0.0, 2.0, params[kEnvMode]);
sp.envMode = (envIdx == 0 ? ggrain::EnvelopeMode::RectRaisedCos
: envIdx == 1 ? ggrain::EnvelopeMode::Triangle
: ggrain::EnvelopeMode::Hanning);
v.gran.setSpawnParams(sp);
const size_t cap = v.gran.capacity();
const size_t want = (size_t)juce::roundToInt(juce::jlimit(1.0, (double)cap, params[kMaxGrains]));
v.gran.setMaxActiveGrains(want);
}
struct SampleBuf { std::vector<float> data; double srcRate{ 44100.0 }; };
static constexpr int kQueueCap = 512;
double sr{ 44100.0 };
std::array<double, kNumParams> params{};
snex::PolyData<Voice, NV> voices;
std::vector<SampleBuf> pool;
std::array<float, kQueueCap> qP0{};
std::array<float, kQueueCap> qVel{};
std::array<float, kQueueCap> qDurMs{};
std::atomic<int> qCount{ 0 };
std::atomic<int> seq{ 0 };
};
}