text: from collections import namedtuple
  import numpy as np
  from untwist import data, utilities, transforms
  
  
  Anchors = namedtuple('Anchors', ['Distortion',
                                   'Artefacts',
                                   'Interferer',
                                   'Quality'],
                       )
  
  
  class Anchor:
      '''
      Anchor signals for a MUSHRA test assessing source separation
      techniques.  Four different anchors are provided for assessing the
      perception regarding interference, distortion, artefacts, and
      overall sound quality.  The first three were designed after [1], the
      quality anchor after [2].
  
      [1] Emiya, V., Vincent, E., Harlander, N., & Hohmann, V. (2011).
          Subjective and Objective Quality Assessment of Audio Source
          Separation. IEEE TASLP, 19(7), 2046–2057.
          http://doi.org/10.1109/TASL.2011.2109381
      [2] Cano, E., Fitzgerald, D., & Brandenburg, K. (2016).
          Evaluation of Quality of Sound Source Separation Algorithms:
          Human Perception vs Quantitative Metrics. In EUSIPCO
          (pp. 1758–1762).
          http://doi.org/10.1109/EUSIPCO.2016.7760550
      '''
  
      def __init__(self,
                   target,
                   others,
                   trim_factor_distorted=0.2,
                   trim_factor_artefacts=0.99,
                   low_pass_artefacts=False,
                   low_pass_cutoff=3500,
                   include_background_in_quality_anchor=True,
                   loudness_normalise_interferer=True,
                   ):
          '''
          target:
              The target audio, e.g. vocals
          others:
              Can be a list of everthing else, or just the accompaniment (Wave).
          trim_factor_distorted:
              Proportion of spectral frames to remove randomly in time.
          trim_factor_artefacts:
              Proportion of time-frequency bins to randomly remove.
          '''
  
          from scipy import signal
  
          # We need a single background
          if isinstance(others, list):
              self.background = sum(other for other in others)
          else:
              self.background = others
  
          if not isinstance(target, data.audio.Wave):
              raise ValueError('target must be of type Wave.')
          self.target = target
  
          points = 2048
          window = signal.get_window('hann', points, True)
          self.stft = transforms.STFT(window, points, points // 2)
          self.istft = transforms.ISTFT(window, points, points // 2)
  
          self.cut_off = utilities.conversion.nearest_bin(low_pass_cutoff,
                                                          points,
                                                          target.sample_rate)
          self.trim_factor_distorted = trim_factor_distorted
          self.trim_factor_artefacts = trim_factor_artefacts
          self.low_pass_artefacts = low_pass_artefacts
          self.include_background_in_quality_anchor = include_background_in_quality_anchor
          self.loudness_normalise_interferer = loudness_normalise_interferer
  
      def distorted_anchor(self):
          '''
          Returns the distortion signal created by low-pass filtering the
          target source signal to a 3.5 kHz cutoff frequency and by randomly
          setting 20% of the remaining time-frequency coefficients to zero,
          see [1].
  
          WARNING: this code can't reproduce the distortion from [1]
          exactly!
          '''
  
          x_fft = self.stft.process(self.target)
  
          x_fft[self.cut_off:] = 0
  
          num_frames_to_remove = int(x_fft.shape[1] * self.trim_factor_distorted)
          idx = np.random.choice(x_fft.shape[1],
                                 num_frames_to_remove,
                                 replace=False)
          x_fft[:, idx] = 0
  
          distortion = self.istft.process(x_fft)
  
          return distortion[:self.target.num_frames]
  
      def inteference_anchor(self):
          '''
          Interference anchor for a MUSHRA listening test.
          The anchor is created by the sum of target signal and the
          interferer. The interferer is formed by summing all interfering
          sources and then setting the overall loudness to that of the target,
          see [1].
          '''
  
          interferer = self.background.copy()
  
          if self.loudness_normalise_interferer:
              interferer.loudness = self.target.loudness
  
          interferer += self.target
  
          return interferer
  
      def artefacts(self):
          '''
          Returns the artefacts signal (musical noise) generated by randomly
          zeroing 99% of the time-frequency bins, see [1].
          '''
  
          x_fft = self.stft.process(self.target)
  
          idx = np.random.choice(
              x_fft.size,
              size=int(x_fft.size * self.trim_factor_artefacts),
              replace=False)
  
          row, col = np.unravel_index(idx, x_fft.shape)
  
          x_fft[row, col] = 0
  
          if self.low_pass_artefacts:
              x_fft[self.cut_off:] = 0
  
          artefacts = self.istft.process(x_fft)
  
          return artefacts[:self.target.num_frames]
  
      def artefacts_anchor(self):
          '''
          Artefacts anchor for a MUSHRA listening test.
          The anchor is defined as the sum of the target with musical
          noise; both equally loud. Musical noise is created by randomly
          zeroing 99% of the time-frequency bins, see [1].
          '''
  
          artefacts = self.artefacts()
  
          artefacts.loudness = self.target.loudness
  
          anchor = artefacts + self.target
  
          return anchor
  
      def quality_anchor(self):
          '''
          Quality anchor for a MUSHRA listening test.
          The anchor is defined as the sum of the distortion anchor,
          artefacts only and interferer only; all equally loud, see [2].
          '''
  
          target_loudness = -23
          signals = []
  
          signals_to_sum = [self.distorted_anchor(), self.artefacts()]
  
          if self.include_background_in_quality_anchor:
              signals_to_sum.append(self.background)
  
          for signal in signals_to_sum:
  
              signal.loudness = target_loudness
              signals.append(signal)
  
          anchor = sum(signals)
          anchor = anchor[:self.target.num_frames]
  
          return anchor
  
      def create(self):
  
          return Anchors(self.distorted_anchor(),
                         self.artefacts_anchor(),
                         self.inteference_anchor(),
                         self.quality_anchor())
  
  
  class RemixAnchor():
  
      def __init__(self,
                   target,
                   others,
                   trim_factor_distorted=0.2,
                   trim_factor_artefacts=0.99,
                   target_level_offset=-14,
                   quality_anchor_loudness_balance=[0, 0],
                   low_pass_cutoff=3500):
          '''
          target:
              The target audio, e.g. vocals
          others:
              Can be a list of everthing else, or just the accompaniment (Wave).
          trim_factor_distorted:
              Proportion of spectral frames to remove randomly in time.
          trim_factor_artefacts:
              Proportion of time-frequency bins to randomly remove.
          target_level_offset:
              The level adjustment applied to the target for the balance anchor.
          quality_anchor_loudness_balance:
              The desired loudness balance of [distorted_audio, artefacts], e.g.
              setting [10, 0] would set the distorted audio to be 10 LU above
              the artefacts. Default is [0, 0] = equal loudness.
          '''
  
          # We need a single background
          if isinstance(others, list):
              self.background = sum(other for other in others)
          else:
              self.background = others
  
          self.target = target
          self.mix = self.target + self.background
  
          self.anchor_gen = Anchor(self.mix,
                                   None,
                                   trim_factor_distorted,
                                   trim_factor_artefacts,
                                   low_pass_artefacts=True,
                                   low_pass_cutoff=low_pass_cutoff)
  
          self.target_level_offset = target_level_offset
  
          self.quality_anchor_loudness_balance = np.array(
              quality_anchor_loudness_balance)
  
      def distorted_anchor(self):
          '''
          Returns the distortion mix created by low-pass filtering the
          target source signal to a 3.5 kHz cutoff frequency and by randomly
          setting 20% of the remaining time-frequency coefficients to zero,
          see [1].
          '''
  
          return self.anchor_gen.distorted_anchor()
  
      def artefacts_anchor(self):
          '''
          Returns the artefacts mix (musical noise) generated by randomly
          zeroing 99% of the time-frequency bins, see [1].
          '''
          return self.anchor_gen.artefacts_anchor()
  
      def interferer_anchor(self):
          '''
          Mixes the background with the target offset by 'target_level_offset'.
          '''
  
          mix = (self.target *
                 utilities.conversion.db_to_amp(self.target_level_offset) +
                 self.background)
  
          return mix
  
      def interferer_anchor_both_sources(self):
          '''
          Returns the target and background as used to create the interferer
          anchor (but not normalised).
          '''
  
          return (self.target *
                  utilities.conversion.db_to_amp(self.target_level_offset),
                  self.background)
  
      def quality_anchor(self):
          '''
          Sum of the distorted mix and artefacts of the mix, at equal loudness.
          You can adjust the loudness balance by setting the attribute
          'quality_anchor_loudness_balance' (default is an array of zeros).
          '''
  
          target_loudness = (np.array([-23.0, -23.0]) +
                             (self.quality_anchor_loudness_balance -
                              self.quality_anchor_loudness_balance.mean())
                             )
  
          signals = []
  
          for signal, loudness in zip([self.distorted_anchor(),
                                       self.anchor_gen.artefacts()],
                                      target_loudness):
  
              signal.loudness = loudness
              signals.append(signal)
  
          anchor = sum(signals)
          anchor = anchor[:self.target.num_frames]
  
          return anchor
  
      def create(self):
  
          return Anchors(self.distorted_anchor(),
                         self.artefacts_anchor(),
                         self.interferer_anchor(),
                         self.quality_anchor())
  