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

Q: Tips for finding the right attenuation factor for SVFilter w.r.t. resonance param #128

Open
balintlaczko opened this issue Feb 9, 2025 · 1 comment

Comments

@balintlaczko
Copy link

Hi! I am trying to implement a patch where I set up an array of SVFilters with a white noise input. I have been playing with the resonance parameter and trying to find some kind of curve that keeps the loudness/amplitude of the output consistent as the resonance increases. I noticed that there is a very modest rise in volume up until around 0.8 or 0.9 where it gets steeper and steeper. Perhaps @ideoforms knows some equation that predicts the output amplitude of the filter given a resonance value and white noise input? This gets especially important when I start to stack filters in a cascade, and I haven't yet found a way to keep the volume of the output relatively smooth. Here is my current attempt:

class FilteredNoise(sf.Patch):
    def __init__(
            self,
            filter_type="low_pass", # can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak', 'low_shelf', 'high_shelf'
            cutoff=440,
            resonance=0.5,
            amplitude=0.5,
            panning=0
        ):
        super().__init__()

        filter_types = ["low_pass", "band_pass", "high_pass", "notch", "peak", "low_shelf", "high_shelf"]
        assert filter_type in filter_types, f"Filter type must be one of {filter_types}"

        cutoff = self.add_input("cutoff", cutoff)
        resonance = self.add_input("resonance", resonance)
        amplitude = self.add_input("amplitude", amplitude)
        panning = self.add_input("panning", panning)
        panning = panning * 0.5 + 0.5

        resonance = sf.Clip(resonance, 0, 0.999)

        noise = sf.WhiteNoise()
        # attenuation: as resonance increases, the amplitude of the filter output decreases
        atten_threshold = 0.5
        db_at_threshold = -1
        max_attenuation = -16
        attenuation_light = sf.ScaleLinLin(resonance, 0, atten_threshold, 0, db_at_threshold)
        attenuation_heavy = sf.ScaleLinLin(resonance, atten_threshold, 1, db_at_threshold, max_attenuation)
        attenuation_db = sf.SelectInput([attenuation_light, attenuation_heavy], resonance > atten_threshold)
        attenuation_coeff = sf.DecibelsToAmplitude(attenuation_db)

        filters = sf.SVFilter(noise, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        filters = sf.SVFilter(filters * attenuation_coeff, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        filters = sf.SVFilter(filters * attenuation_coeff, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        filters = sf.SVFilter(filters * attenuation_coeff, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        filters = sf.SVFilter(filters * attenuation_coeff, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        filters = sf.SVFilter(filters * attenuation_coeff, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        panning = UpMixer(panning, filters.num_output_channels).output
        
        mix = Mixer(filters * amplitude, panning, 2)

        self.set_output(mix)

My helper classes:

class Mixer(sf.Patch):
    def __init__(self, input_sig, pan_sig, out_channels=2):
        super().__init__()
        assert input_sig.num_output_channels == pan_sig.num_output_channels
        n = input_sig.num_output_channels
        panner = [sf.ChannelPanner(out_channels, input_sig[i] / n, pan_sig[i]) for i in range(n)]
        _sum = sf.Sum(panner)
        self.set_output(_sum)


class UpMixer(sf.Patch):
    def __init__(self, input_sig, out_channels=5):
        super().__init__()
        n = input_sig.num_output_channels # e.g. 2
        output_x = np.linspace(0, n-1, out_channels) # e.g. [0, 0.25, 0.5, 0.75, 1]
        output_y = output_x * (out_channels - 1) # e.g. [0, 1, 2, 3, 4]
        upmixed_list = [sf.WetDry(input_sig[int(output_i)], input_sig[int(output_i) + 1], float(output_i - int(output_i))) for output_i in output_x[:-1]]
        upmixed_list.append(input_sig[n-1])
        expanded_list = [sf.ChannelPanner(out_channels, upmixed_list[i], float(output_y[i])) for i in range(out_channels)]
        _out = sf.Sum(expanded_list)
        self.set_output(_out)

Example:

freqs = np.linspace(60, 4000, 10).tolist()
panning = [-1, 1]
testy = FilteredNoise(filter_type="band_pass", cutoff=freqs, panning=panning)
graph.play(testy)

Any tips would be much appreciated! :)

@balintlaczko
Copy link
Author

Okay, for now, the simple trick of dividing the output by its RMS seems to do the trick. Not sure how theoretically sound this is, but it seems to work quite well:

class FilteredNoise(sf.Patch):
    def __init__(
            self,
            filter_type="low_pass", # can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak', 'low_shelf', 'high_shelf'
            cutoff=440,
            resonance=0.5,
            amplitude=0.5,
            panning=0,
            order=3
        ):
        super().__init__()

        filter_types = ["low_pass", "band_pass", "high_pass", "notch", "peak", "low_shelf", "high_shelf"]
        assert filter_type in filter_types, f"Filter type must be one of {filter_types}"

        cutoff = self.add_input("cutoff", cutoff)
        resonance = self.add_input("resonance", resonance)
        amplitude = self.add_input("amplitude", amplitude)
        panning = self.add_input("panning", panning)
        panning = panning * 0.5 + 0.5

        resonance = sf.Clip(resonance, 0, 0.999)

        self.order = np.clip(order, 1, 10)

        noise = sf.WhiteNoise()

        # First one
        filters = sf.SVFilter(noise, filter_type=filter_type, cutoff=cutoff, resonance=resonance)
        # The rest
        for i in range(self.order - 1):
            filters = sf.SVFilter(filters, filter_type=filter_type, cutoff=cutoff, resonance=resonance)

        panning = UpMixer(panning, filters.num_output_channels).output
        
        mix = Mixer(filters, panning, 2)

        mix_rms = sf.RMS(mix)
        mix = mix / mix_rms
        mix = mix * amplitude

        self.set_output(mix)
freqs = np.linspace(60, 4000, 10).tolist()
panning = [-1, 1]
order = 3
testy = FilteredNoise(filter_type="band_pass", cutoff=freqs, panning=panning, order=order)
graph.play(testy)

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

No branches or pull requests

1 participant