I wanted to document a tutorial on how to create a granular engine from scratch in Kyma. This is to help codify some of my own learnings but also based on requests from the community. Additionally, as I’ve lurked on Lines for years I also wanted to start contributing. I’ve tried to write some of this as general purpose, though there may be more than a few references specifically to how to accomplish specifics in Kyma.
While Kyma has some of the most powerful and creative granular prototypes available, I wanted to explore further customization and apply some core principles, as I see them, in new ways.
Some of the more compelling aspects of granular synthesis rest in the idea of per grain definition of parameters. Generally, the per grain parameters have been confined to global values for amplitude envelope window, window shape, panning, pitch, duration, and sample start position. These are often coupled with a jitter function and allow for each grain to vary within these boundaries.
I suspect many of us have wanted to explore: what happens if we could add additional processing to each grain and see where that takes us? For example, what happens if we add waveshaping or filters at the grain level? What about per grain delay networks? Applying the envelope to parameters other than amplitude seems like another rich opportunity, even if strays afield from the core theory and techniques.
One of the principles that I am going to try and adhere to is the idea that there are global controls for any new processing parameter we add and we use randomization to define the per grain control. This may not be a requirement for you, but it is my personal preference based on a love of exploring how random interference patterns create rhythm and texture.
I also like the idea of a strong central reference point and using randomization to create the cloud of variations. One of the questions that arises from that position is the nature modulation and whether it should be continuous or defined only at the start of the grain cycle which a few notable, such as the amplitude window. I am going to split the difference here and try and stick with defining the per grain parameters at the beginning of the cycle and yet allow for the envelope window to be mapped to other parameters and even the ability to create multiple envelope windows as long as they share the same duration as the primary window.
I am not really one for theory nor dogma so it is very likely we may stray or at times not implement those constraints within the examples explicitly for the sake of clarity and time.
Building the foundation granular engine
There are many different types of granular synthesis approaches. This example is going to use sample playback at the core since that one of the most commonly referenced. However, feel free to replace the simple sample playback parts with a morphing spectral engine, complex oscillators, or modal synthesis.
I apologize if this gets too Kyma specific. If there are any questions about how to generalize the approach or other potential solutions, please ask.
I made one upfront design decision which is that the timing and trigger logic is done at control rate (in Kyma that once per millisecond). If you are using grains for high-resolution tasks like time-stretching or pitch-shifting, I recommend replacing those sections with audio rate equivalents though that will add complexity and add DSP overhead. The specific thing you’ll have to build that isn’t currently in the default Kyma audio library is an audio rate set-reset latch (not hard but a bit involved for the purposes of this tutorial). Otherwise, everything outlined here can be done at sample accurate, audio rate. For most applications, the timing and trigger logic doesn’t require that level of accuracy.
Lastly, there are many ways to likely solve several of these problems, my solutions build ideas and concepts put forward by those who came before me and any mistakes, shortcomings, or limitations are my own. I welcome and encourage questions as well as suggestions on how to improve the examples found within.
Step 1: Create a master duration time index
The first thing we need a time index (aka ramp) that does the following:
- Listens for a trigger
- Once triggered, it should not re-trigger until its cycle is complete.
- Can vary in duration, including via randomization
Starting in the under left, we have a simple random trigger generator. We’ll talk more about differing approaches to triggering your granular cloud but for now, I’ve put in a simple static trigger that repeats every 200 ms to make it easy to test and debug.
The trigger enters a simple set-reset latch. The reset for this latch is the end-of-cycle for our index. We test for the end of the cycle by feeding the output of the index into a threshold comparator that triggers once the index approaches 1.
As a precaution against unexpected values, I force positive values to 1 and discard the rest. This is not strictly needed, but is a habit I got myself when dealing with what could be arbitrary trigger sources and I need to create triggers that may be passed around to different destinations which may react to negative values in expected ways. This is a functional approach of validating on output which can be helpful for debugging complex flows.
Moving to the index: our trigger starts the cycle and define the grain duration length.
Jitter is broken out into a separate module. For now, the jitter is a simple random number generator between -1 and 1 that is triggered at the start of each cycle. Jitter is a whole deep dive topic in itself. My preferred approach is to create an expression for minimum and maximum values and then have the jitter distributed between those end points. However, for this tutorial we are going to use the more common “center point” method for now.
Example of a jitter expression
As noted previously, we take the output of this index and feed it back to the start to fetch the end-of-cycle. There are other ways to accomplish this in Kyma including using a sound to global controller but the feedback mechanism is reliable and sample accurate if you happen to want to replace other parts of the triggering logic control flow with sample accurate methods.
Now we have a duration ramp (index) that we’ll use to determine the grain cycle and how long that single shot cycle will run.
Step 2: Create sample playback index and a single grain
We are not done with indexes. For a granular sample playback we need a minimum of two indexes. The second index is used to control the playback of the sample within the grain.
Here is the next evolution of our flow that will define the majority of what constitutes a grain cloud.
The initial structure to the right remains the same. In the middle we add a new index. What is different about this index is that its duration controls the pitch of our sample buffer. So if the duration of the sample is 2 s, to hear the sample at normal pitch, the index will be 2 s. If we want to pitch up by an octave, we shorten the index to 1 s. Kyma has a pre-built control for rate which multiples the duration by the rate value. For now, we’ll use a fixed value and a simple jitter control though there are varying approaches if you want to make this easier to use with a keyboard or traditional note sequencer. One thing to note here is that the sample selection is scripted so that the ramp automatically calculates its length based on the file selected.
We are going to want separate controls for sample start with start jitter but we need to decide how to handle the end of the buffer. The most common approach is to loop back to the beginning of the buffer. To do that, feed the index and the sample start offset into an Add Wrap. This takes the sum of the inputs and wraps any value less than -1 and greater than 1 around. For example, a value of 1.5 becomes -0.5.
In the off chance you want the index to start playing in reverse if the index overflows the -1 to 1 range rather than loop back around, look at the FlipWrap example in the Kyma community library.
We now have an index to control our sample playback buffer so let’s take the final steps to create the single mono grain. First is to apply a waveshaper being indexed by our duration ramp and use that to control the shape of the amplitude of our grain.
As a side note, the envelope shapes for the vast majority of granular approaches are window functions. This is why we see Gaussian, Hann, Hamming, etc, as common selections. There are many justifiable and correct reasons for this, especially anything that needs an even amplitude distribution between overlapping grains (for example, pitch and time stretching). However, if we are looking past those use cases into more experimental areas, there is no reason we can’t use a variety of shapes or even vary the shape per grain. What is important is that the envelope starts and ends at zero with a bit of a fade. If this doesn’t occur, you’ll experience DC offset clicks.
Lastly, add our pan control and associated jitter.
So we now have a single grain that encompasses most of the basic controls found in grain clouds. If you find that you want to be modulating the sample start or sample jitter in real-time, it is recommended that you add a sample and hold that is synched to the grain start. Otherwise, you’ll get what are, likely, undesired discontinuities.
Spend some time testing and debug your single grain before moving on to create a cloud or experimenting with additional layers of complexity.
Step 3: Create a cloud of grains
In Kyma creating a cloud of grains is super easy. Slap a replicator on the end, do not replicate any of the controls, and make each random number generator have a unique seed value per voice. The number of replications is the maximum number of grains.
For the trigger, replace your steady state click with a noise generator into a threshold. Make sure the noise generator has a per voice random seed. The threshold will become the density control. For random controls mixed with periodic, add trigger generators and mix it with the random output.
So now you have your own modular grain cloud generator. You can likely figure out where to go from here in terms of adding processing on a per grain basis. You can also immediately see opportunities to use different random distribution curves on your jitter (something not possible with most other options). The next post will explore some of these branches.