-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 support for tuple of Biquad objects within audiofilters.Filter
.
#9772
Conversation
I would like some insight into a better method of updating the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's any more clever way to detect a changed list length. You could use realloc and only initialize any new slots.
But of course not all changes to the filters list would necessarily change the list length. For instance, code could use filters[0] = new_filter_zero
. Is the code handling this case?
Another option is to say that the contents of the list are snapshotted on assignment or require the value supplied to be a read-only sequence (tuple).
Is this code enough to implement equalizer-type functionality? as it stands, the common biquad constructors (and this includes the new block biquads). In my reading I haven't seen it spelled out exactly how an equalizer would be constructed from biquads, but I feel like that seems to be the most obvious use of a bank of filters. An example which does this would be very illustrative & helpful for me.
shared-module/audiofilters/Filter.c
Outdated
self->filter_states = m_malloc(self->filter_states_len * sizeof(biquad_filter_state)); | ||
if (self->filter_states == NULL) { | ||
common_hal_audiofilters_filter_deinit(self); | ||
m_malloc_fail(self->filter_states_len * sizeof(biquad_filter_state)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm relatively sure that m_malloc
already throws in case of failed allocation, so the call to m_malloc_fail
is not needed; but also, the call to common_hal_audiofilters_filter_deinit
is ineffective. I see this idiom has been used across the audiofilters so it may need to be revised elsewhere as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into this issue and apply changes as necessary throughout. I'm still learning about how best to program within the MicroPython/CircuitPython ecosystem so bear with me! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I began removing the ..._deinit
and m_malloc_fail
statements, but I'm finding that this idiom is fairly common within multiple modules of CircuitPython. As you certain that this isn't the proper handling of m_malloc
?
Here are a few examples:
circuitpython/shared-module/audiomixer/Mixer.c
Lines 28 to 38 in ddfa519
self->first_buffer = m_malloc(self->len); | |
if (self->first_buffer == NULL) { | |
common_hal_audiomixer_mixer_deinit(self); | |
m_malloc_fail(self->len); | |
} | |
self->second_buffer = m_malloc(self->len); | |
if (self->second_buffer == NULL) { | |
common_hal_audiomixer_mixer_deinit(self); | |
m_malloc_fail(self->len); | |
} |
circuitpython/shared-module/audiomp3/MP3Decoder.c
Lines 306 to 327 in ddfa519
self->inbuf.buf = m_malloc(DEFAULT_INPUT_BUFFER_SIZE); | |
if (self->inbuf.buf == NULL) { | |
common_hal_audiomp3_mp3file_deinit(self); | |
m_malloc_fail(DEFAULT_INPUT_BUFFER_SIZE); | |
} | |
if (buffer_size >= 2 * MAX_BUFFER_LEN) { | |
self->pcm_buffer[0] = (int16_t *)(void *)buffer; | |
self->pcm_buffer[1] = (int16_t *)(void *)(buffer + MAX_BUFFER_LEN); | |
} else { | |
self->pcm_buffer[0] = m_malloc(MAX_BUFFER_LEN); | |
if (self->pcm_buffer[0] == NULL) { | |
common_hal_audiomp3_mp3file_deinit(self); | |
m_malloc_fail(MAX_BUFFER_LEN); | |
} | |
self->pcm_buffer[1] = m_malloc(MAX_BUFFER_LEN); | |
if (self->pcm_buffer[1] == NULL) { | |
common_hal_audiomp3_mp3file_deinit(self); | |
m_malloc_fail(MAX_BUFFER_LEN); | |
} | |
} |
circuitpython/shared-module/audiocore/WaveFile.c
Lines 100 to 110 in ddfa519
self->buffer = m_malloc(self->len); | |
if (self->buffer == NULL) { | |
common_hal_audioio_wavefile_deinit(self); | |
m_malloc_fail(self->len); | |
} | |
self->second_buffer = m_malloc(self->len); | |
if (self->second_buffer == NULL) { | |
common_hal_audioio_wavefile_deinit(self); | |
m_malloc_fail(self->len); | |
} |
circuitpython/ports/stm/common-hal/pulseio/PulseIn.c
Lines 87 to 91 in ddfa519
self->buffer = (uint16_t *)m_malloc(maxlen * sizeof(uint16_t)); | |
if (self->buffer == NULL) { | |
// TODO: free the EXTI here? | |
m_malloc_fail(maxlen * sizeof(uint16_t)); | |
} |
There are other cases where instead of m_malloc(...)
there are calls to port_malloc(...)
, malloc(...)
, realloc(...)
, malloc_with_finaliser(...)
, ringbuf_alloc(...)
, and gc_alloc(...)
before m_malloc_fail(...)
. Maybe someone down the line copied that over but with m_malloc(...)
and it stuck within audio and pulseio modules?
Rather than experimenting and going outside of the accepted formatting, I'm copying a lot of my homework from other portions of CircuitPython. This is something I borrowed from audiodelays
without fully understanding the underlying principles. I plan on keeping it as is for now until we develop a plan for all other instances of this methodology (likely another PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finally looked this up, and I see what you mean. The additional check and m_malloc_fail
is definitely not necessary. I'll remove it from this class, but let me know if you'd like me to go around and remove it throughout CircuitPython or save it for a separate PR. @jepler
Lines 86 to 89 in d7a7221
void *ptr = malloc(num_bytes); | |
if (ptr == NULL && num_bytes != 0) { | |
m_malloc_fail(num_bytes); | |
} |
@jepler Using a read-only tuple is the perfect solution here! The filter states would only need assignment within the setter. Thanks for the tip! As for "equalizer" usage, the only applicable use at this time is to combine LPF and HPF to filter out unwanted signal. The example code in the first comment demonstrates that. Future additions of peakingEQ, lowShelf, and highShelf (from the Audio EQ Cookbook) to effect = audiofilters.Filter(
filters=(
synthio.BlockBiquad(
mode=synthio.FilterMode.LOW_SHELF,
frequency=250,
A=0.5, # potential future parameter
Q=1.0,
),
synthio.BlockBiquad(
mode=synthio.FilterMode.PEAKING_EQ,
frequency=2000,
A=2.0,
Q=0.8,
),
synthio.BlockBiquad(
mode=synthio.FilterMode.HIGH_SHELF,
frequency=6000,
A=1.5,
Q=1.2,
),
),
) |
For this kind of equalizer does it work to apply the filters sequentially (e.g., |
@jepler I think that cascading the biquad filters as you've demonstrated will perform well enough for our needs. The primary use of parallel eq is within graphic equalizers where you have a large bank of band pass filters at frequency intervals and mix them together into the output. I could see this as a separate class with a predesignated filter design, but I don't think it's a high priority right now. Here's a research paper which covers this topic: https://mycourses.aalto.fi/pluginfile.php/666520/course/section/128564/Aleksi%20Peussa_1929990_assignsubmission_file_AAT_Seminar_Paper_724441.pdf. Here's a snippet from the conclusion which I think nails it on the head: "While both methods have their benefits, each also has their downsides. Simplicity of design and speed benefit the cascade, while simultaneous computation and accuracy benefit the parallel method." |
audiofilters.Filter
.audiofilters.Filter
.
thank you, I took a quick glance at the paper you linked and I think it will be enlightening when I get a chance to read it through. In CircuitPython we're not likely to be taking advantage of parallel methods of computation anytime soon, so simplicity and adequate single thread performance are great features to enjoy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! I mostly have just nitpicky things, plus one concern about backwards compatibility. Given that in practice there's not much audiofilters code deployed so far I'm inclined not to worry but I'd like Dan to sign off on that as well.
…t and tuple of Biquad objects.
@jepler I may have become heavy-handed with my most recent changes, but hopefully it's for the best. I've done the following:
This retains backwards compatibility while still allowing the use of multiple filters. The only issue I am having now with the property is that if you set it to a tuple/list of invalid objects, it generates a import audiofilters
import synthio
synth = synthio.Synthesizer()
effect = audiofilters.Filter()
try:
effect.filter = ["test"]
except TypeError:
pass
print(effect.filter)
# ('test',) The solution here would be to iterate through items within |
We have a helper for checking items in a sequence for the correct type. Here's an example of its use:
so you'd iterate over the collection and check the type of each object. If you don't do this first, it seems like the object ends up in an inconsistent state. |
That did the trick! Thank you, @jepler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried it out with a bunch of filter combos and worked great for me. Code looks good as well.
As @jepler had comments I'll let him approve if he thinks all his requests were complete. But looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
Support the processing of multiple
synthio.Biquad
objects withinaudiofilters.Filter
by renamingfilter
tofilters
and requiring it be aList[synthio.Biquad]
.Comments:
synthio_biquad_filter_assign
(ie:effect.filters[0] = ...
)effect.filters.append(...)
andeffect.filters.remove(...)
are detected by testing length in audio buffer loop. There may be a better method of detecting these list manipulations.