Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASL modulations #468

Open
trentgill opened this issue Oct 12, 2022 · 1 comment
Open

ASL modulations #468

trentgill opened this issue Oct 12, 2022 · 1 comment
Labels
design Issue needs design consideration feature new features yet to be added help wanted Extra attention is needed

Comments

@trentgill
Copy link
Collaborator

trentgill commented Oct 12, 2022

ref to #438

Inspired by github.com/entzmingerc/drumcrow I'd love to see some extensions to ASL that would allow modulations within an ASL shape. Of course the best system in crow for modulations is ASL itself, so this leads to arbitrary nesting of ASLs. Of course we have CPU & memory limitations, but due to the ASL runtime running natively in C it should end up being far more performant than current solutions which run a fast timer & calculate these modulations in the lua script.

Right now ASLs are tied to hardware outputs, and statically allocated at boot time. This would need to be overhauled to allow a pool of ASLs that can nest.

Syntax and organization of ASL descriptions

At present most ASL descriptions are captured inside functions (eg: ar, lfo, pulse) which themselves return ASL descriptions. The benefit here is that the same description can be applied to any number of ASL machines. It would seem beneficial to continue this pattern, even if it leads to some limitations. This would avoid breaking existing code (which at v3+ seems the right thing to do).

This suggests that nested ASLs should be declared inside of other ASL descriptions, being automatically allocated under the hood. The nested ASLs would need to be named such that they can be controlled with directives internal to a given ASL. Something along the lines of:

-- create a unipolar triangle lfo with amplitude set by a nested asl
dlfo = { dynasl{amp = ar()} -- declare a nested asl called 'amp' with the 'ar' helper function
       , loop{ to( dyn('amp'), 1) -- dynasl's are treated as dynamic variables
             , to( 0, 1)}}

-- assign the asl & start it
output[1]( dlfo )

-- trigger the amplitude envelope every 4 clock ticks
clock.run(function()
  while true do
    clock.sync(4)
    output[1].dyn.amp() -- call a dynasl just like calling a regular one
  end
end)

This is, of course, just a start & I haven't thought through all the repercussions of any specific design decisions here. Just a starting point for further exploration.

Dedicated generators

At present ASLs are always deterministic and randomness can only be approximated with algorithms like LCG (see drumcrow). To simplify usage in a more percussive or noisy context, it may be beneficial to use a dedicated noise generator.

I see 2 clear ways this could be implemented:

Noise as an output shaper

Similar to log or exp etc, we could add noise and bnoise (the latter for bipolar output). This has the benefit the of using the known to function, allowing for amplitude (destination voltage) and duration (time parameter). Thus the amplitude could be controlled by a nested asl to implement a enveloped output level. Setting the duration to a very big number would allow essentially infinite noise generation without ASL doing any substantial work in the VM, and instead just in the signal generation code.

Noise as a generator object

This approach looks to add a new function analogous with to, where we can change the parameter list for configuring the generator. We'd still need a duration parameter so it can coexist with to, but this could explicitly support -1 to mean "forever". There would need to be an amplitude control, but this could optionally take a min & max for explicit bipolar dimension.

A third argument could be used as well to set the internal rate of the noise generation (how frequently a new random value is generated) so we could implement other types of noise than white. And a linked parameter would be related to slew times for transitioning between values (or even just a boolean on/off for discrete steps vs continuous slide). The motivation here is that it could have use in a modulation context for drunk-walk type modulations.

An alternative to these drunk-walk type modulations (and for sample-reduced interpolated noise) would be another, separate dyn directive. Where a special dyn("noise") could be enabled, which would deliver a new value every time it was called. Then the sample rate & slew behaviour could be configured in precise detail in a regular ASL description. The noise impulse would default to some pre-defined range (-1,1) or (0,5) etc. That would look something like:

-- create a random-walk modulation, sliding to a random new value over 1 second
-- note the use of dyn modifier "mul" for scaling the noise as desired
loop{ to( dyn("noise"):mul(0.5), 1)}

Other generators

The above could be categorized as:

  1. New output shaper
  2. New generator function
  3. Special dyn generators

Each of these 3 suggests the possibility of additional variations of each. Before making a decision about which path to follow it makes sense to try and enumerate the other ways in which that method could be extended. In general I would prefer the option that is most general purpose (ie allows the most meaningful variations), as ASL is intended to "describe all the possible modulations". If we are extending ASL to explicitly support audio processing, then we should ask what other audio-processing tasks can be captured within the existing syntax with minimal awkwardness.

Everything in ASL so far has been about generation. By introducing nested ASLs we are getting into processing (ie dynamic destination is essentially amplitude modulation). The next natural step is probably filtering, and it gives me pause to think about how that category of elements could be integrated within this little language. Of course we're not trying to make the next supercollider for crow -- the hardware simply doesn't have the processing power to make that worthwhile, let alone the increasingly sharp learning curve. The introduction of dyn was a big change, and very few people have really gone deep with it -- it's an inherently complex addition to the language -- so I'm hesitant to add a great deal of complexity to the system.

Rambling...


That's all for now, but i'm very open to suggestions & have absolutely no timeframe scheduled. This is more of a thought experiment that could potentially become a feature down the road one day.

@trentgill trentgill added help wanted Extra attention is needed feature new features yet to be added design Issue needs design consideration labels Oct 12, 2022
@entzmingerc
Copy link

entzmingerc commented Nov 6, 2022

Heyo! There's a cool video here I wanted to send you! Adding noise to animation.
This demonstrated how I was thinking of how noise could be used in an ASL table.

Updating my comment from a few days ago:

Being able to add noise from 0-100% to the CV shapes would be great, but I don't think it opens things up as much as nested ASL. If we had a 2-stage LFO going from 0 to 5V, some way to add 25% noise (maybe to the CV shape function) as the voltage goes from 0 to 5V would be great. If we had a noise mutation, that might slot in a bit easier for random numbers.

Something like the following might output a random voltage: 5 + random_range(rand, -rand) every 0.01 seconds.

output[1].action = loop{ to( dyn{ x = 1}:rand{1}, 0.01) }

If we try to do something like 0 to 5V with some randomness along the path, the rand{1} is executed once when we want it executing a lot (with nested ASL we could tell it how often).

output[1].action = loop{ 
to( 0, 0),
to( dyn{ x = 5}:rand{1}, 1),
}

If the CV shape function are accessed each sample output step in the C code, could we add a noise function inside them?
This seems like a small corner case when nested ASLs as you described above sounds like the best next step in complexity.

output[1].action = dynasl{rand = loop{ to( dyn{ x = 1}:rand{1}, 0.01) }}, 
loop{ 
to( 0, 0), 
to(5 + dyn("rand"), 1) } }

This would still need some way to call math.random() or something, otherwise we'd just be implementing an LCG as a dynasl and we're sort of where we started.

So maybe

  1. a new rand{} mutation
  2. nested ASLs seem cool :)

Cheers! Just wanted to respond to the post after thinking about it some more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Issue needs design consideration feature new features yet to be added help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants