I’m probably missing something obvious here, but I can’t seem to get SOULPatchAudioProcessor::sendInputEvent
to work. This application “should” play a 440Hz tone, but instead produces no sound. Debugging in sendInputEvent
shows that inputEventDetails.size() == 0
, so no events are delivered to the loaded patch, despite it having an input event int noteIn
endpoint.
It’s hard to unpick exactly what’s going on there, but most likely you’re somehow using the inputs before the compiler has finished building the code?
I just edited my code in the original post to call sendInputEvent
on a Timer
— but still, when I run it, no sound is produced. I verified that patches are loading properly by changing the SineOsc.soulpatch
path to point to ClassicRingtone.soulpatch
, and the jingle plays just fine. Also debugging shows that patch->getDescription()->name
is indeed SineOsc
, and SineOsc.soulpatch
loads without errors using soul play
.
🤷🏻
When I run your patch using soul play SineOsc.soulpatch I get no sound.
The processor SineOsc.soul uses 2 variables currentNote and amplitude which are never initialized, therefore amplitude is always 0 and you output 0 to audioOut.
Edit the note() method to set currentNote to noteNumber and amplitude to 1.0f so that your processor can output some sound.
Then you need a main graph somewhere in your code. Right now you only have a patch and a processor, but no graph to describe the audio path and to tell SOUL where your note comes from. So check the examples, make it work using soul play and try to generate JUCE code once you have a working app.
Ahh thanks, I just added in those changes. Strangely, moving the note
slider in the GUI generated by soul play
does nothing, but if I replace input event int noteIn
with input event soul::midi::Message noteIn
and change its callback to
event noteIn (soul::midi::Message msg)
{
let noteNumber = soul::midi::getByte2(msg);
phaseIncrement = float (twoPi * processor.period * soul::noteNumberToFrequency (noteNumber));
amplitude = 0.8f;
}
then I hear the proper beeps when I play MIDI notes.
But again, debugging in sendInputEvent
shows that inputEventDetails.size() == 0
, so the player->sendInputEvent
call is never reached in my program.
I thought that a processor
could be labeled [[ main ]]
as well as a graph
, no? It shouldn’t make a difference to sendInputEvent
if I wrap SineOsc
in a graph
like this…
graph SineOscGraph [[ main ]]
{
input event int noteIn;
output stream float audioOut;
let osc = SineOsc;
connection
{
noteIn -> osc.noteIn;
osc.audioOut -> audioOut;
}
}
… right?
With the amplitude set and the noteIn event converted to soul::midi::Message the patch works now. And you are right, the graph is not necessary when there is only one processor labelled main.
For the C++ part I can’t help much because I only use SOUL as a standalone language at the moment. I could not find the documentation of some C++ classes, and if it is not documented I don’t touch it
You could try to replace:
sendInputEvent("noteIn", 69);
with sendMIDIMessage if this method exists. I suppose MIDI inputs and event inputs are handled differently so it may works even though inputEventDetails.size() == 0.
There is SOULPatchAudioProcessor::injectMIDIMessage
, but I need to be able to send non-integer input events for my application, so this won’t do.
Thanks for your help btw
Okay I did some more digging…
When I place a breakpoint at line 892 of include/soul/patch/helper_classes/soul_patch_AudioProcessor.h
, I can see that the newly-created PatchPlayer
has recognized noteIn
as a parameter, but not as an input endpoint —
Meaning — PatchPlayer::getParameters
returns an array of length 1 and PatchPlayer::getInputEventEndpoints
returns an empty array. This is true both with and without the annotation on input event int noteIn
in SineOsc.soul
.
Perhaps I’m missing a step in the initialization of SOULPatchAudioProcessor
… Surely all parameters should also be input endpoints, right?
Peeling back yet another layer of the onion…
It seems like PatchPlayer
constructs its inputEventEndpointSpan
by calling AudioMIDIWrapper::getInputEventEndpoints
,
which gets all the input event endpoints of type InputEndpointType::event
.
But input endpoints are classified as either InputEndpointType::event
OR InputEndpointType::parameter
,
so it looks like input endpoints that are patch parameters are not accessible via PatchPlayer::sendInputEvent
.
Is there a reason why an input endpoint can’t be both a parameter and an event input?
yes, good point, getEventInputEndpoints
should return everything that’s an event input. I’ll sort that out. I don’t think I’ll need to change anything other than making that list return all the endpoints, as the mechanism for posting an event to a parameter input should already work if you have the handle for it.
hmm… I would say though…
If you’ve got an input which is being used as a parameter and you start shoving other values into it without the wrapper knowing about that, then you’re going to get some odd behaviour: the value that the host thinks the parameter has won’t match the value it’s actually using.
So, thinking about it, I’m not 100% sure whether it’d be a good idea to change this. It might be better to force you to make up your mind about whether you’re going to control an input yourself, or leave it to the host to control as a parameter, to avoid confusing situations.
Does that sound sensible, or am I missing a valid reason for needing to control it both ways?
Thanks for looking into it
am I missing a valid reason for needing to control it both ways?
It would be nice to be able to send unbounded values to float
and int
endpoints. Currently those are interpreted as parameter endpoints — and parameters values are clamped to the parameter’s range, which is [0,1]
unless a min
/ max
are specified in the endpoint annotation. Of course, one can always set min
/max
to insanely large values, or wrap the float
/int
in a struct
so that it’s interpreted as an event endpoint rather than a parameter, but both of those options feel like workarounds.
Or have I missed another way to send a float
/int
input event without bounds checking?
Ahh… so for my particular use case I was hoping to pass the output of juce::Time::getMillisecondcounterHiRes
into a patch to synchronize a timeline across reloads. But now I see you’ve written the SOULPatchAudioProcessor::PlayheadState
class for just that!
Off the top of my head, I can’t think of another scenario that requires unbounded input…
Bumping this because I can’t figure out how I can get an input to be treated like a event instead of a parameter.
Right now I’m declaring this in my .soul file:
input event float bpmIn;
event bpmIn (float b)
{
BPM = b;
}
float BPM;
Then in C++ i’m calling this in processBlock:
plugin->sendInputEvent("bpmIn", currentBPM);
but running into the same issue @maxpollack did. Could anyone clarify what I need to change to get this working? I’m just trying to have a BPM global variable in my soul patches that stays up to date with the BPM of a DAW, I feel like this should be possible? Any help would be much appreciated!
You could wrap the type in a struct to bypass the parameter behavior — SOUL has some handy pre-made structs for this purpose in the soul::timeline
namespace.
input event soul::timeline::Tempo tempoIn;
event tempoIn (soul::timeline::Tempo t)
{
bpm = t.bpm;
}
float32 bpm;
Provided your main processor has an input event endpoint of the type soul::timeline::Tempo
, you should be able to call soul::patch::PatchPlayer::applyNewTempo
to set the tempo in your patch from C++ land.
@maxpollack this was hugely helpful and I really appreciate you replying so quickly
For the sake of anyone else who may come across this thread, what finally ended up working for me was using the code Max posted above in my .soul file. Then, in my JUCE project I had to create an AudioPlayhead and set it to be the SOULPatchAudioProcessor’s playhead. So step 1 was creating these two objects:
std::unique_ptr<soul::patch::SOULPatchAudioProcessor> plugin;
juce::AudioPlayHead* currentPlayHead;
and step 2 was putting this in my processBlock():
currentPlayHead = this->getPlayHead();
plugin->setPlayHead(currentPlayHead);