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 }; }; }