Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

synthio: Add LFOs #7985

Merged
merged 36 commits into from
May 23, 2023
Merged

synthio: Add LFOs #7985

merged 36 commits into from
May 23, 2023

Conversation

jepler
Copy link
Member

@jepler jepler commented May 15, 2023

This switches from hard coded tremolo/vibrato, to allowing much more control via nestable LFOs.

The new LFO object's API is like so:

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``.

    `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`."""

A Note object now has several properties that are LFOs, replacing the earlier "depth" and "rate" properties: bend, amplitude, and ring_bend.

LFOs can refer to other LFOs and most things can be re-assigned dynamically, so for instance this program starts with a simple tone, then modulates the amplitude with a first LFO, and then modulates the rate of the first LFO with a second LFO:

n = Note(440, amplitude=.3, waveform=sine)
s.release_all()
input()

s.press((n,))
input()

n.amplitude = synthio.LFO(offset=.3, scale=.2, rate=1, waveform=sine)
input()

n.amplitude.rate = synthio.LFO(scale=4, rate=.25, waveform=sweep_in)          
input()

s.release_all()

The file doesn't seem to be embeddable here but I put a demo up in mp3 format: https://emergent.unpythonic.net/files/sandbox/lfo_test.mp3

Other API changes:

  • Synthesizer methods that took a sequence of notes-or-integers can also now take a single note or integer
  • The release_then_press API becomes change and adds an optional list of LFOs to retrigger. It's now documented that this is intended to be atomic with respect to sound generation, though this is only true if the iterable arguments are actually tuples or lists.

todo:

  • allow a way to re-trigger multiple LFOs during the same instant of audio synthesis
  • allow a way to make LFOs free running, so that they keep being updated even if not associated to a playing note
  • finish updating the Note docs
  • Allow midi note 0 (Closes synthio.midi_to_hz() range should be 0-127 not 1-127 #7999)

@jepler jepler force-pushed the synthio-lfo-dag branch from 7e00e4e to 1244007 Compare May 15, 2023 23:05
@jepler jepler force-pushed the synthio-lfo-dag branch from 1244007 to f832123 Compare May 16, 2023 02:15
jepler added 9 commits May 16, 2023 10:07
Semi-incompatible name change: The method `release_then_press`
is now `change`. For now a compatibility alias is supported.

Everywhere a `NoteSequence` was accepted, a single note is now accepted.
So for instance, `synth.press(30)` can be written instead of requiring
``synth.press((30,))`. The same goes for `change.retrigger`, which
will accept a single LFO or a sequence.
up to +-12 sounds good, right?
@jepler jepler force-pushed the synthio-lfo-dag branch from 5f8fe29 to 3914381 Compare May 17, 2023 21:32
jepler added 13 commits May 19, 2023 11:56
these are always h-type buffers, so let's make the "len" be the element
count, not the byte count.
LFO waveforms are now linearly interpolated by default, but a new
property (interpolated=False) can disable this.

The 'once' logic was improved
this will be used to make MathOperation enum values callable to
construct a Math object with that function
.. in case the items in lfos are not actually LFOs
@todbot
Copy link

todbot commented May 22, 2023

So far in my testing LFOs are great and work well.
One minor addition would be to have phase be in the LFO constructor so we can have multiple LFOs sharing the same waveform but being out-of-phase.

jepler added 3 commits May 22, 2023 10:45
I plotted and eyeballed these and they all looked plausible
I looked at all the results and they pleased me
@jepler
Copy link
Member Author

jepler commented May 22, 2023

I added phase_offset to LFOs, I think it accomplishes that request.

@jepler jepler marked this pull request as ready for review May 22, 2023 15:56
Copy link
Member

@gamblor21 gamblor21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple documentation comments. The rest looked good to me. Just commenting now pending all the test fixes I know you are aware of.

shared-bindings/synthio/Note.c Show resolved Hide resolved
shared-bindings/synthio/Note.c Outdated Show resolved Hide resolved
shared-bindings/synthio/Note.c Outdated Show resolved Hide resolved
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor thing from me. Good otherwise!

shared-bindings/synthio/LFO.c Outdated Show resolved Hide resolved
jepler added 5 commits May 22, 2023 21:58
The underlying routine can return numbers for higher and lower
octaves. Other bits of the code might have frequency limitations
but that doesn't mean we shouldn't let someone get a ~4Hz "note"
by sending in (-12), because that's actually totally plausible as
an LFO frequency.
@jepler
Copy link
Member Author

jepler commented May 23, 2023

Thanks for the review comments, I think I got 'em all.

@jepler jepler requested a review from tannewt May 23, 2023 11:18
Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/12 is 0.0833

shared-bindings/synthio/Note.c Outdated Show resolved Hide resolved
shared-bindings/synthio/Note.c Outdated Show resolved Hide resolved
Co-authored-by: Dan Halbert <halbert@adafruit.com>
@jepler jepler requested a review from dhalbert May 23, 2023 13:59
@dhalbert
Copy link
Collaborator

A couple of board builds are just stalled for CI reasons. No reason to think they would not complete. I will merge.

@dhalbert dhalbert merged commit b26e4ca into adafruit:main May 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

synthio.midi_to_hz() range should be 0-127 not 1-127
5 participants