converting Signal Flow Diagrams in Max/Gen~

SmokeDoepferEveryday's icon

are there a set of rules/conventions for converting signal flow (block) diagrams into Gen? (or even Max in general)

I am just turning my head to DSP related challenges and really want to understand how to replicate accurately from diagrams to patching.

If anyone has any resources/advice such as keys or comparative like for like breakdowns I would really appreciate it. Am interested in both codebox and object methods.

I realise there probably isn't a magic 1:1 translation for more advanced implementations but it would be great to grasp the fundamentals

Max Gardener's icon

In terms of elucidating the difference between codebox and standard Gen operator usage, the section labeled "To codebox Or Not To codebox, That Is The Question…" in the introductory tutorial on the codebox operator in gen~ remains a good and succinct description of the matter. It assumes nothing in terms of your personal preference textual coding vs. working with operators, but it does lay out the question in terms of how Gen actually works. You may find it helpful.

Graham Wakefield's icon

Quickly:

Signal join: [+ ]

Signal split: multiple cables from an outlet. You can add a [pass] object (it has no overhead) if you want to position a split somewhere else.

Mul: [* ]
So, that works for amplifiers assuming the 2nd input is positive only ([abs] or [max 0] can ensure that) but also 4-quadrant multipliers like ring mod if the 2nd input can also be negative.

Z^-1: [history]

Delays: [delay] (time is in samples, so you probably want to use [mstosamps] or [* samplerate] for ms / seconds.

Any variable parameters using [param], with appropriate @min @max if desired.

Switches: [switch] AKA [?] for a 2-input, 1-output;
[gate]/[selector] for multi-output/input;
[mix] for a simple linear crossfader.

[mix] + [history] for a very simple 1-pole lowpass. Feed the out of the mix into the history, the out of the history into the 3rd inlet of mix. The raw input to the 2nd inlet of mix, the filtered signal is the mix output. Mix paramter controls the cutoff. Look in the gen~ examples for how to compute the coefficient, and for other filters.

[and], [or], [not] etc. for logic ops. Use [bool] to convert a signal -- anything nonzero is 'true' (high). Or, use a [> 0], or [> ] for an arbitrary threshold.

Use [latch] for the basis of all sample & hold type circuits.

[> 0] -> [change]or[delta] to detect zero crossings. -> [> 0] for rising only.
[delta] gives slope, [change] just gives sign of slope.

[+=] or [count] for counters / phase accumulators.

I think that covers a lot of it -- people chime in if I missed something important!

👽'tW∆s ∆lienz👽's icon

I think that covers a lot of it -- people chime in if I missed something important!

noice!
i'd add:
1) maybe hipass is well-known, but just to be thorough: take the stuff for lopass, put subtraction after it(subtracting the original signal from the lopass filtered signal) gives you hipass

2) their name is "SmokeDoepferEveryDay" HAAAA! i love it! 🙌
(ok ok truth be told: this was my whole reason for posting☝️😋)

Roman Thilenius's icon


i sometimes wonder why there is not any form of programmatic repatching available which transforms simple Max/MSP patches into their gen/gen~ equivalent.

sure, there is often a completely different "best practice" in gen, but there is also a lot the two world have in common, and an 1:1 translator would be helpful for gen beginners.

Holland Hopson's icon

I've found some block diagrams aren't always clear about coefficients, or they may wrap a more complicated procedure into a single 'triangle' diagram and call it F1. I'm thinking about a state variable filter, here. It took some digging ( https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/ ) to discover that F1 really means 2sin(pi * centerFreq / sampleRate).

Some things remain a mystery to me: H0/2 anyone? I came across it in a diagram for a shelving filter.

SmokeDoepferEveryday's icon

thanks all this has been very helpful, just what I was looking for.

Holland, hope you find the answer – perhaps it is explained somewhere else in the Chamberlin book diagrams?

p.s. RAjA, glad my name is still bringing you joy :)

Ryan Palm's icon

i sometimes wonder why there is not any form of programmatic repatching available which transforms simple Max/MSP patches into their gen/gen~ equivalent.

sure, there is often a completely different "best practice" in gen, but there is also a lot the two world have in common, and an 1:1 translator would be helpful for gen beginners.

YESSSS! 10000% yes! I mean they wrote the friggen code for two different formats. The world it would open up with they could be changed over. It would change everything.

Graham Wakefield's icon

Yes I've thought about this quite a few times.

On the face of it it seems easy: the Max patcher format is a JSON file, as is a gendsp file, so this is a source-to-source translation ("transpilation") from one graph structure to another graph structure; seems like a Node.js script would be quite workable.

But on closer inspection there are quite a few hard challenges:
- how to communicate to the user how/where/why a translation can't be achieved*
- how to deal with the fact that Max MSP has thousands of objects**
- how to deal with the fact that some of these objects encapsulate multiple behaviours, whereas gen~ operators tend to support only one operating mode at once
- how to keep it up to date with changes to Max/MSP objects
- how to ensure the generated gen~ patcher is tidy & readable
- how to choose the best option when more than one implementation is available
- when to choose a gen~ option that is more or less the same, but might be less full-featured
- when to choose a gen~ option that gives more freedom/features, e.g. swapping a send~/receive~ or tapin~/tapout~ feedback path with a [history] or [delay] operator
- how to supply a bunch of gen abstractions that can mirror the features of some of the more complex commonly-used MSP objects***
- when and how to convert message-rate processing to signal-rate processing, e.g. turning trigger bangs into single-sample pulses that gen~ can work with
- etc.

*A tempting idea might be to have a script inspect a patcher and "gen~ify" wherever it can reasonably do so, leaving everything else in place. So, a bit like subpatcher encapsulation in that sense. Perhaps with some heuristic like "only convert to gen~ if it replaces at least 3 MSP objects" etc.

**A good reference point would be the HeavyCC transpiler for Pure Data, which I believe has a whitelist of supported vanilla objects and features. So, so long as you only use those features, heavy will transpile your patch. For this approach to make sense for gen~ you still need a fairly significant "critical mass" of common objects on that whitelist.

***There are some examples of this in the gen~ examples folder, though they might need tweaking. I'm also working on some others -- possible stand-ins for rect~ and saw~ for example, which are not at all trivial.

So it seems like there would need to be a significant amount of work for this to get to the point where it becomes really useful. Still, it sounds like the kind of fun student research project I'd enjoy advising.