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

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