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 = 2, graph_name="Grains", package_name="examples")
mmm_audio.start_audio() 

# for Wayland use the fake mouse
MMMAudio.fake_mouse()

# with a user defined env, the shape of the grain envelope can be customized
mmm_audio.send_floats("times", [0.01, 0.2])
mmm_audio.send_floats("values", [0.0, 1.0, 0.0])
mmm_audio.send_floats("curves", [8])

mmm_audio.send_float("max_trig_rate", 80.0) 

mmm_audio.stop_audio()

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
    var env_params: EnvParams

    def __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.env_params = EnvParams()

        self.start_frame = 0.0 

    @always_inline
    def next(mut self) -> MFloat[num_simd_chans]:
        self.m.update("max_trig_rate", self.max_trig_rate)
        c1 = self.m.notify_update("times", self.env_params.times) 
        c2 = self.m.notify_update("values", self.env_params.values) 
        c3 = self.m.notify_update("curves", self.env_params.curves) 
        if c1 or c2 or c3:
            self.tgrains.set_env_params(self.env_params)

        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)

        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
        comptime if num_output_chans == 2:
            out = self.tgrains.next[2, WindowType.user_defined](self.buffer, 1, impulse, start_frame, 0.4, random_float64(-1.0, 1.0), 1.0)

            return MFloat[num_simd_chans](out[0], out[1])
        else:
            pass
            # for > 2 output channels, uncomment these lines to use the next_pan_az method
            # pan each channel separately to num_output_chans speakers
            # out_az1 = self.tgrains.next_pan_az[num_simd_chans=num_simd_chans](self.buffer, 1, impulse, start_frame, 0.4, random_float64(-1.0, 1.0), 1.0, num_output_chans, 0)
            # out_az2 = self.tgrains2.next_pan_az[num_simd_chans=num_simd_chans](self.buffer, 1, impulse, start_frame, 0.4, random_float64(-1.0, 1.0), 1.0, num_output_chans, 1)

            # return out_az1 + out_az2