Skip to content

For more information about the examples, such as how the Python and Mojo files interact with each other, see the Examples Overview

Grains

Demonstrates granular synthesis using TGrains, using a mouse to control granular playback.

Left and right moves around in the buffer. Up and down controls rate of triggers.

Python Code

from mmm_python import *
mmm_audio = MMMAudio(128, num_output_channels = 8, graph_name="Grains", package_name="examples")
mmm_audio.start_audio() # start the audio thread - or restart it where it left off

mmm_audio.send_float("max_trig_rate", 80.0) # when trigger creates more than the specified number of overlaps, TGrains will add voices to keep up with the trigger rate. 

mmm_audio.stop_audio() # stop/pause the audio thread

mmm_audio.plot(20000)

Mojo Code

from mmm_audio import *

# THE SYNTH

comptime num_output_chans = 2
comptime num_simd_chans = next_power_of_two(num_output_chans)

struct Grains(Movable, Copyable):
    var world: World
    var buffer: SIMDBuffer[2]

    var tgrains: TGrains # set the number of simultaneous grains by setting the max_grains parameter here
    var tgrains2: TGrains 
    var impulse: Phasor[1]  
    var start_frame: Float64
    var m: Messenger
    var max_trig_rate: Float64

    fn __init__(out self, world: World):
        self.world = world  

        # buffer uses numpy to load a buffer into an N channel array
        self.buffer = SIMDBuffer[2].load("resources/Shiverer.wav")

        self.tgrains = TGrains(10, 100, world)  
        self.tgrains2 = TGrains(10, 100, world)
        self.impulse = Phasor[1](self.world)
        self.m = Messenger(world)
        self.max_trig_rate = 20.0

        self.start_frame = 0.0 

    @always_inline
    fn next(mut self) -> MFloat[num_simd_chans]:
        self.m.update(self.max_trig_rate, "max_trig_rate")
        imp_freq = linlin(self.world[].mouse_y, 0.0, 1.0, 1.0, self.max_trig_rate)  # Map mouse Y to a trigger frequency between 1 Hz and max_trig_rate
        var impulse = self.impulse.next_bool(imp_freq, 0, True)  # Get the next impulse sample

        start_frame = Int(linlin(self.world[].mouse_x, 0.0, 1.0, 0.0, Float64(self.buffer.num_frames) - 1.0))
        # if there are 2 (or fewer) output channels, pan the stereo buffer out to 2 channels by panning the stereo playback with pan2
        # if there are more than 2 output channels, pan each of the 2 channels separately and randomly pan each grain channel to a different speaker
        @parameter
        if num_output_chans == 2:
            out = self.tgrains.next[2](self.buffer, 1, impulse, start_frame, 0.4, 0, random_float64(-1.0, 1.0), 1.0)

            return MFloat[num_simd_chans](out[0], out[1]) # because pan2 outputs a SIMD vector size 2, and we require a SIMD vector of size num_simd_chans, you have to manually make the SIMD vector in this case (the compiler does not agree that num_simd_chans == 2, even though it does)
        else:
            # pan each channel separately to num_output_chans speakers
            out0 = self.tgrains.next_pan_az[num_simd_chans=num_simd_chans](self.buffer, 1, impulse, start_frame, 0.4, 0, random_float64(-1.0, 1.0), 1.0, num_output_chans)
            out1 = self.tgrains2.next_pan_az[num_simd_chans=num_simd_chans](self.buffer, 1, impulse, start_frame, 0.4, 1, random_float64(-1.0, 1.0), 1.0, num_output_chans)

            return out0 + out1