Building a Lo-Fi Polysynth in Max (for Live)


Hi! I’ve been very slowly building a polysynth in Max. The goal is for me to learn more about working in Max and with digital audio signals in general, and also to make something that I’ll enjoy using in my own music. I finished a “version zero” a few months ago while working with @jasonw22 for LCRP, and I’ve been slowly working towards a version one.

I’m actually still a ways off from finishing, but since I do have something to share, I figured it might be fun to start a thread about it, and maybe if I’m lucky learn from your feedback about it, ala @billyhologram’s Nearness thread.

The high concept for the synth is to make something inspired by the sounds of early digital music like the Commodore64 or Yamaha FM chips in old cell phones, but without trying for fidelity.


The thing I have to share with you is a gen~ patcher containing (among other things) a variable-slope triangle wave oscillator.

copy me into a Gen patcher!

----------begin_max5_patcher----------
2107.3oc0ZszaiaCD9bxuBBeJwINgj5cebHIEs8xBDflKEaVrPQlNQExhpRz
qR1E6+8RNjRVNwVRtVQEc2XYMiH4vuYFQ9QR+siOZxC7mYESP+.5iniN5aGe
zQfJkhiLxGMYY3yQIgEPwlDwWtjkJlbt9YB1yBPeY3WXK34KQErDVT8ySWsL
NMgIfJSLJimCUg+veMyBWUxrPQzSwoO94bU0gdD04B74H+.a0WDGr5Kp7J5S
qac9JQUyiUJ+9wGqtb9ABmrUIELTY7bwS8EJT+VfhqkmFKiOT90OfhSmydt5
AIworH9pT3ozptbNqPV4PQLO8yasDcBe21hj1AW3bNRGIs.mfk0Xg9bEpPu2
v2pa3SFe3qvk.06TXZKffPs0ffrFEiTJ7hb1euhkF8B5je+qm1azPZAM9dJv
PgqAj2ArjxJkch2.koUZzMu3kLltCMYRSiuFXz2BLhWa4Z3fK7f3jOfPHyiR
2I1HCG1hSQt6K7vaAdsNRhLLQUvCCSNP8gqiE7bFB3Y0QzSgNqwFbCQdYaS.
RBfALrcAHMhIkCAvnNcfLHizKXrwVzKQIrciuy2IF21nk18.idvT.VtcjWRG
zW5rFhW5ZalMWafbFQgn2o25h3yYpRXZvE7TQZ3R.GS9.KMg2CLppzhvHVSH
tAts1BtIMMYQ7WgZST.c2AaOXzGGJ3U77gAZs872sCwnUARvxWdIJ6ovB184
2mlg9YI+SxOptuRuTQTNSMUuR6cXnHTnH2QkBzo2g0RVRIqJIY0KBWlkvJPY
rb0mX9b0CtU0BjKMEq5ir32qIxe+DMUdk1RvXVPIuRYqKKg6uFt+jxYjSA4a
jx3Kbld0zqA4eQofHUXrxsDo7MSu0Ho512RpEU86aoRwKIz5NU7BzIYneBUd
pR5afZQkx6vmpUXzeuP5gU135oYnYxqxNvLDQ94VqoY5+qaYw20ewTKYot4n
8q4NS5rW2fpVmZt8L.sRLo0eCnRM46zap7A6vzV6ORjMcsgmgrWa3yPdSuAT
ZIo0rCSucycETsqLlqYsfKUUzTsLUhgpBklR1H7T1V7QZlxJ6bF3QUvpr9ue
DsCWUYagoW2pyLwo0sqwkUIMqNbYd3MZscFwJaKj0E3LILq6R1M6DyzAOk98
N5cstAt1X45poCdvG35jgatlyFBt4sQBxAlf4+.hBRG39hssPQfz1l0XAKhJ
vUiM2QkVt8.vPvqaromPbLYkq2LMdNhtuHbKbAb5DgDr9aWxHtvi8GaaI0zt
625vXMprFOrIa00u304VgZ0i3iKj7Y4zAFv+OhhJEu+TT0gTJ0jsZqeuDOLL
TWu0uuluXO4vtG7T6ftqHOLsHVsqjnLdxKo7kwgInHNawh3nXosKpIedxsXI
cySujtl.pVEn0sY+OgWhVDmWH1IoTYgdf8nbj0xXwSnmhe7oYB9LU8V2ipKI
zTFD2SNsyNAn5cpjRibx8xM4DnberHd57MZy9wtkNsQKK4LBhRApgNalzanr
HPLx4M1U7Tbdal0pW3ogU0bYqsphGzasprQxjiTHcu6jTjrTQ7kYrzhPACsP
NifjVDewhBl3UckxsR1csUTAysP+UC9Bj7OwSLz73BQXZjzT47kUZjwDQb5p
XwKMoLOaSBy6NgnUVzmTwgrJm30vn67hV4SOSkYroIrLZLAJH8n7ee9wN4QW
9F3MaSaqYM2z116NKoJ90F24xsE+KDwIInTFaNRvQEQgILzCufnP1TDOWM5J
RgrXwJ4njCJAM5.PPqscElnmamPvv9SgGuo3ubHVyPqLyLmcjdSYFyEMnSyy
kC3LDa.WaLzrbgHl4nk7FwvmZpbd9.DCC5l.pFcti6IVPFhXWmXi5r2u2AEA
NM1W86O.LhR+l3sfuJOppuWso7n0VZNSN.WJbBtMJjZ43HRcgVFOOiGmJJpN
sEe3rxnXfAYf9UrlBDOPviVKf9zViO8sWqN6tN60.zHGpk5i6YydyqbOxkAp
NcTXs7DeW856aHPr0DuIlC.w1tQjmmOmku674diB29BiC1eY0CK4ORAF+tiK
t1MBEaHXqemDCG.rMcaAE5Ag.+9jCaqKzg4p5iuR8xBcW9JhOEl9DSz+.bZH
cn8MZexLIs04bcbgti9mETf9D6Ao2FwvGTmsJZz8vNGpaoOiJaYM.FpOCjp1
wRznDnU6hQi.8PM5mSeyw14bbVNleoNP9uqm+akbz+bCbbgYwAogN+i32Wfz
wndlYmq94G0PvfByK7uKnHnuLNNzTt9LoWv.LUjcOGd8PsCE2CCMHSX3222U
w+m7tJXZxfa59DGICBoRbOyXvi.IofsGFsd+oM6z8.UDK32VnY3WPp8gizKH
JLK6Kr7Bi8fdobod+EGJt+4fXbpVDVp1jb1WhqJOrO2SBykqRSHWh1pb8tc+
rqd02SVxkFNcUrIkS5ejlrP7RxqWDV8N+e0p4w7+PDJVU74OvRWoWJnzurHb
UhXSe4COtHNIIhmn6dGU+D4RRMq6bh9omaTWW1OpNm5.aBwW5XuvR5tntvcx
abbZ7JioNjpJY6XGfophZ6R8rcf67oVNVp6vupZz01BSBzV.G3is02IUQj1p
Y0BSeLA54TXaILZyx4Y77prBYUCpK+JA+w7v4pMmuNBqCwZ2MjJlq15dkaG7
IFe5RoCMtpNMScqCF5k4ecxJV0Zx2dfHLJRZfMbtVDOWaOELcq8XAJUU.d.6
f+VNikt28PrpK4Xo9m5NhquLfN7ct+jkjvK26dmO0g.tOOehOAxsbBbT4aMR
W13Dr9HbFVNAXr++F.nGN.1hGEBJxLGq1j4EYW7HKcxwe+3+AkO.Ch.
-----------end_max5_patcher-----------

So, I’ve been shamelessly pulling apart the Max for Live Poli synthesizer to learn how to construct a robust Max for Live device.

Version zero of my synth created “8-bit” wavetables naïvely and attempted to eliminate most of the aliasing by filtering, so naturally I turned to Poli’s oscillator code to see how they do it. Peter McCulloch’s code uses the Polynomial Transition Region method to produce anti-aliased saw and pulse waves. I did find a paper that proposes an algorithm along these lines for the variable-slope triangle wave, but since they only compute the first-order case, I ended up having to rederive the algorithm.

Aurally, I think it sounds quite good, especially with “pulse width” near the middle. Testing it out in Max, there is still some aliasing, particularly as you sweep the frequency, but A/B comparison with the Poli square wave leads me to think we’re in “good enough” territory. I suspect that part of the missing link here is that Poli never lets you completely bypass the filter.

For my future purposes, the above patch contains both Peter’s code for the pulse wave and mine for the triangle wave but outputs only one of the two according to an input. I’ve also included options for two-operator linear FM. I took a nod here from @Galapagoose’s design for FM pairs on Just Friends, but arbitrary FM should be possible.


Questions I’m still working on that I would welcome input on

  • FM works very well, but PM creates bizarre clipping and volume-shift problems. Why is this? (In fact, since shifting the fundamental frequency involves forcing a recalculation of all the polynomial coefficients and so forth, it seems like PM should be easier, since we’re just changing the input of the polynomials.)

  • I tried to use clip and clamp to constrain the pulse width, thus avoiding having to divide by zero at the extremes. However, not only did this not seem to have any effect on the output, division by zero seems to be “fine” in the sense that the code does not crash.

  • I’m not sure of the best way to implement hard sync; currently it seems like I would have to introduce some more state to keep track of whether we’re jumping to zero phase and how far our output is jumping.

  • I have very little idea of whether what I am doing could be improved performance-wise. E.g., does the selector tell Gen~ to turn on and off the parts of the DSP automatically, or is the idiom different.

24 Likes

so excited to try this ! ! ! I’ve just being thinking about ways to computer something like my crap Yamaha keyboard and this just might be it (or something better)

1 Like

Hard sync seems to “just work”? At least in my initial testing it doesn’t seem to sound obviously wrong. Mixing hard sync and FM also quickly makes some wild tones, as expected

Thanks to a suggestion from @zebra, a gen~ implementation of Andrew Simper’s State Variable Filter. It is (gasp), a linear model, but it sounds just fine for my purposes, and it even pings nicely.

inlet 1 is the signal input, inlet 2 expects a frequency value in Hz (shallow FM should work fine, I think it might dislike negative values though), and inlet 3 expects a resonance value between zero and one. Self oscillation at 1, pings work very close to 1, but resonance values above 1 result in loud noises.

copy me into a Gen patcher!

----------begin_max5_patcher----------
1736.3ocybk0haiDD9YO+JTzSS7b39TGABrKKDHvBKjMjW1DFjkksUv5.I4j
IDx+8sUWRdrczH05ZjgfraYMp5utt9ppaxOuZl9xnG8R00di1+oMa1OuZ1L4
sxuwrhwyzCbdzcmSp7wzciBB7ByzuE9sLuGyj2+ia8S0D+6O2sIJwOaafFVa
cRTf11rr3z2rXg6OxhB7cuW7BVr1emW5hUowK92us9u8C8bR9XhS7+Dm4G3m
5shbe7p0khXm32ci1GJkCq3lwNYta8C27PhmaFL+wz6Q2pwQ14ef4n7OLDW0
9RweS39.+vcdYRffKto+J4zOZ4Wuyfo+zSFsOq7QQ427WWcU9ka64xjeX1CD
s2pQluK56Z2oIG+4P8mGVTlDWLF9HbQYsAWzgCWgdeW7J+MXcWc..A.fZl+A
w9dt3J44l+jJl+jpm+kPEtU1Oh8.Apqq8kgCZy0HJ.N3CBbkzFkC9EAb0YOh
k1iKcBWUXPhqAvXSPYROxZjzJuLzjZMxYGquZuwH29x0XDvF0zpi1hbqI0Vb
q+lsBSQ+PgY30DwEx7O75R6RQ3xZCSRH1GG92xn0gI4litg475brrPGq85fk
owTZYVmOG1PhFlIpqPiOkP6ll0ZcO4FmMkPS710pKZOgSyQD0Fbm3sOjx3y8
HGC0EUDSrNxwpKXfL5X3FEzAFFc0DCOoorpCZBLQxwFpytOnK0HCkpMJtiPi
Yeop1n43P.MbW0ZLyIknQdoWuU68xRwtQa472KIAeil67O8.sdVF1PRZvWDa
wZMKClw3Wjojxza0bN.rky+TcAHQRK0BNwcgSOiO9fZi.QezI75X+4u6A2Eu
6gzjWWGnrk9cV7NiI53iIGAlvKtNWEsY90atQqj06mCWJ9oMyc9bnq7KKqra
HzR7m3kJd+NY9QgObxSfQ03FaK06DDXLy.lLlsYMhL9qQeR5pJVWj9q0nwgF
jPvPA.XLfs1fF7jlmujmBqqILnrK0DFkPCw6JznWrPyfduIuWXibwV2Fn1Jb
o5BzlTlma8SyhR9QdKs71TaEBTqbngsjzZfJ5ZUEBTzECNqu2cLIcahIqi.k
LobR8C0vZ+QQhBQ9f38YMa8hgthgapeXnJPq0EKCbJQ5LZ00bEDyKVnA62Ag
00NzRLl5NpPatWXbDpicifvlz1TRgxZw7tlSffuTqXmZ.QJncMSNdRiWbSsc
xqTuYAtUFs1tCOoQLd0BUPWQoLj16Ug4Wv6wnHcs.bVlcbacvSZcAu5NU.Wd
8ZcztjNwTRnOQI4CMCTCnIKHdqYifIWrorghss5bbyIMkvZmzrLmvlwGF1aQ
V6itLorli8qsglxReLfCWhcqMJmzLdKZtG0Fckfr4DGUg7TTk+ZeVz50JzOd
SnPGqVqEmTFyoNAw67Rbx7ZtfG3LdYzdD1uz6RwHa85YmUN4LM+9mB6zn8It
kupxivj1Sy1UdoY9gx10dzCk2Egidn.+UwQhZ5SKCBIWAX1fVlAmCmpFQO+6
Xyhmx5vnmQApJdNapVMdjfF+b3oX1PvvVKCwVOcDkK8dYEGliSFwnfMOgdXz
AIEkrxK44qIRYkFVUP12kStRR5zGZf.YdGcZTz4G5oZzjVPKPIfImYAMfiFP
sk5SnsSxA+NLH8aETEXbFVOCFFvtfATPYv0iGP.mrCnk0e2n7CbkZK981gko
hjrp1BqmpFtJpF1..RhRKm75LBvhjn7S1Y2yFP.FE1m+clArSilOMZncUUJJ
Q94I5HW0gRGZnpn6qNLubtlkjQcwinVmzGGnSaxQ+9xB9EvqxXLhayYpZqie
IRNwoUCxWh0WxXH57inRyhFWqcHUV1HsnUolzCi5oFgYq5bq2InTgmGyd.rx
HJEgAU65MG1TDCKXuQfyVlb3PacnFqoZ4yWxifhfYsfozggCcvBpxpQTiz0g
MMDZikbzfu1phmmz8ru1bTrpRpI9BDtY4g8qbzfSTWExMT7XDKTEWSygH2uJ
UIiGBVFTkpGmMD1XTUkD5kHBpb5nXDzRW9pCfRFeWO5nPpPoX2TR0zn6mnwb
UE8y2FnhHuT3XtIHUHSxQsGA1sHUi3L30anjsLwrZQ2u3r1pJ4JVuoiu0AwX
x.swXzc.rJY6KWY5mMkJgbvCRdLURUSvCP1EaUEzfGbPEIisFiJeUpCARQO3
tIXUpGsb90OIohw5PzXPthb65qkpRt4CgKgkpBBUwV73DG+MujzhmVJC8.mu
FIscrtUNzODFJ2kI8Duu4W97xsQT2Iwcqelma19D47R+wh+iEPOHRXEFt2uv
6SfNgHk6OVnSfWZrC.D8Uow2uwKT+pec0+aYMqlO
-----------end_max5_patcher-----------
4 Likes

agh - beautiful. so glad you implemented this as I am still not prepared to take in long signal processing papers. will be stealing soon 4 my own explorations ! hopefully this will also help me understand what the heck a filter is.

1 Like

Is a gen patcher different from a max patcher?

Can you share this as an .amxd for those of us who are further back on the learning curve?

:slight_smile:

2 Likes

not an answer to your questions, but by looking at your FM implementation I would suggest of taking the Mod.Width by multiplying the FM Index with the Mod.Frequency (in1 * in5 * in6 instead of in1*in6).
The equation is like this:

[Modulator Width = Modulator Frequency * Index]
or
[Modulator Width = Base Frequency * Harmonicity Ratio * Index]

FM

1 Like

oh, good catch! I’m so used to implementing PM rather than FM that I didn’t realize that I needed to account for the ratio that way!

When the whole polysynth is ready I will definitely share it as a Max for Live device!

A Gen patcher is made by making a gen~ object and opening it like you would a patcher object. The inside of Gen patches operate on the level of samples, rather than at Max control message or MSP signal vector speeds, so objects are a little different and so forth. Sadly playing around with the internals of my patch might not be super enlightening, but hopefully the resulting gen~ object gives an interesting alternative to the builtin filters Max comes with.

2 Likes

20 chars of thanks !

would also love to get a M4L version of this… gen objects require a full max license:confused:

1 Like

I just got Max 8 yesterday after I gave up on trying to learn 7 about two years ago. I do love node-based programming environments, but MAX is very confusing to me in so many regards.

I try to open new from clipboard with the link in the top post to no avail. Then I started a new patcher, created a gen~ object, and right clicked to try and open a Gen patcher and the open options I have are help and reference.

To open a gen~ object, do one of two things: you can lock the patch and then double-click on it, or you can double click while holding the Apple command key.

it worked!

I still can’t figure out how to use it, but I got the patch to open :laughing:

Regarding phase modulation, I have two approaches in Audulus (the node-based program I am most familiar with) and wondering how they relate to your polynomial coefficients.

One is just to use sin expression, since any value put into it will be between -1 and 1. It would be something like sin( phasor + phase ) I suppose you would have to scale the inputs by 2π if you want to get technical, but this works very well for DX-7 style sounds.

The other is using the fractional expression, which just takes any number as returns the non-integer portion, to prevent any glitches in reading a waveform mapped to a 0-1 floating point value. If there is no fract(x) expression, perhaps there is a floor(x) function for rounding down, in which case you could use floor(x)-x to create an equivalent function.

Thanks, I’m slowly getting all that. It’s just that my brain is so heavily acclimated to Audulus, I’m just not sure how to even hood up a midi controller to the patch and connect it to the output. Looks like I’ll be watching Max tutorials this weekend :nerd_face:

Post any you recommend :slight_smile:

1 Like

I appreciate the offer and may message you. I don’t want to clutter up this thread too much unless it pertains directly to phase modulated poly synths.