plt.title(title) plt.xlabel('Frequency [Hz]') plt.ylabel('Magnitude [dB]') plt.legend(legend) fs = 44100 b, a = filters.calcCoefsLPF2(1000, 10, fs) plotNonlinearFilterResponse(b, a, 'Nonlinear Resonant Lowpass') plt.ylim(-15) plt.grid() plt.savefig('Pics/NL-LPF.png') #%% r = 2 adsp.plot_static_curve(lambda x: np.tanh(x), gain=r) adsp.plot_static_curve(lambda x: adsp.soft_clipper(x), gain=r) adsp.plot_static_curve(lambda x: adsp.hard_clipper(x), gain=r) plt.title('Comparing Saturating Nonlinearities') plt.legend(['Tanh Clipper', 'Soft Clipper', 'Hard Clipper']) plt.xlim(-2, 2) plt.grid() plt.savefig('Pics/Sat-NLs.png') #%% from matplotlib import patches def zplane(p, z): ax = plt.subplot(111)
# ### Nonlinear Gain # # The next thing to consider is that a nonlinear function can also be written # as a dependent gain. For example, the dependent gain for the $\tanh$ # nonlinearity looks like this: # # $$ # g_{tanh} (x) = \frac{\tanh(x)}{x} # $$ # # For the $\tanh$ nonlinearity (as with many nonlinearities), the dependent # gain will always be in between 0 and 1, and as the input increases, the # gain decreases to zero. #%% adsp.plot_static_curve (lambda x : np.tanh (x)) adsp.plot_static_curve (lambda x : np.tanh (x)/x) plt.title (r'Comparing $\tanh$ nonlinearity, to dependent gain') plt.legend (['Tanh', 'Dependent gain']) #%% [markdown] # ### What happens to the poles? # # Now for the million dollar question: what happens to the poles of the filter # when we add the nonlinear elements? The basic answer is that the $a$ # coefficients are multiplied by the dependent gain values. Then the new pole # locations can be described by # # $$ # p_{nonlinear} = \frac{-g_{tanh}(x) a_1 \pm \sqrt{g_{tanh}^2(x) a_1^2 - 4g_{tanh}(x) a_2}}{2} # $$
# for an interesting type of distortion effect. # # ## GRU Architecture # # A GRU is made up of two simple nonlinearities: the # sigmoid and $tanh$ nonlinearities, both shown below. # While these curves look similar, note that the # sigmoid function goes from 0 to 1, while the $tanh$ # function goes from -1 to 1. # %% def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) plt.figure() adsp.plot_static_curve(sigmoid) plt.title('Sigmoid Nonlinearity') plt.savefig('Pics/sigmoid.png') plt.figure() adsp.plot_static_curve(np.tanh) plt.title('Tanh Nonlinearity') plt.savefig('Pics/tanh.png') # %% [markdown] # Using these basic nonlinear building blocks we can # construct a simple type of GRU known as a "minimal # gated unit" (introduced by # [Heck and Salem, 2017](https://arxiv.org/abs/1701.03452)). # The signal processing architecture for this unit is # shown below, with "Sg" denoting the sigmoid nonlinearity.
# %% import numpy as np import audio_dspy as adsp import scipy.signal as signal import matplotlib.pyplot as plt plt.style.use('dark_background') # %% def dropout(x, a): if isinstance(x, np.ndarray): t = np.copy(x) for n in range(len(x)): t[n] = dropout(t[n], a) return t B = np.sqrt(a**3 / 3) if x > B: return x - B + (B / a)**3 if x < -B: return x + B - (B / a)**3 return (x / a)**3 adsp.plot_static_curve(lambda x: dropout(x, 0.6), gain=2) plt.title('Static Curve for Dropout Nonlinearity') # %%
plt.title(title) plt.xlabel('Frequency [Hz]') plt.ylabel('Magnitude [dB]') plt.legend(legend) fs = 44100 b, a = filters.calcCoefsLPF2(1000, 10, fs) plotNonlinearFilterResponse(b, a, 'Nonlinear Resonant Lowpass') plt.ylim(-15) #%% import audio_dspy as adsp r = 2 adsp.plot_static_curve(lambda x: np.tanh(x), range=r) adsp.plot_static_curve(lambda x: 1.5 * adsp.soft_clipper(x), range=r) adsp.plot_static_curve(lambda x: adsp.hard_clipper(x), range=r) plt.title('Comparing Saturating Nonlinearities') plt.legend(['Tanh Clipper', 'Soft Clipper', 'Hard Clipper']) #%% from matplotlib import patches def zplane(p, z): ax = plt.subplot(111) uc = patches.Circle((0, 0), radius=1, fill=False,
# %% def tri_wave(x, freq, fs): p = float((1 / freq) * fs) x = x + p / 4 return 4 * np.abs((x / p) - np.floor((x / p) + 0.5)) - 1 def sine_wave(x, freq, fs): return np.sin(2 * np.pi * x * freq / fs) # %% fs = 44100 plt.figure() adsp.plot_static_curve(lambda x: tri_wave(x, fs / 2, fs), gain=5) plt.title('Triangle Wavefolder Static Curve') plt.figure() adsp.plot_static_curve(lambda x: sine_wave(x, fs / 2, fs), gain=5) plt.title('Sine Wavefolder Static Curve') # %% [markdown] # When we apply this simple wavefolding technique to an input sine # wave, we get back the expected "folding" behavior. # %% N = fs / 50 n = np.arange(N) freq = 100 x = np.sin(2 * np.pi * n * freq / fs)
# at 50 kHz, the signal will be reflected back to $48 - (50 - 48) = 46$ # kHz. This reflected signal is known as an "aliasing artifact" and # is typically considered undesirable, particularly since the artifact # is not harmonically related to the original signal. For a more # in-depth explanation of aliasing, see [here](https://theproaudiofiles.com/digital-audio-aliasing/). # # Typically, digital systems use filters to supress any signal above # the Nyquist frequency, so that aliasing artifacts don't occur. However, # when using a nonlinear function, the digital system can create signal # above the Nyquist frequency, thus creating aliasing artifacts. As an # example let's look at a specific type of distortion known as # "hard-clipping" distortion. A hard-clipper is a waveshaping distortion # with a waveshaping curve that looks as follows: # %% adsp.plot_static_curve(adsp.hard_clipper, gain=5) plt.grid() plt.title('Hard Clipper Response') # %% [markdown] # If I now create a 1.2 kHz sine wave, and process it through a # hard-clipping distortion with no aliasing, I would see the following # frequency response: # %% def plot_fft(x, fs, sm=1.0/24.0): fft = 20 * np.log10(np.abs(np.fft.rfft(x) + 1.0e-9)) freqs = np.fft.rfftfreq(len(x), 1.0 / fs) return freqs, fft def process_nonlin(fc, FS, nonlin, gain=10):