#synthio feedback and questions

1 messages Β· Page 1 of 1 (latest)

left glen
#

@floral pulsar @agile hazel (and anyone else) let's have the discussion here if that works for y'all

#

I'll be in and out all weekend

agile hazel
#

πŸ‘

agile hazel
#

@left glen excellent, thanks, this is great. One idea @floral pulsar had that would be good to discuss is the notion of having an LFO in synthio that could be directed at other parameters for modulation.

#

So, for example, the depth of tremelo could modulated by a free-running LFO with a particular frequency and waveshape.

velvet thunder
#

I'm less then even a newbie at most of this but I wonder does it make sense to come up with an interface all filters can use so we could feed A->B->C->D and allow people to mix and match and order as they wish?
(spending my weekend deep in YT and other resources now that I'm sucked in)

agile hazel
#

here's a simple example of introducing an LFO to modulate the volume of a voice

#

@velvet thunder are you referring to low pass/high pass filters and the like?

velvet thunder
agile hazel
#

@velvet thunder right on, this is one of the great things about creating and using synths. The signal chain is exposed and re-configurable in varying degrees -- from "very" in modular synths, to "somewhat" in semi-modulars, to "not at all" in synths that only offer preset configurations.

left glen
#

I've started working on a more general LFO concept. So far it's not actually hooked to the audio synthesis, but here's the gist and some testing...

import array  
import synthio
import ulab.numpy as np
    
SAMPLE_SIZE = 1024
VOLUME = 32767 
ramp = np.linspace(-VOLUME, VOLUME, SAMPLE_SIZE, endpoint=False, dtype=np.int16)
sine = np.array(
    np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
    dtype=np.int16,
)

l = synthio.LFO(ramp, rate=7, offset=1)
m = synthio.LFO(sine, rate=8, offset=l, scale=8) 
n = synthio.LFO(sine, rate=m, offset=-2, scale=l)
lfos = [l, m, n]

for i in range(200):
    print(" ".join(str(v) for v in synthio.lfo_tick(*lfos)))

lfo_tick is just for testing. the 3 graph traces are l, m, and n, which are just a nonsense combination. In reality, you'd say note.bend = n, assigning it an LFO directly to bend the pitch of a note.

#
BlockInput = Union("LFO", float)

class LFO:
    """A low-frequency oscillator

    Every `rate` seconds, the output of the LFO cycles through its `waveform`.
    The output at any particular moment is ``waveform[idx] * scale + offset``.
    Internally, the calculation takes place in fixed point for speed.

    `rate`, `offset`, `scale`, and `once` can be changed at run-time.

    An LFO only updates if it is actually associated with a playing Note,
    including if it is indirectly associated with the Note via an intermediate
    LFO.

    Using the same LFO as an input to multiple other LFOs or Notes is OK, but
    the result if an LFO is tied to multiple Synthtesizer objects is undefined."""

    def __init__(
        self,
        waveform: ReadableBuffer,
        *,
        rate: BlockInput = 1.0,
        scale: BlockInput = 1.0,
        offset: BlockInput = 0,
        once=False,
    ):
        pass
    waveform: Optional[ReadableBuffer]
    """The waveform of this lfo. (read-only, but the values in the buffer may be modified dynamically)"""
    rate: BlockInput
    """The rate (in Hz) at which the LFO cycles through its waveform"""
    offset: BlockInput
    """An additive value applied to the LFO's output"""
    scale: BlockInput
    """An additive value applied to the LFO's output"""

    once: bool
    """True if the waveform should stop when it reaches its last output value, false if it should re-start at the beginning of its waveform"""

    phase: float
    """The phase of the oscillator, in the range 0 to 1 (read-only)"""

    value: float
    """The value of the oscillator (read-only)"""

    def retrigger():
        """Reset the LFO's internal index to the start of the waveform. Most useful when it its `once` property is `True`."""
floral pulsar
# left glen ```py BlockInput = Union("LFO", float) class LFO: """A low-frequency oscill...

This is great! Multiple thumbs up. And the value property is perfect for letting us do wavetable sweeps or emulating filter sweeps via wave mixing (e.g. waveform[:] = lerp(wave1, wave2, lfo.value) (something I really like doing)
I would love the ability to have a free-running LFO, instead of having its phase reset on note on. This would allow beat-sync'd effects for the LFO that is independent of the cadence of the played notes. (this concept would be useful for all modulators like tremolo & vibrato too) But I can understand if that's not possible.

left glen
#

in the current design, an LFO wouldn't advance unless it's associated with a playing note. this limitation might be lifted. right now no LFOs are reset except on a manual retrigger but they would stop when not attached to a playing note.

#

(hypotehtically a synthesizer could have a list of "associated LFOs" that you maintain by hand, and they would advance merely by this association)

#

let me see if I can get the basics to work again first πŸ™‚

left glen
velvet thunder
#

Question if it isn't to long, what did you use to generate the FIR filter? been reading a lot about them but haven't looked too in detail about creating them yet

left glen
#

sadly their "python script that generates the coefficients" doesn't work on ulab, several functions are missing.

agile hazel
#

@left glen the LFO demo is great, modulating the modulator is super helpful for keeping things sounding alive, love it

left glen
#

you can even chain an lfo to itself and use it (e.g., l.offset=l), you just can't print() it without an error.

velvet thunder
#
import ulab.numpy as np

def sinc(x):
    #x = np.asanyarray(x)
    y = np.pi * np.where(x == 0, 1.0e-20, x)
    return np.sin(y)/y

def blackman(M):
    values = np.array([0.0, M])
    M = values[1]

    if M < 1:
        return np.array([], dtype=values.dtype)
    if M == 1:
        return np.ones(1, dtype=values.dtype)
    n = np.arange(1-M, M, 2)
    return 0.42 + 0.5*np.cos(np.pi*n/(M-1)) + 0.08*np.cos(2.0*np.pi*n/(M-1))

# Configuration.
fS = 20000  # Sampling rate.
fL = 5000  # Cutoff frequency.
N = 47  # Filter length, must be odd.

# Compute sinc filter.
h = sinc(2 * fL / fS * (np.arange(N) - (N - 1) / 2))

# Apply window.
h *= blackman(N)

# Normalize to get unity gain.
h /= np.sum(h)

print(h.tolist())```
#

That should (hopefully) let someone calculate FIR filter coefficients at runtime

#

Just noticed that the high pass/band pass filters have slightly different code but should be pretty easy to make something to handle all cases. I'll take a look at it soon if no one else does before. (If no one thinks this adds value then please let me know too πŸ™‚ )

left glen
#

Cool! I was working on something similar, but didn't finish before the day was done

left glen
#

here's my version, with mark's implementation of sinc: https://gist.github.com/jepler/2ad54d4cb80825435d166a44928c6d7e

try:
    from ulab import numpy as np
except ImportError:
    import numpy as np


def sinc(x):
    y = np.pi * np.where(x == 0, 1.0e-20, x)
    return np.sin(y)/y

def lpf(fS, f, N):
    if not (N & 1):
        raise ValueError('filter length must be odd')
    h = np.sinc(2 * f / fS * (np.arange(N) - (N - 1) / 2))
    h[(N-1)//2] = 1 # repair bad sinc() function
    return h * (1/np.sum(h))

def hpf(fS, f, N):
    if not (N & 1):
        raise ValueError('filter length must be odd')
    h = -lpf(fS, f, N)
    h[(N - 1) // 2] += 1
    return h

def brf(fS, fL, NL, fH, NH):
    hlpf = lpf(fS, fL, NL)
    hhpf = hpf(fS, fH, NH)

    if NH > NL:
        h = hhpf
        h[(NH-NL) // 2 : (NH-NL) // 2 + NL] += hlpf
    else:
        h = hlpf
        h[(NL-NH) // 2 : (NL-NH) // 2 + NH] += hhpf

    return h

def bpf(fS, fL, NL, fH, NH):
    hlpf = lpf(fS, fL, NL)
    hhpf = hpf(fS, fH, NH)
    return np.convolve(hlpf, hhpf)

if __name__ == '__main__':
    print("lpf(24000, 2040, 13) # 1920Hz transition window")
    print(list(lpf(24000, 2040, 13)))

    print("hpf(24000, 9600, 13) # 960Hz transition window")
    print(list(hpf(24000, 9600, 23)))


    print("bpf(24000, 1200, 11, 3960, 15) #  2400Hz, 1600Hz transition windows")
    print(list(bpf(24000, 1200, 11, 3960, 15)))

    print("brf(24000, 960, 19, 2400, 13) # 1200, 1800Hz transition windows")
    brf_tst = brf(24000, 960, 19, 2400, 13)
    print(brf_tst)

    print("brf(24000, 960, 13, 2400, 19) # 1200, 1800Hz transition windows")
    brf_tst = brf(24000, 960, 13, 2400, 19)
    print(brf_tst)
Gist

GitHub Gist: instantly share code, notes, and snippets.

velvet thunder
#

Is there worth in using the Window type functions? I don't know enough to know the value of it. I used blackman just because that was what the filter setting was on.

left glen
#

and I used "rectangular" because it gave the smallest number of coefficients for any given settings

#

bold suggestion: listen to a rectangular and then a blackman filter and decide for yourself if the difference is important to your ear

#

I also don't understand the relationship between N and transition bandwidth

velvet thunder
#

Good idea, have to try to hook them up later to listen.

left glen
#

1/32767 is -45dB. The default filter with blackman window is all lower than -45dB past about 0.137Hz (top transition bandwidth nominally 0.140Hz) using 59 coefficients. With rectangular window it only gets down to about -25dB but requires just 13 coefficients.

I haven't really pushed the boundaries as far as how many coefficients are practical; processing time goes up with more coefficients (surprise!) and will eventually begin skipping.

#

my gist updated to allow calculating blackman window

#

first is no filter, 2nd is rectangular window 13 coefficients, 3rd is blackman window 59 coefficients. tbh I dont hear much difference between 2 & 3

velvet thunder
#

Maybe a bit in the high end but my work headphones aren’t exactly quality. For a lot of people it may be a trade off based on the microcontroller chosen. I have to try to push the M7 later today

left glen
#

and of course we can overclock the rp2040 now in circuitpython! was wondering how long it took to calculate a filter ... (times in seconds for 100 repetitions) ```py

microcontroller.cpu.frequency = 125000000
t0 = time.monotonic_ns(); _=[mkfilter.brf(48000,4800,17,3600,27) for i in range(100)]; t1= time.monotonic_ns(); (t1-t0) / 1e9 / 100
0.0040625
microcontroller.cpu.frequency = 180000000
t0 = time.monotonic_ns(); _=[mkfilter.brf(48000,4800,17,3600,27) for i in range(100)]; t1= time.monotonic_ns(); (t1-t0) / 1e9 / 100
0.00282227
microcontroller.cpu.frequency = 250000000
t0 = time.monotonic_ns(); _=[mkfilter.brf(48000,4800,17,3600,27) for i in range(100)]; t1= time.monotonic_ns(); (t1-t0) / 1e9 / 100
0.00203125

left glen
#

changes I'll be pushing in a bit just pushed:

  • Synthesizer.change is replacing release_then_press. The old name will be supported as a compatibility alias for now.
  • change can accept single notes (e.g., synth.change(press=37) to start a MIDI note)
  • change has a new retrigger= kwarg for retriggering LFOs (single LFO or sequence of LFOs)
  • Synthesizer.lfos is a list of LFOs that are advanced regardless of whether they're associated with an active note, so you can now have free-running LFOs as long as you have a playing synthesizer to hang them off of.
left glen
#

hum I thought a synthio.LFO was configurable so as to compute A*B or A+B but now I can only figure out how to compute B-A (waveform=CONSTANT_MINUS_ONE, scale=A, offset=B)

Should I introduce additional kinds of arithmetic nodes, or how is this traditionally done?

velvet thunder
#

Just FYI I did try a 127 tap FIR filter across 12 simultaneous notes on the M7 and noticed zero delay in anything going on. (not sure if the number of notes mattered but figured I'd try).

left glen
#

Nice! The FIR is applied to all filtered notes together, so the number of playing notes has a modest effect

velvet thunder
#

One strange behavior that I cannot attribute yet (maybe my filter coefficients) is once I surpassed around 10Khz the sound came back. It was a low pass filter at 2500Hz with 500Hz transition. Between 3Kz to around 10Khz it worked great. But beyond that back to normal. I'll keep poking at that though.

left glen
#

that's interesting. what sample rate?

#

It's of course possible (nay, likely) there's an error in my FIR implementation

velvet thunder
#

Sorry was out for a bit. I used sample rate 10K, with 2KHz frequency and 127 coefficients

agile hazel
left glen
#

continuing to play with filters. here's audacity's plot of a square wave sweep no filter (but with envelope); folowed by 13-tap-rectangular, 59-tap-blackman, and last is 59-tap-rectangular.

#
filter_rectangular = mkfilter.LPF(48000,800, 13)
filter_rectangular_big = mkfilter.LPF(48000,800, 59)
filter_blackman = mkfilter.LPF(48000, 800, 59, win=mkfilter.blackman)
#

all have some high frequency content, but 13-tap-rectangular has the most. oh and the sweep frequency should have topped out at 3840Hz which is 4x the filter's nominal cut-off frequency

velvet thunder
#

Everything else aside actually seeing the "rectangle" steps in the 13-tap filter is pretty cool.

#

I was looking briefly to see how that website calculated the number of taps based on transition bandwidth. Would be interesting to specify I want a 1Khz vs 5Khz transition. Though not sure it is useful or not.

left glen
#

function C4(e, d, c, a) {
  if (e === 'Kaiser') {
    var g = Math.ceil((c - 8) / (2.285 * 2 * Math.PI * (d / a))) + 1;
    if (!(g % 2)) {
      g++
    }
    return g
  }
  var f;
  switch (e) {
    case 'Rectangular':
      f = 0.91;
      break;
    case 'Hamming':
      f = 3.1;
      break;
    case 'Blackman':
      f = 4.6;
      break
  }
  var g = Math.ceil(f / (d / a));
  if (!(g % 2)) {
    g++
  }```the page actually uses 4.6 for blackman
#
>>> def nsamples(fS, fB, factor=4.6):
...     b = fB/fS
...     return round(factor/b) | 1
... 
>>> nsamples(48000, 800) # fiiir website uses 277
277
#

I assume the synthio filter has any detectable filter response at higher frequencies with blackman window, because the filter coefficients have been rounded off

left glen
#

soon there will be a Math block that can perform one of a range of math operations. You can use it everywhere you used an LFO, so for instance you can use a Math block to add together an overall bend value plus a vibrato amount from an LFO. You can also use it for a source of a control value you want to distribute to multiple other blocks.


class MathOperation:
    """Operation for a Math block"""

    SUM: "MathOperation"
    """Computes ``a+b+c``. For 2-input sum, set one argument to ``0.0``"""

    ADD_SUB: "MathOperation"
    """Computes ``a+b-c``. For 2-input subtraction, set ``b`` to ``0.0``"""

    PRODUCT: "MathOperation"
    """Computes ``a*b*c``. For 2-input product, set one argument to ``1.0``"""

    MUL_DIV: "MathOperation"
    """Computes ``a*b/c``. If ``c`` is zero, the output is ``1.0``."""

    SCALE_OFFSET: "MathOperation"
    """Computes ``(a*b)+c``"""

    OFFSET_SCALE: "MathOperation"
    """Computes ``(a+b)*c``. For 2-input multiplication, set ``b`` to 0."""

    LERP: "MathOperation"
    """Computes ``a * (1-c) + b * c``."""

    CONSTRAINED_LERP: "MathOperation"
    """Computes ``a * (1-c') + b * c'``, where ``c'`` is constrained to be between ``0.0`` and ``1.0``."""

    DIV_ADD: "MathOperation"
    """Computes ``a/b+c``.  If ``b`` is zero, the output is ``c``."""

    ADD_DIV: "MathOperation"
    """Computes ``(a+b)/c``.  For 2-input product, set ``b`` to ``0.0``."""

    MID: "MathOperation"
    """Returns the middle of the 3 input values"""

    MAX: "MathOperation"
    """Returns the biggest of the 3 input values"""

    MIN: "MathOperation"
    """Returns the smallest of the 3 input values"""

    ABS: "MathOperation"
    """Returns the absolute value of ``a``"""

class Math:
    """An arithmetic block

    Performs an arithmetic operation on up to 3 inputs. See the
    documentation of ``MathOperation`` for the specific functions available.

    The properties can all be changed at run-time.

    An Math only updates if it is actually associated with a playing `Synthesizer`,
    including indirectly via a `Note` or another intermediate Math.

    Using the same Math as an input to multiple other Maths or Notes is OK, but
    the result if an Math is tied to multiple Synthtesizer objects is undefined.

    In the current implementation, Maths are updated every 256 samples. This
    should be considered an implementation detail.
    """

    def __init__(
        self,
        operation: MathOperation,
        a: BlockInput,
        b: BlockInput = 0.0,
        c: BlockInput = 1.0,
    ):
        pass
    a: BlockInput
    """The first input to the operation"""
    b: BlockInput
    """The second input to the operation"""
    c: BlockInput
    """The third input to the operation"""
    operation: MathFunction
    """The function to compute"""

    value: float
    """The value of the oscillator (read-only)"""

#

and this also opens the field for more types of blocks with less work. not that I know other kinds of blocks I want to make right now.

#

integrator?

floral pulsar
#

this is pretty cool! An integrator could be fun. In some synths they call it a "lag processor" (lol!) and it was often used to take a stepped random value (i.e. "sample & hold") and smooth out the transitions between the random values

#

I am very happy lerp() is a base operation. I've been copying my lerp function around into almost all my Python & Arduino programs

#

I've not done much with FIR filters for synths. Usually I do IIR style filters because the memory requirements are lower. Like, my homemade filters have looked a lot like the bottom of this page: https://basicsynth.com/index.php?page=filters

#

So I guess my question is: how do I make an adjustable frequency FIR filter so I can do filter sweeps?

left glen
#

I don't know that there's an effective way to create frequency filter sweeps with FIR filters.

#

Initially I drew an incorrect conclusion that IIRs couldn't do high pass but based on that page it seems they can. If they can do high, low, band, and notch then maybe it makes sense to change..

velvet thunder
floral pulsar
#

yeah, if it's glitchless (and recalc is fairly cheap) then it doesn't really matter

#

also, the topic of "synthesis filters" is so varied and crazy that I think there's no one right way

#

a big deal when the "virtual analog" synths were coming out in the 90s was that they had DSP algorithms that emulated the wonky filter circuits from cherished 70s synths. So not IIR nor FIR but something else entirely

velvet thunder
#

Managed to load a raw audio waveform into synthio without much trouble:

with open('bass2.raw','rb') as file:
    raw = file.read()
bass = np.zeros(len(raw)//2, dtype=np.int16)
for i in range(0,len(raw),2):
    bass[i//2] = raw[i+1] << 8 | raw[i]
raw = None # free memory```
left glen
#

the memoryview().cast() technique might help get the result you need for raw files too, with less code

velvet thunder
#

Yeah that's where my lack of python experience shows, I'll have to look at it. I knew it was messy

#

Yup: bass = np.array(memoryview(raw).cast('h'), dtype=np.int16). Thanks I knew it could be cleaner

velvet thunder
#

I've tested a fair amount of the new PR functionality but more on a "does it work" basis, I'm not an expert on how to use it to sound good. I know it is in draft I can go through the code more thoroughly when it's good to go

left glen
#

did you have some success with drum sounds as synthio notes? I'm wondering if that needs any special support.

#

appreciate any and all testing. just knowing you're not getting crashes or ear-splitting bad sounds is a help. or at least not ear-spitting bad sounds you think are attributable to bugs in synthio

velvet thunder
velvet thunder
left glen
#

playing as a wav file is good if you don't want to read the whole thing into RAM, but of course you can't change the pitch..

#

or pan it, if you have stereo output

floral pulsar
#

shoot if we can pipe WAVs into synthio, next thing may be to support arbitrary loop points in WAV, and then we're a quick jump to using the huge number of SoundFonts

left glen
#

I don't want to get into WAVs that don't fit within RAM but mmmmaybe adding arbitrary loop points would be doable

#

we are trying to wrap up the major synthio work soon so this might be a contribution for someone else to make

floral pulsar
#

yes

#

agreed

#

I finally got time to play with synthio.LFOs. Very fun! Question: is there a way to run an LFO without connecting it to a synth parameter? Right now I'm connecting it to ring_bend with ring_freq=1 so I can use the LFO as a visual indicator to let you adjust it before bringing it in to the sound, like this:

note = synthio.Note( frequency=110 )  # 110 Hz = A2

wave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)

lfo1 = synthio.LFO(rate=0.3, waveform=wave_sine)
note.ring_frequency = 1  # inadible
note.ring_bend = lfo1  # now we can get LFO values
synth.press( (note,) )  # turn on the note and the LFO

led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=1.0)  # visual indicator

while True:
    print("lfo", lfo1.value, lfo1.phase)
    led.fill( (map_range(lfo1.value, -1,1, 0,255), 0,0 ) )
    time.sleep(0.05)
left glen
#

yes, Synthesizer has this property:

    lfos: List[LFO]    
    """A list of LFOs to advance whether or not they are associated with a playing note.    
   
    This can be used to implement 'free-running' LFOs. LFOs associated with playing notes are advanced whether or not they are in this list.    
   
    This attribute is read-only but its contents may be modified by e.g., calling append() or remove(). It is initially an empty list."""    
#

(but the synthesizer itself has to be playing)

floral pulsar
#

ahh!

#

hmm, I do not see that in #7985. Am I on the wrong PR?

#

oh I see, it's only constructed after the synth is running. nevermind!

left glen
#

hum which has the effect that it's not shown in the repr of a synthesizer

floral pulsar
#

However, I am unclear how to use it. This prints an empty list:

note = synthio.Note( frequency=110 )  # 110 Hz = A2
lfo1 = synthio.LFO(rate=0.3, waveform=wave_sine)
synth.press( (note,) )
while True:
    print(synth.lfos)
    time.sleep(0.05)
left glen
#

synth.lfos.append(lfo1)

floral pulsar
#

ahah!

#

AttributeError: 'Synthesizer' object has no attribute 'append' 😦 no wait I am dumb

left glen
#

(oh a synthesizer doesn't have a useful repr anyay)

floral pulsar
#

yeah no problem there. but I do like tabbing through in the REPL to see the properties of an object

#

and yes, synth.lfos.append(lfo1) works as expected

left glen
#

one thing to be aware of is if you ever don't need lfo1 again you have to manually remove it from synth.lfos

floral pulsar
#

right

#

I'm just going to keep adding LFOs and never remove them. I also never reboot my linux server box. 1035 days uptime!

left glen
#
This attribute is read-only but its contents may be modified by e.g., calling `synth.lfos.append()` or `synth.lfos.remove()`. It is initially an empty list.
#

would this be better in the docs?

#

or lfos.append()?

floral pulsar
#

hmmm. this is where my inexperience with "the Python way" is a hindrance. It is not immediately obvious that synth.lfos is a list I can modify. So either that paragraph you have above or a small example (synth.lfos.append(synth.LFO(rate=1)); while True: print(synth.lfos[0].value)) would be good

left glen
floral pulsar
#

true

left glen
#

at some point we'll start doing synthio guides and that'll have more examples

floral pulsar
#

and it is okay to do?

lfo1 = synthio.LFO(rate=1)
note = synthio.Note(110)
synth.lfos.append(lfo1)
note.amplitude = lfo1
left glen
#

you mean, can an lfo be associated with a note AND in the LFOs list? Yes. It could be associated with 12 notes, or indirectly 3 times -- it'll still advance at 1Hz

floral pulsar
#

perfect

#

that's awesome

left glen
#

if not it's a bug

floral pulsar
#

this will let us have easy visual indicators of LFOs

#

this is very exciting

left glen
#

I'm going to head out, but I'll read back if you have any other questions or suggestions .. hope you'll do another video!

floral pulsar
#

yes! working on some hardware to let me stretch out a bit more. Will also be doing a MacroPad RP2040 demo or two using my MacroPadSynthPlug (if you or anyone on this thread wants one, send me a DM with a shipping address and I'll mail it off)

left glen
#

πŸ‘‹

velvet thunder
left glen
left glen
#

@floral pulsar A couple of specific requests, depending on the time you can devote to it:

  1. if any of the naming is bad, tell me and we can come up with better naming together. secondarily, the same for the docs, any huge omissions that would be good to rectify.
  2. It would be great if you would collaborate with me on a guide for synthio. if it helps, there is budget to pay an external author. I could write the dry "here are the base capabilities" parts and you could write "here are some ways to achieve common effects or evoke sounds" to help people with a synth background learn to translate their ideas into synthio. a basic 'live midi to synthio synthesizer' program would be cool too. This would be separate from any project guides using synthio, which might happen in the future.
  3. identifying any items related to synthio that could go in an adafruit-maintained or community-maintained library and/or contributing to such a library
#

8.1.0 will likely go out WITHOUT the current PR in it, which will go in 8.1.1 or 8.2.0 (the next release)

#

hopefully after the current PR we can label the API as stable

floral pulsar
# left glen <@352910176736772096> A couple of specific requests, depending on the time you c...
  1. Will do. I'll give it a look over tomorrow and give responses by Tues end. (today is mostly non-CirPy meetings for me) So far it makes sense with the APIs I've used. I've not yet touched filters as that doesn't have an immediately recognizable of an API like synthio.setFilter(LOWPASS, frequency=1234, q=1.3)
  2. Yes, I would love to help out. I was already thinking of making a "synthio tricks" repo showing common setups & techniques & gotchas. I think your tests in tests/circuitpython/synth* & tests/circuitpython-manual/synthio are very interesting. I've been cribbing from them. Maybe collate them into a "reference" section at the back of the guide?
  3. Synthio already has so much! A month ago I was thinking "audio rate" stuff would live in synthio and anything "control rate" would be user libraries. But some control-rate like LFOs & envelopes really straddle that arbitrary distinction, so I am glad they are in the core.
    One user-level library could be a version of envelopes that allows more complicated & looping envelopes. (and may just do clever twiddling of synthio.LFO)
    If the synthio.filter API stays the way it is, another good user library would be a translator from filterType/cutoff/resonance -> FIR coeffs
left glen
#

the reason for having LFOs in the core is so that they can continue advancing at the right rate no matter what 'blocking' things a user might do with the rest of their circuitpython code (right up until you overload the CPU and it can't keep up no matter what)

floral pulsar
#

Here's my notes on the API thus far. I have been having so much fun with it.

#

well darn discord markdown renderer is weirder than github's

left glen
#

@floral pulsar Is 4.75V = A4 a 'standard'?

floral pulsar
#

kinda? it seems not well defined what the "base note" is. but I believe integer voltages are C (e.g. 0V = C2, 1V = C3, 3V = C4, etc)

#

the people to ask about this may be @cobalt lance or Stargirl

#

I know I am forever having to tune various oscillators in my rack to get them all on the same note, but once that's done, the 1V/Oct signals keeps them running in parallel, melody-wise

left glen
#

oh it's a simple bug

floral pulsar
#

it seemed like it πŸ™‚

cobalt lance
left glen
#
>>> voct_to_hz(5)
261.625
>>> voct_to_hz(4.75)
219.9995246407531
```but now it's off by 2 octaves from that table..
floral pulsar
#

everything is off by octaves in eurorack lol

left glen
#
(261.625, 261.625)
>>> synthio.midi_to_hz(61), synthio.voct_to_hz(2.0833)
(277.1820320617506, 277.1756278709341)
floral pulsar
#

nice

left glen
#
  • Attaching an LFO to bend seems to disable ringmod. Is this intended?
    Can you enter a bug with this? My testing tried to cover each thing in isolation but not always everything in combination. there are lots of combinations.
floral pulsar
#

Sorta unrelated adafruit/circuitpython process question for you: if something like this change to voct_to_hz(), which presumably is just two or so lines of functionality and then a few files to change for name refactoring, would you create a PR, submit it, get it merged, or would you commit straight to main?

floral pulsar
left glen
#

It'd go through the PR process, everything does

floral pulsar
#

right, cool thanks!

left glen
#

what'd be the purpose of adding a restriction to bend range? Would a MID math operation let you do that in a more general way?

floral pulsar
#

okay maybe I am thinking too literally of mapping synth config to synthio. Synths typically have a parameter that is "bend range" for a patch, that defaults to +/- one octave but lots of people change it to +/- two semitones because bending a whole octave is only for the heavy metalers of the world. but yes, since we can tune the value sent into .bend we can just do that. And if we need more than that, just adjust .frequency

#

so nix that one

left glen
#

I think making frequency (&ring_frequency) into BlockInputs will be too much work to dash out right away, so please feel free to file a feature request issue about that. It makes me wonder if the "bend"s should go away to be replaced by a BEND math op instead...

#

I was pretty sure +-12 octaves of bending was unreasonable but I worried +-1 octave wasn't enough. what do I know.

floral pulsar
#

hahaha yeah there is no right answer

#

synthio.Note.frequency is noted as BlockInput in the RTD parameter list but not in the constructor

left glen
#

yeah I'll fix the docs to reflect float in both places

#

similarly, an integrator (whether it's a Math or a third kind of block) will need to be a feature request

#

@floral pulsar similarly for changing filters away from the FIR approach to something else, and especially for making them per-note rather than synth-wide, it'll need to be a feature request.

floral pulsar
#

totally

#

I figured many of my suggestions would be unimplementable or future feature request

left glen
#

I appreciate the detailed feedback! I'm happy there were some very easy and actionable items. The feature requests are very specific and actionable too, it seems quite possible that other folks could pick up and implement some of those things.

#

I'll leave envelope & waveform as properties on the synthesizer for now; they do have the advantage of making some code simpler, when all the sounds it produces are the same. but it's also a leftover from when a note was just a number... and that was barbaric

#

synthesizer.press(60)

floral pulsar
#

I was wondering if that was why. It makes it very quick to make a "simple tune player" sort of thing

left glen
#

It also relates to the evolution -- synthio was contributed by a community member, to simply play canned MIDI style tunes .. and it evolved by steps after I started working on it in march

floral pulsar
#

so much!

left glen
#

the waveform was square, there was no envelope, only 2 notes were supported, etc. It was deliberately minimal.

floral pulsar
#

"the waveform was square" will now be on a tshirt with a little _|-|_|-|_ graphic

left glen
#

half tempted to riff on creation myth at this point, but I'd end up making myself g-d so that's not cool

floral pulsar
#

πŸ™‚

floral pulsar
floral pulsar
#

oh btw @left glen thank you for making midi_to_hz() take a float too. It's handy

left glen
#

perhaps someday a person will be able to play https://www.youtube.com/watch?v=sxbporF6GCw on circuitpython

From They Might Be Giants’ album My Murdered Remains. Get it direct from the band http://bit.ly/2EiIG60

He’s the kind of dog
when presented with his heart’s desire
will turn his nose away
He’s the kind of dog
who sees a smiling friend and says
β€œI need no smiling friend today”
He’s a dog who made his money
selling troubled assetsΒ 
Now he s...

β–Ά Play video
#

Composed using a set of "meticulously retuned piano samples", with about 31 tones to the octave, according to John Linnell

left glen
velvet thunder
#

@floral pulsar I have to still look over your notes (pun now intended!) but if there are some features you'd like first let me know, I wouldn't mind taking a run at a couple improvements if Jeff has to move onto other priorities. Always easier to learn how the code works doing something worthwhile at the same time.

floral pulsar
#

but two things I am very interested in: 1. LFO or Envelope-based wavemixing 2. real-time adjustable filter

left glen
#

if you have two Notes that start in the same change() call and they have the same frequency & bend, they should match forever. so you could use the two notes amplitude inputs (one via a math block configured to compute 1-x) to fade between different waves.

#

filters I do not have an easy answer for.

velvet thunder
#

I wonder if any radio projects have a solution for filters. Same basic problem just different frequencies. (More a thought to myself on somewhere to look). The delay to recompute may just be okay in radio though

velvet thunder
floral pulsar
# velvet thunder For wave mixing you mean for example having 33% saw and 66% sine and changing th...

Exactly so! We can approximate some types of filter movements by mixing between a high-harmonic waveform and a filtered version of that waveform. (e.g. in a certain sense, a sine wave is just a heavily filtered saw wave). So to approximate a filter envelope I have been doing things like:

    waveform[:] = lerp( wave_sine, wave_saw, lfo_filter.value )

It would be neat if the was some built-in feature for wave mixing. Not sure what form that would take though

floral pulsar
#

And if anyone would like to have a go at doing simple IIR/delayline filters, here's some example pseudo code showing how few lines it can be: https://beammyselfintothefuture.wordpress.com/2015/02/16/simple-c-code-for-resonant-lpf-hpf-filters-and-high-low-shelving-eqs/ If I have some time next week I may try playing with this in the CirPy core myself

velvet thunder
#

I was looking at Pitch Envelopes (in general) and was wondering is this a feature worth having or could it be done just using the Bend property to change frequency with an LFO to that? (leaning to thinking yes?)

floral pulsar
left glen
left glen
#

[1977, already doing synthesis with envelope!]

floral pulsar
velvet thunder
#

Anyone had any thoughts on the FIR filters per note? Not sure if it would be too intensive (or have tried it yet) but playing about making a drum kit and wondering.

left glen
#

Probably switch to a different filter algorithm entirely, one that can be tuned or swept. FIR doesn't seem to be amenable to changing dynamically like that from my readings

velvet thunder
#

Makes sense. The filter really makes a difference getting a good kick drum sound but it is fairly low frequency. So then a high hat would be lost

left glen
#

weird, it worked a minute ago πŸ˜•

#

ok I made a bug when fixing it to do stereo filter.

#

This audio file has a bunch of simple scenarios inside it:

  • frequency-swept sine wave tests
    • unfiltered
    • lpf, cutoff #1
    • lpf, cutoff #2
    • hpf, cutoff #1
    • hpf, cutoff #2
    • bpf, cutoff #1
    • bpf, cutoff #2
  • frequency-swept square wave, same 7 scenarios as above
  • frequency-swept noise wave, same 7 scenarios as above
  • frequency-swept filter tests
    • static frequency sine wave
    • static frequency square wave
    • static frequency noise wave
floral pulsar
velvet thunder
left glen
#

there seem to be some new pops at the start/end of a note. especially of notes that are all super attenuated by the filter.

#

I'm suspecting there's a problem around the initialization of the history values, called "x" and "y". but it could also be an arithmetic problem with fixed point math as well.

left glen
#

oh and a bug that stops synthesizing all the notes but one

left glen
#

I still don't understand why there's a pop-pop-pop at the start of an 'almost fully filtered' note.

#

zero-initializing the history values must be wrong, but then what is right?

velvet thunder
#

Does anything show up that may be a clue if you look at the sounds waveform in audacity?

left glen
#

the result of my biquad filtering has a DC offset! ouch.

#

so when you get out to the end the note is filtered but the DC offset remains so you hear pops

left glen
#

there are still some artifacts at the end; I use an envelope that goes instantly to 0, so maybe it's expected.

floral pulsar
#

zero-time envelope rates are a common cause of clicks/pops in other synths, if that helps.

#

but the DC offset sounds like the real culprit

left glen
#

for me the big pops at the start are gone but small pops at the end remain.

velvet thunder
#

I am having issues with the bandpass filter not working.

bpf = synth.band_pass_filter(100,10000)
note1 = synthio.Note(frequency=200, waveform=sinwave1, filter=bpf)
synth.press(note1)```
left glen
#

I didn't try the band pass filter so it may well be wrong

velvet thunder
#

On a KB RP2040

left glen
#

does low pass work?

velvet thunder
#

Low and high work

left glen
velvet thunder
#

I'll double check it, no problem. May be later tonight.

#

I didn't notice any real pops on my kick drum sound I was working on with a LPF but it was meant to slightly "pop" at the start even without

left glen
#

this is a pop at an end of an almost entirely filtered note. 2nd image is a pop at the end of a not almost entirely filtered note

velvet thunder
#

Ahhh nm I see what I did. I was assuming the two arguments were the bottom/top ends of the filter. Not the middle/Q. So I was giving it impossible values.

left glen
#

unfiltered versions have visible steps, because envelope is only calculated at what is analagous to webaudio "k-rate", 256 samples I think in the case of circuitpython

#

but that ending linear slide followed by a cut to 0 when the note truly stops is .. weird

velvet thunder
#

I had a highly filtered note and saw the following at the end:

#

Almost like the filter stopped but it kept playing the sound for a fraction

left glen
#

there's clearly something .. interesting .. happening when a note releases.

left glen
#

visually it's much nicer when I filter then apply envelope & panning.. which makes sense if it's the step changes due to envelope that make the end wonky

#

haven't listened to it yet

left glen
#

frustratingly there are a lot more crackles with it re-organized like this. not just at the start or end of a note. hum, I'm becoming convinced I'm hearing the steps of the envelope now, when I wasn't before.

velvet thunder
#

I haven't noticed any crackling in anything I have done yet (admittingly not a ton). I'm going from the 2040 via a PCM5102 directly to my mic in port to listen from that

left glen
#

I should figure out what specific parameters I'm hearing little pop/clicks with. My test is on the prop feather prototype, with my midi keyboard playing arpeggios. I'm using a low-pass filter which is set by two of the sliders on my keyboard. When the corner frequency is low, I hear this artifact on high pitched notes that are almost fully filtered, during their attack & decay phases. It's slow enough to feel like distinct pops, which made me think it might be the steps of attack & decay (which are only recalculated every 256(?) samples, and are calculated as 8-bit values so the steps are pretty big)

left glen
#

OK here's where I hear it on my rp2040:

that's the filter Q & frequency and the resulting calculated filter..

```the note involves a bend but removing that doesn't change the outcome. It's also using a specific waveform, I didn't test if that was critical (AKWF_overtone_0011.wav)

I don't hear it on my host computer test harness so either it's a 32 vs 64 bit calculation difference, or .. idk. I will put my concern about it aside for now.
floral pulsar
# left glen OK here's where I hear it on my rp2040: ```0.5 184.997 Biquad(a1=-1.85974, a2=0....

I think I missed how to go from WAV to BufferProtocol used by synthio? I think I do not understand all the various "array of values" types in Python. I have tried things like:

import synthio, adafruit_wave
import ulab.numpy as np
w = adafruit_wave.open("AKWF_granular_0001.wav")
n = w.getnframes()
try1 = list(memoryview(w.readframes(n)).cast("h"))
try2 = np.array( try1, dtype=np.int16)
note = synthio.Note( 220, waveform=try1) # or try2
left glen
#
import adafruit_wave as wave
def read_waveform(filename):
    with wave.open(filename) as w:
        if w.getsampwidth() != 2 or w.getnchannels() != 1:
            raise ValueError("unsupported format")
        return memoryview(w.readframes(w.getnframes())).cast('h')

waveform = read_waveform('AKWF_overtone_0011.wav')
#

that gives you a "memoryview" form, which should be OK for a note's waveform property but doesn't support the arithmetic like lerp

#

this gives the lerp'able ulab numpy array: ```py
from ulab import numpy as np
def read_waveform_ulab(filename):
with wave.open(filename) as w:
if w.getsampwidth() != 2 or w.getnchannels() != 1:
raise ValueError("unsupported format")
return np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)

#
>>> read_waveform_ulab('AKWF_overtone_0011.wav')
array([647, 1810, 2791, ..., -2157, -1234, -12], dtype=int16)
floral pulsar
#

Awesome thanks. That does work. is 'memoryview' in this case a map on the filesystem or an overlay in RAM? if I wanted to plop it into an np array... ahhh yes

#

thank you

left glen
#

it does read it into RAM, there's not currently a way to "memory map" the data from CIRCUITPY. (and it's not foreseeable that there will be)

floral pulsar
#

gotcha. I want to try scrubbing through a WAV with synthio and with the info in those two functions I can do that

left glen
#

interesting concept!

floral pulsar
# left glen interesting concept!

if it can work, then synthio can be a granular synth too. it probably will not work at first because there is cleverness to "feather" the start & end of the grains so it's not glitchy. but we may be able to do the feathering a little

left glen
#

you won't have control over how many times a buffer gets used .. does it matter?

#

or is that exactly the thing you're referring to

floral pulsar
#

yes, if I understand your question. granular synths basically take a long sample and chop it up into hundreds or thousands of tiny partially-overlapping chunks (called "grains"), then it plays each grain essentially as synthio does now for a waveform. If your "grain index" increments linearly, picking grains one by one, you'll hopefully get a recognizable recreation of the original. but the fun comes when you move that "grain index" in non-linear ways

#

I have low expectations any of my experiments in this will work. Even the best granular synths sound a little like... fuzzy underwater stream rebuffering

floral pulsar
left glen
#

I have no idea what I'm hearing right now but I'm into it

floral pulsar
#

ikr

floral pulsar
#

oops shoulda posted my comments here instead of in the PR. apologies.

left glen
#

either is fine, I appreciate the feedback

#

like Envelope, the biquad filter is a read-only object. Even more so than envelope the consequence of "running" the filter with partially updated values could be bad. A Note can have its entire filter property re-assigned at runtime, though.

left glen
#

so to be clear: the use case of changing filter parameters on a playing note is supported, but it is done by assigning to the Note's filter property, not assigning to the BIquad's properties. This does present some challenges for updating the filter of multiple notes simultaneously.

If this feature should be held until there's a filter object with a Block-settable 'frequency' and 'q_factor' property, then we can put it on hold.

floral pulsar
#

right okay I get the thinking now

#

lemme try some tests

#

also I think I'm hearing some of the glitching you were talking about when a filter is engaged (no attempts at changing it). I cannot reproduce it reliably yet

left glen
#

I wonder if the limited precision of the coefficients & intermediate calculations is just too low in some cases.

floral pulsar
#

That would make sense. I also seem to recall talk about having to massage filter models to behave less ideally at various edge cases; and the positive-feedback / self-oscillating characteristics of hiQ filters could definitely be a place where that could be true

velvet thunder
#

I have been trying to think of ways to have changing filters and/or chained filters/effects (like reverb) but haven't had any time to experiment. But I don't think that should hold up pushing this out.

I will try to do a code review tomorrow if no one else beats me there. I did while it was in draft and nothing stood out though.

left glen
#

Yeah this design doesn't allow for topologies of filters either.

agile hazel
#

Filter is working well for me. Filter frequency cutoff works very nicely, resonance is pretty subtle. I am able to consistently get clicking artifacts if i sweep cutoff to 0.0 and leave it there.

#

i'm able to prevent this if i keep the filter cutoff above 17.

floral pulsar
#

The filters can sound pretty good! Here's a quicky example using bandpass (the most well-behaved filter imo) https://www.youtube.com/watch?v=wIc6rovZ4aA it emits some farts and burps every now and then (perhaps related to that 256-sample-block-to-block state issue that was causing popping?)

Testing out brand new filter capability in synthio.
The filters are still pretty untamed and emit some burps and farts along the way, but they're pretty rad.
code: https://gist.github.com/todbot/c4cad5f17dfe643f4d796ef91cd24036

β–Ά Play video
velvet thunder
#

Random thought I've had for filters and potential effects. Would it make sense to add to a note a chain of effect->filter->something->output to process?

Not saying it is easy, but I can sorta see in the code where it may be possible to process a chain. How biquads work but instead of just one filter, many. And if it is a set interface other effects could be written.

Speed is a potential issue too... just random thoughts I had during the code review and last night thinking about it.

agile hazel
#

If I understand correctly you're suggesting a polyphonic approach that includes per-voice signal paths, yes? A lot of synths are paraphonic (multiple notes/voices at once, but all use the same waveform/voice, filter, and envelope signal path) due to processing constraints or size constraints for analog synths, some are polyphonic (multiple notes, each can be a different voice running through it's own filter and envelope signal path). I haven't run across polyphonic synths that have effects available on a per voice basis other than if you wanted to patch that with a modular synth.

velvet thunder
#

I guess that is what I was thinking. I hadn't really thought of a global signal path (as the filters were global and then they were per note). But for processing there may be something to sharing LFOs and other effects. That would be another change I believe.

Just thinking about ideas really still for when time permits me to look again.

left glen
floral pulsar
left glen
#

oooohhhhhhh

floral pulsar
#

self.audio = audio should solve it

left glen
#

do you want to jump into the thread on the forum or would you like me to follow up?

floral pulsar
#

I can do it, yes

left glen
#

you rock, thank you

floral pulsar
#

np

floral pulsar
#

hey @left glen (or anyone else who may know), what's the status on loadable native modules in a CircuitPython mpy? I'd love to try different filter algorithms with synthio (notably a Moog-style ladder filter emulation), and these compute-only synth algorithms would be perfect to try with native modules

left glen
#

it's not enabled with any builds by default.

floral pulsar
#

dang

#

I was hoping it was a feature of mpy-cross πŸ™‚

velvet thunder
#

I'd have to try to remember the details but I did have an example where I put an AHRS class in a native module. You also have to ensure you compile it for the right class of CPU for the board

floral pulsar
#

yep, that's not a problem to me, a different MPY for each arch

floral pulsar
#

New low-priority question for @left glen: Does synthio implement any kind of voice-stealing algorithm, and if so, what? E.g. if max_polyphony is 11 and 11 voices are .press()ed, what happens when the 12th voice is .press()ed?

left glen
#

If there's no idle voice, but there is a voice in "decay" phase, the one with the lowest envelope volume is chosen. But a playing (attack/decay/sustain phase) note is never replaced. Happy to entertain replacing this algorithm with a better one.

#

in the source code, find_channel_with_note if-block on note == SYNTHIO_SILENCE is where the decision would be made. The info to know the "oldest note" currently playing would have to be tracked, it's not available now.

floral pulsar
#

Cool thanks. That's a pretty good algorithm. Voice-stealing choices are another one of these black arts and no one choice is right for every situation. e.g. oldest note is another common strategy but it really messes up if you have a long-duration held bass note and a high pitch arpeggio steals it away, destroying the low-end of a song

floral pulsar
#

Another question for @left glen: how does one get the current value of a synthio.Envelope? There does not appear to be a .value . Alternatively, I can't find anything in synthio.Note that indicates current note volume

left glen
#

I think you're right that this is missing.

left glen
#

I think it would have to be a property of a Note, since the Envelope just holds the information about the times & levels

floral pulsar
#

right. I'll look into it. it may be simple enough I can even submit a PR. bwa ha ha

left glen
#

at a quick look it may be a bit involved ...

floral pulsar
#

drat. okay I'll file an issue in any case. I can approximate knowing when a note is silent from note_off and release_time

#

it's not a big deal

left glen
#

is that what you actually need, an is_playing property?

floral pulsar
# left glen is that what you actually need, an `is_playing` property?

For the particular case I was dealing with, yes, a Note.is_playing or Envelope.is_playing would work. Specifically, I am creating filter modulation envelopes that need to roughly track what the amplitude envelope is doing so they know when to stop modulating after key release. But I can dead reckon it by knowing key release time and amplitude envelope release_time

velvet thunder
#

@floral pulsar Feel free to tag me on the issue if you aren't working on it. I may have a bit more time soon and looking for something to poke at. From the sounds of it having something that returns where in the envelope (phase and how far into it) you are for a note is what is needed?

left glen
#

expanding a bit on the mention of voice stealing -- there's probably more to be done before that's 100% possible, but the idea is that python code armed with the new note state info can decide on a voice to release if it knows too many are already playing... and then pass that info into .change()

agile hazel
#

@left glen is the note signal flow oscillator > envelope > filter or oscillator > filter > envelope?

#

(both are valid and common in typical synth workflows)

left glen
agile hazel
#

Gotcha, thanks @left glen

floral pulsar
#

okay so I'm not suggesting that this has to be the logo for synthio...

agile hazel
#

Blinka may wanna hit the dumbell curls a little bit