-
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
audiofilters: Add Distortion effect and implement LFO ticking #9776
base: main
Are you sure you want to change the base?
Conversation
…ule based on Godot's AudioEffectDistortion class.
I think we need to add more fluff for the full effects package as well.
|
I don't know exactly what you mean by "fluff", but 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.
Additionally, the added file(s) in this module need to be separately listed in the UNIX port (ports/unix/variant/coverage/mpconfigvariant.mk). This is the cause of the CI failures that prevent per-board builds from starting.
It would be nice if at least one basic test of the distortion functionality is added to tests/circuitpython so that this functionality is smoke tested during CI.
Thank you for the review, @jepler ! I'll dig in as soon as I get the chance. |
…oint calculations.
…CROPY_FLOAT_CONST, MP_ROM_INT, and synthio_block_slot_get_limited.
I've noticed a few areas within |
[copied from Discord] I don't know that these other objects need a
For the first case, having some object that is not a synthesizer/audio source at all might be the right solution. For the second case, I don't know if that is applicable to the e.g., an audio echo effect. (*) |
Thank you for the explanation. I don't think I properly understood the purpose of the
As for your note on the variable number of samples, I think that 256 samples is a generally ideal pace, but if the buffer size is not a factor of 256, it would cause uneven ticking of the blocks. Yet, using the entire buffer size may not be ideal either because the block inputs may not increment at a desirable pace. We may need a separate accumulator within |
@jepler I've taken some time to look into how the block ticking is actually handled, and I still don't think that the block system is ready to have this update to audioeffects classes.
In the example layout above, it would make sense to include
In either of the two examples above, the global block ticking variables (ie: I think the solution is to store these global variables locally on each object and add them as arguments to That all said, I still don't plan to include this fix here, and it should instead be addressed in a separate PR connected to #9872. |
This is why the rule exists that a block can only be associated with one audio source and associating it with more than one audio source is an undiagnosed error or undefined behavior, however you want to call it. Here's why (I think that) a single global tick is sufficient, given that rule: As a footnote, this also ignores the possibility that typedef struct synthio_block_base {
mp_obj_base_t base;
uint8_t last_tick;
mp_float_t value;
} synthio_block_base_t; |
so with two synthesizers, a block used in synth A will see Why does the |
@jepler I see what you mean and why the uneven I put in some work to separate those three global variables into a struct that is passed through to all necessary block functions: main...relic-se:circuitpython:local_block_ticking. I'm good to scrap this in favor of your approach. I've also changed my mind on the 256 sample increments, and I think I'll make the audioeffects just tick the block inputs based on the size of the buffer. It'll make for a much simpler update. |
@jepler I've updated I've tested the following script with and without the update and can confirm that it works now:
Same goes for this test with a
|
If you test the above examples with a large buffer_size on the effect, let's say 16384, you can clearly hear the stepping in the delay time of the output. Though it is very unlikely that a user would use a buffer_size of this amount, it does bring up a valid case for keeping lfo ticking to |
…AX_DUR` intervals.
I've moved the block value calls around in order to force
These changes will like cause merge conflicts with #9876 regarding |
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.
Couple small comments on the code. I'll test this on hardware shortly. Sorry for the delay!
shared-module/audiofilters/Filter.c
Outdated
@@ -296,16 +289,24 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o | |||
} | |||
} | |||
|
|||
// tick all block inputs | |||
shared_bindings_synthio_lfo_tick(self->sample_rate, length / self->channel_count); |
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.
Could this and line 306 be factored up out of the if/else block? The compiler probably does anyways.
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.
Good call, and looking deeper at it, this would actually cause an issue with unsigned 16-bit sources due to while (length--) {...}
above it.
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.
On second thought, we rely on the remaining length of the sample to calculate the ticking length (see the n
variable below). In this logic branch, the sample is set to NULL
and hence we tick the LFO in one fell swoop instead. In order to optimize this, we'd have to calculate n
beforehand and add an additional self->sample != NULL
check. I don't see this helping too much. If it's still a concern, it's simpler to just remove LFO ticking when there's no sample.
shared-module/audiodelays/Echo.c
Outdated
mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); | ||
mp_float_t decay = synthio_block_slot_get_limited(&self->decay, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); | ||
|
||
uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms); |
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.
We may want to check that delay >= 0. Never did in the original code (my bad) but a LFO could go negative and weird things happen.
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.
Casting it to an unsigned integer does force it to be >= 0, but a negative result might cause it to wrap around. If it's 0, I believe it will do a minimum of 1 sample of delay. I'll look into it.
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.
maybe it should be synthio_block_slot_get_limited(1, some_calculated_maximum)
then?
Technically under the C99 standard, conversion from a negative floating point value to an unsigned integer value is undefined behavior.
When a finite value of real floating type is converted to an integer type other than _Bool,
the fractional part is discarded (i.e., the value is truncated toward zero). If the value of
the integral part cannot be represented by the integer type, the behavior is undefined. (6.3.1.4.1)
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've taken a different approach here. Storing this value and current_delay_ms
as uint32_t
was an arbitrary choice, so I've instead reverted both back to floats. I've added in another pre-calculated variable for the length of one audio frame (sample) in milliseconds, and I'm checking against that determine if we need to recalculate delay buffer parameters. The minimum value of the delay is handled within recalculate_delay
. Before, delays could only be manipulated by an LFO in millisecond intervals because of this, whereas now they can be controlled with sample-level precision by an LFO. This could be useful for chorus/phaser type effects.
As for some_calculated_maximum
, it's difficult to sum that up because that maximum value depends on the freq_shift
property. If using the standard delay, it is limited to max_effect_ms
, but with freq_shift
, I believe it is max_effect_ms*256
because of the 8 sub-bits in the buffer pointer. You lose a lot of audio quality when going that long, though.
I'm tempted to move this logic into recalculate_delay
if it needs more refinement.
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 didn't try the code and I don't understand the distortion algorithm, but I noted a few things that may benefit from attention.
shared-module/audiodelays/Echo.c
Outdated
mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); | ||
mp_float_t decay = synthio_block_slot_get_limited(&self->decay, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); | ||
|
||
uint32_t delay_ms = (uint32_t)synthio_block_slot_get(&self->delay_ms); |
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.
maybe it should be synthio_block_slot_get_limited(1, some_calculated_maximum)
then?
Technically under the C99 standard, conversion from a negative floating point value to an unsigned integer value is undefined behavior.
When a finite value of real floating type is converted to an integer type other than _Bool,
the fractional part is discarded (i.e., the value is truncated toward zero). If the value of
the integral part cannot be represented by the integer type, the behavior is undefined. (6.3.1.4.1)
// Soft clip | ||
if (self->soft_clip) { | ||
if (wordf > 0) { | ||
wordf = MICROPY_FLOAT_CONST(1.0) - MICROPY_FLOAT_C_FUN(exp)(-wordf); |
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.
it's up to your testing that the perf is OK but I remain surprised that it's even feasible to call a transcendental math function for every sample. microcontrollers are a lot faster than they used to be.
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 believe I borrowed the hard/soft clip option from daisyduino. The difference is noticeable depending on the scenario.
Regarding performance, I think it really depends on the extent of the user code. If you are just processing audio through this effect, it seems to run just fine. I haven't tested this effect within a larger application as of yet.
Hard clip is definitely the more performant option. There may need to be some notes on performance within the documentation.
As for the other algorithms, I don't fully understand them either. But I've done my best to refactor them to reduce unnecessary floating point calculations where possible. If anyone can reference less intensive algorithms with similar results, I'd be happy to switch over.
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 tried it on the RP2350 and seems to work fine for me. No crashes.
…variable sample length.
…ned 16-bit audio.
New audio effects class,
audiofilters.Distortion
, to distort audio samples using one of the available modes available in theaudiofilters.DistortionMode
enum.Todo:
DistortionMode.CLIP
,DistortionMode.OVERDRIVE
andDistortionMode.WAVESHAPE
algorithms.Comments:
audiofilters.Filter
effect? (ie:filter.play(distortion.play(sample))
)pre_gain
andpost_gain
properties are currently measured in decibels which requires the addition of adb_to_linear
function to make those values linear. Would it be more ideal to forego decibels altogether and make those properties linear?Parameters, documentation copy and algorithms are mostly credited to Godot Engine under the MIT License.
Example Code:
Closes #9872.