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

Add synthio.BlockInput support to synthio.Note waveform loop parameters #9788

Merged
merged 7 commits into from
Nov 4, 2024

Conversation

relic-se
Copy link

@relic-se relic-se commented Nov 4, 2024

The following int properties of synthio.Note have been updated to support synthio.BlockInput:

  • synthio.Note.waveform_loop_start
  • synthio.Note.waveform_loop_end
  • synthio.Note.ring_waveform_loop_start
  • synthio.Note.ring_waveform_loop_end

The most common use-case for this feature is controlling PWM/duty-cycle on a square waveform with a synthio.LFO object (see example below), but it could be utilized for other wave-shaping scenarios.

This PR addresses the following issue: #9780.

import audiobusio
import board
import math
import synthio
import ulab.numpy as np

MIN_DUTY = 10 # percentage
SIZE = 256
VOLUME = 32000
SAMPLE_RATE = 48000

audio = audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
audio.play(synth)

waveform = np.concatenate((
    np.full(size_hi := math.floor(SIZE * MIN_DUTY / 100), VOLUME, dtype=np.int16),
    np.full(size_lo := SIZE - size_hi, -VOLUME, dtype=np.int16)
))

duty_lo = math.ceil(size_hi + size_lo * MIN_DUTY / (100 - MIN_DUTY * 2))
duty_range = (SIZE - duty_lo) / 2
duty_mid = duty_range + duty_lo

note = synthio.Note(
    frequency=110,
    waveform=waveform,
    waveform_loop_end=synthio.LFO(
        scale=duty_range,
        offset=duty_mid,
    ),
)
synth.press(note)

In this PR, I've also included two small bug fixes that I discovered along the way:

  • Bad reference to ring_waveform_loop_start during note frequency calculation.
  • Default value of synthio.Note.amplitude changed from 0.0 to 1.0 in documentation to better reflect actual implementation.

@relic-se
Copy link
Author

relic-se commented Nov 4, 2024

In the example I shared originally, the duty cycle does not change linearly and produces an output that is not the best demonstration of controlling a square wave duty cycle through synthio.

I've modified the example to demonstrate controlling duty cycle with LFO in a linear fashion. This uses a "window" technique by controlling both waveform_loop_start and waveform_loop_end in tandem. The loop "window" should always have a size of SIZE // 2 (this would be affected by any drift between the two synthio.LFO objects).

import audiobusio
import board
import synthio
import ulab.numpy as np

SIZE = 256
VOLUME = 32000
SAMPLE_RATE = 48000
RATE = 0.1

audio = audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
audio.play(synth)

waveform = np.concatenate((
    np.full(SIZE // 2, VOLUME, dtype=np.int16),
    np.full(SIZE // 2, -VOLUME, dtype=np.int16)
))

note = synthio.Note(
    frequency=110,
    waveform=waveform,
    waveform_loop_start=synthio.LFO(
        scale=SIZE // 4,
        offset=SIZE // 4,
        rate=RATE,
    ),
    waveform_loop_end=synthio.LFO(
        scale=SIZE // 4,
        offset=SIZE * 3 // 4,
        rate=RATE,
    ),
)
synth.press(note)

Copy link
Member

@jepler jepler left a comment

Choose a reason for hiding this comment

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

this looks nice and simple, appreciate you picking up the work and unlocking some new capabilities!

It's especially nice that with properly configured blocks you can make sure the "window" remains always the same size for square wave modulation!

I think the docstrings need to be updated to reflect that out of range values for start/end are always clipped to 0/len-1, and none are ever rejected with ValueError anymore.

@kevinjwalters please test if you can

shared-bindings/synthio/Note.c Show resolved Hide resolved
shared-bindings/synthio/Note.c Outdated Show resolved Hide resolved
//| waveform_loop_start: int


//| waveform_loop_start: BlockInput
//| """The sample index of where to begin looping waveform data.
//|
//| Values outside the range ``0`` to ``waveform_max_length-1`` (inclusive) are rejected with a `ValueError`.
Copy link
Member

Choose a reason for hiding this comment

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

do these docstrings need to be updated? I haven't read below to see how you deal with out of range values from a varying BlockInput

Copy link
Author

Choose a reason for hiding this comment

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

I kept the conditions mostly the same, but it's now handled within the calls to synthio_block_slot_get_limited. I've updated the docstrings to remove references to ValueError since that is no longer applicable. The major difference is that the code no longer checks the loop_start/loop_end values to see if they are outside the range and instead just clamps them to the valid range. I've removed portions of the docstrings which mentioned this.

Copy link
Author

Choose a reason for hiding this comment

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

I'll keep this conversation open until you've reviewed the changes. Let me know if you have any other suggestions.

@jepler
Copy link
Member

jepler commented Nov 4, 2024

The test failure occurs because the repr of a note has changed slightly:

-(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, waveform_loop_start=0, waveform_loop_end=16384, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None, ring_waveform_loop_start=0, ring_waveform_loop_end=16384),)
+(Note(frequency=830.6076004423605, panning=0.0, amplitude=1.0, bend=0.0, waveform=None, waveform_loop_start=0.0, waveform_loop_end=16384.0, envelope=None, filter=None, ring_frequency=0.0, ring_bend=0.0, ring_waveform=None, ring_waveform_loop_start=0.0, ring_waveform_loop_end=16384.0),)

notice how e.g., the ring_waveform_loop_start now prints as the float 0.0 instead of the int 0.

Do you know how or want to learn how to run the tests locally and update the expected output files? Otherwise, it looks like I could push that change to this branch, let me know if you'd like

@relic-se
Copy link
Author

relic-se commented Nov 4, 2024

I haven't figured out how to build unix build-standard with synthio support just yet, but I'll dive into that soon. I'm assuming your solution is to update the tests to use floats instead rather than attempt to get integers working with BlockInput?

And I'd rather fix the tests first before merging.

@jepler
Copy link
Member

jepler commented Nov 4, 2024

yes, failed tests stop everything else from building, so it'll need to be fixed.

In ports/unix make VARIANT=coverage test is the basic way to build & run tests that include synthio.

You can make VARIANT=coverage print-failures. Then in the tests directory there's a script endorse.py which can be used to copy the new output file into an expected (".exp") file, or you can do it manually in the shell or however you like to manipulate/rename files.

The third target to know about is make VARIANT=coverage clean-failures which removes anything in the results directory.

@relic-se
Copy link
Author

relic-se commented Nov 4, 2024

Thank you very much for the instruction, @jepler ! I'll tackle that this afternoon and get the PR ready for merging.

@relic-se relic-se marked this pull request as draft November 4, 2024 16:41
@relic-se relic-se marked this pull request as ready for review November 4, 2024 19:33
Copy link
Member

@jepler jepler left a comment

Choose a reason for hiding this comment

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

looks great now!

@jepler jepler merged commit f7f471f into adafruit:main Nov 4, 2024
426 checks passed
@relic-se relic-se deleted the synthio_waveform_loop_blockinput branch November 4, 2024 21:56
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.

2 participants