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

WavetableOscSIMD

Example of a wavetable oscillator using custom wavetables loaded from files.

This example uses SIMDBuffer instead of Buffer to load the wavetable. This allows for more efficient processing for wavetables with a small number of channels (2-8), where the number of channels is known ahead of time, but it should not be used with wavetables that have a large number of waveforms.

This example also uses Mojo-side Poly vs PVoiceAllocator.

Python Code

import sys
from pathlib import Path

# 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 *

def main():
    mmm_audio = MMMAudio(128, graph_name="WavetableOscSIMD", package_name="examples")
    mmm_audio.start_audio() 

    import threading, mido, time

    # open your midi device - you may need to change the device name
    in_port = mido.open_input('Oxygen Pro Mini USB MIDI')

    # PolyPal correctly formats messages to be sent to a Synth that uses a Poly object
    poly_pal = PolyPal(mmm_audio, "poly", 10)

    # Create stop event
    global stop_event
    stop_event = threading.Event()
    def start_midi():
        while not stop_event.is_set():
            for msg in in_port.iter_pending():
                if stop_event.is_set():  # Check if we should stop
                    return

                if msg.type in ["note_on", "note_off", "control_change"]:
                    if msg.type == "note_on":
                        poly_pal.send_ints([msg.note, (msg.velocity)])  
                    if msg.type == "note_off":
                        poly_pal.send_ints([msg.note, 0.0])  
                    if msg.type == "control_change":
                        print(f"Control Change: {msg.control} Value: {msg.value}")
                        # Example: map CC 1 to wubb_rate of all voices
                        if msg.control == 1:
                            wubb_rate = linexp(msg.value, 0, 127, 0.1, 10.0)
                            mmm_audio.send_float("wubb_rate", wubb_rate)
                        if msg.control == 33:
                            mmm_audio.send_float("filter_cutoff", linexp(msg.value, 0, 127, 20.0, 20000.0))
                        if msg.control == 34:
                            mmm_audio.send_float("filter_resonance", linexp(msg.value, 0, 127, 0.1, 1.0))

            time.sleep(0.01)
    # Start the thread
    midi_thread = threading.Thread(target=start_midi, daemon=False)
    midi_thread.start()

if __name__ == "__main__":
    main()

Mojo Code

from mmm_audio import *

struct OscVoice(PolyObject):
    var osc: Osc[1,Interp.sinc,0]
    var tri: LFTri[]
    var world: World
    var env: ASREnv
    var gate: Bool
    var freq: Float64
    var vol: Float64
    var wubb_rate: Float64
    var messenger: Messenger
    var triggered: Bool
    var just_offset: List[Float64]

    fn check_active(mut self) -> Bool:
        return self.env.is_active

    # Poly will use this function to release the voice when it receives a note off message for the note that this voice is playing. 
    fn set_gate(mut self, gate: Bool):
        self.gate = gate

    # necessary to ensure a fresh env when the voice is copied by Poly
    fn reset_env(mut self):
        self.env = ASREnv(self.world)

    fn __init__(out self, world: World, name_space: String = ""):
        self.osc = Osc[1,Interp.sinc,0](world)
        self.tri = LFTri(world)
        self.env = ASREnv(world)
        self.gate = False
        self.freq = 440.0
        self.vol = 1.0
        self.wubb_rate = 0.5
        self.messenger = Messenger(world, name_space)
        self.world = world
        self.triggered = False
        self.just_offset = [0.0, 0.1173, 0.0391, 0.1564, -0.1369, -0.0196, -0.0978, 0.0196, 0.1369, -0.1564, 0.1760, -0.1173]

    fn next(mut self, ref buffer: SIMDBuffer) -> MFloat[1]:
        osc_frac = self.tri.next(self.wubb_rate, 0.75, trig=self.gate) * 0.5 + 0.5
        return self.osc.next_vwt(buffer, self.freq, osc_frac = osc_frac) * self.env.next(0.01,0.2,0.7,self.gate,2) * self.vol

struct WavetableOscSIMD(Movable, Copyable):
    comptime wavetables_per_channel = 8
    comptime num_messages = 10

    var world: World  
    var voices: List[OscVoice]
    var buffer: SIMDBuffer[Self.wavetables_per_channel]
    var file_name: String
    var messenger: Messenger
    var filter_cutoff: Float64
    var filter_resonance: Float64
    var moog_filter: VAMoogLadder[1,1]
    var poly: PolyGate


    fn __init__(out self, world: World):
        self.world = world
        self.file_name = "resources/small_wavetable8.wav"

        self.buffer = SIMDBuffer[Self.wavetables_per_channel].load(self.file_name, num_wavetables=self.wavetables_per_channel)
        self.voices = [OscVoice(self.world, "voice_"+String(i)) for i in range(8)]

        self.messenger = Messenger(world)
        self.filter_cutoff = 20000.0
        self.filter_resonance = 0.5
        self.moog_filter = VAMoogLadder[1,1](self.world)
        self.poly = PolyGate(8, 16, world, "poly")

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

    fn loadBuffer(mut self):
        self.buffer = SIMDBuffer[Self.wavetables_per_channel].load(self.file_name, num_wavetables=self.wavetables_per_channel)

    fn next(mut self) -> MFloat[2]:
        if self.messenger.notify_update(self.file_name, "load_file"):
            self.loadBuffer()

        # the callback function sent to the Poly, to be called whenever a new trigger is received from Python.
        # the kinds of messages the Messenger can receive are defined by the type of the `note` argument in the callback function
        fn callback(mut poly_object: OscVoice, mut vals: List[Int]):
            if vals[1] > 0: # the call_back will be called for both note on and note off messages
                midi = vals[0] + poly_object.just_offset[vals[0] % 12]
                print(vals[0], midi)
                poly_object.freq = midicps(midi)
                poly_object.vol = Float64(vals[1]) / 127.0
        # the poly has an internal Messenger that receives messages from Python. these have to be in the form of a List[Float64] or a List[Int]
        # for next_gate, the first value in the list is the note to trigger and the second value is the velocity or volume of the note, where 0 denotes a note off message. the callback function receives the list of ints or floats as the second argument, so the PolyObject can be controlled by the message from Python.
        self.poly.next(self.voices, call_back=callback)

        # add the output of all the voices
        var out = 0.0
        for ref voice in self.voices:
            out += voice.next(self.buffer)

        self.messenger.update(self.filter_cutoff, "filter_cutoff")
        self.messenger.update(self.filter_resonance, "filter_resonance")
        wubb_rate = 0.0
        got_wubbed = self.messenger.notify_update(wubb_rate, "wubb_rate")
        if got_wubbed:
            for ref voice in self.voices:
                voice.wubb_rate = wubb_rate
        sample = self.moog_filter.next(out, self.filter_cutoff, self.filter_resonance)

        return sample * 0.5