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

MidiSequencer

A sequenced polyphonic synthesizer controlled via a MIDI keyboard.

This example demonstrates a couple differnt concepts: - How to Pseq and Pxrand from mmm_python.Patterns to create sequences of notes and other parameters. - How to create a coroutine to schedule note triggers using the MMMAudio scheduler.

Python Code

if True:
    from mmm_python import *

    # instantiate and load the graph
    mmm_audio = MMMAudio(128, graph_name="MidiSequencer", package_name="examples")
    mmm_audio.start_audio()

    from mmm_python.Patterns import Pseq, Pxrand
    import numpy as np
    import asyncio
    from mmm_python.functions import midicps, linexp

    global scheduler
    scheduler = Scheduler()

    poly_pal = PolyPal(mmm_audio, "poly", 10)
    filter_seq = Pseq([linexp(i/100, 0, 1, 100, 5000) for i in range(0, 101)] + [linexp(i/100, 0, 1, 4999, 101) for i in range(0, 101)])
    mmm_audio.send_float("filt_freq", filter_seq.next()) # update filter frequency before each note


# load the sequencer function
async def trig_synth(wait):
    """A counter coroutine"""
    count_to = np.random.choice([7, 11, 13, 17]).item()
    mult_seq = Pseq(list(range(1, count_to + 1)))
    fund_seq = Pxrand([36, 37, 43, 42])
    i = 0
    fund = midicps(fund_seq.next())
    while True:
        mmm_audio.send_float("filt_freq", filter_seq.next()) # update filter frequency before each note
        poly_pal.send_floats([fund * mult_seq.next(), 100 / 127.0])  # note freq and velocity scaled 0 to 1
        await asyncio.sleep(wait)

        i = (i + 1) % count_to

        if i == 0:
            fund = midicps(fund_seq.next())
            count_to = np.random.choice([7, 11, 13, 17]).item()
            mult_seq = Pseq(list(range(1, count_to + 1)))

# start the routine with the scheduler
rout = scheduler.sched(trig_synth(0.04))
rout.cancel() # stop just this routine

# stop all routines
scheduler.stop_routs() # you can also stop the routines with ctl-C in the terminal

mmm_audio.stop_audio()
mmm_audio.start_audio()

Mojo Code

from mmm_audio import *

# Synth Voice - Below is a polyphonic synth. The first struct, TrigSynthVoice, is a single voice of the synth. Each voice is made up of a modulator oscillator, a carrier oscillator, and an envelope generator. 

# TrigSynthVoice follows the pattern of a triggerd PolyObject - it has a set_trigger function that Poly calls to trigger the voice.

struct TrigSynthVoice(PolyObject):
    var world: World  # Pointer to the MMMWorld instance
    var env: Env
    var mod: Osc[]
    var car: Osc[1, Interp.linear, 0]
    var sub: Osc[]
    var bend_mul: Float64
    var note: List[Float64]
    var trigger: Bool

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


    # Poly will use this function to trigger the voice.
    fn set_trigger(mut self, trigger: Bool):
        self.trigger = trigger

    fn __init__(out self, world: World):
        self.world = world
        self.mod = Osc(self.world)
        self.car = Osc[1, Interp.linear, 0](self.world)
        self.sub = Osc(self.world)
        self.env = Env(self.world)
        self.env.params = EnvParams([0.0, 1.0, 0.75, 0.75, 0.0], [0.01, 0.1, 0.2, 0.5], [1.0])
        self.bend_mul = 1.0
        self.note = List[Float64]()
        self.trigger = False

    @always_inline
    fn next(mut self) -> Float64:
        # if there is no trigger and the envelope is not active, that means the voice should be silent - output 0.0
        if not self.env.is_active and not self.trigger:
            return 0.0
        else:
            bend_freq = self.note[0] * self.bend_mul
            var mod_value = self.mod.next(bend_freq * 1.5, osc_type=OscType.sine)  
            var env = self.env.next(self.trigger)  
            var mod_mult = env * 0.5 * linlin(bend_freq, 1000, 4000, 1, 0) #decrease the mod amount as freq increases
            var car_value = self.car.next(bend_freq, mod_value * mod_mult, osc_type=OscType.sine)  

            car_value += self.sub.next(bend_freq * 0.5) 
            car_value = car_value * 0.1 * env * self.note[1]  

            return car_value

    # if you want to use this voice without Poly
    fn next(mut self, trigger: Bool) -> Float64:
        self.set_trigger(trigger)
        out = self.next()
        return out


struct MidiSequencer(Movable, Copyable):
    comptime num_messages = 10

    var world: World 
    var voices: List[TrigSynthVoice]
    var current_voice: Int
    var messenger: Messenger
    var num_voices: Int
    var svf: SVF[]
    var filt_lag: Lag[]
    var filt_freq: Float64
    var bend_mul: Float64
    var poly: PolyTrigger

    fn __init__(out self, world: World, num_voices: Int = 8):
        self.world = world
        self.num_voices = num_voices
        self.current_voice = 0

        self.messenger = Messenger(self.world)

        self.voices = [TrigSynthVoice(self.world) for _ in range(num_voices)]  # Initialize the list of voices

        self.svf = SVF(self.world)
        self.filt_lag = Lag(self.world, 0.1)
        self.filt_freq = 1000.0
        self.bend_mul = 1.0
        self.poly = PolyTrigger(initial_num_voices=num_voices, max_voices=64, world=world, name_space="poly")

    @always_inline
    fn next(mut self) -> MFloat[2]:
        var out = 0.0

        # the callback function sent to the Poly, to be called whenever a new trigger is received from Python.
        fn call_back(mut voice: TrigSynthVoice, mut vals: List[Float64]):
            voice.note = [vals[0], vals[1]]
        # 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]. 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=call_back)

        # add the values of the voices that are not being triggered 
        for i in range(len(self.voices)):
            out += self.voices[i].next()

        self.messenger.update(self.filt_freq, "filt_freq")
        if self.messenger.notify_update(self.bend_mul, "bend_mul"):
            # if bend_mul changes, update all the voices
            for i in range(len(self.voices)):
                self.voices[i].bend_mul = self.bend_mul

        out = self.svf.lpf(out, self.filt_lag.next(self.filt_freq), 2.0) * 0.6

        return out