Alga: interpolating live coding environment

Alga: interpolating live coding environment

Hello everyone!

I have just decided to finally publicly share a project that I have been working on for a while, and I thought this would be a good place to share it.

Alga is a new language for live coding that focuses on the creation and connection of sonic modules. Unlike other audio software environments, the act of connecting Alga modules together is viewed as an essential component of music composing and improvising, and not just as a mean towards static audio patches. In Alga , the definition of a new connection between the output of a module and the input of another does not happen instantaneously, but it triggers a process of parameter interpolation over a specified window of time.

As of now, Alga only exists in the form of AlgaLib, an extension for the SuperCollider language. While AlgaLib contains all the core features of Alga, I also plan to build a custom syntax on top of the audio implementation (similarly to what TidalCycles does with SuperDirt).

To install AlgaLib, you can either:

  1. Use the Quarks system: Quarks.install("https://github.com/vitreo12/AlgaLib")
  2. git clone this repository to your Platform.userExtensionDir.

These are a couple of simple examples that show some of the very basic features of Alga:

Check the Help files and the Examples folder for usage and examples!

Let me know what you think and if you have any questions! :slight_smile:

41 Likes

This looks amazing, congratulations! Having Pdefs or Ndefs that interpolate between values is something I’ve been wanting for a long time and this seems to achieve that. I look forward to checking this out!

2 Likes

Forgive if this is a newbie assumption, but I thought that xset did interpolate inputs to a ndef. Or do you mean something different by that?

Edit to OP: is alga right now a more controllable way to do jitlib things? I’ve certainly run into some limitations in jitlib, and I’m interested in where Alga improves on that model.

Hello there!

In JITLib, the xset method creates a new Synth, fading its volume in and fading the old one’s out. On the other hand, Alga keeps the Synth running, but interpolates the values of the connections themselves, resulting in a smooth transition (however long).

Together with this interpolation feature, other differences with JITLib would be:

  1. Alga takes care of converting the rate of any parameter accordingly, giving the option of “patching anything to anything” that modular synthesizers have.

  2. Alga takes care of not only mapping inputs of a module (via the NamedControl syntax), but also its outputs (see AlgaNode's and AlgaSynthDef's help files to see what I mean here, together with the Examples folder).

  3. Alga takes care of ordering all the nodes in the server so that no audio delay is introduced when patching modules together (unless feedback connections are introduced, as shown in the Examples folder).

5 Likes

Ah that’s lovely. I wonder if I should port PHONOTYPE to this; the “the synth restarts when it’s rearranged” thing is one of those limitations of jitlib I found annoying!

2 Likes

This is exactly the direction innovation in modular electronic music must go I believe. Away from static states towards continuously patchable processes – expressive patching.

Away from the prison of generative sweet spots, which resist directed musical development entirely, towards methods that allow composition of goal-oriented music, if not encourage it.

Thank you for sharing this. I would love to try it out if I ever manage to switch from Max to SuperCollider.

6 Likes

Thank you for your interest!

This is exactly why I decided to build Alga in the first place.

I believe that in digital software we tend to emulate way too much the paradigms that exist in the hardware world, both good and bad ones: the idea of patching is an amazing way to determine signal flow, but why should we be limited to the same constraints that static hardware patching poses us? Would it not be nice to determine connections on the fly without having to run into the neccessary discrepancy between the state of the system before the patching and one that comes after?

These are some of the concerns that I aim to address with this project, and I am glad that it is something that other people would want to work with. :slight_smile:

7 Likes

This looks & sounds wonderful, thanks so much for sharing! Two questions:

  • When interpolating Patterns, is it possible to have the interpolation being stepped (or should I say quantized) to e.g. a musical scale?
  • Would it be possible to control the interpolation processes’ speed and position manually, ultimately allowing the performer to manually crossfade between the “old” and “new” patch/pattern…or (gasp!) scroll thru the entire path (composition)?
1 Like

Thanks for checking it out :slight_smile:

  1. There actually is an option for stepped interpolation using the sampleAndHold argument to the from function. This, however, does not take into account any musical scale / values. In fact, AlgaPatterns do not support Pbinds\degree, \octave, etc… because I personally found that redundant and confusing, as it could be easily handled from user’s perspective. Perhaps this is my bias towards atonal music speaking… :smiley: I do plan to add that feature in the future, but it would be quite a headache to figure out how to handle the more complex interactions that AlgaPattern support (e.g., receiving the connections from different AlgaNodes).

  2. The speed is already controllable via the interpTime variable and the time argument that all Alga related functions provide. Also note that at any re-trigger of an interpolation process, Alga will detect the change and start the new one from the current point, despite the previous interpolation was not over yet. This can perhaps give a “manual” feel to it. Regarding controlling the entire process manually, it could be feasible to implement it, but I’d struggle to think of a valid interface for it, considering SuperCollider’s textual nature.

3 Likes

Oh I’m loving this concept! These examples are very well scoped and clear as well. Not only can I read what is going on, but I distinctly can hear it too!

I don’t know if this makes sense, but hearing the way these sounds change makes me think about the “material” of the underlying sonic structure. Patches that instantaneously change feel like something very strong with hard edges like steel. But to me, this feels very elastic and jelly like. It almost makes me want to have a text editor with similar behavior, like the text has some kind of highly viscous fluid simulation applied to it and moving the cursor around exerts force on it…

3 Likes

Thank you very much!

I really tried to make the examples as simple as I could, but I still believe there’s room for improvement there. I’d like to cover more complex corner cases and to make a proper JITLib / Alga comparison.

I had the very same feeling the first time I tried this approach, especially when involving feedback (like in the FB examples). I’m glad this idea sparked something for you too! :smiley:

4 Likes

Regarding controlling the entire process manually, it could be feasible to implement it, but I’d struggle to think of a valid interface for it, considering SuperCollider ’s textual nature.

Two points:
(1) interpolation time could also not be linear, but variable curve?

(2) Maybe controllers with relative midi cc could suffice? Making interpolation time one of the methods available, the other being a continuous control of any sort.

1 Like

Those are great points!

  1. I actually am already working on an interface based around Env that would allow the user to specify any kind of interpolation shape. It will be available on the next release :slight_smile:

  2. I’m not sure I understood what you meant, but I want to add something regarding midi cc and manual controls. I guess the misunderstanding is that Alga is a tool that allows to automate the interpolation behavior. Manual handling of interpolation can probably be handled already outside of Alga, and I don’t believe it would make much sense within this environment. For example, how would you mix the continuous controls (manual) with the automated ones that Alga leverages? That would probably require additional interface, adding complexity to what I believe already has a quite simple interface to use (despite the complexity that lies behind the hood). In any case, I’m always open to new ideas and use cases, so let me know if you had any specific situations in mind.

1 Like

What a great idea. Transitions are where the meat is. Imho themes, rhythms, loops, hooks and all the other compositional building blocks are easy to come up with. The in between is the hard part. Look at artists like aphex twin. What would the music be without the crazy transitions. Most stories need a motivation for the hero to be at certain place, transitions glue it all together. Hope to find time playing with this soon. Your videos piqued my interest. Looking forward to what else you come up with.

1 Like

For example, how would you mix the continuous controls (manual) with the automated ones that Alga leverages?

I was just wondering, instead of interpTime, is it not possible have any control source value with range [0; 1] (where 0 is initial state and 1 is destination state)? Whether it’s envelope, kr sine or adjusted midi. But I’m guessing one can work around this (apart from patterns) by time:0 and mul:control_source of modulation source?

Or am I misunderstanding the concept?
I haven’t yet begun with SC properly!

Not really, not in its current stage. I could perhaps look into having a kr method to control that, but I don’t really see how such a feature would integrate with what is already there. Such a thing would mean to lose all the directionality features that Alga has (e.g. going from a before to an after, an LFO, on the other hand, is a cyclic process, with no direction, if that makes sense).

Perhaps, but that’s fine! This is an interesting discussion that perhaps can lead to some additional features in the future! :slight_smile:

1 Like

Ah, I see. I just reckoned, considering it keeps all states in memory for clear(), so maintaining bidirectionality.

Anyways, I can’t wait to try it out soon.

1 Like

This looks very, very interesting. It may be the closest thing I’ve seen so far to the sort of live coding sound design type environment that I’ve been imagining for a while. I can’t wait to have some time and space to try it out!

2 Likes

Quick bump to announce that I just released a new version with quite a number of improvements and new features. Here is the full CHANGELOG:

1.1.0

  • Added the interpShape option. This allows to specify an interpolation shape in the form of an Env. All connection methods have also been updated to receive a shape argument to set the Env for the specific connection. Check the Examples/AlgaNode/10_InterpShape.scd below:

    (
    Alga.boot({
        a = AlgaNode(
            { SinOsc.ar(\freq.kr(440)) },
            interpTime: 3,
            interpShape: Env([0, 1, 0.5, 1], [1, 0.5, 1])
        ).play
    })
    )
    
    //The connection will use the Env declared in interpShape
    a <<.freq 220;
    
    //Temporary Env (standard ramp)
    a.from(880, \freq, shape: Env([0, 1]))
    
    //Using the original interpShape
    a <<.freq 440;
    
  • Added multithreading support. Now, if booting Alga with supernova, it will automatically parallelize the arrangement of the nodes over the CPU cores. Check the Examples/Extras/Multithreading.scd below:

    //The code to test
    (
    c = {
        var reverb;
    
        //Definition of a bank of 50 sines
        AlgaSynthDef(\sines, {
            Mix.ar(Array.fill(50, { SinOsc.ar(Rand(200, 1000)) * 0.002 }))
        }).add;
    
        s.sync;
    
        //A reverb effect
        reverb = AlgaNode({ FreeVerb1.ar(\in.ar) }).play(chans: 2);
    
        //50 separated banks of 50 sine oscillators.
        //Their load will be spread across multiple CPU cores.
        50.do({
            var bank = AlgaNode(\sines);
            reverb <<+ bank;
        });
    
        //Print CPU usage
        fork {
            loop {
                ("CPU: " ++ s.avgCPU.asStringPrec(4) ++ " %").postln;
                1.wait;
            }
        }
    }
    )
    
    //Boot Alga with the supernova server
    //Alga will automatically spread the load across multiple CPU cores.
    Alga.boot(c, algaServerOptions: AlgaServerOptions(supernova: true, latency: 1));
    
    //Boot Alga with the standard scsynth server.
    //Note the higher CPU usage as the load is only on one CPU core.
    Alga.boot(c, algaServerOptions: AlgaServerOptions(latency: 1))
    
  • Added support for using the same AlgaSynthDefs for both AlgaNodes and AlgaPatterns:

    //1
    (
    Alga.boot({
        AlgaSynthDef(\test, {
            SinOsc.ar(\freq.kr(440))
        }, sampleAccurate: true).add;
    
        s.sync;
    
        //a = AN(\test).play;
    
        b = AP((
            def: \test,
            amp: AlgaTemp({
                EnvPerc.ar(\atk.kr(0.01), \rel.kr(0.25), \curve.kr(-2), 0)
            }, sampleAccurate: true),
            dur: 0.5,
        )).play(chans:2)
    });
    )
    
    //2
    (
    Alga.boot({
        y = AP((
            def: { SinOsc.ar(\freq.kr(440)) * \amp.kr(1) },
            amp: AlgaTemp({
                EnvPerc.kr(\atk.kr(0.01), \rel.kr(0.25), \curve.kr(-2), 0)
            }),
            dur: 0.5,
        )).play(chans:2)
    })
    )
    
    //3
    (
    Alga.boot({
        y = AP((
            def: { SinOsc.ar(\freq.kr(440)) },
            amp: Pseq([
                AlgaTemp({
                    EnvPerc.ar(\atk.kr(0.01), \rel.kr(0.25), \curve.kr(-2), 0) * 0.25
                }, sampleAccurate: true),
                AlgaTemp({
                    EnvPerc.ar(\atk.kr(0.001), \rel.kr(0.1), \curve.kr(-2), 0) * 0.25
                }, sampleAccurate: true),
            ], inf),
            freq: Pwhite(440, 880),
            dur: Pwhite(0.01, 0.2),
        )).play(chans:2)
    })
    )
    
  • Added support for sustain / stretch / legato to AlgaPatterns:

    //1
    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar * EnvGen.ar(Env.adsr, \gate.kr) }, //user can use \gate
            sustain: Pseq([1, 2], inf), //reserved keyword
            dur: 3
        )).play
    })
    )
    
    //2
    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar },
            amp: AlgaTemp({ EnvGen.ar(Env.adsr, \gate.kr) }, sampleAccurate: true),
            sustain: Pseq([1, 2], inf),
            dur: 3
        )).play
    })
    )
    
    //3
    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar(\freq.kr(440)) *
                EnvGen.kr(Env.adsr(\atk.kr(0.01), \del.kr(0.3), \sus.kr(0.5), \rel.kr(1.0)), \gate.kr, doneAction: 2) *
                \amp.kr(0.5)
            },
            dur: 4,
            freq: Pseed(1, Pexprand(100.0, 800.0).round(27.3)),
            amp: Pseq([0.3, 0.2], inf),
            //sustain: 4,
            legato: Pseq([1, 0.5], inf),
            rel: 0.1,
            callback: { |ev| ev.postln },
        )).sustainToDur_(true).play
    })
    )
    
    //Same as:
    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar(\freq.kr(440)) *
                EnvGen.kr(Env.adsr(\atk.kr(0.01), \del.kr(0.3), \sus.kr(0.5), \rel.kr(1.0)), \gate.kr, doneAction: 2) *
                \amp.kr(0.5)
            },
            dur: 4,
            freq: Pseed(1, Pexprand(100.0, 800.0).round(27.3)),
            amp: Pseq([0.3, 0.2], inf),
            sustain: 4,
            legato: Pseq([1, 0.5], inf),
            rel: 0.1,
            callback: { |ev| ev.postln },
        )).play
    })
    )
    
  • Added the AlgaStep class. This class allows to schedule actions not on a specific beat, but at a specific “pattern trigger” that will happen in the future:

    (
    Alga.boot({
        a = AlgaPattern((
            def: { SinOsc.ar(\freq.ar(440)) * EnvPerc.ar * 0.5 },
            dur: 1
        )).play
    })
    )
    
    //Schedule 2 triggers from now
    a.from(Pseq([220, 880], inf), \freq, time: 1, sched: AlgaStep(2))
    
    //Schedule at the next trigger
    a.from(Pseq([0.5, 0.25], inf), \dur, sched: AlgaStep(0))
    
  • Added the AlgaProxySpace class. This allows to quickly define AlgaNodes and AlgaPatterns in a fashion that is similar to SC’s ProxySpace. Check the help files and the Examples folder for a deeper look at all of its features.

    p = AlgaProxySpace.boot;
    
    //A simple node
    ~a = { SinOsc.ar(100) };
    
    //Use it as FM input for another node
    ~b.play(chans:2);
    ~b.interpTime = 2;
    ~b.playTime = 0.5;
    ~b = { SinOsc.ar(\freq.ar(~a).range(200, 400)) };
    
    //Replace
    ~a = { SinOsc.ar(440) };
    
    //New connection as usual
    ~b.from({ LFNoise1.ar(100) }, \freq, time:3)
    
  • Introducing AlgaPatternPlayer, a new way to trigger and dispatch AlgaPatterns:

    (
    Alga.boot({
        //Define and start an AlgaPatternPlayer
        ~player = AlgaPatternPlayer((
            dur: Pwhite(0.2, 0.7),
            freq: Pseq([440, 880], inf)
        )).play;
    
        //Use ~player for both indexing values and triggering the pattern
        ~pattern = AP((
            def: { SinOsc.ar(\freq.kr + \freq2.kr) * EnvPerc.ar },
            freq: ~player[\freq],
            freq2: ~player.read({ | freq |
                if(freq == 440, { freq * 2 }, { 0 })
            }),
        ), player: ~player).play;
    })
    )
    
    //Interpolation still works
    ~pattern.from(~player.({ | freq | freq * 0.5 }), \freq, time: 5) //.value == .read
    ~pattern.from(~player.({ | freq | freq * 2 }), \freq2, time: 5)
    
    //Modify dur
    ~player.from(0.5, \dur, sched: AlgaStep(3))
    
    //If modifying player, the interpolation is still triggered on the children
    ~player.from(Pseq([330, 660], inf), \freq, time: 5)
    
    //Removing a player stops the pattern triggering
    ~pattern.removePlayer;
    
2 Likes

New release: version 1.2.

This one comes up with the much anticipated feature of time interpolation! It is now in fact possible to interpolate the dur parameter of an AlgaPattern, or the entire tempo of Alga with the global Alga.interpTempo method.

Also, lots of other features. Here is the full CHANGELOG:

1.2

New features

  • AlgaPattern: it is now possible to interpolate the 'dur' parameter!

    (
    Alga.boot({
        //Pattern to modify
        a = AP((
            def: { SinOsc.ar * EnvPerc.ar(release: 0.1) * 0.5 },
            dur: 0.5
        )).play(chans:2);
    
        //Metronome
        b = AP((
            def: { EnvPerc.ar(release: SampleDur.ir); Impulse.ar(0) },
            dur: 1
        )).play(chans:2);
    })
    )
    
    //Using Env and resync
    a.interpDur(0.1, time: 5, shape: Env([0, 1, 0.5, 1], [2, 3, 4]))
    
    //No resync
    a.interpDur(0.5, time: 3, resync: false)
    
    //With resync + reset
    a.interpDur(Pseq([0.25, 0.25, 0.125], inf), time: 5, reset: true)
    
    //Other time params work too!
    a.interpStretch(0.5, time: 3, shape: Env([0, 1, 0.5, 1], [2, 3, 4]))
    
  • AlgaPattern now supports scalar parameters. This is now the preferred way of interfacing with Buffer parameters (check Examples/AlgaPattern/03_Buffers.scd).
    Also, scalar parameters can be used for optimization reasons for parameters that need to be set only once at the trigger of the Synth instance, without the overhead of the interpolator.

    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar(\f.ir) * EnvPerc.ar },
            f: Pseq([440, 880], inf)
        )).play(chans: 2)
    })
    )
    
    //Change at next step
    a.from(Pseq([330, 660], inf), \f, sched: AlgaStep())
    
    //Change at next beat
    a.from(Pseq([220, 440, 880], inf), \f, sched: 1)
    
  • AlgaPattern and AlgaPatternPlayer now support Pfunc and Pkey.
    All scalar and non-SynthDef parameters are now retrievable from any parameter. These are ordered alphabetically:

    (
    Alga.boot({
        a = AP((
            def: { SinOsc.ar(\f.kr) * EnvPerc.ar },
            _f: 440, 
            f: Pfunc { | e | e[\_f] }
        )).play(chans: 2)
    })
    )
    
    //Interpolation still works
    a.from(Pfunc { | e | e[\_f] * 2 }, \f, time: 3)
    
    //These can be changed anytime
    a.from(880, \_f, sched: AlgaStep())
    
  • AlgaPattern: added the lf annotator for functions in order for them to be declared as LiteralFunctions.
    These are used for keys that would interpret all Functions as UGen Functions, while they however could represent a Pfunc or Pif entry:

    (
    Alga.boot({
        a = AP((
            //.lf is needed or it's interpreted as a UGen func
            def: Pif(Pfunc( { 0.5.coin }.lf ), 
                { SinOsc.ar(\freq.kr(440)) * EnvPerc.ar },
                { Saw.ar(\freq.kr(440)) * EnvPerc.ar * 0.5 }
            ),
    
            //No need to .lf here as 'freq' does not expect UGen functions like 'def' does
            freq: Pif( Pfunc { 0.5.coin },
                AT({ LFNoise0.kr(10) }, scale: [440, 880]),
                Pseq([ AT { DC.kr(220) }, AT { DC.kr(440) }], inf)
            )
        )).play(chans:2)
    })
    )
    
  • AlgaSynthDef: can now correctly store and retrieve AlgaSynthDefs with the write / load /
    store calls. By default, these are saved in the AlgaSynthDefs folder in the AlgaLib folder,
    but it can be changed to wherever. The definitions in AlgaSynthDefs are automatically read at
    the booting of Alga. Other definitions can be read with the Alga.readAll / Alga.readDef
    calls.

Bug fixes

  • Major parser rewrite that leads to more predictable results and a larger amount of patterns supported.
6 Likes