``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112``` ``` import numpy as np from numba import jit @jit(nopython=True) def my_clip_min(x, clip_min): # does the work of np.clip(), which numba doesn't support yet # TODO: keep an eye on Numba PR https://github.com/numba/numba/pull/3468 that fixes this inds = np.where(x < clip_min) x[inds] = clip_min return x @jit(nopython=True) def compressor_4controls(x, thresh=-24.0, ratio=2.0, attackTime=0.01, releaseTime=0.01, sr=44100.0, dtype=np.float32): """ Thanks to Eric Tarr for MATLAB code for this, p. 428 of Hack Audio book. Python version here used with permission. Our mods for Python: Minimized the for loop, removed dummy variables, and invoked numba @jit to make this "fast" Inputs: x: input signal sr: sample rate in Hz thresh: threhold in dB ratio: ratio (should be >=1 , i.e. ratio:1) attackTime, releaseTime: in seconds dtype: typical numpy datatype """ N = len(x) y = np.zeros(N, dtype=dtype) lin_A = np.zeros(N, dtype=dtype) # functions as gain # Initialize separate attack and release times alphaA = np.exp(-np.log(9)/(sr * attackTime))#.astype(dtype) alphaR = np.exp(-np.log(9)/(sr * releaseTime))#.astype(dtype) # Turn the input signal into a uni-polar signal on the dB scale x_uni = np.abs(x).astype(dtype) x_dB = 20*np.log10(x_uni + 1e-8).astype(dtype) # Ensure there are no values of negative infinity #x_dB = np.clip(x_dB, -96, None) # Numba doesn't yet support np.clip but we can write our own x_dB = my_clip_min(x_dB, -96) # Static Characteristics gainChange_dB = np.zeros(x_dB.shape) i = np.where(x_dB > thresh) gainChange_dB[i] = thresh + (x_dB[i] - thresh)/ratio - x_dB[i] # Perform Downwards Compression for n in range(x_dB.shape): # this loop is slow but not vectorizable due to its cumulative, sequential nature. @jit makes it fast(er). # smooth over the gainChange if gainChange_dB[n] < lin_A[n-1]: lin_A[n] = ((1-alphaA)*gainChange_dB[n]) +(alphaA*lin_A[n-1]) # attack mode else: lin_A[n] = ((1-alphaR)*gainChange_dB[n]) +(alphaR*lin_A[n-1]) # release lin_A = np.power(10.0,(lin_A/20)).astype(dtype) # Convert to linear amplitude scalar; i.e. map from dB to amplitude y = lin_A * x # Apply linear amplitude to input sample return y.astype(dtype) # Classes for Effects. First is the generic/main class. All others are subclass of this class Effect(): """Generic effect super-class sub-classed Effects should also define a 'go_wc()' method to execute the actual effect Network will call go() with normalized knob values, which then will call go_wc() The go_wc() method should return two value: y, x where y is target output and x is input signal """ def __init__(self, sr=44100.0, dtype=np.float32): self.name = 'Generic Effect' self.knob_names = ['knob'] self.knob_ranges = np.array([[0,1]], dtype=dtype) # min,max world coordinate values for "all the way counterclockwise" and "all the way clockwise" self.sr = sr self.is_inverse = False # Does this effect perform an 'inverse problem' by reversing x & y at the end? def knobs_wc(self, knobs_nn): # convert knob vals from [-.5,.5] to "world coordinates" used by effect functions return (self.knob_ranges[:,0] + (knobs_nn+0.5)*(self.knob_ranges[:,1]-self.knob_ranges[:,0])).tolist() def info(self): # Print some information about the effect assert len(self.knob_names)==len(self.knob_ranges) print(f'Effect: {self.name}. Knobs:') for i in range(len(self.knob_names)): print(f' {self.knob_names[i]}: {self.knob_ranges[i]} to {self.knob_ranges[i]}') if self.is_inverse: print(" <<<< INVERSE EFFECT <<<<") # Effects should also define a 'go_wc' method which executes the effect, mapping input and knobs_nn to output y, x # We return x as well as y, because some effects may reverse x & y (e.g. denoiser) def go_wc(self, x, knobs_wc): raise Exception("This effect's go_wc() is undefined") # this is the 'main' interface typically called during training & inference, using normalized knob values [-.5,.5] def go(self, x, knobs_nn, **kwargs): knobs_w = self.knobs_wc(knobs_nn) return self.go_wc(x, knobs_w, **kwargs) class Compressor_4c(Effect): # compressor with 4 controls def __init__(self, **kwargs): super(Compressor_4c, self, **kwargs).__init__() self.name = 'Compressor_4c' self.knob_names = ['threshold', 'ratio', 'attackTime','releaseTime'] self.knob_ranges = np.array([[-30,0], [1,5], [1e-3,4e-2], [1e-3,4e-2]]) def go_wc(self, x, knobs_w): return compressor_4controls(x, thresh=knobs_w, ratio=knobs_w, attackTime=knobs_w, releaseTime=knobs_w, sr=self.sr), x class Compressor_4c_Large(Effect): # compressor with 4 controls, larger ranges for parameters def __init__(self, **kwargs): super(Compressor_4c_Large, self, **kwargs).__init__() self.name = 'Compressor_4c_Large' self.knob_names = ['threshold', 'ratio', 'attackTime','releaseTime'] self.knob_ranges = np.array([[-50,0], [1.5,10], [1e-3,1], [1e-3,1]]) def go_wc(self, x, knobs_w): return compressor_4controls(x, thresh=knobs_w, ratio=knobs_w, attackTime=knobs_w, releaseTime=knobs_w, sr=self.sr), x ```