Superfm

Oh boy, this is a lot saner! The syntax looks more straightforward this way -- using the envelope numbers as parameters instead of part of the function name seems to make much more sense. Thank you so much <3

1 Like

This is beaufitul, thank you so much :sparkles:

When I try to evaluate the functions it gives me the following error:

Warning: GHCi | mytidalfuncs.tidal:76:1: error:
Warning: GHCi |     • No instance for (Show (Int -> Pattern Double -> ControlPattern))
Warning: GHCi |         arising from a use of ‘print’
Warning: GHCi |         (maybe you haven't applied a function to enough arguments?)
Warning: GHCi |     • In a stmt of an interactive GHCi command: print it

Any idea why?

Could you post the code that is going wrong?

I can only guess at what you are doing otherwise, but one way to get that error message is to execute one of the functions on its own with no parameters.

e.g.


fmamp

will produce:

Warning: GHCi | <interactive>:247:1: error:
Warning: GHCi |     • No instance for (Show (Int -> Pattern Double -> ControlPattern))
Warning: GHCi |         arising from a use of ‘print’
Warning: GHCi |         (maybe you haven't applied a function to enough arguments?)
Warning: GHCi |     • In a stmt of an interactive GHCi command: print it

That is just Haskell telling you it doesn't know how to print a value of that type (in GHCi 'it' is the result of the last expression executed, and 'print it' is trying to print the result).

I just pasted your code in a new file called mytidalfuncs.tidal and then evaluated it from another file with:

:script mytidalfuncs.tidal

You need to make the loaded script into a multiline command by wrapping it in :{ :} like this:

:{
    -- sets the amount of operator 'op' in the superfm output mix
    -- (1 <= op <= 6)
    fmamp :: Int -> Pattern Double -> ControlPattern
    fmamp op = pF ("amp" ++ show op)

    -- sets the ratio for operator 'op'.
    -- the frequency is note * ratio + detune Hz
    -- (1 <= op <= 6)
    fmratio :: Int -> Pattern Double -> ControlPattern
    fmratio op = pF ("ratio" ++ show op)

    -- set the detune for operator 'op'
    fmdetune :: Int -> Pattern Double -> ControlPattern
    fmdetune op = pF ("detune" ++ show op)

    -- set the modulation of oerator opa by operator opb
    -- if opa == opb, then the modulation amount is multiplied by the
    -- 'feedback' parameter
    fmmod :: Int -> Int -> Pattern Double -> ControlPattern
    fmmod opa opb = pF ("mod" ++ show opa ++ show opb)

    -- feedback
    fmfeedback :: Pattern Double -> ControlPattern
    fmfeedback = pF "feedback"

    -- Envelope definition: each operator has an envelop with 4 steps
    fmeglevel :: Int -> Int -> Pattern Double -> ControlPattern
    fmeglevel op step = pF ("eglevel" ++ show op ++ show step)

    -- Envelope definition: sets the rate at which the envelope moves
    -- between steps.  Low numbers are slow, high numbers are fast.
    fmegrate :: Int -> Int -> Pattern Double -> ControlPattern
    fmegrate op step = pF ("egrate" ++ show op ++ show step)
:}

If I save that as fm.tidal and load it using :script fm.tidal it seems to work (no errors at least). I can't check if it actually works because my SuperCollider install is currently broken due to updating my mac to Big Sur beta....

1 Like

That was definitely the problem, thank you :slightly_smiling_face:

Hi all,

I'm the developer of the Superfm synth for SuperDirt. I am very happy and touched to see other people using it. This is very exciting.

Sorry for the late reply, I've been more into Supercollider than Tidal lately. I wasn't aware of this thread. A friend pointed it to me just now.

I see there's quite a bit of confusion on this synth, so I'm planning on setting up an online workshop, or tutorial, or stream, or whatever, to give some guidelines on how this synth works. I have a busy month ahead, but I'll try to do it sometime during October. I don't think it's possible to do it before. Would you be up for it? Maybe a YouTube stream? What do you people think?

I'll try to give some answers to the different questions.

That would be absolutely awsome! This is the full list:

-- modulators
mod11 = pF "mod11"
mod12 = pF "mod12"
mod13 = pF "mod13"
mod14 = pF "mod14"
mod15 = pF "mod15"
mod16 = pF "mod16"
mod21 = pF "mod21"
mod22 = pF "mod22"
mod23 = pF "mod23"
mod24 = pF "mod24"
mod25 = pF "mod25"
mod26 = pF "mod26"
mod31 = pF "mod31"
mod32 = pF "mod32"
mod33 = pF "mod33"
mod34 = pF "mod34"
mod35 = pF "mod35"
mod36 = pF "mod36"
mod41 = pF "mod41"
mod42 = pF "mod42"
mod43 = pF "mod43"
mod44 = pF "mod44"
mod45 = pF "mod45"
mod46 = pF "mod46"
mod51 = pF "mod51"
mod52 = pF "mod52"
mod53 = pF "mod53"
mod54 = pF "mod54"
mod55 = pF "mod55"
mod56 = pF "mod56"
mod61 = pF "mod61"
mod62 = pF "mod62"
mod63 = pF "mod63"
mod64 = pF "mod64"
mod65 = pF "mod65"
mod66 = pF "mod66"
-- operator envelope generator levels
eglevel11 = pF "eglevel11"
eglevel12 = pF "eglevel12"
eglevel13 = pF "eglevel13"
eglevel14 = pF "eglevel14"
eglevel21 = pF "eglevel21"
eglevel22 = pF "eglevel22"
eglevel23 = pF "eglevel23"
eglevel24 = pF "eglevel24"
eglevel31 = pF "eglevel31"
eglevel32 = pF "eglevel32"
eglevel33 = pF "eglevel33"
eglevel34 = pF "eglevel34"
eglevel41 = pF "eglevel41"
eglevel42 = pF "eglevel42"
eglevel43 = pF "eglevel43"
eglevel44 = pF "eglevel44"
eglevel51 = pF "eglevel51"
eglevel52 = pF "eglevel52"
eglevel53 = pF "eglevel53"
eglevel54 = pF "eglevel54"
eglevel61 = pF "eglevel61"
eglevel62 = pF "eglevel62"
eglevel63 = pF "eglevel63"
eglevel64 = pF "eglevel64" 
-- operator envelope generator rates
egrate11 = pF "egrate11"
egrate12 = pF "egrate12"
egrate13 = pF "egrate13"
egrate14 = pF "egrate14"
egrate21 = pF "egrate21"
egrate22 = pF "egrate22"
egrate23 = pF "egrate23"
egrate24 = pF "egrate24"
egrate31 = pF "egrate31"
egrate32 = pF "egrate32"
egrate33 = pF "egrate33"
egrate34 = pF "egrate34"
egrate41 = pF "egrate41"
egrate42 = pF "egrate42"
egrate43 = pF "egrate43"
egrate44 = pF "egrate44"
egrate51 = pF "egrate51"
egrate52 = pF "egrate52"
egrate53 = pF "egrate53"
egrate54 = pF "egrate54"
egrate61 = pF "egrate61"
egrate62 = pF "egrate62"
egrate63 = pF "egrate63"
egrate64 = pF "egrate64"
-- operator output levels
amp1 = pF "amp1"
amp2 = pF "amp2"
amp3 = pF "amp3"
amp4 = pF "amp4"
amp5 = pF "amp5"
amp6 = pF "amp6"
-- operator frequency ratios
ratio1 = pF "ratio1"
ratio2 = pF "ratio2"
ratio3 = pF "ratio3"
ratio4 = pF "ratio4"
ratio5 = pF "ratio5"
ratio6 = pF "ratio6"
-- operator frequency detuners
detune1 = pF "detune1"
detune2 = pF "detune2"
detune3 = pF "detune3"
detune4 = pF "detune4"
detune5 = pF "detune5"
detune6 = pF "detune6"
-- other
feedback = pF "feedback"
lfofreq = pF "lfofreq"
lfodepth = pF "lfodepth"

Yes, it'd be great! There are 6 presets already in the # voice parameter, but since I didn't really know what to put in them at first I let them be random for the time being. 0 is the default customizable one; [1-5] are random presets that are consistent for one boot - they are generated every time you boot SuperDirt and cleared when quitting the server, so you never get the same. Anyhow, by playing with it I came across a couple of nice presets that I will store in the # voice at some point, but will share here for now.

Double saw:

d1 $ s "superfm"
# n 0
# amp1 1
# amp2 0
# amp3 1
# amp4 0
# amp5 0
# amp6 0
# ratio2 1.01
# mod12 0.75
# mod34 0.7
# mod45 0.6

Double pulse:

d1 $ s "superfm"
# n 0
# amp1 1
# amp2 0
# amp3 1
# amp4 0
# amp5 0
# amp6 0
# ratio2 2.01
# ratio4 2
# ratio5 2
# mod12 0.75
# mod34 0.7
# mod45 0.6

FM bass:


d1 $ s "superfm"
# octave 4
# n 0
# amp1 1
# amp2 1
# amp3 0
# amp4 0
# amp5 0
# amp6 1
# ratio2 2
# ratio3 3
# ratio4 4
# ratio5 0.5
# ratio6 0.25
# feedback 1
# mod11 1
# mod16 1
# mod23 1
# mod34 1
# mod45 1
# mod66 1
# egrate61 1
# egrate62 10
# egrate63 1
# eglevel62 0.13
# eglevel63 1.5
# room 0.5

Wow! That's very very interesting. I love it. It would be so cool to have these.

This is absolutely brilliant. Thank you so much for posting it. I like it much better than the original function syntax, too. I'm not a Haskell person, so I very much appreciate this.

I think that's it for now. I'm open to further suggestions and improvements. I'll do my best on keeping track of any input. And would definitely love to see what interesting presets come out and may add them as voices, too. I myself discovered FM not long ago and am still in dipers, so any help will be very much appreciated.

Cheers!

10 Likes

This is fantastic, thank you for posting this!!

I've been in love with superfm even though I have no idea what I'm doing by fiddling with the parameters. I'd love to get a better notion of what's going on, and finally try to get FM theory by having a practical application.

+1 for a Youtube workshop, I'll definitely be there if it happens.

(as an aside, I found the random preset feature really interesting, in that you cannot keep the same sound over work sessions. There were a couple of times that I stumbled into some really interesting presets, but since I don't know how to analyse their parameters, they were bound to be lost on reboot. I'm sure there's a life analogy in this.)

1 Like

haha yes, probably. When I add the other presets I will keep at least one random. I like the suprises it give, as well as the unpredictability of it.

Great! There are some people interested in it in the community here in Barcelona, so I'll start working on it. Will let you know when it happens.

This is wonderful! I actually built the synth to be able to learn and practice FM myself in a fast and fun way.

3 Likes

Hey all! I was trying another approach, based on @paul1's code. Since everything uses numerical indices, the parameters work nicely with arrays.

These are the necessary definitions

-- Parameters
let fmamp op = pF ("amp" ++ show op)
    fmratio op = pF ("ratio" ++ show op)
    fmdetune op = pF ("detune" ++ show op)
    fmmod opa opb = pF ("mod" ++ show opa ++ show opb)
    fmegrate op step = pF ("egrate" ++ show op ++ show step)
    fmeglevel op step = pF ("eglevel" ++ show op ++ show step)
    fmfeedback = pF "feedback"

-- Array functions
let fmparam function = foldr (#) (gain 1) . zipWith function [1..]
    lfma = fmparam fmamp
    lfmr = fmparam fmratio
    lfmd = fmparam fmdetune
    lfmer op = fmparam (fmegrate op)
    lfmel op = fmparam (fmeglevel op)
    lfmm opa = fmparam (fmmod opa) -- didn't test, should work

And this is how you use it:

d1 $ stut 2 0.7 0.125 $ s "superfm"
  |+| note (arp "pinkyup" "[0,4,8,12] [0,3,7,11]" )
  # fmfeedback 1
  # fmdetune 2 1
  # lfmel 1 [1, 0.5, 0, 0]     -- EG Level (Operator 1)
  # lfmer 1 [10, 0.1, 0.1, 1]  -- EG Rate (Operator 1)
  # lfmel 2 [1, 0, 0, 0]       -- EG Level (Operator 2)
  # lfmer 2 [1, 0.3, 0.7, 1]   -- EG Rate (Operator 2)
  # lfmel 3 [1,0.2,0,1]        -- EG Level (Operator 3)
  # lfmer 3 [10,0.5,0.4,1]     -- EG Rate (Operator 3)
  # lfma [1, 1, 0, 0, 0, 1]    -- Amps (Operators 1..6)
  # lfmr [1, 0.26, 0.5]        -- Ratios (Operators 1..3)

Sorry about the names, they're very confusing right now. Also, big shoutout @loopier , great job with this SynthDef :slight_smile:

edit: the (gain 1) on fmparam is a placeholder - I couldn't find a neutral ControlMap function (like id)
edit2: adding lfmm for the fmmod function

7 Likes

This is superb. That was my initial thought but didn't find a way to implement it. Looks beautiful. Thanks!

1 Like

The array functions are much nicer than setting everything individually.

Does this work?

-- Array functions
let fmparam function (x:xs) = foldr (#) (function 1 x) (zipWith function [2..] xs)
    lfma = fmparam fmamp
    lfmr = fmparam fmratio
    lfmd = fmparam fmdetune
    lfmer op = fmparam (fmegrate op)
    lfmel op = fmparam (fmeglevel op)

My haskell is rusty and was never very good to start with, so there's probably a more elegant way to do it.

3 Likes

It works very well and it's a lot more compact, thank you @paul1 !

d1
    $ stut 2 0.7 0.125
    $ slow 3 $ s "superfm" 
    |+| note (arp "pinkyup" "[0,4,7,12] [0,5,7,9]" )
    # fmfeedback 1
    # lfma [1, 1, 1, 0, 0, 0]
    # lfmr [1, (range 0 4 (slow 4 sine)), 0.26, 0.5, 0.5, 0.5]
    # lfmd [0, 1, 0, 0, 0, 0]
    # fmmod 1 1 "<0 1.25>"
    # fmmod 1 2 (range 0 4 (slow 4 sine))
    # fmmod 1 3 (range 0 4 (slow 3 saw))
    # fmmod 3 2 (range 0 3 (slow 2 sine))
    # lfmel 1 [1, 0.5, 0, 0, 0, 0]
    # lfmer 1 [10, 0.1, 0.1, 1, 0, 0]
    # lfmel 2 [1, 0, 0, 0, 0, 0]
    # lfmer 2 [1, 0.3, 0.7, 1, 0, 0]
    # lfmel 3 [1, 0.2, 0, 1, 0, 0]
    # lfmer 3 [10, 0.5, 0.4, 1, 0, 0]
    # lpf 1000
    # room 0.3
3 Likes

I just posted an anouncement for a streaming I'll be broadcasting next Thrusday to give an overview of the synth. Hoping to see you there.

3 Likes

Interesting syntax.

What does the l mean in lfma?

Isn't env more clear then eg? In SuperCollider I don't think I ever saw 'eg', but 'env' seems to be common to use in relation with envelopes. For now this seems more clear to me as a 'fm-newbie':

fmamp
fmratio
fmmod
fmenvlevel
fmenvrate

Shorter is not always better :wink:

Maybe even:

fmamps
fmratios
fmdetunes
fmmod
envlevel
envrate

In SuperCollider you would write a envelope like

([0, 1, 1, 0], [2, 1, 3])

Where the first numbers are the levels and the last two numbers are the time or rate (att, sus, rel) in this case. A syntax like that isn't possible in Tidal/Haskell?

Although this syntax is shorter, I find myself having to think what each parameter is every time I have to change something. So, for me, is more convenient to have them separated at least between levels and rates. Just a matter of taste, I guess.

I like this one. Maybe it should be envlevels, envrates, fmmods and fmratios to denote the array, shouldn't it?
And how about op instead of fm as prefix?
As for the envelopes, envr and envl might be a nice short version.

Thinking about the single-value syntax I figured a more descriptive alternative would be to make a mix of ADSR and 4-stage-envelope naming. Maybe something like:

atkl1 -- attack level for operator 1, instead of eglelvel11
atkr1 -- attack rate for operator 1, instead of egrate11
decl1 -- decay level for operator 1, instead of eglelvel12
decr1 -- decay rate for operator 1, instead of egrate12
susl1 -- sustain level for operator 1, instead of eglelvel13
susr1 -- sustain rate for operator 1, instead of egrate13
rell1 -- release level for operator 1, instead of eglelvel14
relr1 -- release rate for operator 1, instead of egrate14

or maybe

latk1
ratk1
ldec1
rdec1
...
-- or
ar1
al1
dl1
dr1
sl1
sr1
rl1
rr1
1 Like

Good thoughts.

You have x amount of levels for the envelope, including the initial and end level. Then there are the timevalues between them, which is always levels -1 (so 4 -1 = 3 in this case).

For me it feels a bit strange to tear the envelope apart in individual arguments, which you can sort the way you want

atkl1
decl3
rell1
relr6

for instance. In the Supercollider syntax, it's relation and position in the enveloppe is more clear to me at least.

Also using larger numbers for faster rates, feels a bit contractional to me. In most languages/software a smaller number would give you a faster attack. You told that you copied it from the original DX7, but I wonder if converting it to software should stick with that. That's just what my initial thoughts are, I'm not really qualified to judge this properly, just some thoughts/feedback.

Is there any relation of superfm with this DX7 implementation I found for supercollider?

I guess it's now time to start experimenting with the superfm synth. Thanks for the synth implementation and the livestream. I'm having quite some fun with it already. :sunglasses:

1 Like

I understand what you mean. For me, when live coding, I usually just tweak one or two envelope parameters, not the hole envelope. If I just want to change the decay time, for example, it makes more sense to me to just write the value for that parameter than the whole envelope thing because I have to consider the other parameters too, and remember their values.

I must confess that is still confusing for me too, because I'm used to Supercolllider's syntax with times instead of rates. The advantage of keeping the DX7 scheme is that is easier to port the loads of documented patches out there, which are all set in rates.

No, there isn't. That implementation is a serious one, really cool and sounds awesome, but it's quite complex. Superfm is just using Supercollider's FM7 Ugen, with added envelopes and mappings to the control and mod matrices. I haven't implemented fixed operator frequencies, mod sensitivity or keyboard breakpoints, band depths and curves (yet ; P).

But having you all enjoying it as you do is motivating to do it!

3 Likes

Personally I'm more fan of a bit longer but clearer names likes these, compared to the very short ones (ar1 etc.)

But it the end it's your synth, you decide :wink: @loopier

1 Like