Skip to content

Duty cycle modulation for synthio Note object (AKA PWM, commonly used on square wave oscillators) #9780

Closed
@kevinjwalters

Description

@kevinjwalters

A common modulation techniques for synthesizers is modulation of the duty cycle on one or more oscillators. A simple example is a triangle wave LFO modulating a square or approximate square wave. It would be useful to have some efficient way of varying the duty cycle on at least a square wave and having that controlled by the usual myriad of synthio options.

Workarounds

I've been doing this by reading the value from an LFO and manually changing the waveform in python code. It's not perfect but better than I thought it would be. I raised it as part of Adafruit Forums: synthio mysteries - high frequency notes, envelope release, LFO mod of square wave duty cycle (PWM) and lpf making noise and @jepler mentioned another technique using ulab to (re)create the arrays using np.where.

I did a quick bit of benchmarking and for a 2048 element waveform (fairly large to get a nice sound on the changing duty cycle) it's actually more efficient to change what's needed in python code than make a whole new array in ulab. So here you can see the python code takes about 1-2ms to just update what needs to be changed in the waveform (for rapid updates) vs 8ms for ulab code. The latter is constant, the former varies with degree of change but the nature of duty cycle modulation is it's highly unlikely to be desirable to make a big change. There's some plus 7-8ms every now and again which I'm guessing is GC. Perhaps surprisingly the python code is much quicker on a (normal 125MHz clocked) Pi Pico WH and won't be allocating any new objects/causing GC.

Adafruit CircuitPython 9.1.4 on 2024-09-17; Cytron EDU PICO W with rp2040
>>>
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
DELTA [416, 44, 11, 17, 11, 28, 44]
CP (ms) [51.9104, 3.1128, 1.00708, 1.34278, 1.09863, 2.13623, 4.1809]
ULAB (ms) [7.96509, 14.5569, 14.8926, 7.78198, 14.2822, 13.7024, 13.5193]
DELTA [22, 6, 17, 11, 16, 23, 50]
CP (ms) [1.83105, 0.671396, 1.4038, 2.13622, 1.40381, 2.80761, 4.24195]
ULAB (ms) [8.05664, 14.9841, 13.6719, 6.7444, 14.3738, 13.855, 14.2822]
DELTA [22, 5, 11, 11, 23, 22, 6]
CP (ms) [1.83105, 0.671383, 2.16675, 2.13623, 1.70898, 1.70898, 0.793455]
ULAB (ms) [8.39234, 15.0757, 7.84302, 13.3362, 14.0991, 15.0147, 7.93457]
DELTA [11, 5, 11, 17, 11, 28, 44]
CP (ms) [1.12915, 2.13623, 7.65991, 1.31225, 7.35474, 2.0752, 3.05176]
ULAB (ms) [7.96509, 7.99561, 7.99561, 7.84302, 8.1787, 13.7634, 14.4348]
DELTA [23, 5, 11, 11, 17, 22, 50]
CP (ms) [1.83105, 0.640878, 1.00708, 0.976562, 1.31225, 8.1482, 3.41797]
ULAB (ms) [7.96509, 14.9536, 9.24682, 14.3127, 7.75146, 8.05663, 14.4043]
DELTA [17, 11, 11, 16, 12, 22, 50]
CP (ms) [1.49535, 1.06812, 1.0376, 1.31226, 1.15967, 2.74658, 3.50952]
ULAB (ms) [7.93457, 13.7329, 14.8315, 7.75146, 13.9465, 13.7634, 14.4653]
DELTA [16, 11, 11, 17, 11, 28, 44]
CP (ms) [2.59399, 1.06812, 1.06812, 1.34276, 1.12915, 2.10571, 4.11987]
ULAB (ms) [7.78199, 14.7095, 14.8315, 7.72095, 13.7939, 15.0452, 13.6108]
DELTA [22, 6, 17, 11, 16, 23, 44]
CP (ms) [1.83105, 0.671382, 1.34276, 1.00708, 1.34278, 1.7395, 3.17382]
ULAB (ms) [7.87354, 14.8621, 13.855, 7.75146, 14.2517, 14.7705, 14.3128]
DELTA [22, 11, 11, 17, 11, 22, 6]
CP (ms) [1.77002, 1.12915, 1.43432, 1.34276, 1.06812, 1.64796, 0.762936]
ULAB (ms) [7.87354, 13.3362, 14.3738, 7.65991, 14.2517, 13.7024, 14.5874]
DELTA [17, 11, 11, 17, 11, 27, 45]
CP (ms) [1.49537, 1.15967, 1.34278, 1.37328, 1.12915, 2.07518, 3.14331]
ULAB (ms) [7.78198, 13.2752, 14.4653, 7.65992, 14.1296, 13.5803, 14.6484]

BTW, I gave up using the value from a synthio.LFO object in my code as the update every 256 samples (at 32k = 8ms) was too coarse.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions