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: (mmm_audio.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]]

    fn __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)

        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

        self.osc0_freq = 220.0
        self.osc1_freq = 440.0
        self.osc2_freq = 220.0
        self.osc3_freq = 220.0

        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]

        self.m = Messenger(world)
        self.fb = [0.0, 0.0, 0.0, 0.0]
        self.osc_frac = [0.0, 0.0, 0.0, 0.0]

    fn next(mut self) -> MFloat[2]:

        self.m.update(self.osc0_freq, "osc0_freq")
        self.m.update(self.osc1_freq, "osc1_freq")
        self.m.update(self.osc2_freq, "osc2_freq")
        self.m.update(self.osc3_freq, "osc3_freq")

        self.m.update(self.osc0_mul[0], "osc0_mula")
        self.m.update(self.osc0_mul[1], "osc0_mulb")

        self.m.update(self.osc1_mul[0], "osc1_mula")
        self.m.update(self.osc1_mul[1], "osc1_mulb")

        self.m.update(self.osc2_mul[0], "osc2_mula")
        self.m.update(self.osc2_mul[1], "osc2_mulb")

        self.m.update(self.osc3_mul[0], "osc3_mula")
        self.m.update(self.osc3_mul[1], "osc3_mulb")

        self.m.update(self.osc_frac[0], "osc_frac0")
        self.m.update(self.osc_frac[1], "osc_frac1")
        self.m.update(self.osc_frac[2], "osc_frac2")
        self.m.update(self.osc_frac[3], "osc_frac3")


        for i 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])

            self.fb = [osc0, osc1, osc2, osc3]

            self.over.add_sample(MFloat[2](osc0, osc1))

        return self.over.get_sample()