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

ManyOscillators

Example showing how to use ManyOscillators.mojo with MMMAudio.

You can change the number of oscillators dynamically by sending a 'set_num_pairs' message.

Python Code

from mmm_src.MMMAudio import MMMAudio
mmm_audio = MMMAudio(128, graph_name="ManyOscillators", package_name="examples")
mmm_audio.start_audio() # start the audio thread - or restart it where it left off


# set the number of oscillator pairs -----------------------|


mmm_audio.send_int("num_pairs", 2)  # set to 2 pairs of oscillators

mmm_audio.send_int("num_pairs", 14)  # change to 14 pairs of oscillators

mmm_audio.send_int("num_pairs", 300)  # change to 300 pairs of oscillators

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

Mojo Code

from mmm_src.MMMWorld import MMMWorld
from mmm_utils.Messenger import *
from mmm_utils.functions import *
from mmm_src.MMMTraits import *

from mmm_dsp.Osc import Osc
from random import random_float64
from mmm_dsp.Pan import pan2

# THE SYNTH


# The synth here, called StereoBeatingSines, is not yet the "graph" that MMMAudio will 
# call upon to make sound with. StereoBeatingSines is a struct that
# defines some DSP behavior that can be called upon by 
# the ManyOscillators graph below.

struct StereoBeatingSines(Representable, Movable, Copyable):
    var world: UnsafePointer[MMMWorld] # pointer to the MMMWorld
    var osc1: Osc
    var osc2: Osc
    var osc_freqs: SIMD[DType.float64, 2] # frequencies for the two oscillators
    var pan2_osc: Osc # LFO for panning
    var pan2_freq: Float64 # frequency for the panning LFO
    var vol_osc: Osc # LFO for volume
    var vol_osc_freq: Float64 # frequency for the volume LFO

    fn __init__(out self, world: UnsafePointer[MMMWorld], center_freq: Float64):
        self.world = world

        # create two oscillators. The [2] here is *kind of* like an array
        # with two elements, but the more accurate way to look at it is a
        # SIMD operation with a width of 2. For more info on MMMAudio's SIMD 
        # support, see: https://spluta.github.io/MMMAudio/api/ 
        # Just FYI, it's not 2 because this is a stereo synth, it's 2 to
        # create some nice beating patterns. The output is stereo because later
        # the pan2 function positions the summed oscillators in the stereo field

        self.osc1 = Osc(world)
        self.osc2 = Osc(world)

        self.pan2_osc = Osc(world)
        self.pan2_freq = random_float64(0.03, 0.1)

        self.vol_osc = Osc(world)
        self.vol_osc_freq = random_float64(0.05, 0.2)
        self.osc_freqs = SIMD[DType.float64, 2](
            center_freq + random_float64(1.0, 5.0),
            center_freq - random_float64(1.0, 5.0)
        )

    fn __repr__(self) -> String:
        return String("StereoBeatingSines")

    @always_inline
    fn next(mut self) -> SIMD[DType.float64, 2]:
        # calling .next on both oscillators gets both of their next samples
        temp = self.osc1.next(self.osc_freqs[0]) + self.osc2.next(self.osc_freqs[1])

        # modulate the volume with a slow LFO

        temp = temp * (self.vol_osc.next(self.vol_osc_freq) * 0.5 + 0.5)

        pan2_loc = self.pan2_osc.next(self.pan2_freq)  # Get pan position

        return pan2(temp, pan2_loc)  # Pan the temp signal

# THE GRAPH
# This graph is what MMMAudio will call upon to make sound with (because
# it is the struct that has the same name as this).

struct ManyOscillators(Copyable, Movable):
    var world: UnsafePointer[MMMWorld]
    var synths: List[StereoBeatingSines]  # Instances of the StereoBeatingSines synth
    var messenger: Messenger
    var num_pairs: Int64

    fn __init__(out self, world: UnsafePointer[MMMWorld]):
        self.world = world

        # initialize the list of synths
        self.synths = List[StereoBeatingSines]()

        self.messenger = Messenger(self.world)
        self.num_pairs = 10

        # add 10 pairs to the list
        for _ in range(self.num_pairs):
            self.synths.append(StereoBeatingSines(self.world, random_exp_float64(100.0, 1000.0)))

    @always_inline
    fn next(mut self) -> SIMD[DType.float64, 2]:

        if self.messenger.notify_update(self.num_pairs,"num_pairs"):
            if len(self.synths) != Int(self.num_pairs):
                if self.num_pairs > len(self.synths):
                    # add more
                    for _ in range(self.num_pairs - len(self.synths)):
                        self.synths.append(StereoBeatingSines(self.world, random_exp_float64(100.0, 1000.0)))
                else:
                    # remove some
                    for _ in range(len(self.synths) - self.num_pairs):
                        _ = self.synths.pop()

        # sum all the stereo outs from the N synths
        sum = SIMD[DType.float64, 2](0.0, 0.0)
        for i in range(len(self.synths)):
            sum += self.synths[i].next()

        return sum * (0.5 / Float64(self.num_pairs))  # scale the output down