-
Notifications
You must be signed in to change notification settings - Fork 263
Tracking issue for equalizer Source #742
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
Comments
I like the idea. I was anyway surprised that we didn’t have biquads in yet. This equalizer could be just a convenience struct with a bank of biquads underneath it.
Balancing between things we can do and complexity involved, I’d be happy with the band values mutable at runtime, and the number of bands fixed at compile time. It’s up to the user to create the pipeline correctly and so AGC later. |
I like the idea, it might get complex however, so be warned :)
I am in favor for implementing both, I think that is what we are moving towards API wise anyway. If your open to it you could try having the float one build from the periodic access variant.
Lets hash out an API and the start looking at a nice algorithm? let source = Decoder::try_from(File::open("music.ogg")?)?;
// generic (number of bands) is inferred from array len.
// array elements are frequency ranges for each bin.
let equalized = source.equalized([0..440,440..1000,2000..4000]).expect("you passed in non overlapping ranges");
// The controller can be send to another thread, under the hood is uses the atomic floats
// (or something else if that ends up being more performant)
// Alternatively a user could wrap equalized in periodic access and use that.
let (equalized, controller) = equalized.with_controller();
controller.set_bin_gain(2, 0.5).expect("band 2 exists");
// no need for runtime check, use const generics to ensure array is proper length;
controller.set_all_gains([1.0,1.0,1.0]); |
Speeking of algorithms, @iluvcapra you know your stuff. Any suggestions for a good equalizer implementation? |
I have an experimental equal-loudness algorithm here with a seven-band equalizer: https://github.com/roderickvd/pleezer/blob/main/src/loudness.rs Eventually I want to extract and port these into Rodio, but haven’t gotten around to it and won’t in the foreseeable future. Happy if someone else already wants to. |
This it might be easier to do what I proposed instead of putting in a range itself i.e. 100 to 400 (ill clarify what I'm thinking) We can infer a transition between each number let my_array = [12.0, 18.0, 28.0, 33.0, 45.0, 60.0, 78.0, 83.0]; This will simplify the logic a little bit and allow seamless transitions between ranges. Example:
Then in between each one we can calculate a transition.
While something like This was the idea I'm thinking about, but I'm open to others I'm just trying to clarify the idea with more examples, so people can understand and critique it. Edit1: Came up with something like this to possibly calculate in bulk all the numbers in-between: pub fn calculate_db_transition(
start_num: i32,
start_db: f32,
end_num: i32,
end_db: f32,
) -> Result<Vec<f32>, Box<dyn Error>> {
// Input validation
if end_num <= start_num {
return Err("End number must be greater than start number".into());
}
let steps = (end_num - start_num) as usize;
let mut results = Vec::with_capacity(steps + 1);
// Precompute the constant step size
let step_size = (end_db - start_db) / steps as f32;
// Generate results using the precomputed step size
for i in 0..=steps {
results.push(start_db + step_size * i as f32);
}
Ok(results)
} Output:
|
If you needed a graphic EQ like this I think you'd just cascade a set of biquads, though in real life each filter would have more orders for tighter slopes on each band? |
makes sense 👍, so that would look something like this then:
which would slowly turn the gain from 1.0 to 0.5 (eventually halving 'volume') between 500hz and 1000hz. Then between 1000 and 2000 the gain is 0.5 from 2k to 4khz it then changes to 1.0 again. Unresolved questions:
|
Thank you!. For me there are some magic words in there, @UnknownSuperficialNight is this clear for you?
Should we add those first and then build this on top of that? |
You can look at the example I linked above how it processes a bank ("cascades") of biquads. You can take out all of the equal-loudness normalization and what remains is pretty simple: pub struct EqualLoudnessFilter {
// change this into a `Vec`
filters: [biquad::DirectForm1<f32>; NUM_BANDS],
}
impl EqualLoudnessFilter {
pub fn process(&mut self, input: f32) -> f32 {
let mut output = input;
for filter in &mut self.filters {
output = filter.run(output);
}
output
}
}
Yes.
Yes, offer logarithmic options.
You need to watch out for cascading too many biquads. That can: Instead of introducing many "steps" you will probably want to use reasonable Q values to create overlapping responses in between the center frequencies ("bands") you've set. From my example: /// Center frequencies for each filter band in Hz
const BAND_FREQUENCIES: [f32; NUM_BANDS] = [
31.5, // Low shelf
80.0, // Low-mid peak
250.0, // Mid peak 1
500.0, // Mid peak 2
2000.0, // Upper-mid peak
6300.0, // Presence peak
12500.0, // High shelf
];
/// Q factors for each filter band
const BAND_Q: [f32; NUM_BANDS] = [
Q_BUTTERWORTH_F32, // Low shelf
1.0, // Low-mid peak
1.2, // Mid peak 1
SQRT_2, // Mid peak 2
1.2, // Upper-mid peak
1.5, // Presence peak
Q_BUTTERWORTH_F32, // High shelf
]; |
You should be able to do biquads with `BltFilter` in blt.rs. (This Source is perhaps not very well-named, I was digging through trying to find this without remember the name and it took me a bit.)
|
A little, I would have to read up more on this before I can fully understand it. |
please ask for help if you get stuck on anything, your time is valuable! |
I've been thinking about this for a while. What do you think of the possibility of adding an equalizer?
I don't have a specific implementation in mind, but a general idea would be to create a source that we can pass an array into, where this array represents the bands in an equalizer.
In the image above, it defines each band in an array, then connects the values between, i.e., let's say the user adds
12hz
and18hz
to the array. We can then amplify8db
on the18hz
, and no amplification for the12hz
. Then between 13,14,15,16,17hz will be linearly interpolated for them to ascend in amplification till it reaches the18hz
, then it will go down for a third number if we have one.This allows users to define their own band ranges and how much they want them amplified.
Here is a visualization of
[12, 18, 28]hz
.We would need a baseline amplification separate from the array, which would control the overall band's amplification like a
pre-amp
. This would allow users to lower or increase all bands relative to the pre-amp before applying the individual amp on each band.Regarding implementation, I would like some input before starting coding.
Note: If using this with
AGC
, this would need to take precedence before theAGC
to avoid distortion issues. Otherwise, theAGC
would amplify it and then the equalizer would amplify as well, creating distortion. However, if we amplify with the equalizer first and feed it into theAGC
, there should be no issues.Another thing to consider is real-time adjustments. Most people want to be able to change the equalizer settings in real time, so we would need to use
periodic_access
oratomic_float
. Maybe implement both to satisfy the most users, and we would need mutable access to the array itself for the user to be able to add or remove bands during runtime.The text was updated successfully, but these errors were encountered: