• Drawing gradient along a path? Thermometer style gradient?

    Scripting
    1
    0 Votes
    1 Posts
    1 Views
    No one has replied
  • 0 Votes
    1 Posts
    6 Views
    No one has replied
  • AAX Build on MacOS

    General Questions
    10
    0 Votes
    10 Posts
    86 Views
    ustkU

    @David-Healey Well so an SDK downgrade would be the best thing to test, at least to see if the process is the good one

  • Show default preset name on first launch

    General Questions
    8
    0 Votes
    8 Posts
    139 Views
    ustkU

    @dannytaurus Oh nice one! 👍

  • 0 Votes
    1 Posts
    16 Views
    No one has replied
  • Bank headers in flat-list preset browser?

    Scripting
    2
    0 Votes
    2 Posts
    32 Views
    David HealeyD

    @dannytaurus No the column just lists the files. A one column browser is pretty easy to create with a viewport or panel though.

  • New Release - RustEQ le bleu

    General Questions
    4
    9 Votes
    4 Posts
    114 Views
    DanHD

    @ustk very sexy!

  • 0 Votes
    2 Posts
    51 Views
    griffinboyG

    @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 }; }; }
  • HISE Update broke my project.

    Scripting
    29
    0 Votes
    29 Posts
    201 Views
    David HealeyD

    @Chazrox said in HISE Update broke my project.:

    If its not an xml problem whats my next move?

    If it was me I'd crack open a debugger and see what it says, not a simple thing to explain how to here though.

  • I'm having trouble exporting my project in HISE.

    General Questions
    3
    0 Votes
    3 Posts
    70 Views
    E

    @ulrik Thanks for clarifying that detail, I was able to export my practice projects.

  • Is the HISE REST server ready for use?

    AI discussion
    20
    0 Votes
    20 Posts
    196 Views
    trillbillyT

    I havent used Claude but Codex works better than expected as well.

    Ive used it with VS Code and linked it directly to my HISE projects.

    No GUI creation or anything but with effects, scriptnode, debugging, etc it has been a huge time saver.

  • Is the CSS debugger gone?

    Bug Reports
    3
    0 Votes
    3 Posts
    46 Views
    ustkU

    @David-Healey That's where I use it (the blank part above it the bottom of my property editor)
    But it's now back, I just needed to remove it and re-insert, probably a compromised window XML...

  • Mask does not scale properly on HiDPI or Retina display

    Bug Reports
    23
    0 Votes
    23 Posts
    4k Views
    ustkU

    @Chazrox I thought I fixed it about 2 weeks ago. It was a line that was already there so I simply uncommented it and it was working nicely in Hise. But I suspected it made my plugin unstable/causing crash, so I just went back a step.

    I'm not 100% sure it was the real cause, but since the line was commented, I think @Christoph-Hart had a good reason...

  • Automatic license file placement, like user presets

    General Questions
    11
    0 Votes
    11 Posts
    146 Views
    dannytaurusD

    @David-Healey No, because this is about persisting the file to the user's disk.

    Although as I said earlier, I think the final best answer is to display the license directly in the plugin somewhere.

    Then nobody could ever accuse the developer of not making the license easily available.

    Website goes down - no problem
    User moves the plugin to another machine - no problem
    User wipes/moves/modifies their AppData folder - no problem

    If the license is in the plugin itself, it can't be accidentally moved, deleted or overwritten, and it can't become unavailable because of server outage.

    Probably going this route now...

  • 3 Votes
    8 Posts
    224 Views
    ustkU

    @Lindon Yup that's because it needs to be thought more as "container" rather than strictly "installer". PKG or ZIP are containers so -> notarisation

  • Trying to compile FX plugin...new error on me...

    General Questions
    7
    0 Votes
    7 Posts
    171 Views
    HISEnbergH

    @Lindon Yes you spotted both of the main issues:

    JuceHeader collision. Previous versions used #include "../Binaries/JuceLibraryCode/JuceHeader.h" which was fine for the DLL, but exporting to VST3/AU caused the JuceHeaders (project info like companyName, versionString, etc.) to be pulled in twice. Solution is just to use #include <JuceHeader>

    .cpp.inc is resolved on all Fx to just .cpp (it was a leftover from the batch compile I ran).

    A handful of effects couldn't be compiled together — some of the amp/saturation nodes (the ones based on a shared __Gain_H template) and a few of the k-reverbs. Individually they were fine, but combining them caused build errors. It came down to a couple of effects accidentally reusing the same internal names behind the scenes, so they stepped on each other once everything compiled together. That's now sorted out, and the full set compiles as one.

    The repo is up-to-date now and should be more stable.

  • Testers Needed for Rhapsody v3

    General Questions
    38
    1 Votes
    38 Posts
    2k Views
    dannytaurusD

    @David-Healey Looks great! I kid ye not, I'm noodling on my website redesign now and used very similar colours and layout!

    Total coincidence! 😂

    CleanShot 2026-06-18 at 19.05.42@2x.png

    CleanShot 2026-06-18 at 19.05.32@2x.png

  • 0 Votes
    4 Posts
    177 Views
    P

    @ustk @griffinboy that def does so thank you so much.

    P

  • Velocity layers overlapping issue

    General Questions
    3
    0 Votes
    3 Posts
    105 Views
    lijas90L

    @lijas90 Thanks!

  • MIDI Learn improvement

    General Questions
    6
    0 Votes
    6 Posts
    138 Views
    ustkU

    4 over 4, that's enough stats for me 😎