Close
Community Post

This article is not about a TOP Switcher

Download the final Product of this Writeup on Patreon:

 

The core aim of this article is to explain several paradigms and techniques that not only can be used to build a TOP Switcher but also offer a more general way of thinking when developing components, whether small or large. As a byproduct, you’ll learn how to create a TOP Switcher.

Do not start with the UI. I fell into this trap myself before. When we're not used to programming, we primarily know software from interfacing with it through a GUI. We're often unaware of the inner workings of the software we're using. If we start with the UI and couple it directly with functionality, we'll inevitably be bound to that UI. But what if we want to use the component in a context without a UI? What if we want to create presets in a generic way or attach external control mechanisms like MIDI or OSC?

Let's step back for a second and make a plan for our switcher without considering how it will look to the user.

We want to:

  • Define an arbitrary number of TOPs in our network.
  • Select which TOP should be visible on our single output.
  • Be able to transition between our outputs over a defined time.

Now, step by step, we'll create a little boilerplate COMP in TD as our starting point.

In 2023, Derivative implemented Parameter Sequences. They allow us to set a group of parameters that can be repeated via code or by the user. This is perfect because we can make a sequence of TOP parameters to create an arbitrary number of inputs that we can feed with data. We could also create a number parameter to allow for changing the number of inputs, but this is new and exciting, so let's do it!

To create a sequence, first, generate a simple TOP parameter, right-click, and use Add to Sequence, giving it a name. In this case, let's call it Sources.

 

Now, we can add and remove references with the click of a button—no manual interaction needed.

For the other two parts, timing and index, we simply add an Int and a Float parameter named Selected Index and Transition Time. For testing purposes, let's add three TOP entries to our Sources parameter and provide them with three placeholder TOPs.

Now let's go inside our switcher to create the logic.

What we're looking for is a way of selecting the TOPs, changing their opacity, and combining them all together into a unified output. Let's start from the left of our data flow. Let's create a Selector COMP with another TOP parameter and a Level parameter. Inside, we feed a Select TOP into a Level TOP, and finally, into an Out TOP named videoOut. Reference the Level parameter in the Opacity parameter of the Level TOP, and the TOP parameter in the TOP parameter of the Select TOP. Let's name the whole COMP selector0.

Great. Now we need one of these selectors for every parameter in the sequence we created. Well, we all know the drill with replicators by now, with only two differences:

  • We need to set the Suffix Start to 0, as sequences start at 0.
  • Instead of using a table, we generate replicants by number.

Pass the selector0 as our master operator, set operatorPrefix to selector, and set the number of replicants to the number of parameters in our sequence len(parent().seq.Sources).

We should now have 3 selector COMPs named selector0-2. To reference the actual components, we use a bit of Python:

parent().seq.Sources[ me.digits ].par.Top

Me digits here evaluates to the index of the selector COMP. So, selector0 will reference Sources0 TOP, and so on. Now, recreate the replicants so this change translates to all selectors. You should now see the references to our TOPs in the selectors.

Now, we need to control the level. Let's use one of my favorite tools, the Fan CHOP. You can use it to pass one index and get an arbitrary number of outputs, with the one meeting the index being on, and all others being off.

Feed a Parameter CHOP with only the Selected Index parameter selected into a Fan CHOP. Then, in the Fan CHOP, let's combine a bit of f-string Python with another powerful tool in TD—pattern expansion. When passing certain strings to parameters, like channel names, TD interprets and expands them. The default for the Fan CHOP is already using that. For instance, chan[1-8] results in channels named chan1, chan2, chan3... chan8. In our case, we want to create one channel for every selector we have, right now level0, level1, level2. In pattern expansion, this translates to level[0-2]. With a bit of f-string, it will look like this:

 f"level[0-{len( parent().seq.Sources) - 1 }]"

Quick writeup of what's happening here: parent().seq.Sources gives us the Sequence object we created in the first step. It can be evaluated as a list of blocks, each containing the parameter. Using len() gives us the number of blocks in our sequence.

The Fan CHOP should now output three channels, with one being at 1 and the others at 0. Feed that into a null CHOP named level.

In our selector0, we now have two ways of referencing the level, either by name or index. Both behave the same in the end, with the name being slightly more complex using f-string but more concise in the actual output.

Now, if we change the Selected Index parameter of our parent COMP, it switches around.

 

Almost done! The last thing we need is the timed action, but this is almost trivial. Rerange the Fan to -1 to 1, multiply it by the inverse (1/n) of the time, and feed it into a Speed CHOP with limits set to 0 and 1. Now, changing the index fades between the different values.

To combine all the TOPs, we'll use another powerful feature: pattern matching! Create a Composite COMP with the composition mode set to Add, and put the following expression in the TOPs parameter: selector*/videoOut. That's it. It will automatically select all videoOut TOPs in our selectors, as they are the only operators meeting the criteria. We don't have to adjust anything now. When adding or removing any element from our sequence, the Composite TOP will handle the final composition.

Except for one thing! Everything seems skewed. Let's fix this by creating a transparent Constant TOP, passing it as an input to our Composite TOP, and changing the parameter Fixed Layer to Layer 1. By adjusting the Pre-Fit Overlay parameter, you can change how the elements behave. Finally, let's export the Pre-Fit of the composite and the resolution of the transparent Constant TOP to our parent as custom parameters.

Time for the UI!

Some of you might be wondering why we did all of this without having a proper UI. As I said, we can already use this component without a UI via OSC, MIDI, and even preset systems like TauCeti or TD-Morph. Also, in theory, we can change and update the functionality without ever touching the UI components, which makes working in a team much easier, allowing for updates on the go without merge conflicts.

We'll keep the UI pretty straightforward: one preview of the output and a list of elements to click on. The elements should highlight which one is currently selected.

First, a little bit of setup. We need a new Container COMP with a new COMP parameter called Target. For ease of use, I like to create an iOP evaluating the Target parameter. This way, we can just write iop.Target to get a reference to the switcher. Also, create a parent shortcut set to Ui.

First, the preview. Let's create a container with the width set to fill and the height set to 200. The background parameter should be set to the output operator of our switcher: iop.Target.op("out1") in my case. Set the TOP fill parameter to Best and we're golden.

Now to the selectors. First, we need data of the selectors that we can work with. For this, we use an OpFind DAT, point it to iop.Target, filter for selector*, and output the path column. Pass it to a null named selectorsData.

Next, we create a prefab that we will replicate and give it some custom parameters:

  • Target is a COMP parameter set to the expression parent.Ui.op("selectorsData")[me.digits, "path"].
  • Index is an integer giving us the digits of the target via me.par.Target.eval().digits.
  • Finally, a Toggle parameter with the expression set to iop.Target.par.Selectedindex.eval() == me.par.Index.eval().

Now, some eyecandy: On the Look page, add the following expression to the four border parameters: me.par.Selected.eval() + 1, which will evaluate to 1 or 2, depending on if the target switcher has selected the same index as this UI element. Change border B to green. To see what we are selecting, add me.par.Target.eval().op("select1") as the background TOP parameter.

Now to actual UI work. Create a container set to fill on width and height, and set the layout order to 1. In the Children tab, set the Fit method to Fit Best, Align to Left to Right, and Max per line to 4 (or any other value that looks good). Deactivate the Display parameter of the Prefab COMP.

Now, use a Replicator with the newly created container as the destination parameter, the Prefab as the master operator, and the selectorsData table as the blueprint. In the callbacks, uncomment c.par.display = True.

Almost there! Create a Panel Execute DAT and add the following as the panel parameter value: selector/item*.

This will watch all panel COMPs inside the destination of our replicator, so all the buttons pointing to our selectors in our switcher. One single line will get us there:

iop.Target.par.Selectedindex.val = panelValue.owner.par.Index.eval()

What’s happening here? Well, panelValue is an object, not just a raw value. Using owner, we can access the operator that is being pressed. Luckily, we gave it a custom parameter, allowing us to fetch the information we need. We set the value of the Selected Index parameter of our Target Switcher to the index in our button.

Where am I going with this? We have a switcher. But as I said, this isn't about the switcher itself—it's about the core ideas and techniques used here. We have only one source of truth: the Switcher COMP. The UI is merely a reflection of the state of the switcher. Any change we make to the switcher is directly reflected in the UI. We only need to adjust the system at one point—never more than one. We can remove or copy the UI to other locations without any issue. This is what we should strive for. The more moving parts a system has, the easier it is to break. The more points we need to adjust when making changes to a system, the easier it is to forget something—and, yeah, break it!

Comments