# Modulator
Check out this video for an introduction to the application and crate!
[CLICK HERE to go to the Modulator Play application repository](https://github.com/apessino/modulator_play)
A trait for abstracted, decoupled modulation sources. This crate includes:
1. The `Modulator` trait definition
2. An environment (host) type for modulators `ModulatorEnv`
3. A number of ready to use types that implement the modulator trait
**Changes in version 0.4.0**
- Revised behavior for `ScalarSpring::undamp` parameter
-- Domain is now between 0.0 (full damping) and 1.0 (all damping removed)
-- Undamped spring simulation is unconditionally stable for any size timestamp
-- When `undamp==1.0` spring can oscillate indefinitely
-- Spring oscillation will lose energy proportional to the timestep duration
**Changes in version 0.3.0**
- Updated to Rust 2021 edition
- Update to latest version of `rand`, propagated API changes
- Replaced the hash used by the modulator environment with `metro` - we don't really care about hash safety for the env, and metro is much faster than the default `std` hasher
- (Repo only) Fixed a bug in `Newtonian` introduced by a merged PR; this change was never published
- (Repo only) Removed dependencies and arena-based env, which were added by a merged PR but had issues; this change was never published
**Introduction**
-----
Modulators are _sources_ of change over time which exist independently of the
parameters they affect, their _destinations_.
The architecture presented here was inspired, in part, by the world of _audio
synthesis_, so let us introduce the main concepts by drawing a parallel to it.
A synthesizer is a musical instrument in which electronic waveform generators
produce a basic sound, which is then filtered, amplified and output.
While the method of waveform generation and the processing applied to it are
key to the sonic result, by themselves they are not sufficient to produce
interesting, lively results.
To increase complexity and depth, _modulations_ can be added to mutate synthesis
parameters over time and produce evolving, organic sounds. A synthesist
can sculpt the output by _connecting_ modulation sources to destinations.
Modulation sources include periodic functions, low frequency oscillators, noise
generators, performance controls, etc.
Destinations are typically parameters that affect the amplitude, frequency, or
harmonic structure of the sound. This results in effects such as tremolo, vibrato,
spectral and timbric variations to waveforms over time.
Modulation sources and destinations should ideally be completely decoupled. A
destination should be able to factor input from a compatible source by establishing
a connection between the two.
This generic approach, which originated with modular synthesizers, adds enormous
breadth to the range of sounds that can be programmed for an instrument.
The same modulation model that makes these electronic sounds rich can be used in other
domains. Modulations can add life and variety to any set of parameters used by a computer
program. Non-interactive visual elements can be animated, user feedback can be augmented,
AI entity behavior can evolve over time, and much more.
**Useful modulators included**
-----
When it comes to animating an attribute, be it visual, auditory or behavioral, it is
often the case that we want the result to be:
* Random, unscripted and without a scripted _feel_
* Controllable, precisely bound
* Dependably smooth, with no singularities
* Physically correct, instinctively pleasing
This crate provides modulators such as `ScalarSpring`, `Newtonian`, `ScalarGoalFollower`
and `ShiftRegister` which, by themselves and in combination, allow the creation
of modulations that have some or all of the properties above.
**How modulators work**
-----
A modulator needs to be able to do at least the following:
* Return its value at the current moment in time
* Evolve its status as a function of advancing time
Let `m` be a value of a type that implements the `Modulator` trait, then:
let value = m.value();
returns the current value of the modulator. To evolve the modulator by `dt`
microseconds use:
m.advance(dt);
In practice, the latter is rarely done directly, as using an _environment_ (a host
for modulators) such as the included `ModulatorEnv` type, is much more convenient.
**Modulator environments**
-----
The `ModulatorEnv` type is an _owning host_ for modulators. Generally, you create
one or more environments in your application, such as:
// Somewhere in a struct...
m1: ModulatorEnv, // hosts modulators that give scalar f32 values
// Somewhere in constructor of that struct...
m1: ModulatorEnv::new(),
The above creates a modulator environment `m1` in a struct, probably a modulation
struct that collects all state/data related to modulation for the app.
Then, somewhere in the application, the environment must be _ticked_ forward by the
elapsed `dt` microseconds of the current frame, like this:
// Here st is the modulation data struct that contains m1, dt is elapsed micros
st.m1.advance(dt);
The environment advances all the enabled modulators it hosts. It is important
to notice two things about `ModulatorEnv`:
1. The environment owns the modulators it hosts
2. The environment is generic in the same value T as its hosted modulators
Point 2 means that, since trait `Modulator` is generic in T, the value type,
then all modulators in an environment must have the same T. All modulator types
provided with this crate are `Modulator`, that is: their value is a scalar
of type `f32`.
Point 1 means that the lifetime of the modulator is managed by the environment,
so you can "create and give" your modulators and let the environment drop them
when it is dropped (`ModulatorEnv` provides methods to manually manage the lifetime
of its modulators, if desired).
Here is an example using the `Wave` modulator. The `Wave` modulator is the simplest
of the included types - it takes a closure/`Fn` to update its value, and it has
amplitude and frequency values. Since it uses a closure it can actually make
any signal: a waveform, a constant, a random number, etc. For example:
// Create a sine wave modulator, initial amplitude of 1 and frequency of 0.5Hz
let wave = Wave::new(1.0, 0.5).wave(Box::new(|w, t| {
(t * w.frequency * f32::consts::PI * 2.0).sin() * w.amplitude
}));
// Give the modulator to the environment
st.m1.take("wave_sin", Box::new(wave));
This creates a wave modulator that produces a sine with amplitude 1 and frequency
of 0.5Hz. The closure receives the modulator `w` and elapsed time (t: `f32`) in
seconds.
Once created, `wave` is _given_ to host `m1` which takes ownership of it and tags
it with key `"wave_sin"`.
Another example:
// Create a wave modulator, amplitude (2.0) here is used to define walk bounds,
// while frequency (0.1) is the random range the value moves each time it advances
let wave = Wave::new(2.0, 0.1).wave(Box::new(|w, _| {
let n = w.value + thread_rng().gen_range(-w.frequency, w.frequency);
f32::min(f32::max(n, -w.amplitude), w.amplitude)
}));
// Now give the modulator to the environment
st.m1.take("wave_rnd", Box::new(wave));
This closure offsets the modulator's current value each `advance(dt)` by a random
offset (set by frequency) and caps it between -/+ amplitude. This creates a
simple random walk.
Once the modulators above have been created and given to the host, their value can be
read anytime as follows:
let v0 = st.m1.value("wave_sin"); // current value of sine modulator
let v1 = st.m1.value("wave_rnd"); // current value of random walk modulator
**Modulator details**
-----
Notice that modulators should cache their `value` when they are advanced, which
means that, even if advancing could be expensive, reading their value must
always be fast. Furthermore, modulators are advanced by the environment all
at once to ensure that reading of interdependent values is always consistent.
It is **important** to notice that modulators are __not__ guaranteed to be
reversible. Most will not be, in fact. They can only evolve _forward_ in time.
The reason for this restriction is that, while modulators are generally expected
to be _frame rate independent_ (they should express their evolution as a function of
time), they are also frequently going to have __discrete state changes__.
For example, the included modulator `ScalarGoalFollower` picks a random value, sets
it as the _goal_ for a contained sub-modulator, then observes it until it determines
that the sub-modulator has arrived to its goal. Once it does, the follower makes
a new goal and repeats the process.
This kind of discrete-state, randomized behavior would be costly to make reversible
and would require caching of the randomly generated values, amongst other problems.
Since being reversible is not critical in the vast majority of applications, the
`Modulator` trait does not make it a part of its contract - versatility is preferred.
Modulators are generally __expected__ to be frame-rate independent, but not required.
All of the ones provided with the crate are, even those that include discrete events
such as the `ScalarGoalFollower` described above, and they evolve consistently even
with varying frame lengths.
The `Wave` modulator is a special case, since it uses a closure to compute its value
it might or might not be time-based depending on the given function.
Recall the "sine wave" closure we gave to `Wave` earlier, its implementation is
obviously a function of time (and, in this simple case, it would be reversible too).
The "random walk" closure, on the other hand, is neither, as the rate at
which the value is updated is a function of the number of times `advance(dt)` is
called, rather than elapsed time. A random walk that changes in frequency depending
on frame rate would be of limited use, and in production code we would implement
a more sophisticated random walk with update rate expressed in changes per second.
**Modulator lifetime and interaction**
-----
A `ModulatorEnv` host only knows two things about the modulators it owns:
1. They implement `Modulator`
2. They have the same `T` (value type)
This means that the only operations the environment can perform on its modulators
are the ones defined by the `Modulator` trait.
While the modulator types provided in `sources.rs` are all designed specifically
for their role as modulators, other types can implement the modulator trait and
acquire modulation capabilities (although in such cases they probably won't be
stored in an _owning_ environment).
It is clear that `ModulatorEnv` contents are heterogeneous - the only thing they
are known to have in common is that they `impl Modulator` for the same `T` as
the environment. This is a proper use case for Rust's **trait objects**, and
in fact that's how `ModulatorEnv` stores the modulators it owns.
Often modulators are created, added to an environment and then factored into
calculations at destination points, addressed by the symbolic name that was
given to the host when added. For example:
// Here we are updating some value by scaling it with a modulator, source
// is the name of the modulator in environment m1
self.height = self.base + self.range * st.m1.value(source);
Still, at times you will want to access a modulator out of an environment and
modify something about it, perhaps to modulate one of its settings
by another modulator.
Since `ModulatorEnv` stores its contents as trait objects, borrowing a modulator
back requires knowing its type and downcasting it. Suppose we want to modulate the
amplitude of our previous `"wave_sin"` modulator by another modulator, in the
same environment, called `"amp_mod"`:
let ampmod = st.m1.value("amp_mod"); // amplitude modulation value
if let Some(sw) = st.m1.get_mut("wave_sin") { // borrow trait object
if let Some(ss) = sw.as_any().downcast_mut::() { // safely cast it
ss.amplitude = 1.0 + ampmod; // modify its amplitude attribute
}
}
Here, we read the current value of `"amp_mod"` then we mutably borrow a reference
to the `"wave_sin"` trait object. The `as_any()` method is part of the `Modulator`
trait, so all modulators must implement this conversion, typically just like this:
fn as_any(&mut self) -> &mut Any {
self
}
Once the trait object has been converted into an `Any` we use the `downcast_mut`
method to safely convert it to its original type, which of course must be known.
In the case above, we downcast to `Wave` and then modulate the amplitude of
`"wave_sin"` by the current value of `"amp_mod"`.
Notice that, while the `ModulatorEnv` type is convenient and useful in a large
number of cases, it is not required. Countless alternative approaches to hosting
modulators are possible, including not having a dedicated host at all. Modulators
only need to be accessible and be advanced appropriately, and `ModulatorEnv` is
just one approach to doing so.
**Other methods of the `Modulator` trait**
-----
Besides `value()`, `advance()` and `as_any()` the `Modulator` crate defines several
other methods. Mostly these are optional and modulators are not required to
implement them in a meaningful manner. See the trait methods for details, and then
the implementation for each of the included modulators.
Finally, notice the modulator enabled status methods:
/// Check if the modulator is disabled
fn enabled(&self) -> bool;
/// Toggle enabling/disabling the modulator
fn set_enabled(&mut self, enabled: bool);
Notice that `ModulatorEnv` checks the enabled status of its modulators and will
__not__ advance them if they are disabled. This allows the pausing/unpausing of
modulators.
**The included modulators**
-----
Several modulators are provided in `sources.rs`. Each is documented locally,
but we will provide a summary here.
1. **`Wave`**
Simple modulator using a value closure/`Fn`, with frequency and amplitude. The
closure receives self, elapsed time (in seconds) and returns a new value.
2. **`ScalarSpring`**
Critically damped spring modulator. Moves towards its set `goal` with `smooth` seconds
of delay, critically damping its arrival so it slows down and stops at the goal without
overshooting or oscillation.
If overshooting is desired, positive values of `undamp` can be set to add artificial
overshoot/oscillations around the goal.
3. **`Newtonian`**
A modulator that uses classical mechanics to move to its `goal` - it guarantees smooth
acceleration, deceleration and speed limiting regardless of settings.
The goal calculation computes an analytical solution to the motion equation. When
a new goal is set, `speed_limit`, `acceleration` and `deceleration` values are
picked from their respective ranges, then movement begins with the value starting
from current value with 0 velocity, accelerating at the selected rate up to the speed
limit, then decelerating at the selected rate of deceleration so that it is _guaranteed_
to come to a stop at the goal.
The analytical solution to the motion equation ensures that, regardless of input, the
value always accelerates and decelerates at the picked rates, and never exceeds the
speed max. If there is not enough time to reach peak speed, the value accelerates as
much as it it can while ensuring that it will decelerate and come to a stop (0 speed)
exactly at `goal`.
4. **`ScalarGoalFollower`**
A programmable goal follower. Picks a `goal` within one of its `regions` for its owned
`follower` modulator, then monitor its progress until the follower gets to `threshold`
distance to the goal and has velocity of `vel_threshold` or less, at which point it
considers it arrived.
Once a goal has been reached, it picks a pause duration microseconds from `pause_range`,
waits for the pause to elapse, then picks a new goal and repeats the process.
This modulator can be given any other modulator type as its owned `follower`, but a
type that is unable to pursue and arrive to its given `goal` is, of course, never going
to satisfy the conditions for arrival.
5. **`ShiftRegister`**
Inspired by classic analog shift registers like those used in Buchla synthesizers, this
modulator has a vector of values `buckets` containing values selected from `value_range`.
A `period` of the register is the length of time, in seconds, that the value takes to
visit all the buckets in the register. Once a period is over, the value moves back to
the first bucket and continues to move.
If `interp` is `ShiftRegisterInterp::None` then the value returned corresponds to the
current bucket being visited. If it is `ShiftRegisterInterp::Linear` then it is the
linear interpolation of the current bucket and the next. If it is
`ShiftRegisterInterp::Quadratic` then the value is the result of polynomial interpolation
of the values of the previous, current and next bucket.
Every time the value leaves a bucket (it is done visiting it for the period) it has
`odds` chances of _replacing_ the value in the bucket it just left, where `odds` ranges
from 0.0 (value never changes) to 1.0 (value always changes).
Parameter `age_range` can be used to specify an age (in periods) over which the odds
of a value changing increase linearly. For example: if `odds` is set to 0.1 (10%) and
`age_range` is set to [200, 1000) then for the first 200 periods a value's odds of
changing are 10%, and between 200 and 1000 periods they increase from 10% to 100%. By
default `age_range` is set to [u32::MAX, u32::MAX] so the odds never change.
The result is that the shift register is periodic and exhibits a pattern (given low
enough odds), but still evolves over time in an organic way.
Copyright© 2018-22 Ready At Dawn Studios