Forum
    • Categories
    • Register
    • Login

    C++ Global Cables // How to add multiple cables to an existing compiled network.

    Scheduled Pinned Locked Moved Scripting
    2 Posts 2 Posters 37 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • ChazroxC
      Chazrox
      last edited by Chazrox

      As the title says....

      If I have a working network and I want to add a value readout through a global cable. My understanding is that I create a new cable in onInit, create GC C++ code, then use that information to create the cable in my '.h' file. Is this correct? Do I have to completely unload the network and reload it again? I tried to just add a new cable with script then edit the .h file and recompile but I get errors. I use the same method for creating global cables in a network that isnt already compiled so I know im getting it right somewhere but why does this way of adding cables to an existing network give me compile error when recompiling networks to dlls?

      griffinboyG 1 Reply Last reply Reply Quote 0
      • griffinboyG
        griffinboy @Chazrox
        last edited by griffinboy

        @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 };
            };
        }
        
        
        1 Reply Last reply Reply Quote 0
        • First post
          Last post

        21

        Online

        2.4k

        Users

        13.8k

        Topics

        120.1k

        Posts