For more information about the examples, such as how the Python and Mojo files interact with each other, see the Examples Overview
FM4¶
Python Code¶
import sys
from pathlib import Path
# This example is able to run by pressing the "play" button in VSCode
# that executes the whole file.
# In order to do this, it needs to add the parent directory to the path
# (the next line here) so that it can find the mmm_src and mmm_utils packages.
# If you want to run it line by line in a REPL, skip this line!
sys.path.insert(0, str(Path(__file__).parent.parent))
from mmm_python import *
from mmm_python.GUI import Handle, ControlSpec
from mmm_python import *
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QCheckBox
app = QApplication([])
def main():
mmm_audio = MMMAudio(128, graph_name="FM4", package_name="examples")
mmm_audio.start_audio()
sliders = []
# Create the main window
window = QWidget()
window.setWindowTitle("FM4")
window.resize(600, 100)
# Create main horizontal layout
main_layout = QHBoxLayout()
layouts = []
# Create left vertical layout
layouts.append(QVBoxLayout())
layouts[0].setSpacing(0)
# Create right vertical layout
layouts.append(QVBoxLayout())
layouts[1].setSpacing(0)
# Add both layouts to the main horizontal layout
main_layout.addLayout(layouts[0])
main_layout.addLayout(layouts[1])
def add_handle(name: str, min: float, max: float, exp: float, default: float, layout_index: int = 0, resolution: int = 1000):
# make the slider
slider = Handle(name, ControlSpec(min, max, exp), default, callback=lambda v: mmm_audio.send_float(name, v))
sliders.append(slider)
# add it to the layout
layouts[layout_index].addWidget(slider)
# send the default value to the graph
mmm_audio.send_float(name, default)
add_handle("osc0_freq", 0.2, 4000.0, 0.125, 100)
add_handle("osc1_freq", 0.2, 4000.0, 0.125, 10)
add_handle("osc2_freq", 0.2, 4000.0, 0.125, 10)
add_handle("osc3_freq", 0.2, 4000.0, 0.125, 10)
add_handle("osc0_mula", 0, 3000.0, 1, 0)
add_handle("osc0_mulb", 0, 3000.0, 1, 0)
add_handle("osc1_mula", 0, 3000.0, 1, 0)
add_handle("osc1_mulb", 0, 3000.0, 1, 0)
add_handle("osc2_mula", 0, 3000.0, 1, 0)
add_handle("osc2_mulb", 0, 3000.0, 1, 0)
add_handle("osc3_mula", 0, 3000.0, 1, 0, 1)
add_handle("osc3_mulb", 0, 3000.0, 1, 0, 1)
add_handle("osc_frac0", 0, 1.0, 1, 0, 1, 4)
add_handle("osc_frac1", 0, 1.0, 1, 0, 1, 4)
add_handle("osc_frac2", 0, 1.0, 1, 0, 1, 4)
add_handle("osc_frac3", 0, 1.0, 1, 0, 1, 4)
button = QPushButton("randomize")
button.clicked.connect(lambda: [s.set_value(s.spec.unnormalize(rrand(0.0001, 1.0))) for s in sliders])
layouts[1].addWidget(button)
# window.closeEvent = lambda event: (app.quit())
# Set the layout for the main window
window.closeEvent = lambda event: (MMMAudio.exit_all(), event.accept())
window.setLayout(main_layout)
# Show the window
window.show()
window.raise_()
# Start the application's event loop
app.exec()
if __name__ == "__main__":
main()
Mojo Code¶
from mmm_audio import *
struct FM4(Movable, Copyable):
var world: World
comptime os_index = 2
comptime times_oversampling = 1 << Self.os_index
var over: Oversampling[2, Self.times_oversampling]
var osc0: Osc[1, Interp.sinc, 0]
var osc1: Osc[1, Interp.sinc, 0]
var osc2: Osc[1, Interp.sinc, 0]
var osc3: Osc[1, Interp.sinc, 0]
var osc0_freq: MFloat[1]
var osc1_freq: MFloat[1]
var osc2_freq: MFloat[1]
var osc3_freq: MFloat[1]
var osc0_mul: List[MFloat[1]]
var osc1_mul: List[MFloat[1]]
var osc2_mul: List[MFloat[1]]
var osc3_mul: List[MFloat[1]]
var m: Messenger
var fb: List[MFloat[1]]
var osc_frac: List[MFloat[1]]
def __init__(out self, world: World) :
self.world = world
self.over = Oversampling[2, Self.times_oversampling](world)
self.osc0 = Osc[1, Interp.sinc, 0](world)
self.osc1 = Osc[1, Interp.sinc, 0](world)
self.osc2 = Osc[1, Interp.sinc, 0](world)
self.osc3 = Osc[1, Interp.sinc, 0](world)
# adjust the phase multipliers for oversampling
self.osc0.phasor.freq_mul /= Self.times_oversampling
self.osc1.phasor.freq_mul /= Self.times_oversampling
self.osc2.phasor.freq_mul /= Self.times_oversampling
self.osc3.phasor.freq_mul /= Self.times_oversampling
# set the initial frequencies
self.osc0_freq = 220.0
self.osc1_freq = 440.0
self.osc2_freq = 220.0
self.osc3_freq = 220.0
# initial modulation amounts for each oscillator
self.osc0_mul = [0.0, 0.0]
self.osc1_mul = [0.0, 0.0]
self.osc2_mul = [0.0, 0.0]
self.osc3_mul = [0.0, 0.0]
# the value that controls the warping of the oscillators' waveforms
self.osc_frac = [0.0, 0.0, 0.0, 0.0]
# output of each oscillator to be fed back in the next audio cycle
self.fb = [0.0, 0.0, 0.0, 0.0]
self.m = Messenger(world)
def next(mut self) -> MFloat[2]:
self.m.update("osc0_freq", self.osc0_freq)
self.m.update("osc1_freq", self.osc1_freq)
self.m.update("osc2_freq", self.osc2_freq)
self.m.update("osc3_freq", self.osc3_freq)
self.m.update("osc0_mula", self.osc0_mul[0])
self.m.update("osc0_mulb", self.osc0_mul[1])
self.m.update("osc1_mula", self.osc1_mul[0])
self.m.update("osc1_mulb", self.osc1_mul[1])
self.m.update("osc2_mula", self.osc2_mul[0])
self.m.update("osc2_mulb", self.osc2_mul[1])
self.m.update("osc3_mula", self.osc3_mul[0])
self.m.update("osc3_mulb", self.osc3_mul[1])
self.m.update("osc_frac0", self.osc_frac[0])
self.m.update("osc_frac1", self.osc_frac[1])
self.m.update("osc_frac2", self.osc_frac[2])
self.m.update("osc_frac3", self.osc_frac[3])
# the oversampling loop
for _ in range(Self.times_oversampling):
fm_0 = self.fb[1] * self.osc0_mul[0] + self.fb[2] * self.osc0_mul[1]
osc0 = self.osc0.next_basic_waveforms(self.osc0_freq + fm_0, osc_frac=self.osc_frac[0])
fm_1 = osc0 * self.osc1_mul[0] + self.fb[3] * self.osc1_mul[1]
osc1 = self.osc1.next_basic_waveforms(self.osc1_freq + fm_1, osc_frac=self.osc_frac[1])
fm_2 = osc1 * self.osc2_mul[0] + self.fb[3] * self.osc2_mul[1]
osc2 = self.osc2.next_basic_waveforms(self.osc2_freq + fm_2, osc_frac=self.osc_frac[2])
fm_3 = osc0 * self.osc3_mul[0] + osc1 * self.osc3_mul[1]
osc3 = self.osc3.next_basic_waveforms(self.osc3_freq + fm_3, osc_frac=self.osc_frac[3])
# feedback all the oscillators for the next cycle
self.fb = [osc0, osc1, osc2, osc3]
# add the sample to the oversampling buffer (we only hear the first two oscillators)
self.over.add_sample(MFloat[2](osc0, osc1))
# downsample the oversampled signal and return
return self.over.get_sample() * 0.25