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 'Note' with arbitrary frequency and more #7933

Merged
merged 19 commits into from
May 8, 2023

Conversation

jepler
Copy link
Member

@jepler jepler commented May 4, 2023

Here's the doc for the new class, which can be used where integer note numbers were used before:

class Note:
    def __init__(
        self,
        frequency: float,
        amplitude: float = 1.0,
        waveform: Optional[ReadableBuffer] = None,
        envelope: Optional[Envelope] = None,
        tremolo_depth: float = 0.0,
        tremolo_rate: float = 0.0,
        vibrato_depth: float = 0.0,
        vibrato_rate: float = 0.0,
    ) -> None:
        """Construct a Note object, with a frequency in Hz, and optional amplitude (volume), waveform, envelope, tremolo (volume change) and vibrato (frequency change).

        If waveform or envelope are `None` the synthesizer object's default waveform or envelope are used.

        If the same Note object is played on multiple Synthesizer objects, the result is undefined.
        """
    frequency: float
    """The base frequency of the note, in Hz."""
    amplitude: float
    """The base amplitude of the note, from 0 to 1"""
    tremolo_depth: float
    """The tremolo depth of the note, from 0 to 1"""
    tremolo_rate: float
    """The tremolo rate of the note, in Hz."""
    vibrato_depth: float
    """The vibrato depth of the note, from 0 to 1"""
    vibrato_rate: float
    """The vibrato rate of the note, in Hz."""
    waveform: Optional[ReadableBuffer]
    """The waveform of this note. Setting the waveform to a buffer of a different size resets the note's phase."""
    envelope: Envelope
    """The envelope of this note"""

Looking for feedback from @jedgarpark @todbot @BlitzCityDIY about this API.

I love how the tremolo & vibrato make the sounds of my 'personal chimes' project richer. However, I don't know if this is what the API should look like.

I also wonder if the Envelope properties should be repeated in this object, instead of this object containing an Envelope; unlike Envelope, Note is designed so that its properties can be changed dynamically and the changes are reflected right away.

I think it also fixes the "audio glitch at auto-reload" problem in rp2040, which is great if someone else can confirm it. it can still glitch if USB enumeration happens while playing audio, so there's still an 'audio while interrupts disabled/defferred' problem

Finally, it tries again to improve the situation with output levels. Now, the sustain_level is relative to the attack_level, and setting an attack_level below 1.0 can mean that the auto envelope adjustment may never have to come into effect; for example, if you're playing up to 5 notes then attack_level=0.2 (i.e., 1/5) means no adjustment will ever happen. Of course, when you play just one note it's much less loud than before. And if you play 6 notes the adjustment down in volume will be smaller as a percentage of amplitude. But the point is, you know something about how many notes you want to play at once and can set it accordingly.

From my play/test code, creating some note objects:

            possible_notes = [
                    synthio.Note(synthio.midi_to_hz(KEY+scales[scale_number][n]+o), vibrato_depth=0.005, vibrato_rate=2, amplitude=.4)
                    for n in pentatonic_offsets
                    for o in [-12, -36, -24]]

randomizing the vibrato and tremolo before striking the note:

                    note = random.choice(possible_notes)
                    note.vibrato_rate = random.random() * 2 + .5
                    note.vibrato_depth= random.random() * .007
                    note.tremolo_rate = random.random() * 2 + .5
                    note.tremolo_depth = random.random() * .1
                    synth.press((note,))

Frequency sweeps can be accomplished by setting note.frequency from Python code in the forever-loop, there's not a built-in frequency start/stop/rate sweep. In my testing this seemed to be adequate, at least for simple programs.

jepler added 12 commits May 4, 2023 07:23
to convert notes in the MIDI 1-127 note scale to floating point Hz
This class allows much more expressive sound synthesis:
 * tremolo & vibrato
 * arbitrary frequency
 * different evelope & waveform per note
 * all properties dynamically settable from Python code
.. and account releasing notes at their sustain level until they're
done.

this ameliorates the effect where multiple releasing notes
don't seem to actually be releasing, but stay at a constant volume.
and re-vamp overall envelope calculation again.

Now, if you set a low overall attack level like 0.2 this avoids the
"diminishing volume" effect when many notes sound at once. You need
simply choose a maximum attack level that is appropriate for the max
number of voices that will actually be played.
.. without changing the current note amplitude
this may fix a weird crash during shutdown
The internal flash cache wasn't being properly used, because
`write_blocks` unconditionally performed the flash write.

Fixing this so that the write's not done until `internal_flash_flush`
fixes the problem in my test program with i2sout & synthio.

as a future optimization, `flash_read_blocks` could learn to read out
of the cache, but that's probably not super important.
@jepler
Copy link
Member Author

jepler commented May 4, 2023

Probably the separate Note.amplitude property should be removed, the envelope is ample to give a per-note amplitude.

@jepler
Copy link
Member Author

jepler commented May 4, 2023

per-note envelope, which I didn't test before pushing, 🤣 doesn't work 😓

jepler added 2 commits May 4, 2023 10:16
for similar reasons as Envelope. The mandatory frequency argument can
still be given as a positional argument.
@jepler
Copy link
Member Author

jepler commented May 4, 2023

and the rp2040 squawk-at-reload came back :(

the squawk-at-reload comes back if you try to use a finally block to ramp off all voices smoothly at reload time.

.. and simplify the envelope advance logic by handling
'instant' values more intelligently.
@jepler
Copy link
Member Author

jepler commented May 4, 2023

I now believe per-note envelope, including dynamically modifying a per-note envelope, works.

@BlitzCityDIY
Copy link

BlitzCityDIY commented May 5, 2023

thanks Jeff for all of this! this is my first time trying out synthio so trying to catch up a bit. just to make sure i'm understanding everything properly- the idea is that you'd make an array of notes and then tremolo and vibrato is affected live while you're playing? and then at this time, the envelope is more static and is only affected before/after a note is played? in that case i do like the idea of adding the envelope properties to this note object so that everything can be affected while a note is playing. <-- just saw your most recent comment about envelope per note now added, yay! will try that out.

i'm discovering that i need to add a better speaker to my I2S breakout because i'm getting a lot of crackles but i'm using a wii classic controller and tying the values to the different properties. super fun.

one thing i tried is this:

notes = []
hz_tones = [130.81, 164.81, 196.00]
for i in range(hz_tones):
    n = synthio.Note(hz_tones[i], vibrato_depth=0.005, vibrato_rate=2, amplitude=.4)
    notes.append(n)

and got an error:
TypeError: unsupported types for __lt__: 'int', 'list'
i'm guessing i'm just not using the proper syntax.

i tried originally testing with the metro m7 but when i was uploading the UF2, the drive was disappearing so i could not get to CIRCUITPY. not sure if JP or Todd experienced the same?

@jedgarpark
Copy link

@BlitzCityDIY cracks and pops on speaker is familiar -- i get the best performance using this speaker https://www.adafruit.com/product/4445 with this amp https://www.adafruit.com/product/3006 On some projects I've given that amp 5V from a separate supply to make it happier.
The Metro M7 not showing up has happened a few times, I'm not sure if it's a USB cable issue or other.

@jepler
Copy link
Member Author

jepler commented May 5, 2023

the traceback is due to use of range where it's not needed:

>>> for i in range(hz_tones): print(i)
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'list'
>>> for i in hz_tones: print(i)
... 
130
164
196

desktop python gives a slightly different error text that's maybe easier to understand:

>>> for i in range(hz_tones): print(i)
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object cannot be interpreted as an integer

since range() expects its argument(s) to be integers.

@jepler
Copy link
Member Author

jepler commented May 5, 2023

I've mostly been using other boards than the M7 for this, including the pygamer and now the prop feather.

@BlitzCityDIY
Copy link

oh wow that's right, thanks Jeff. my tiredness is showing ha. i'll try to get some demo code written with the wii controller and post it up tomorrow 🙂

jepler added 4 commits May 6, 2023 21:35
this has the side effect of making some notes more accurate, the new
frequency= value in the test is closer to the true midi frequency of
830.609...Hz.
Now the vibrato 'units' are 1.0 = one octave, 1/12 = one semitone,
1/1200 = one cent. Before, the units were somewhat arbitrary and were not
perceptually "symmetrical" around the base frequency.

For vibrato_depth = 1/12 and base frequency of 440,

before: pitch from 403.33 to 476.67Hz, not corresponding to any notes
after: pitch from 415.30 to 466.16Hz, corresponding to G# and A#
This really improves the loudness of the output with multiple notes
while being a nice simple algorithm to implement.
@jepler
Copy link
Member Author

jepler commented May 8, 2023

@todbot you'll appreciate this. before and after of building up a chord from 9 notes. The note envelope has an attack level of 1.0, sustain level of 0.8, and a sine waveform with a volume of 14700

While some clipping will occur, overall I think it's an improvement. In technical terms, it's a dynamic range compressor with a fixed threshold and a hard knee. https://en.wikipedia.org/wiki/Dynamic_range_compression

The waveforms below are from the manual test program circuitpython-manual/synthio/note/code.py which can be run on a host computer and captured directly to a wave file, so the peaks are the actual data, not a recording. This wave sounds a bit clippy when played, but my real world example simply sounds louder than before.

image
image
image
image

@jepler jepler marked this pull request as ready for review May 8, 2023 17:58
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.

Looks great! Thanks for the print/record follow up from last time too. Also interested to see if this helps RP2040 flash issues.

@tannewt tannewt merged commit 9e4dea7 into adafruit:main May 8, 2023
307 checks passed
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.

None yet

4 participants