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

WavetableOsc

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

You can load your own wavetable files by sending a string message to the "load_file" parameter with the full path to the wavetable file.

MMM_Audio can load commercial .wav files, designed for Vital or Serum, as wavetables. The wavetable should be a single channel audio file made up of one or more cycles of a waveform, each a power of 2 in length. The wavetable will be looped to create the oscillator waveform.

Also demonstrates how to use the PVoiceAllocator class to manage multiple voices for polyphonic MIDI input.

Python Code

from mmm_python import *
mmm_audio = MMMAudio(128, graph_name="WavetableOsc", package_name="examples")
mmm_audio.start_audio() 

# load a different wavetable if you like - these are just example paths - change to your own files
# if the number of instances of the wave found in the wavetable file is different than the default 256, you may need to change the "wavetables_per_channel" parameter
mmm_audio.send_int("wavetables_per_channel", 128) # set this to the number of waveforms in your wavetable file

mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 10.wav'")
mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 11.wav'")
mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 12.wav'")
mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 13.wav'")
mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 14.wav'")
mmm_audio.send_string("load_file", "'/Users/ted/dev/BVKER - Custom Wavetables/Growl/Growl 15.wav'")

if True:
    import supriya_midi as midi

    # find your midi devices
    ports = midi.list_ports()
    print(f"Available MIDI ports: {ports}")
    port_num = ports.index('Oxygen Pro Mini USB MIDI')

    # open your midi device - you may need to change the device name
    in_port = midi.MidiIn()
    in_port.open_port(port_num)

    voice_allocator = PVoiceAllocator(8)

def note_on(msg):
    voice = voice_allocator.get_free_voice(msg.note_number)
    if voice == -1:
        print("No free voice available")
    else:
        voice_msg = "voice_" + str(voice)
        print(f"Note On: {msg.note_number} Velocity: {msg.velocity} Voice: {voice}")
        mmm_audio.send_float(voice_msg +".freq", midicps(msg.note_number))  # note freq and velocity scaled 0 to 1
        mmm_audio.send_bool(voice_msg +".gate", True)  # note freq and velocity scaled 0 to 1

def note_off(msg):
    found, voice = voice_allocator.release_voice(msg.note_number)
    if found:
        voice_msg = "voice_" + str(voice)
        print(f"Note Off: {msg.note_number} Voice: {voice}")
        mmm_audio.send_bool(voice_msg +".gate", False)  # note freq and velocity scaled 0 to 1

def cc(msg):
    print(f"Control Change: {msg.controller_number} Value: {msg.controller_value}")
    # Example: map CC 1 to wubb_rate of all voices
    if msg.controller_number == 1:
        wubb_rate = linexp(msg.controller_value, 0, 127, 0.1, 10.0)
        for i in range(8):
            voice_msg = "voice_" + str(i)
            mmm_audio.send_float(voice_msg +".wubb_rate", wubb_rate)
    if msg.controller_number == 33:
        mmm_audio.send_float("filter_cutoff", linexp(msg.controller_value, 0, 127, 20.0, 20000.0))
    if msg.controller_number == 34:
        mmm_audio.send_float("filter_resonance", linexp(msg.controller_value, 0, 127, 0.1, 1.0))

def midi_callback(msg, timestamp, data=None):
    msg = midi.MidiMessage.parse(msg)
    print(f"Received {msg=}")
    if type(msg) == midi.NoteOnMessage:
        note_on(msg)
    if type(msg) == midi.NoteOffMessage:
        note_off(msg)
    if type(msg) == midi.ControllerChangeMessage:
        cc(msg)

in_port.set_callback(midi_callback)

Mojo Code

from mmm_audio import *

struct OscVoice(Movable, Copyable):
    var osc: Osc[1,Interp.quad,TimesOversampling.x2]
    var tri: LFOsc[]
    var world: World
    var env: ASREnv
    var gate: Bool
    var freq: Float64
    var wubb_rate: Float64
    var messenger: Messenger

    def __init__(out self, world: World, name_space: String = ""):
        self.osc = Osc[1,Interp.quad,TimesOversampling.x2](world)
        self.tri = LFOsc(world)
        self.env = ASREnv(world)
        self.gate = False
        self.freq = 440.0
        self.wubb_rate = 0.5
        self.messenger = Messenger(world, name_space)
        self.world = world

    def next(mut self, ref buffer: Buffer) -> MFloat[1]:
        self.messenger.update("gate", self.gate) 
        self.messenger.update("freq", self.freq) 
        self.messenger.update("wubb_rate", self.wubb_rate)
        osc_frac = self.tri.next[OscType.triangle](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.1,self.gate,2)

struct WavetableOsc(Movable, Copyable):
    var world: World  
    var osc_voices: List[OscVoice]
    var wavetables_per_channel: Int
    var buffer: Buffer
    var file_name: String
    var messenger: Messenger
    var filter_cutoff: Float64
    var filter_resonance: Float64
    var moog_filter: VAMoogLadder[1,TimesOversampling.x2]

    def __init__(out self, world: World):
        self.world = world
        self.file_name = "resources/Growl 15.wav"
        self.wavetables_per_channel = 256
        self.buffer = Buffer.load(self.file_name, num_wavetables=self.wavetables_per_channel)
        self.osc_voices = List[OscVoice]()
        for i in range(8):
            self.osc_voices.append(OscVoice(self.world, "voice_"+String(i)))

        self.messenger = Messenger(self.world)
        self.filter_cutoff = 20000.0
        self.filter_resonance = 0.5
        self.moog_filter = VAMoogLadder[1,TimesOversampling.x2](self.world)

    def loadBuffer(mut self):
        self.buffer = Buffer.load(self.file_name, num_wavetables=self.wavetables_per_channel)

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

        var sample = 0.0
        for ref voice in self.osc_voices:
            sample += voice.next(self.buffer)

        self.messenger.update("filter_cutoff", self.filter_cutoff)
        self.messenger.update("filter_resonance", self.filter_resonance)
        sample = self.moog_filter.next(sample, self.filter_cutoff, self.filter_resonance)

        return sample * 0.5