Advanced Scene Switcher

Advanced Scene Switcher 1.28.1

AaronD

Active Member
Just for completeness:
The fade does indeed perform a volume changes at a fixed interval of 100ms.
Yep, that lines up with my observation.

For those that might be worried, I also use that fade for live audio, and the pops/zipper there are completely lost in the audio itself. I never noticed until I used it with a pure sine that was filtered out later, which leaves ONLY the artifacts and is probably the worst possible case for being able to hide them.

But it's still not good for what I'm doing, and I'm not really sure how it could be fixed with OBS's API. A faster update rate would only make a higher-frequency zipper sound, so that's not the solution... Maybe just leave it and understand that it's fine for actual audio, but not so much for test/control signals.

If verbose logging is enabled the advanced scene switcher should log when it is sending out websocket messages and receiving them.
Maybe that could help in figuring out which message gets lost / ignored?
I'll see if I can make it misbehave on record. It's intermittent, which makes it harder, but not particularly rare.

Sure that would be much appreciated! :)
I intended to start work on supporting MIDI devices anyway so that fits right in.
Ooo! Nice!

PM sent.
 

AaronD

Active Member
Something else I just thought of:

Before you do OSC, maybe you could do generic UDP. That would both give you a base to build a bunch of things on top of, like OSC, VISCA over IP, etc., and allows the more technical of us to still do whatever you haven't gotten around to yet.

Possibly the same for TCP also. Generic-only to start with, just to have something that the more technical of us can do something with, and then use it as a base for a bunch of specific things later.
 

AaronD

Active Member
I'll see if I can make it misbehave on record. It's intermittent, which makes it harder, but not particularly rare.
Is this useful?
  • WebSocket-Test.zip
    • logs
      • OBS's logs, copied from ~./config/obs-studio/...
      • stdout and stderr from master and slave, all separate, with timestamps
    • profiles
      • copied from ~./config/obs-studio/... after the test is over and closed
    • scenes
      • copied from ~./config/obs-studio/... after the test is over and closed
    • OBS-Test.sh (script used to start both instances of OBS, and capture their stdout and stderr)
I start hammering the connection at 11:11:44. It's hard to tell with this minimal setup whether it misses one or not, but I think it should still show up in the log.
 

Attachments

  • WebSocket-Test.zip
    62.1 KB · Views: 22

Warmuptill

Active Member
Is this useful?
  • WebSocket-Test.zip
    • logs
      • OBS's logs, copied from ~./config/obs-studio/...
      • stdout and stderr from master and slave, all separate, with timestamps
    • profiles
      • copied from ~./config/obs-studio/... after the test is over and closed
    • scenes
      • copied from ~./config/obs-studio/... after the test is over and closed
    • OBS-Test.sh (script used to start both instances of OBS, and capture their stdout and stderr)
I start hammering the connection at 11:11:44. It's hard to tell with this minimal setup whether it misses one or not, but I think it should still show up in the log.

I can see 84 messages being sent out on the master side and 84 being received on the slave side.
So I would assume that nothing was dropped or lost.

I also performed a local test with a setup similar to the following and did not observe any dropped or lsot messages either.

ReceiveTest.PNG

SendTest.PNG


Maybe the network connection is not the problem and instead some other area is causing issues?
 

AaronD

Active Member
I can see 84 messages being sent out on the master side and 84 being received on the slave side.
So I would assume that nothing was dropped or lost.

I also performed a local test with a setup similar to the following and did not observe any dropped or lsot messages either.

View attachment 93166
View attachment 93165

Maybe the network connection is not the problem and instead some other area is causing issues?
Hmm...

I tried another test, and I think I got it to fail this time. I had the Master alternate between two scenes, once per second, that were easy for the Slave to make identical, and tell the Slave to do that. If it missed one, then it would not look the same anymore. And I recorded what the Slave came up with.

At 1:31 in the recording, which was started at 14:04:38 in the logs (so this point in the recording corresponds to 14:05:39 in the logs), the Slave did not undo the Master's change.

Same ZIP structure as before, with the recording file added to the root.
 

Attachments

  • WebSocket-Test.zip
    616.8 KB · Views: 34

Warmuptill

Active Member
Hmm...

I tried another test, and I think I got it to fail this time. I had the Master alternate between two scenes, once per second, that were easy for the Slave to make identical, and tell the Slave to do that. If it missed one, then it would not look the same anymore. And I recorded what the Slave came up with.

At 1:31 in the recording, which was started at 14:04:38 in the logs (so this point in the recording corresponds to 14:05:39 in the logs), the Slave did not undo the Master's change.

Same ZIP structure as before, with the recording file added to the root.
I think I finally understand what is causing the problem.
The issue is not one of messages being dropped (e.g. due to network issues), but a classic race condition in the plugin's logic.

87 event messages were sent out with 44 "A" messages and 43 "B" messages.
87 messages were received with 44 "A" messages and 43 "B" messages.

However, it was possible for messages of the "Event" type to arrive at exactly the moment after the all macro conditions have been checked but still before the websocket buffers have been cleared for the next interval.

This seems to be the case for one of these messages in the log you shared:
...
Apr 15 14:05:38 info: [adv-ss] try to sleep for 49
Apr 15 14:05:38 info: [adv-ss] condition websocket returned 0
Apr 15 14:05:38 info: [adv-ss] Macro RecvA returned 0
Apr 15 14:05:38 info: [adv-ss] condition websocket returned 0
Apr 15 14:05:38 info: [adv-ss] Macro RecvB returned 0
Apr 15 14:05:38 info: [adv-ss] received event msg "B"
--- reset for the next check interval happens here ---

Apr 15 14:05:38 info: [adv-ss] try to sleep for 18
Apr 15 14:05:38 info: [adv-ss] condition websocket returned 0
Apr 15 14:05:38 info: [adv-ss] Macro RecvA returned 0
Apr 15 14:05:38 info: [adv-ss] condition websocket returned 0
Apr 15 14:05:38 info: [adv-ss] Macro RecvB returned 0
Apr 15 14:05:38 info: [adv-ss] try to sleep for 50
...

(Seems like there was also slight "scheduling hiccup" as the plugin took unusually long for the interval to complete (only sleeping for 18ms instead of 50ms to make up for lost time) which makes it easier to enter this problem scenario)

A build with a fix should be available here in a few minutes:

Thanks a lot for making me aware of this issue and performing these tests! :)
 

AaronD

Active Member
The issue is not one of messages being dropped (e.g. due to network issues), but a classic race condition in the plugin's logic.

...

it was possible for messages of the "Event" type to arrive at exactly the moment after the all macro conditions have been checked but still before the websocket buffers have been cleared for the next interval.

...

(Seems like there was also slight "scheduling hiccup" as the plugin took unusually long for the interval to complete (only sleeping for 18ms instead of 50ms to make up for lost time) which makes it easier to enter this problem scenario)
That would do it! Makes me feel better about networked control signals too, as all of them still got there despite the hiccup at the receiving end.

I'm a little bit surprised though, that you don't dequeue them individually as you process them, but instead leave them all there and then clear the list at the end. I've really come to like asynchronous buffers like that, because I don't have to care! Each thing manages its side of the buffer in isolation, and it kinda "just works".
The only race conditions would be to see if the buffer is empty or full:
  • If empty() returns a false positive, it just moves a message to the next cycle. No big deal.
  • If empty() returns a false negative...that can't actually happen because it's synchronous to the removing code.
  • If full() returns a false negative...again that can't happen because it's synchronous to the adding code.
  • If full() returns a false positive, then the buffer still wasn't big enough (one away from full is still practically full), and so it's really no different from a true positive.
You do want to make the pointers, indices, iterators, whatever volatile of course, so that the other code can read them accurately, but concurrency is really not an issue with that structure even though it happens.

A build with a fix should be available here in a few minutes:

Thanks a lot for making me aware of this issue and performing these tests! :)
I'll see what I can do with that. Thank you!
 

Warmuptill

Active Member
I'm a little bit surprised though, that you don't dequeue them individually as you process them
Thanks for the suggestion!

I am afraid I don't think that is an option as multiple websocket conditions can share the same connection.
If I would dequeue messages as soon as any one condition processes them other conditions will miss messages.
(Sorry if I misunderstood something your suggestion)
 

AaronD

Active Member
You got me brainstorming again. Here we go! :-)



Thanks for the suggestion!

I am afraid I don't think that is an option as multiple websocket conditions can share the same connection.
If I would dequeue messages as soon as any one condition processes them other conditions will miss messages.
(Sorry if I misunderstood something your suggestion)
Yeah, that does put a wrinkle in it. It might take an intermediate layer to make that work. Keep both the new asynch buffer and the current full-frame buffer, with the asynch first. A receiving thread enqueues messages as they come, and a different macro-execution thread synchronously 1) dequeues them into its own buffer, 2) runs all the macros, and then 3) clears its own buffer.

All-synchronous management makes the full-frame buffer work, with no chance of a race, so that multiple conditions can look for the same thing, and the asynch buffer before it that works on individual messages, allows the message receiver to still be in a different thread. Depending on how the parallel macros work, that might be anywhere from easy to impossible.



I also noticed, last time you direct-linked to a bit of code, that you're using inline delays instead of a non-blocking state machine. That's fine for simple things, but another huge thing that I learned early on when writing embedded applications (8-bit single CPU, 5 MIPS, about 300 bytes of RAM, and no operating system - bare metal) is to never block for anything. Always use a state machine, and figure out something that you can check periodically to see if a delay has expired, or whatever it is that you're waiting for. If not, the same thread moves on to the next parallel task, without updating the flag or status variable for this one. The next scan sees that it's in the same state, and checks that condition again. (see footnote)

So in what is technically a single thread, my embedded applications drive a bunch of independent modules that all do their thing simultaneously, spending most of their time in various wait states. And because nothing blocks, it also gets to the single watchdog instruction quite often, at the top of the single main loop. The structure for each module is something like:
C:
void module_name()
{
    if (paused)
    {
        return;
    }

    switch(state)
    {
    default:
        //do starting stuff
        state = A;

        //fall-through to A stuff on the same scan (optional)
        //scans are frequent enough that it doesn't really matter

    case A:
        if (A_not_ready)
        {
            return;
        }
        //do A stuff
        state = B;
        return;         //don't fall-through: we'll look at B stuff on the next scan

    case B:
        if (B_not_ready)
        {
            return;
        }
        //start B stuff
        state = B2;
        return;         //B stuff takes a while, so let the other modules have a scan here
    case B2:
        //finish B stuff
        state = C;
        return;

    case C:
        if (C_not_ready)
        {
            return;
        }
        //do C stuff
        state = D;
        return;

    //etc.
    }
}
And the main function:
C:
void main()
{
    //init

    while(1)
    {
        reset_watchdog();   //appears exactly once in the entire program, right here
                            //    if I don't hit it often enough, I get a hardware reset

        module_1();         //these modules are all strictly-non-blocking state machines as above
        module_2();         //    so they operate independently of each other
        //etc.
    }
}
This is somewhere between multi-threaded asynchronous and single-threaded synchronous. Or a version of cooperative multitasking if you'd rather think of it that way. Each block of each module is synchronous to itself, and guaranteed-atomic except for interrupts. (interrupts in my projects are so non-interactive that they hardly matter anyway except for some lost time) And with just a little bit of planning, the modules can communicate with each other synchronously as well. Only the ISR(s) *must* be asynch.

To complete the connection back to Adv. SS, I see the macros working as above, while an ISR handles incoming messages. In reality, those are just different threads that respond to different events - message or timer - but that's the mentality when I look at it:
  • The message-event "ISR" enqueues each message individually into an asynch buffer, and then leaves.
  • The timer-event "main loop" dequeues all messages individually into its own synch buffer, runs the macros as non-blocking state machines, clears the synch buffer, and repeats.
If Qt already gives you an asynch buffer for messages, that you can tap into directly without an event handler, then you might just use that. Drop the "ISR" entirely, and have the "main loop" dequeue individually from there into its own. Either way, tight loop until the asynch is empty (okay if a race misses the last one; we'll get it on the next scan), then synchronously use what it's accumulated in its own buffer.



Footnote:

Thinking about the macros at this point, there's no difference between waiting at the top for a condition to be true, and waiting somewhere in the middle before continuing. That could make things interesting!

So instead of just a time, the Wait action now has any condition or combination of conditions available to it, and there's no need to have separate conditions at the top...or you could keep the conditions at the top, and encode them as a required Wait action.

One reason to not have conditions at the top is what I might call an "offset loop", where the entry point is somewhere in the middle, and then it's a normal loop from there. But to make it work like that with a fixed entry point, the entire loop needs to be "rolled around" or "offset" like a belt around two pulleys, so the condition ends up in the middle.

Thus, another way to think about macros, is that each one is always an infinite loop, with a Conditional Wait at the top. But it may not necessarily be at the top anymore if it ends up working like this, nor is it limited to just one.

With that much flexibility though, the "Run Macro Actions" action would need to be re-done. Maybe it could change to "Force Continue", and have a "Forever" condition to put in the Wait action to make a useful subroutine again? Then, the "Force Macro Continue" action could have a checkbox to wait until the callee arrives back at the same Wait again (function call) or not (thread fork). For even more flexibility, you could have the option to wait until the callee arrives at the same Wait (full cycle) or any Wait (stepped sequence).
And of course, the forced condition doesn't have to be "Forever". It could be anything in any combination.
 

AaronD

Active Member
A build with a fix should be available here in a few minutes:

Thanks a lot for making me aware of this issue and performing these tests! :)
That seems to have fixed the hiccup. 5 minutes of switching once per second, and it never missed one. At this point, I think I'm just proving a negative, unless the code structure itself makes it guaranteed.

For the new MIDI part, it appears that it only really works for Note On and Note Off commands so far, as the details are hard-coded to that. I've used SysEx'es enough in Media Automation - intentionally avoiding the music commands so that my stuff can co-exist with musical stuff - to know that SysEx'es definitely do not work like that!

If there aren't any musical things to interfere with, then the Note commands might be exactly the right thing to use, because they can be tested in both directions with a cheap MIDI keyboard just by swatting the keys or listening for a sound. That by itself can be a huge step forward. But since I'm looking to control a DAW that understands MIDI as a synth controller to ultimately produce sound, I don't think I can do that. (or if I can, it'll cause problems later) OSC is still probably the better bet for that application.

Or......since the problem with OBS using audio signals for control, was the pop or zipper sound from instantaneous changes, maybe I can use a sidechained gate in OBS to make a smooth signal. On/off with its problematic pops goes nowhere, except the sidechain to a gate. The gate then, has its timing controls to produce a smooth transition that stays on its side of the DAW's audio filter. That would tie up all of OBS's global audio inputs, just to have that many copies of the generated sine to work with, but since the "real" audio stays in the DAW now, that's okay. OSC would still be better though.
 

murdocklawless

New Member
timing is not working properly, rarely it's working. my B scene should turn back to A scene after 3 seconds but generally stuck on the B scene.

anybody faced same problem?

obs 29.0.2
advanced scene switcher 1.21.1
 

AaronD

Active Member
timing is not working properly, rarely it's working. my B scene should turn back to A scene after 3 seconds but generally stuck on the B scene.

anybody faced same problem?

obs 29.0.2
advanced scene switcher 1.21.1
What are your settings?
  • Is it actually running? Is it waiting for something that turns out to never be true? Is the macro paused? Is the plugin itself active?
  • Is there something else that still enforces B? So it does try to switch but it gets overridden and you don't see it?
You're essentially saying, "It doesn't work," and nothing more, when there are countless possible reasons why and all of them have different solutions. We need enough detail to pick one of them.



As much as people might be intimidated by the thought, this has become a programming language. So to use it well, you kinda need that mentality:

The machine is stupid. Or "maliciously compliant" if you will. It does EXACTLY what you tell it, even if you don't understand what you're actually telling it. Interpersonal skills are useless for programming, which is what this is. I know of several programmers that don't have them: they're very good at coding, but they're also hard to work with. (Warmuptill is not one of those, by the way)

We don't intentionally make our programs hard for everyone else to use; they just reflect the nature of the machine. Natural human interaction is a major field of study, and people spend their entire careers on very small parts of it while still barely making a dent.
 

AaronD

Active Member
Or......since the problem with OBS using audio signals for control, was the pop or zipper sound from instantaneous changes, maybe I can use a sidechained gate in OBS to make a smooth signal. On/off with its problematic pops goes nowhere, except the sidechain to a gate. The gate then, has its timing controls to produce a smooth transition that stays on its side of the DAW's audio filter. That would tie up all of OBS's global audio inputs, just to have that many copies of the generated sine to work with, but since the "real" audio stays in the DAW now, that's okay. OSC would still be better though.
I also realized that I hadn't followed up on this. Sorry for the delay.

Turns out that the only sidechain-able Filter in OBS is the Compressor. That's slightly annoying, because if you treat the presence or absence of the control signal as a binary 1 or 0, a sidechained compressor practically results in a logical inversion or "NOT gate", whereas a sidechained gate does not invert. But it's enough to work, given careful naming to reduce confusion when reading the macros.

It also doesn't eliminate the pop entirely, but it's reduced enough to be manageable, and it depends on the timing settings: Attack and Release.
  • I doubt that the culprit is "feedthrough" of the sidechain or control signal to the output, like you have to be careful about in an analog compressor or gate because all analog signals want to mix with each other, while software signals are inherently isolated and you have to connect them explicitly.
  • I suspect that it has to do with a sharp corner in OBS's Compressor's response curve: instead of a nice ramp from one level to another, OBS's compressor has a "kink" to start, and a nice cushioned landing. A bit like standing on a trap door and landing in soft pillows. That, combined with (I suspect) finding a single gain setting for the entire buffer instead of per-sample, creates a big difference between samples again, at the boundary between buffers, and that difference is a pop.
    • So I'm using fairly slow timing settings, to try and balance a seemingly "instant" response time with not (audibly) popping. Much slower than I could use with any other compressor, digital or analog.
So yes, OBS has some token audio processing, but pretty much anything external is going to be better! As I said before, you probably won't notice with real-world sound (not artificially generated), but it's there.

Not to mention that OBS does all of its processing and outputs with only 16-bit resolution, which is still (barely) better than a pro analog rig and so you probably won't notice that either, but pretty much anything external is going to be better there too. 32 bits is practically the minimum acceptable for serious digital audio processing, and pretty much anything more complex than a stereo to 2.1 crossover is going to use 40 - 64 bits. That's because it's easy for a serious DSP engineer to make something to handle that many bits, and it reduces the cumulative roundoff error to less than a 24-bit output can resolve. Thus, mathematically perfect as far as the top 24 bits are concerned. For distribution, 16 bits are okay, but not so much for serious processing.

So there's two reasons to do ALL of the audio processing outside of OBS, so that OBS only sees a finished soundtrack to pass through unchanged. OSC is going to be nice when it comes!
 

murdocklawless

New Member
What are your settings?
  • Is it actually running? Is it waiting for something that turns out to never be true? Is the macro paused? Is the plugin itself active?
  • Is there something else that still enforces B? So it does try to switch but it gets overridden and you don't see it?
You're essentially saying, "It doesn't work," and nothing more, when there are countless possible reasons why and all of them have different solutions. We need enough detail to pick one of them.



As much as people might be intimidated by the thought, this has become a programming language. So to use it well, you kinda need that mentality:

The machine is stupid. Or "maliciously compliant" if you will. It does EXACTLY what you tell it, even if you don't understand what you're actually telling it. Interpersonal skills are useless for programming, which is what this is. I know of several programmers that don't have them: they're very good at coding, but they're also hard to work with. (Warmuptill is not one of those, by the way)

We don't intentionally make our programs hard for everyone else to use; they just reflect the nature of the machine. Natural human interaction is a major field of study, and people spend their entire careers on very small parts of it while still barely making a dent.

I didn't say it doesn't work at all, it rarely works, but it's usually stuck on the B scene. I asked in general because I don't know what data I need to provide for solution.

your questions,

yes it's actually running.
no, it's not waiting something never be true.
macro is not pausing
plugin itself is active
no, there is not something enforcing scene B.

here are my two macros screenshot,

https://i.imgur.com/WAELsua.png
https://i.imgur.com/MKUJ6hP.png

I attached the log file.
 

Attachments

  • log.txt.txt
    20.1 KB · Views: 23

AaronD

Active Member
I didn't say it doesn't work at all, it rarely works, but it's usually stuck on the B scene. I asked in general because I don't know what data I need to provide for solution.

your questions,

yes it's actually running.
no, it's not waiting something never be true.
macro is not pausing
plugin itself is active
no, there is not something enforcing scene B.

here are my two macros screenshot,

https://i.imgur.com/WAELsua.png
https://i.imgur.com/MKUJ6hP.png

I attached the log file.
From those screenshots, I don't see a reason why it shouldn't work, but I would have done it differently. It might just be a matter of preference and not the problem at all, but I would:
  • Change the "For exactly" modifier to "For at least", and check "Perform actions only on condition change" above it.
    • Depending on how "For exactly" actually works, this might actually be the problem because "exact" rarely happens.
    • Where your version creates a single pulse all in one step that might be missed, mine uses two steps: one to create a different constant on either side of the desired time, and another to create a pulse out of that step-change. Speaking as a programmer, the two-step version is much more reliable in general, in everything from industrial controls to toys.
    • If Adv. SS already uses the two-step method internally, then it probably won't make any difference. But as a programmer, I still like mine better.
  • Combine the two macros into one, by adding a second condition with the Or operator.
    • This is entirely a matter of preference, and depends on having the exact same action(s) for each condition. As soon as you do something different for each one, you can't combine them anymore.
 

murdocklawless

New Member
From those screenshots, I don't see a reason why it shouldn't work, but I would have done it differently. It might just be a matter of preference and not the problem at all, but I would:
  • Change the "For exactly" modifier to "For at least", and check "Perform actions only on condition change" above it.
    • Depending on how "For exactly" actually works, this might actually be the problem because "exact" rarely happens.
    • Where your version creates a single pulse all in one step that might be missed, mine uses two steps: one to create a different constant on either side of the desired time, and another to create a pulse out of that step-change. Speaking as a programmer, the two-step version is much more reliable in general, in everything from industrial controls to toys.
    • If Adv. SS already uses the two-step method internally, then it probably won't make any difference. But as a programmer, I still like mine better.
  • Combine the two macros into one, by adding a second condition with the Or operator.
    • This is entirely a matter of preference, and depends on having the exact same action(s) for each condition. As soon as you do something different for each one, you can't combine them anymore.
I tried plugin at fresh obs install and it worked without any problems. Maybe it conflict with one of the other plugin.
 

Warmuptill

Active Member
I tried plugin at fresh obs install and it worked without any problems. Maybe it conflict with one of the other plugin.
Thanks for reporting back that the issue was resolved! :)
And sorry for getting back to you so late!

The only things I noticed in the provided logs / screenshot was:
  • There was a sudden time jump in the log that I do not really understand:
    02:53:29.473: [adv-ss] try to sleep for 300
    02:53:46.903: [adv-ss] Macro Replay Slow Motion returned 0
  • You seem to have selected to "Wait until transition to target scene is complete".
    Maybe the transition never "ended" or was extremely long and thus the plugin was stuck waiting.
But I guess if everything is working now there is no need to change anything :)
 

AaronD

Active Member
@Warmuptill I hope this is a quick and easy thing: Could the scene regex (first screenshot) have an option to check during transition, like the specific-scene does (second screenshot)? Looking for an easy way to have everything else transition *during* the scene switch, instead of waiting for the scene switch to finish first, while keeping the easy way to add content and keep the automation.

Thanks!
1682127799278.png

1682127777653.png
 

Warmuptill

Active Member
@Warmuptill I hope this is a quick and easy thing: Could the scene regex (first screenshot) have an option to check during transition, like the specific-scene does (second screenshot)? Looking for an easy way to have everything else transition *during* the scene switch, instead of waiting for the scene switch to finish first, while keeping the easy way to add content and keep the automation.

Thanks!
View attachment 93349
View attachment 93348
Sure, a build will be available here in a few minutes:
This build also contains the websocket race condition fix.
 
Last edited:
Top