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 create a variable number of UGens in MMM-Audio. In the ManyOscillators.mojo file, the ManyOscillators struct holds a List of OscillatorPair structs. Each OscillatorPair contains two oscillators (sine wave generators) that can be detuned from each other. We can dynamically change the number of oscillator pairs by sending an integer message to the "num_pairs" parameter.
Python Code¶
from mmm_python.MMMAudio import MMMAudio
mmm_audio = MMMAudio(128, graph_name="ManyOscillators", package_name="examples")
mmm_audio.start_audio()
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_audio import *
# 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[interp=Interp.linear] # first oscillator
var osc2: Osc[interp=Interp.linear] # second oscillator
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[interp=Interp.linear](self.world)
self.osc2 = Osc[interp=Interp.linear](self.world)
self.pan2_osc = Osc(self.world)
self.pan2_freq = random_float64(0.03, 0.1)
self.vol_osc = Osc(self.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