@d-healey I really like that UI. Very simple, accessible, and smooth looking - for lack of a better word!

Best posts made by Orvillain
-
RE: Need filmstrip animations
-
RE: Can We PLEASE Just Get This Feature DONE
Free mankini with every commercial license???
-
RE: Orv's ScriptNode+SNEX Journey
Lesson 5 - SNEX code in a bit more detail.
So I'm by no means an expert in C or C++ - in fact I only just recently started learning it. But here's what I've sussed out in regards to the HISE template.... and template is exactly the right word, because the first line is:
template <int NV> struct audio_loader
Somewhere under the hood, HISE must be setup to send in an integer into any SNEX node, that integer corresponding to a voice. NV = new voice perhaps, or number of voices ????
The line above declares a template that takes this NV integer in, and creates a struct called audio_loader for each instance of NV. Indeed we can prove this by running the following code:
template <int NV> struct audio_loader { SNEX_NODE(audio_loader); ExternalData data; double note = 0.0; // Initialise the processing specs here void prepare(PrepareSpecs ps) { } // Reset the processing pipeline here void reset() { } // Process the signal here template <typename ProcessDataType> void process(ProcessDataType& data) { } // Process the signal as frame here template <int C> void processFrame(span<float, C>& data) { } // Process the MIDI events here void handleHiseEvent(HiseEvent& e) { double note = e.getNoteNumber(); Console.print(note); } // Use this function to setup the external data void setExternalData(const ExternalData& d, int index) { data = d; } // Set the parameters here template <int P> void setParameter(double v) { } };
There are only three things happening here:
- We set the ExternalData as in a previous post.
- We establish a variable with the datatype of double called 'note' and we initialise it as 0.0. But this value will never hold because....
- In the handleHiseEvent() method, we use e.getNoteNumber() and we assign this to the note variable. We then print the note variable out inside of the handleHiseEvent() method.
Now when we run this script, any time we play a midi note, the console will show us the note number that we pressed. This is even true if you play chords, or in a scenario where no note off events occur.
That's a long winded way of saying that a SNEX node is run for each active voice; at least when it is within a ScriptNode Synthesiser dsp network.
The next line in the script after the template is established is:
SNEX_NODE(audio_loader);
This is pretty straight forward. The text you pass here has to match the name of the script loaded inside your SNEX node - not the name of the SNEX node itself.
Here you can see my SNEX node is just called: snex_node.
But the script loaded into it is called audio_loader, and so the reference to SNEX_NODE inside the script has to also reference audio_loader.
-
RE: scriptAudioWaveForm and updating contents
@d-healey said in scriptAudioWaveForm and updating contents:
@Orvillain Did you try,
AudioWaveform.set("processorId", value);
?Yeah I did, and it does update it based on a follow up AudioWaveform.get('processorId') call - but the UI component doesn't seem to update, and still shows data from the previous processorId. When I compile the script, then the UI updates one time... but not on subsequent calls to the set method.
I figured I needed to call some kind of update() function after setting the processorId, but no such luck so far.
-
RE: c++ function optimization using vectorization or SIMD???
@griffinboy said in c++ function optimization using vectorization or SIMD???:
It's more popular nowadays to store waveforms in frequency domain using FFT, and to silence bins above Nyquist before inverse FFT. Either that or use filters to make mipmaps (multiple copies of your waveform at different pitches, with antialiasing filters applied, baked into the copies, play back the appropriate pre-antialiased file for the pitch) optionally doing so at 2x oversampling and using additional interpolation to remove aliasing that happens from extra processes that happen in Realtime.
Cheers dude! I was aware of this, but I wanted to see how far I could get with sinc. Turns out, quite far! I've got 22% CPU usage for about 30 voices now. Which isn't really super optimal, but it was a fun project.
That paper you linked me a while back - https://www.mp3-tech.org/programmer/docs/resampler.pdf - was what got me interested.
I think I understand the process you mean though, for the mipmapping approach. Something like:
- Oversample original audio x2 (juce::dsp::oversampling can handle this)
- Set up a root note
- For mip-maps below the root note - lowpass and downsample (dsp::FilterDesign::designFIRLowpassWindowMethod then keep every 2nd sample)
- For mip-maps above the root note - upsample and then lowpass (use the same oversampling approach here for the upsampling and then the same kind of FIR filter???)
- Store each level, and then move on to the playback engine
I think that'd be the approach??
Playback engine-wise, I'd still need to have an interpolation method to playback notes in between the mipmap levels I would guess. Can Hermite cover this, or do I need to go polyphase still?
-
RE: Getting debug output to the compiler console..
Hey apologies for the bump. But I got this to work by putting:
JUCE_LOG_ASSERTIONS=1into the preprocessor definitions and then rebuilding. I get data from my custom c++ node printing to the visual studio log:
It wasn't working until I did that.
-
RE: Orv's ScriptNode+SNEX Journey
Lesson 4 - SNEX Node layout.
I'm still wrapping my head around how the SNEX node works.
The first thing to note is, SNEX code does not support strings. The documentation for HISE does make this clear, but if you haven't seen it yet... then I've told you again! Here's the docs link:
https://docs.hise.audio/scriptnode/manual/snex.html#getting-startedAs the docs say:
The Scriptnode Expression Language (SNEX ) is a simplified subset of the C language family and is used throughout scriptnode for customization behaviour.Which means that most of the syntax you're used to when writing interface scripts, is just not going to be the same. There are some overlaps however - Console.print() is still used in SNEX scripts. However, print messages only get logged to the console when you put the SNEX node into debug mode. Which you can do by clicking this button:
From what I can tell, by default we have the following methods:
- prepare
- reset
- process
- processFrame
- handleHiseEvent
- setExternalData
- setParameter
Each one of these methods has a purpose. I'm still experimenting to figure out what those are, but here's what I've come up with so far:
- prepare
This is called when you compile or initialise your SNEX node, and it seems to run for each audio channel. I would guess this is meant to setup global parameters like sample rate and block size. Things that do not change from voice to voice. - reset
This is called when you trigger a voice, in my case from midi. When using a ScriptNode Synthesiser, the midi passes into the node automatically. This is where you would initialise variables that can hold different values from voice to voice, but that must start out with the same default value each time. - process
Haven't quite figured this one out yet. - processFrame
Haven't quite figured this one out yet. - handleHiseEvent
This is called when you trigger a HiseEvent - typically a midi event. This is where you would parse out your midi notes, velocities, controllers, and program changes; any midi data really. - setExternalData
This is called whenever there is a change to the external data. In our case, that would be the AudioFile we added in previous steps. So for example if you went to the complex data editor for the External AudioFile Slot (in the node editor) and loaded a new file, this method would get called. This is where you would resize any arrays that you're using to store the sample data, for example. - setParameter
This is called whenever a parameter inside the SNEX node is adjusted. You can parse the parameters out by using if statements and checking P against 0, 1, 2, 3, etc, depending on how many parameters you actually have.
SNEX has some hard-coded variable names, most of which I don't know yet. But a valuable one is "ExternalData". Consider this code:
template <int NV> struct audio_loader { SNEX_NODE(audio_loader); ExternalData data; // Initialise the processing specs here void prepare(PrepareSpecs ps) { } // Reset the processing pipeline here void reset() { } // Process the signal here template <typename ProcessDataType> void process(ProcessDataType& data) { } // Process the signal as frame here template <int C> void processFrame(span<float, C>& data) { } // Process the MIDI events here void handleHiseEvent(HiseEvent& e) { } // Use this function to setup the external data void setExternalData(const ExternalData& d, int index) { data = d; } // Set the parameters here template <int P> void setParameter(double v) { } };
Most of it doesn't do anything. But we have established that ExternalData is linked to a variable called data. We can also see this in the data table view:
Notice how ExternalData is a Data Type, and it is named data. Also notice how it has a variety of sub attributes - dataType, numSamples, numChannels, etc.
Let's swap out the file loaded in the AudioFile editor:
Notice how numSamples has updated, and also numChannels.
Back to the code:
// Use this function to setup the external data void setExternalData(const ExternalData& d, int index) { data = d; } // Set the parameters here template <int P> void setParameter(double v) { }
The data variable we established as ExternalData at the top of the script, is now actually having the data pushed into it by the setExternalData method - which has two inputs; "d" and "index".
This shows the very very basics of getting sample data into a SNEX script. But we're still not doing anything with it yet.
-
RE: Orv's ScriptNode+SNEX Journey
@d-healey I don't mean to be rude, but please don't distract from the purpose of this thread. Beautiful code and efficient code isn't the point here.
The point is to demonstrate how the API works, and for there to be a resource for people who come along in the future looking to do sample loading from their scripts, and looking to do advanced things in ScriptNode or SNEX.
I know full well that in a real world scenario, you wouldn't specify a bunch of files each as an individual const, and you'd put them in a key value pair inside of an array, or perhaps a function acting as a meta-object.
-
RE: Orv's ScriptNode+SNEX Journey
Lesson 3 - using the SNEX node with sample content.
This one is something I'm still getting my head around. @Christoph-Hart kindly provided a one shot SNEX node demo, which you can find by going to the example snippet browser in the Help menu:
This will open a whole new window where you can experiment with snippets. Maybe I'll go over the specific snippet in another post, but for this one... we're starting fresh, and we're going to just do the basics.
So.... here we have a basic interface script that gives us some file const references, an AudioSampleProcessor retreiving the AudioSampleProcessor from a Scriptnode Synthesiser in our module tree. That synthesiser has a DspNetwork assigned to it:
Right now, if we run the code... it will fail to find the AudioSampleProcessor as explained above. Let's add a SNEX node:
When you do this, it will be blank. You will need to click the three dot menu icon and choose "create new file" - strangely enough, you have to do this even when creating an embedded network. But fine. Let's do it:
We need to give it a name:
At this point, the node becomes active, indicated by the green highlight text:
Now if you open the same menu, you get more options:
We're going to select 'Add AudioFile':
You can see that now there is an extra icon in the SNEX node, which opens the AudioFile "Complex Data Editor" panel.
We can add an External AudioFile Slot using the icon on the right hand side:
And now you can see that the data editor will display whatever sample you assign to that AudioSampleProcessor from your script:
So here we see that file_r has been loaded into the buffer, and if we wanted to do file_f instead we could change the code to do that:
Note - you will not be able to playback the audio at this stage, as your SNEX code will be completely empty. If you click the icon on the right side, it will open the code editor for this SNEX node:
So whilst the data is loaded, our code isn't doing anything with it.
Latest posts made by Orvillain
-
RE: SNEX Wave Shaper
I actually just did this the other day:
https://forum.hise.audio/topic/9656/orv-s-scriptnode-snex-journey/9?_=1752509228365It is a bit crap, but it proves the concept.
-
RE: Script processors are blank after my last export.
Without trying to teach you to suck eggs, if you're not using some sort of version control, you really should. Grab yourself a copy of Fork and a Github account, and commit your project every time you make significant changes.
I've had previous versions of HISE do similar things, where scripts would disappear or be completely empty. Haven't had it happen for a while though. I'm trying to track the develop branch as closely as possible, so I get the most recent fixes and such things.
Anyway, here's the link to Fork:
https://git-fork.com/ -
RE: Broadcaster attachment design pattern
@d-healey Yeah, I think I can essentially boil down 11 functions down to 3 functions with this. Pretty cool, cheers!
-
RE: Broadcaster attachment design pattern
@d-healey More elegant than what I have right now I am thinking!
-
RE: Broadcaster attachment design pattern
@d-healey Indeed, indeed.
for (engine in SharedData.engines) { local capturedEngine = engine; SharedData.broadcasters[capturedEngine + "ModeControl"].addListener("string", "metadata", function[capturedEngine](index) { BroadcasterCallbacks.setActiveModeForSlot(capturedEngine, index); }); }
This did seem to work however.
-
RE: Broadcaster attachment design pattern
@d-healey said in Broadcaster attachment design pattern:
Another suggestion is instead of using an array of strings and a for in loop. Just use a for loop that counts from 0 to the number of engines you have. Then you can just declare a NUM_ENGINES constant at the top of your script and use it whenever you need to know the number of engine you have.
If I try that, for example (different feature area this time):
for (i = 1; i < SharedData.engines.length; i++) { SharedData.broadcasters["Engine" + i + "ModeControl"].addListener("string", "metadata", function(index) { BroadcasterCallbacks.setActiveModeForSlot("Engine" + i, index); }); }
Then I get:
Interface:! BroadcastersBuilder.js (260): Can't reference local variables in nested function body {{SW50ZXJmYWNlfEJyb2FkY2FzdGVyc0J1aWxkZXIuanN8ODQ3MHwyNjB8NTc=}}
-
RE: Roadmap to HISE 5
@d-healey OK, apparently I can't show you a video, because now I also cannot get my demo snippet to work!! How strange.
This is so weird. I was sure it was working. Can't explain it, because I wasn't getting an error or anything previously, and my graph data WAS definitely updating, because in the video I sent to my client you can see the graph switching indexes - which is the whole goal of this.
I wonder if it was actually an older commit? I can't really remember now, But as recent as July 2nd, I was updating the Data array for a FloatingTile, using .parseAsJSON() and then setting the index to a value, and then performing a .toString() call on it, to shove the modified data back into the FloatingTile.
Anyway... trace() does work.... but this baffles me tbh. Maybe Chris can say if something changed. The only reason I posted in here was because he was asking for feedback.
local filter1Graph = Content.getComponent("ft_Filter1Graph"); // gets a component on screen local filter1GraphData = filter1Graph.get("Data").parseAsJSON(); // parses the Data array for the component filter1GraphData.Index = graphMap[filterType][0]; // this just returns an integer from an array, and sets the index property to that integer filter1Graph.set("Data", filter1GraphData.toString()); // this shoves the Data back in as a string
In the full code, this was literally the code that was working until very recently.
-
RE: Roadmap to HISE 5
@d-healey said in Roadmap to HISE 5:
@Orvillain said in Roadmap to HISE 5:
Was definitely working.
Can you show me a video?
Will do. But I'm very sure this was working at some point. I wrote the code and even sent a video to the client showing them this feature area working.
(updating graph index so that it looks at a different exposed filter graph from a custom filter node)
-
RE: Broadcaster attachment design pattern
@d-healey Yeah, there's some underlying closure thing going on I think.