Q: how to stop a Node without "disabling" it in a larger graph?
I learned about this behavior while trying to find a workaround for #132. If a node is stopped via Node.stop() it will stay "disabled" even if another Node that has it as an input is played. How can I play/stop a Node in a way that it will still work as a component in larger graphs? Example:
import signalflow as sf
graph = sf.AudioGraph.get_shared_graph()
if graph is not None:
graph.destroy()
graph = None
if graph is None:
graph = sf.AudioGraph(start=True)
node_1 = sf.SineOscillator(440)
node_2 = sf.Constant(0.1)
node_3 = node_1 * node_2
If I play node_3 I hear the expected result:
node_3.play()
...and it also stops as expected upon:
node_3.stop()
Then I play node_1 which also works, and now plays the sine tone without the attenuation:
node_1.play()
...but stopping it will "disable" it, so the node_3 won't work anymore either:
node_1.stop() # stops playing the unattenuated sine
node_3.play() # now this is silent since node_1 is "disabled"
Is this expected behavior? If so is the user expected to re-instantiate every Node after it was stopped (if it is also an input for another Node)? Like this?
node_1 = sf.SineOscillator(440)
node_2 = sf.Constant(0.1)
node_3 = node_1 * node_2
node_3.play()
node_3.stop() # (!!!)
node_1.play()
node_1.stop() # now it's disabled so we have to re-instantiate it
node_1 = sf.SineOscillator(440)
# node_3.play() # this would throw NodeAlreadyPlayingException (???)
node_3 = node_1 * node_2
node_3.play() # now it will work
Another problem is that if I re-do node_1, node_3 becomes "busted", it wrongly throws NodeAlreadyPlayingException, so I have to remake that too. This could mean that if a Node deep in a network is stopped, then all downstream nodes in its tree would have to be re-created (e.g. imagine node_4 = node_3 * 1; node_5 = node_4 * 1).
The whole reason I started investigating this is because I noticed that my "hack" in #132 did make set_input() work with Nodes, but then turns out the master_envelope Patch got disabled after calling stop() on it, freezing it in a 0-output state (and rendering my results silent).
Hi @balintlaczko and thanks for the report! This is an interesting case...
Some context: stop() was introduced as syntactical sugar so that you don't need to explicitly disconnect the node from every output that it's connected to. What it does beneath the hood is to disconnect the node from every output node that it's connected to, and then set the corresponding input of that node to zero. Which in most cases is the natural thing to do:
- if a node is connected to the main
AudioOut,stop()behaves as expected - if a node is connected to a single other output node, it behaves as expected
- but if it's connected to multiple outputs (as in your case), seemingly strange things can happen
So, in your case, the system is behaving as expected: the sine is disconnecting from all of its outputs. But I agree that the expected behaviour is counter-intuitive 😅
One potential fix for this might be to raise an exception if stop() would disconnect from multiple outputs, and require that the user explicitly disconnects it from everything that it's connected to one by one. But this is painful for common cases, e.g. if the user wants to pass a dry BufferPlayer signal to the output, plus a wet reverberated version of the same signal. Here, stop() would still be doing the right thing.
Another option would be to trace the complete path of outputs from the node to the AudioOut, and only disconnect those links that are currently "playing" (e.g., have an uninterrupted path to the output). This would probably behave more logically.
Other suggestions welcomed!
Thanks a lot! Is there a way to explicitly connect/disconnect a Node from/to the AudioOut instead? So that I could connect/disconnect it dynamically while let's say another parallel branch is still connected.
Actually, I think graph.output.remove_input(sine) should do this? Untested, but let me know if it works!
Actually, I think graph.output.remove_input(sine) should do this? Untested, but let me know if it works!
Unfortunately that seems to hang, although I don't get any errors. I think it is actually more related to the node being an input to something else as well, because I get the same kind of hanging with the following experiment using a Bus:
Setup cell:
import signalflow as sf
graph = sf.AudioGraph(start=True)
my_bus = sf.Bus(1)
node_1 = sf.SineOscillator(440)
node_2 = sf.Constant(0.1)
node_3 = node_1 * node_2
node_4 = sf.SineOscillator(550)
my_bus.play()
If I plug node_1 into my bus, it is fine, and I can also remove it without hanging:
my_bus.add_input(node_1)
my_bus.remove_input(node_1)
I can also do this with node_3and node_4, but once I do that, something breaks about node_1:
my_bus.add_input(node_3)
my_bus.remove_input(node_3)
my_bus.add_input(node_4)
my_bus.remove_input(node_4)
I suspect there is something in remove_input that creates an issue? Not sure. But if I now add node_1 to the bus, it will be okay, however, when I try to remove it the cell will hang forever:
my_bus.add_input(node_1) # still okay
my_bus.remove_input(node_1) # here it hangs forever (only if this is called after node_3 was removed)
There is no error message or crash, just the hanging. I tested all add_input and remove_input calls being in separate cells, and it is exactly my_bus.remove_input(node_1) that will hang.
Intriguing! Never seen anything like this before. I'm a bit swamped with a couple of other projects but will try to take a look at the weekend.