How would one write, if possible, a C++ MetaSound node that, from the audio thread, could send events back to a Blueprint in the game thread? This would be so massively useful.
Background:
I was starting to look at DialogueWave/Voice and realized it’s dependent on OnAudioFinished to orchestrate the back and forth verbal banter. This event appears to only be fired when a MetaSound dies (it also isn’t fired when virtualization/culling occurs but that is a different question). I was imagining being able to use DialogueWave/Voice via a living MetaSoundSource that doesn’t die.
I was hoping to place the players sound output for story dialog in a 2D sound manager I use that has other functionality embedded. Since the MetaSoundSource can’t or shouldn’t close, it can’t allow the audio component to fire a Finished event. There seems to be no way to provide a blueprint with status from MetaSounds, so I was wondering if that’s impossible, might be done in the future, or could be done now with writing a custom MetaSound node in C++.
You could start by looking at the Audio Synesthesia plugin source code:
Unfortunately, it seems to only do it in the editor, so you might need to do something extra on top of that.
In general, you’ll probably want to set some variables inside the audio real-time thread (writing a regular audio engine processor) and then read them inside a Tick() function in an actor. If you use non-blocking communications like InterlockedExchange() you can do this without risking blocking the audio thread.
I’m assuming you already read the “creating metasounds nodes” page:
The Audio Synesthesia is new to me. I’ve ignored the term not knowing what it is.
Yes, I did the same plugin for the C++ node and that all worked well. I keep meaning to go through the more complex pitch shift tutorial but haven’t yet. When you refer to writing a regular audio engine processor, are we still talking about a metasound plugin node or something more in the guts of the audio engine?
Excellent. I’ll research it. And thanks for the info on InterlockExchange(), I’ll look that up too. I just wanted to verify that it was possible without hosing up the engine.
Sorry to have to do a follow-on here, but I think I misinterpreted the comment “inside the audio real-time thread”. I interpreted that to mean a variable with a metasound plugin since that would be running in the audio thread.
I’ve set up a plugin (just a copy of the delay node as shown in one of Aaron’s tutorials, the PitchShift example). In it, I put a new variable, a test bool to see if I could get at the value, manipulate it from the node, then retrieve it from the audio thread. I was able to get the game thread to access the bool variable (at least from the compiler’s perspective). But it appears the memory I end up pointing to is not the value of the variable. It defaulted to false, was set false if delay feedback was 0, otherwise was set to true if delay feedback were increased. It always came out true even if I removed the set statements from the plugin and attempted to query the default value.
I’m now assuming you weren’t referring to a variable within the MetaSound node, but possibly something else? What I tried could have failed due to inappropriate casting but that was the only way I could get the compiler to accept it. I’m thinking it’s more that I’m in left field as far as where such a variable should live.
Though this is obviously not of use, here is what I tried, references to the bool returned true no matter what the value was within the metasound itself, so I’m apparently randomly accessing some memory other than what I think I am. The context of this code is in a UActorComponent that fires off the metasound via an NewObject and Play. This is the status routine called from Blueprint to attempt to read the value of the variable. The variable is the MSPluginBoolFlag. By the way, I made no attempt to thread-safe lock this. I was just trying to see if such a reference could point to the data.
I would have been surprised if what I did would work. I’ve never done any thread programming but it would seem there would have to be some sort of glue to validly point into another threads memory.
All threads share memory. There are, however, two other problems:
The allocation/deallocation of nodes may be done in different threads and not be synchronized, so you may read deallocated memory if the node you’re reading from goes away.
You may read some data exactly when the other thread is writing it, which may lead to “tearing” – you read one-half of the thing from before the write, and the other half after the write. This is the problem that InterlockedExchange() fixes. (It turns out, on regular intel/AMD CPUs, in 64-bit mode, with naturally aligned 8-byte reads or smaller, tearing is not a thing anyway, but on AMD it can be, and if the objects aren’t aligned, it can be, and if the object is bigger, it can be.)
To solve problem 1, you need a little more deliberation. Let me see if I can find some code.
… and it seems the problem is more “how do I actually get to the instance” than the specifics of data.
I mean, worst case, you could use a global variable, as long as there’s only one of these nodes in effect at once
I see, I was under the mistaken impression that InterlockedExchange() was some form of locking mechanism for exchange between threads. I’ll look for more info.
Yes, agreed, the question is more fundamental than an audio issue, rather how memory is to be accessed. Looks like I need to do some studying. I appreciate the effort you put into the response and i’ll check the Analyzer out. You’ve actually provided several searchable terms to follow up on and that is worth a lot.
Global? I considered it, but the placement of a global bool array where plugins and the game could both see it to get it through the compile, would probably make moving between versions a bother. It also kind of violates the concept of a plugin (though I don’t particularly care about that since no one else will work with my code). I’m not above globals as they are simple and straightforward, but in this case it didn’t look like it would be.
That can be solved by having the plugin expose a function that returns a pointer to the array, rather than directly exposing the variable. At that point, it would be a “singleton,” which the object-oriented people for some reason think are something different from a global, so that might even pass code review in such an environment.
The function could be a blueprint library function or something, if you want it to be auto-registered and easy to get at. Anyway – that’d be the hacky version.
Excellent idea. Still studying at the moment but I will try that when I redo it.
It hasn’t been lost on me that some prefer to create a complex superstructure of code to avoid a global. I was a C programmer in a past life, so the concerns are confusing to me. Fortunately, only my cat reviews my code.