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