4. Using more effects and programmatic sound system creation

This tutorial will go into some more interesting use cases for py-modular and hopefully demonstrate concepts where py-modular can shine in comparison to traditional music creation environments. We will be building off the previous tutorials, and implementing some more effects and programmatic system designs. First, we need to import some nodes to create the sound system and a utility function to generate random numbers.

from py_modular.time.transport import GlobalTransport
from py_modular.sound.oscillators import Tri, Random
from py_modular.effects.mixer import Atten
from py_modular.effects.dsp import MultiChannelDelay
from py_modular.time.events import TriggerEvent
from py_modular.time.envelopes import ExpEnv
from py_modular.time.transport import EventSequencer

from random import randint

We will essentially copy most of the design from the last tutorial and implement it as a function. Each time we call this function, we will create another instance of the ping sound, but each created instance will be slightly different because of the random oscillator. Additionally, we will use the randint method to generate random event sequences for each instance of the ping sound.

def get_ping_sound():
    pitch_variation = Random(freq=0.76, gain=50.0, offset=100.0)
    pitch_envelope = ExpEnv(1.0, gain=10.0, curve_gain=0.8)
    gain_envelope = ExpEnv(1.0, curve_gain=0.92)
    trigger = TriggerEvent([pitch_envelope, gain_envelope])
    sequencer = EventSequencer([trigger], sequence=[randint(30000, 60000) for i in range(randint(2, 6))])
    ping_sound = Tri(freq=250.0, gain=0.75, gainct=gain_envelope, fmct=[pitch_envelope, pitch_variation])
    return ping_sound, sequencer

Then we will define some number of pings to create. This can be any number, but since py-modular is not very optimized for real time performance at the time of writing this, the larger the number, the more audio buffer underruns will occur. Even with just one ping, we should get an interesting result. We also will create some lists so that we can store ping and event instances along the way

num_pings = 2
event_handlers = []
sounds = []

We can iterate through our number of pings to create and store the returned instances in the previous lists.

for i in range(num_pings):
    sound, sequencer = get_ping_sound()
    event_handlers.append(sequencer)
    sounds.append(sound)

Then we add in the best part, the MultiChannelDelay. The delay takes an array of sounds to be processed, in this case our generated pings, a list of delay times for each channel in samples, and a value for delay feedback. For feedback, somewhere between 1.1 and 1.3 will give a good atmospheric result.

delay = MultiChannelDelay(sounds, [8477, 30298], 1.32)

We can then create our global transport with the event handlers. To add the delay, we will just add the node to both channels of the global transport. Since it is a multichannel delay, it will take care of deciding which sounds go to which channels.

global_transport = GlobalTransport(event_handlers, input_device=15, output_device=15)
global_transport.chs[0].add_node(delay)
global_transport.chs[1].add_node(delay)

global_transport.start()

Warning

Since py-modular is not optimized for real time performance yet, this tutorial may perform poorly on computers that are not high performance. We recommend using the recording utilities to create .wav files and listen back to the results that way for now.

The output should be similar to this, keeping in mind the pitches are randomly generated