A Walk Around the Block - An Introduction to gen~ and filtering

    One of the features of gen~ that has interested Max users from the beginning is the ability to create patches that process audio input on a per-sample basis (as opposed to MSP, which bases its processing model on groups of samples, called the signal vector size). Single-sample processing in gen~ means that the signal vector size no longer set the shortest allowable delay time in samples to the current signal vector size, which lets users experiment with things like pitched/ringing delays. You can find a tutorial exploring these features here.
    Another area in which gen~ users have ventured involves using gen~ to explore and implement various varieties of audio filters. While there are a lot of examples of audio filters that are specified using mathematical formulas...
    ...audio filters are also often depicted using block diagrams rather than mathematical equations. You can use those block diagrams in your gen~-patching practice to create those filters using standard gen~ operators, even if you're just getting started in your Gen journey. In this brief tutorial, I’m going to
    • Show you a couple of simple examples of filters implemented in gen~, and show you a neat trick that’s helped me make sense of using block diagrams to create gen~ patches, and work through a couple of simple examples.
    • Give you a little advice about what to watch out for as you explore this area further (to avoid some of the problems that bedeviled me along the way, to be honest).
    application/zip 12.50 KB
    Download the patches used in this tutorial
    Let's get started!

    A First Trip Around the Block (Exercising Your Biquad Muscles)

    Let’s start with an example of a digital filter that every MSP user knows and loves: the biquad (short for biquadratic) filter. People love this general-purpose digital filter because you can set it up for practically any configuration by doing nothing more than adjusting the parameters (or coefficients) for the filter.
    In the MSP world, the biquad~ object is an implementation of this incredibly versatile filter, and Max/MSP helpfully includes a couple of external objects that provide a graphic interface that outputs filter coefficients to the biquad~ object (the filtergraph~ object), as well as a non- graphic object for outputting those coefficient values (the filtercoeff~ object).
    In fact, it may be the very first glimpse you've ever had of a filter block diagram! In fact, if you take a look at the coefficients tab of the biquad~ object’s help file, you'll find a block diagram of a biquad filter,.
    Spoiler: This block diagram has an error in it. We'll talk about that in a minute.
    Here’s a standard block diagram of one version of a biquad filter from the Wikipedia article on biquadratic filters:
    I'm showing you both block diagrams to make a point: It's often the case that the same block diagram is depicted in slightly different ways, so it's helpful to see if you can find more than one example of any filter (or physical model or waveguide) block diagram you want to use to start from.
    You can think of this block diagram as a map of the flow of data, and — as with any map — you always need to start by getting the lay of the land. In terms of the overall input and output, the flow of data is left (starting at x[n]) to right (ending with y[n]), with arrows generally demonstrating the order in which calculations are done. Several graphic items show up and are repeated several times
    • A box with Z^(-1) in it
    • Three big circles with a cross in them
    • Smaller hollow triangles that are labeled
    Decoding them will help us to see what’s going on – because each of those symbols will be replaced by a gen~ operator (or operators, sometimes) when we create the filter patch. Let’s look at each one:

    Blocks to Operators

    The Z boxes are a convention used to represent single sample values. If Z is the current sample, then Z^(-1) is the previous sample.
    Some filter block diagrams use variants of this to indicate “N samples ago” by Z^(-N), where N is the number of samples’ worth of delay, or — in slightly more rare occasions — Z^(-M) to indicate a moving sample value rather than a fixed number of samples. In gen~, we can represent “the previous sample” (Z^(-1)) using a history operator.
    You’ll notice several circles with a cross in them. It's not a cross at all — it’s actually a gigantic plus sign rather than a graphic of some sort. The arrows helpfully indicate what things are being added together and where the result of that addition is routed in the block diagram.
    The third and final item is a group of five triangles, all of which are labeled (b0/b1/b2/-a1/-a2). If you take a look at the output of the filtergraph~ object as you manipulate the graphic interface, you’ll notice that the left outlet sends a list of five numbers as its output:
    If you’ve looked at the input labels for the filtercoeff~ object, you’ll notice that there are also 5 outputs sent to the right 5 inlets of the biquad~ object.
    Each of those five labeled triangles correspond to the parameters we use as multipliers to configure the biquad~ object to respond as the kind of filter we want. We can use the gen~ param operator to represent them.
    So we know what gen~ operators we’ll use to patch our biquad filter, but how are they hooked together?

    Turning Heads

    I’m going to show you a really simple and elegant way to help you translate this filter diagram to a gen~ patcher. I learned it from Jon Christopher Nelson, who gave a workshop on physical modeling using gen~ that I attended. I hope it’s as great a help to you as it has been for me.
    Jon did something interesting when it came time to do his patching: he took the block diagram you see above, and he rotated it 90 degrees, and redrew the labels. Here’s the result:
    For me, that simple transformation suddenly oriented the diagram so that the information flowed from top to bottom… like the ordinary Max patching I do all the time. To this day, I’m a little embarrassed and fascinated about why that made understanding things so much clearer to me, but it’s true. I started by laying out the history, param, and + operators in a way that corresponded to my block diagram, and then started thinking about their interconnection.

    Making Connections

    I’m going to point something out to you that I didn’t notice at first when I started creating this patch. Yes, that’s right. I completed what I thought was a version of the patch and it promptly blew up as soon as I turned the audio on. It was a simple thing error that it took me a couple of minutes to find, and it's also the error in the block diagram in the coefficients tab of the biquad~ object's help file:
    Take a look at the labels for a1 and a2 in the block diagram. Notice the minus sign preceding them? I didn’t notice it the first time around. That parameter is a negative number, which means that adding a negative number is actually subtracting that value.
    The other part of my patch conversion was connected to noticing that all of those + operators in a row were actually summing the results, which meant that I could replace those three + operators with a single (minus) operator and take advantage of the fact that multiple inputs to the inlet of any gen~ + or operator are summed. The outputs of param operators b0, b1, and b2 are all connected to the left inlet of a operator, and the outputs of param operators a1 and a2 are summed by their connection to the right inlet of the operator. Neat trick, eh?
    Finally - having blown up my filter after making a mistake with the patching, I decided that I might save my ears and my speakers by the simple act of adding a clip operator to my patch at the very end to slip the output to a friendlier output range of -1 to 1. It's a good thing to add to any gen~ patch, actually — for me, anyway.
    Once I’d sussed that out, it was just a matter of connecting the operators to one another as shown in my block diagram. Here’s what I wound up with:
    It was time to test out my port of the block diagram. I grabbed a copy of the biquad~ help file and modified it so that the parameter messages matched my patch, plugged my gen~ patch in where the biquad~ object should be, and turned on the audio....
    Success (pause for a well-deserved celebratory libation and a bad imitation of Zorba the Greek dancing….)!
    Now that I’ve got a working filter I built from a block diagram, let’s compare it to the biquad filter contained in the Gen examples folder (gen~.biquad.maxpat)

    Try It Yourself

    That wasn't so hard, was it? Okay - here's an example you can try turning to a gen~ patch yourself. It's a variation of the standard biquad filter with a little different topology:
    See if you can do the "rotate and replace" trick and create your own gen~ version [remember to keep an eye on those a and b labels and watch out for minus signs!].
    ...and no peeking until you've taken a run at it. The biquad_exercise.maxpat patcher in the patches folder contains a solution to the problem.

    Let's Try Another One

    We're on a roll, so let's try a little more complex example. Like the biquad filter, this block diagram describes an example of a delay-based effect that can be produce varied output (vibrato, flanger, chorus, and doubling) based on how its input parameters are set. This bit of digital signal processing was first described in this paper by Jon Dattorro (whose name you may know from the Dattorro reverb). Here's what it looks like, and we've included a listing of how the inputs can be varied to produce different output effects:
    Although you may never have seen this bit of patching before, you may have heard it. This patch forms the basis of the original Pluggo Generic Effect plug-in, which has also been ported to Max for Live as a part of the Pluggo series (the Max for Live device shares a common ancestor with the above patch, but is implemented differently).
    Okay. Let's start by doing our "rotate the block diagram" trick and looking at what we have in terms of top-to-bottom signal flow:
    With one exception, this block diagram is pretty easy, now that we know how to translate the triangles and the "cross in a circle" blocks: each triangle is a param operator, and each circle is replaced by a + operator. But what about the Z^(-N+M) box — how might we patch it? As I mentioned earlier, Z^(-N) refers to a delay value which may be greater than the single sample delay we used the history operator to represent. Our other hint comes from the input to the block in the diagram labeled modulator.
    Let's look at the settings associated with various effects again:
    Instead of working with single sample delays, we will be using a standard gen~ delay operator with two taps (outputs) One of them will be a delay time in samples (the Delay column in the original block diagram) set as a fixed number of samples. That number of samples to delay our input by will be altered further by adding a value in samples that is derived from a modulated sine wave whose output is scaled to a specific range in samples (described in the Modulator column).
    Our delay module in the block diagram is outputting two different delay values - the fixed delay value (the Z^(-N) portion of what happens in the delay block) is used for Feedback (the FB column in the original block diagram), which the summed fixed delay and modulated delay value are sent on be scaled by the Feedforward control (the FF column in the original block diagram).
    Here's what the result looks like - for the sake of ease of reading, I've outlined the Z^(-N+M) portion of the block diagram. The feedback (FB), feedforward (FF) and blend (BL) portions of the gen~ patch are simple substitutions and summing operations. The delay portion of our block diagram takes the -N and M values in milliseconds in the tables, but those need to be translated to a number of samples at the current MSP's current sample rate. The mstosamps operator handles that for us in both cases.
    To manage the delay, we're using a 2-tap delay (set by the second argument to the delay operator). The left output of the delay operator outputs the fixed sample delay value for the feedback calculation and summing (set by the value sent to the in 2 operator. The right inlet's value (in samples) is derived by summing the fixed delay input and whatever the current sample count from our sine function is sent to the in 3 operator.
    It's really not that difficult to figure out, once you get the hang of it.
    The dattorro_effects.maxpat patch provides you with an example to play with that also includes a lovely way to deal with modulating the input to that third inlet to the gen~ object (especially useful for those chorus and doubling effects), courtesy of our friend and benefactor Jon Christopher Nelson. The jons_cool_modulation patcher lets you set frequency and amplitude for the modulating sine function, but also to set a degree of randomness to the output that smoothly transitions from setting to setting. It's worth a little careful study.

    Exploring The Next Block(s) Over

    There are several other places where DSP programming uses block diagrams for explanation/demonstration. Here are a couple of links you can use to begin your explorations:
    As you explore these techniques and go off in search of block diagrams you might like to try implementing in gen~, I’ve got a few suggestions you might want to keep in mind.
    Diagrams vs. Specifications
    I've discovered along the way that it's often not enough to just copy a block diagram and start patching from there. Part of my frustrations wind up stemming from a simple observation: block diagrams for filter design are often meant to provide conceptual models rather than a starting point for implementation.
    One obvious place you may notice this is a situation where you realize that your block diagram has absolutely no delay for feedback — that's simply not possible in the digital world (a caveat: Some filter algorithms can be rearranged to have better frequency or phase response, while preserving 0 delay in the main path through. The trapezoidal SVF is an example of one of those).
    Here's a real-world example of this from the Max Forum: In this thread, the paper showed a diagram of what a circuit should conceptually be, and then the text explained how this is impossible due to zero delay feedback. If you read on, you'll find how they solve the zero delay feedback problem by re-arranging the math.
    You may also find — particularly in the case of academic papers — that there'll be another kind of error in the diagram that's corrected in the text of the paper. These aren't things that happen all the time, but it never hurts to read to the end of the paper you're grabbing your block diagram from (ask me how I know this....).
    It's also sometimes the case that the way that someone characteristically describes signal flow in a block diagram isn't actually the way that most people do the calculation — the amazing and wonderful Waveguides website describes and example of that pretty succinctly in their section on Implementation Details, for example.
    That said, there are a few block diagram labels you're likely to encounter that don't need to scare you off at all. Blocks in a diagram that correspond to filters can easily be substituted by grabbing examples of those filters from the gen~.filters.maxpat examples file and just dropping it in, along with some param operators, for example. Here are a few others:
    Sigmas and Crosses
    From time to time, you’re likely to see a Greek letter sigma (∑) in a part of the block diagram. Here's an example of it taken from a block diagram you might even recognize - it's that same generalized effects block diagram we just worked with (in fact, it's the illustration the author himself uses in his paper on the subject):
    Since you're familiar with the block diagram you just worked with, you'll probably notice right away that it’s the same position as the the + operator substitution we made for the “plus sign in the circle” in our previous examples. You can just substitute the + operator whenever you see a sigma like that and you’ll be fine.
    Integrators (Leaky and Otherwise)
    One of the other symbols you're likely to run across is the Integral symbol ( ∫ ). That can be a tricky one to encounter, since it might mean a simple integrator (which would be a simple substitution using the += operator), but it might also mean that you need a "leaky integrator" (would would be a history operator, a + operator, and a * operator for the leak factor). It’s not always easy to trust the diagrams, and you may need to go beyond the block diagram into the text to figure out what you need.
    Learning To Block With The Problems You Tackle
    As you experiment with building filters, you may find something interesting related to looping delay lines. You'll probably find yourself in a situation where the output of your filter vanishes when you crank up the feedback. It's not you - there's a name for what's happening: DC offset.
    It happens as a result of averaging process — very slight positive or negative average offsets get amplified inside the loop until the signal gets messed up. In the filtering world, the usual solution is to add a 30Hz. highpass filter into the loop. The dcblock operator in gen~ is a simple solution that handles that for you without the need to copy and paste 30Hz. highpass filters into your patch.

    Watch This Space....

    I hope this has helped to demystify those block diagrams for those of you starting out with your gen~ programming.
    By the way, writing this article reminded me once again of the amazing workshop that Jon Christopher Nelson presented at a previous SEAMUS conference. I got in touch with him to thank him once again for his patch rotation hack, and asked if he'd be willing to submit his physical modeling gen~ patches as a Max Package Manager submission. The good news is that he said, "Yes!"
    So keep your eyes peeled for its appearance in the Package Manager. It's a great example of "content you need."

    by Gregory Taylor on
    Feb 2, 2021 8:30 PM

    • Duncan Wilson's icon
      Duncan Wilson's icon
      Duncan Wilson
      Feb 03 2021 | 5:30 pm
      Just what I'm looking for right now. Cheers.
    • nouserid's icon
      nouserid's icon
      Feb 03 2021 | 5:40 pm
      Aw yis, can't wait to read this!
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      Awesome work here, Gregory! (going to have so much fun modding that all-in-one Dattorro for modulatory-madness 😋)
      i like that you mentioned a 'trick' by way of rotating the block-diagram, i've seen that done but not felt it helped me much: i think it's a difference between visual and text based programming... code got me reading left-to-right, patching gets me thinking in a more visually-mapped area of space(where top-down can often look more orderly because of the way max-objects' inlets and outlets appear)... but what i like most about it all is that we can individually find a best way to reframe things for ourselves - to me, that's always been my greatest asset in learning any tech -> if i know how to reframe things towards my own individual style of positive-growth, i'll always encounter 'techno joy' in everything i'm learning. (...and that joy in learning for me is greater than any pride i'll feel at the end of having learnt)
    • Gregory Taylor's icon
      Gregory Taylor's icon
      Gregory Taylor
      Feb 03 2021 | 8:50 pm
      The American writer Walker Percy used to refer to experiences that reoriented you as "rotations" - a term that's really stuck with me. They're often little things (I still remember the first time I encountered that trick with two mirrors where you can see yourself as other people see you — i.e. "flipped" from the image of yourself that's stared back at you from every mirror everywhere rather than your "reflected self"), and enjoy them. Jon's simple act of reorienting the image so that if matched more easily with my tendency to patch top-down (I suppose that I hadn't really realized just how strongly I did so until that moment, in fact) was a revelation... so much so that the possibility ot passing that observation along to someone else it might help outweighed the possible shame or embarrassment that such a simple hack would help dispel the confusion I strove to hide. May my world continue to fill irregularly with such moments, and may I find the courage to pass them along.
    • Steve Meyer's icon
      Steve Meyer's icon
      Steve Meyer
      Feb 04 2021 | 11:43 pm
      This is really an excellent way to translate these block diagrams. This is another superb write-up and demo of how powerful and useful the world of gen~ is!
      I have a question to see if I am understanding both the block diagrams and gen~: In the vertically oriented block diagram for the first filter example, I was not sure if I am supposed to read the Z^-1 squares as identical copies of the same state or if the two Z^-1 on the left are one iteration behind the two on the right. Another way to ask this would be, are all of the Z^-1 boxes in this diagram x[n]^-1 or are the ones on the left x[n]^-2?
      Based on the way I am reading the gen~ patch, because it is implemented by chaining one [history] operator after another, I believe the Z^-1 boxes on the left would be equivalent to x[n]^-2.
      Do I have this correct?
    • Roman Thilenius's icon
      Roman Thilenius's icon
      Roman Thilenius
      Feb 05 2021 | 1:12 am
      great tutorial, but i still wouldnt be able to translate from the one to the other thing.
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      hope it's ok to answer, Gregory can correct me if wrong here, just testing my own understanding:
      I was not sure if I am supposed to read the Z^-1 squares as identical copies of the same state or if the two Z^-1 on the left are one iteration behind the two on the right
      not identical copies, if i read you correctly, you have it right: the second z^-1 in each series will be delayed by two samples at that point(however, it can be confusing to write it like "x[n]^-2" because that would mean you're taking 2 samples of history, if there's a history before that, it would be an incorrect 3 samples of history at that point... but i'm just being clear, you're correct that at that point in the signal path, that particular sample would've been delayed by two samples).
    • Steve Meyer's icon
      Steve Meyer's icon
      Steve Meyer
      Feb 05 2021 | 4:00 am
      Thanks for confirming and for the clarification on how "x[n]^-2" reads. That is good to know.
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      no wait, i'm wrong about the clarification part, and apologies for being knit-picky which is what caused the confusion:
      on how "x[n]^-2" reads
      i just realized you're referring to the equation(like "x[n-2]"), not the block-diagram(and also, the block diagram used in the biquad~ helpfile also uses the superscript -2... yet it's still clear enough because 'n' always refers to the current sample; originally i thought you were referring to the block-diagram, and substituting out the z-1 for a 2-sample delay).
      my bad, you're correct in writing it like that, too(because 'n' is always the current sample, writing everything relative to 'n' makes it clear; taking it further, x[n] is the current sample of the 'feedforward'(input directly into the summation point), whereas y[n] is the current sample of the 'feedback'(output from the summation point fedback into it)). ok, and now i'm clear on it too 😂
    • vichug's icon
      vichug's icon
      Feb 05 2021 | 12:57 pm
      super tutorial ! after reading some other tutos about filters that resuurfaced the c74 forums recently, this is exactly the right time :) the rotation unexpectedly helped me too :p the little other things to read block diagrams are actually invaluable, like the concise integrator explanation, i hope i have integrated that now.
    • Steve Meyer's icon
      Steve Meyer's icon
      Steve Meyer
      Feb 05 2021 | 4:10 pm
      R∆J∆, on rereading, I'm also seeing that I contributed to the confusion. I was trying to follow Gregory Taylor's lead in the article for writing the symbols in prose, but I left off the parentheses he used that make it a bit more clear. Thanks, again.
    • Max Gardener's icon
      Max Gardener's icon
      Max Gardener
      Feb 05 2021 | 5:12 pm
      I have taken the liberty of correcting Gregory's diagram reversal of coefficients in the original rotated biquad diagram, with thanks to Raja and Steve Meyer.
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      ah, sweet, looks good :) happy to help!
      and Vichug wrote:
      i hope i have integrated that now
      👆haha, i see what ya did there 😉 actually, i should mention an extra thanks for that part, too, that was one thing i'd never understood: the way to create regular or leak-factor integrator and how to interpret that symbol properly 👍
    • Holland Hopson's icon
      Holland Hopson's icon
      Holland Hopson
      Feb 05 2021 | 9:08 pm
      Very helpful article! Here's a list of block code marks with their translations that I keep handy. x(n) = input y(n) = output (n) = "sample of the moment" (n-1) = delay of one sample z^-1 = single sample delay (history operator) z-n = some variable sample delay (delay operator) z-m = "moving" delay (delay operator w/modulation of delay time) triangle = multiply (*~ or * in Gen), amplifier small '-' sign = invert a value (subtract instead of add) circle with "+" = addition operator black dot = branching point T = 1/sample rate = 1 sample (block diagram refers to this as a variable delay? Perry Cook defines it as 1/sample rate)
      g = cutoff freq (and is approximately g = 1 – exp(–2πfc/fs), where fc is the desired cutoff frequency and fs is the sampling rate.)
      nonlinear lookup = tanh (and similar)
      Now, who can tell me what H0/2 means?
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      Now, who can tell me what H0/2 means?
      🙋‍♂️...tis something to do with a Hilbert basis? (edit: but you probably saw it in a block diagram? not an equation? then i'm probably wrong there) as soon as i understand this page enough to translate into gen(😭), i'll be able to tell you: https://en.wikipedia.org/wiki/Wavelet_transform 😜 also to add to the general list, more found in equations than block diagrams: 'theta' (i'm unable to type the symbol properly... looks like a slanty 0 but with a horizontal line through it), often referring to a running phase in radians(especially input for trig functions)
      oh! this one trips me up: ∂ - sometimes also as a small 'd', symbol for 'partial derivatives' (usually found in a ridiculously complex equation that makes you cry for hours on end... especially if you take the AI courses on Coursera offered by Stanford University Professor, Andrew Ng 😭) and who can forget the almighty: ∆ - 'delta', a symbol of change! 🙌💪 wherein two samples turn into a difference! a river turns into an ocean! and an 'A' turns into something that makes me feel like i stand out more 😋
    • Rick's icon
      Rick's icon
      Feb 10 2021 | 7:27 pm
      Don't know where your looking exactly but H0 is random data used as a statistical test aka null hypothesis https://en.wikipedia.org/wiki/Exclusion_of_the_null_hypothesis
    • deligut's icon
      deligut's icon
      Feb 12 2021 | 4:35 pm
      I have another question: I would have thought that only the frequency of the LFO would be extracted via the mstosamps function and then fed into the delay function as a parameter (via the moduclated delay tap). However, the jons_cool_modulation patcher is also using amplitude as a parameter for the LFO. How is the amplitude of the LFO affecting the delay function?
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      How is the amplitude of the LFO affecting the delay function?
      the amplitude maps to a range of change(the amp value you enter in the master patch determines how wide of a range in milliseconds the delay time would range between), while the frequency controls a rate of change(the freq value you enter in the master patch determines how fast changes happen).
      (for example: cycle outputs -1 to 1(mapped along a sinusoidal trajectory), you might enter an amp value of 17(for 17ms), cycle will now be multiplied by that so the output afterwards will be between -17 and 17, 'mstosamps' will convert these values to samples. while only the speed of change to traverse one sinusoidal cycle (from -17 to 17 and back again) is determined by the frequency of the LFO).
      hope that helps to make sense 🍻
    • deligut's icon
      deligut's icon
      Feb 13 2021 | 4:30 pm
      Thanks a lot for the clarification.
    • Benjamin Whateley's icon
      Benjamin Whateley's icon
      Benjamin Whateley
      Mar 08 2021 | 12:10 am
      This is a very exiting set of tutorials, I feel giddy in a way I haven't in a while.
    • Benjamin Whateley's icon
      Benjamin Whateley's icon
      Benjamin Whateley
      Mar 09 2021 | 3:48 pm
      I have a question?
      In the example patch the modulation input is bipolar. Should a bipolar signal be used?
    • 👽R∆J∆ The Resident ∆lien👽's icon
      👽R∆J∆ The Resident ∆lien👽's icon
      modulation input is bipolar. Should a bipolar signal be used?
      yes it can be(doesn't need to be)... you can tell it was intentional here, though, especially because the 'clip -1 1' op makes sure it doesn't go outside a workable range. the whole design is based around the 'fixed delay' tap time acting as a 'median' frequency around which the modulation can wander.
      uh oh: i notice the pic has 'clip -1 1' there, but when i download the .zip, the files included do not(i think this might be a helpful thing to add to the patch... although i'm tempted to take the saturation method from Graham's suggestion to Gregory's Oopsy tutorial #2 and use that there, instead 😋).
    • Benjamin Whateley's icon
      Benjamin Whateley's icon
      Benjamin Whateley
      Mar 10 2021 | 3:47 pm
      Thank you! (: