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[0])
      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[0]):   # 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][0]} to {self.knob_ranges[i][1]}')
        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[0], ratio=knobs_w[1], attackTime=knobs_w[2], releaseTime=knobs_w[3], 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[0], ratio=knobs_w[1], attackTime=knobs_w[2], releaseTime=knobs_w[3], sr=self.sr), x