def shift(self, shift, mode=sumpf_internal.ShiftMode.OFFSET): """Returns a spectrogram, which is shifted in time. Positive shifts result in a delayed spectrogram, while negative shifts prepone it. In mode ``OFFSET``, it is allowed to pass ``None`` for the ``shift`` parameter, which sets the offset of the resulting :class:`~sumpf.Spectrogram` to 0. The shift can be performed in different ways, which can be specified with the ``mode`` parameter. The flags, that can be passed to this parameter are defined and documented in the :class:`sumpf.Spectrogram.shift_modes` enumeration. :param shift: an integer number of samples, by which the spectrogram shall be shifted :param mode: a flag from the :class:`sumpf.Spectrogram.shift_modes` enumeration :returns: the shifted :class:`~sumpf.Spectrogram` """ if shift == 0: return self elif mode == Spectrogram.shift_modes.OFFSET: if shift is None: return Spectrogram(channels=self._channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=0, labels=self._labels) else: return Spectrogram(channels=self._channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset + shift, labels=self._labels) else: if mode == Spectrogram.shift_modes.CROP: channels = sumpf_internal.allocate_array(shape=self._channels.shape, dtype=numpy.complex128) if shift < 0: channels[:, :, 0:shift] = self._channels[:, :, -shift:] channels[:, :, shift:] = 0.0 else: channels[:, :, 0:shift] = 0.0 channels[:, :, shift:] = self._channels[:, :, 0:-shift] elif mode == Spectrogram.shift_modes.PAD: channels = sumpf_internal.allocate_array(shape=(len(self), self.__frequencies, self._length + abs(shift)), dtype=numpy.complex128) if shift < 0: channels[:, :, 0:self._length] = self._channels channels[:, :, self._length:] = 0.0 else: channels[:, :, 0:shift] = 0.0 channels[:, :, shift:] = self._channels elif mode == Spectrogram.shift_modes.CYCLE: channels = sumpf_internal.allocate_array(shape=self._channels.shape, dtype=numpy.complex128) channels[:, :, 0:shift] = self._channels[:, :, -shift:] channels[:, :, shift:] = self._channels[:, :, 0:-shift] return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def _algebra_function(self, other, function, other_pivot, label): """Protected helper function that implements the broadcasting of spectrums or arrays with different shapes when using the overloaded math operators. :param other: the object "on the other side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :param other_pivot: a default value, that is used as first operand for samples, where only the other object has data (e.g. when the two spectrums don't overlap due to different lengths). If ``other_pivot`` is ``None``, the data from the other object is copied. :param label: the string label for the computed channels :returns: a :class:`~sumpf.Spectrum` instance """ if isinstance(other, Spectrum): if len(self) == 1: channels = sumpf_internal.allocate_array( shape=(len(other), self._length), dtype=numpy.complex128) function(self._channels[0], other.channels(), out=channels) elif len(other) == 1: channels = sumpf_internal.allocate_array( shape=self.shape(), dtype=numpy.complex128) function(self._channels, other.channels()[0], out=channels) elif len(self) < len(other): channels = sumpf_internal.allocate_array( shape=(len(other), self._length), dtype=numpy.complex128) function(self._channels, other.channels()[0:len(self)], out=channels[0:len(self)]) if other_pivot is None: channels[len(self):] = other.channels()[len(self):] else: function(other_pivot, other.channels()[len(self):], out=channels[len(self):]) else: channels = sumpf_internal.allocate_array( shape=self.shape(), dtype=numpy.complex128) function(self._channels[0:len(other)], other.channels(), out=channels[0:len(other)]) channels[len(other):] = self._channels[len(other):] return Spectrum(channels=channels, resolution=self.__resolution, labels=(label, ) * len(channels)) elif isinstance(other, (SampledData, sumpf.Filter)): return NotImplemented else: # other is an array or a number try: return Spectrum(channels=function( self._channels, other, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels) except TypeError: return NotImplemented
def __init__(self, bits=None, seed=None, sampling_rate=48000.0, length=None): """ :param bits: the integer size of the shift register, with which the MLS is generated :param seed: a seed for the random initialization of the shift register :param sampling_rate: the sampling rate of the MLS as a float :param length: the length of the signal """ if length is None: if bits is None: bits = 16 length = 2**bits - 1 elif bits is None: bits = int(math.ceil(math.log2(length + 1))) random = numpy.random.default_rng(seed) state = random.choice((False, True), size=bits) if True not in state: state[random.integers(low=0, high=len(state) - 1)] = True sequence = scipy.signal.max_len_seq(bits, state, length)[0] channels = sumpf_internal.allocate_array(shape=(1, len(sequence))) channels[0, :] = sequence channels *= 2.0 channels -= 1.0 Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=("MLS", )) self.__bits = bits
def __init__(self, frequency=1000.0, phase=0.0, sampling_rate=48000.0, length=2**16): """ :param frequency: the square wave's frequency in Hz :param phase: a phase offset in radians :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sine wave """ channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) duration = (length - 1) / sampling_rate omega_t_plus_phase = numpy.linspace( phase, 2.0 * math.pi * frequency * duration + phase, length) numpy.sin(omega_t_plus_phase, out=channels[0]) numpy.sign(channels, out=channels) Wave.__init__(self, frequency=frequency, channels=channels, sampling_rate=sampling_rate, offset=0, labels=("Square wave", ))
def output(self): """Computes the merged spectrum and returns it. :returns: a :class:`~sumpf.Spectrum` instance """ if len(self.__spectrums) == 0: # pylint: disable=len-as-condition return sumpf.Spectrum() elif len(self.__spectrums) == 1: return next(iter(self.__spectrums.values())) # simply return the first and only spectrum else: # find the number of channels and the merged spectrum's length number_of_channels = sum(len(s) for s in self.__spectrums.values()) length = max(s.length() for s in self.__spectrums.values()) channels = sumpf_internal.allocate_array(shape=(number_of_channels, length), dtype=numpy.complex128) labels = [""] * number_of_channels # fill in the data if self.__mode == MergeSpectrums.modes.FIRST_DATASET_FIRST: self.__first_dataset_first(channels, length, labels) elif self.__mode == MergeSpectrums.modes.FIRST_CHANNELS_FIRST: self.__first_channels_first(channels, labels) else: raise ValueError("invalid mode: {}".format(self.__mode)) # create and return the result return sumpf.Spectrum(channels=channels, resolution=next(iter(self.__spectrums.values())).resolution(), labels=labels)
def output(self): """Computes the merged signal and returns it. :returns: a :class:`~sumpf.Signal` instance """ if len(self.__signals) == 0: # pylint: disable=len-as-condition return sumpf.Signal() elif len(self.__signals) == 1: return next(iter(self.__signals.values())) # simply return the first and only signal else: # find the offset, the number of channels and the merged signal's length offset = min(s.offset() for s in self.__signals.values()) number_of_channels = sum(len(s) for s in self.__signals.values()) length = max(s.offset() + s.length() for s in self.__signals.values()) - offset channels = sumpf_internal.allocate_array(shape=(number_of_channels, length), dtype=numpy.float64) labels = [""] * number_of_channels # fill in the data if self.__mode == MergeSignals.modes.FIRST_DATASET_FIRST: self.__first_dataset_first(channels, length, offset, labels) elif self.__mode == MergeSignals.modes.FIRST_CHANNELS_FIRST: self.__first_channels_first(channels, offset, labels) else: raise ValueError("invalid mode: {}".format(self.__mode)) # create and return the result return sumpf.Signal(channels=channels, sampling_rate=next(iter(self.__signals.values())).sampling_rate(), offset=offset, labels=labels)
def __init__(self, bits=None, seed=None, sampling_rate=48000.0, length=None): """ :param bits: the integer size of the shift register, with which the MLS is generated :param seed: a seed for the random initialization of the shift register :param sampling_rate: the sampling rate of the MLS as a float :param length: the length of the signal """ if length is None: if bits is None: bits = 16 length = 2 ** bits - 1 elif bits is None: bits = int(math.ceil(math.log2(length + 1))) random_ = random.Random() random_.seed(seed) state = [random_.choice((False, True)) for _ in range(bits)] if True not in state: state[random_.randint(0, len(state) - 1)] = True sequence = scipy.signal.max_len_seq(bits, state, length)[0] channels = sumpf_internal.allocate_array(shape=(1, len(sequence)), dtype=numpy.float64) channels[0, :] = sequence channels *= 2.0 channels -= 1.0 Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=("MLS",)) self.__bits = bits
def __algebra_function_other_has_one_channel(self, other, function, label): channels = sumpf_internal.allocate_array(shape=self.shape()) function(self._channels, other.channels()[0], out=channels) return Signal(channels=channels, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=(label,) * len(self))
def __init__(self, plateau=0, sampling_rate=48000.0, length=8192, symmetric=True): """ :param plateau: an integer length or a float factor for the signal length, that specifies, how many samples in the middle of the signal shall be 1.0 without being affected by the window's fade in and fade out. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the window function :param symmetric: True, if the window's last sample shall be the same as its first sample. False, if the window's last sample shall be the same as its second sample. The latter is often beneficial in segmentation applications, as it makes it easier to meet the "constant overlap-add"-constraint. """ self.__symmetric = symmetric self.__plateau = sumpf_internal.index(plateau, length) channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) if self.__plateau == 0: if symmetric: channels[0, :] = self._function(length) else: channels[0, :] = self._function(length + 1)[0:-1] else: fade_length = (length - self.__plateau) // 2 # the length of the fade in or the fade out if symmetric: fade = self._function(2 * fade_length) else: fade = self._function(2 * fade_length + 1) channels[0, 0:fade_length] = fade[0:fade_length] channels[0, fade_length:length - fade_length] = 1.0 channels[0, length - fade_length:] = fade[fade_length:2 * fade_length] Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=(self._label(),))
def decay_model(self, start, stop): """Computes an exponential decay curve, that is fitted to the segment of the energy decay curve, which is specified by the start and stop parameters. The :class:`~sumpf.Signal`, that is returned by this method, can be plotted alongside this energy decay curve in order to check, if the reverberation time, that is computed from the specified segment, is realistic. In order to produce a meaningful result, the start and stop parameters must frame the segment, in which the semi-logarithmic plot (dB plot) of the energy decay curve falls linearly over time. The given start and stop parameters are the indices of the respective samples of the signal. These indices can be given as (negative) integers, which work just like :class:`list` indices, or as floats between -1.0 and 1.0, which will be scaled to the length of the signal. :param start: the sample index of the first sample in the segment :param stop: the sample index of the first sample after the segment :returns: a :class:`~sumpf.Signal` """ channels = sumpf_internal.allocate_array(shape=self.shape()) i = numpy.arange(self._length) for c, (m, n) in zip(channels, self.__solve(start, stop)): c[:] = numpy.power(10.0, (m / 10.0) * i + (n / 10.0)) return Signal(channels=channels, sampling_rate=self.sampling_rate(), offset=self.offset(), labels=self.labels())
def __init__(self, start_frequency=20.0, stop_frequency=20000.0, phase=0.0, interval=(0, 1.0), sampling_rate=48000.0, length=2**16): """ :param start_frequency: the start frequency in Hz :param stop_frequency: the stop frequency in Hz :param phase: a phase offset in radians (e.g. pass pi/2 for a cosine sweep) :param interval: a tuple, list or array of two numbers, that specify the indices of the samples, at which the start and the stop frequencies shall be excited. The sweep will continue outside this interval. This functionality is useful, if the sweep shall be faded in or out and the range between the start and the stop frequency shall not be affected by the fading. The indices can be specified as integer indices or as floats between 0.0 and 1.0, which are mapped to 0 and ``length``. Negative indices (also possible with floats) are relative to the end of the signal. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sweep """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) # generate the sweep start, stop, t, T, sweep_offset, a, b, k = linear_sweep_parameters( start_frequency=start_frequency, stop_frequency=stop_frequency, interval=interval, sampling_rate=sampling_rate, length=length) t *= -1.0 t += T s = 2.0 / ( stop - start ) # a scaling factor, so that the convolution of the sweep and the inverse sweep results in a unit impulse if length <= 8192 or not numexpr: # for short sweeps NumExpr is slower than NumPy array = t * t array *= b array += a * t array += phase + 0.0 numpy.sin(array, out=channels[0, :]) channels *= s else: numexpr.evaluate(ex="s * sin(phase + a * t + b * (t**2))", out=channels[0, :], optimization="moderate") # store the values and parameters Sweep.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=-stop - start, function=lambda tau: stop_frequency - k * (tau - sweep_offset))
def __algebra_function_other_has_one_channel(self, other, function, label): channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) function(self._channels, other.channels()[0], out=channels) return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=(label,) * len(self))
def conjugate(self): """Returns a spectrum with the complex conjugate of this spectrum's channels. :returns: a :class:`~sumpf.Spectrum` """ channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) numpy.conjugate(self._channels, out=channels) return Spectrum(channels=channels, resolution=self.__resolution, labels=self._labels)
def __abs__(self): """Operator overload for computing the magnitude of the spectrum with the built-in function :func:`abs`. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.absolute(self._channels, out=sumpf_internal.allocate_array(self.shape())), resolution=self.__resolution, labels=self._labels)
def __neg__(self): """Operator for inverting the phase of the spectrum with ``-spectrum``. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.negative(self._channels, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def __init__(self, seed, sampling_rate, length, label): """ :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the noise signal """ self.__seed = seed channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) channels[0, :] = self._function(length) Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=(label,))
def __abs__(self): """Operator overload for computing the magnitude of the spectrum with the built-in function :func:`abs`. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.absolute( self._channels, out=sumpf_internal.allocate_array(self.shape())), resolution=self.__resolution, labels=self._labels)
def __neg__(self): """Operator for inverting the phase of the spectrum with ``-spectrum``. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.negative( self._channels, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def __neg__(self): """Operator overload for computing the sample-wise negative of the signal with ``-signal``. :returns: a :class:`~sumpf.Signal` instance """ return Signal(channels=numpy.negative(self._channels, out=sumpf_internal.allocate_array(self.shape())), sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def __abs__(self): """Operator overload for computing the sample-wise absolute of the signal with the built-in function :func:`abs`. :returns: a :class:`~sumpf.Signal` instance """ return Signal(channels=numpy.fabs(self._channels, out=sumpf_internal.allocate_array(self.shape())), sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def __algebra_function_different_type(self, other, function): channels = sumpf_internal.allocate_array(self.shape()) try: function(self._channels, other, out=channels) except TypeError: return NotImplemented else: return Signal(channels=channels, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def _algebra_function_right(self, other, function): """Protected helper function for overloading the right hand side operators. :param other: the object "on the left side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :returns: a :class:`~sumpf.Signal` instance """ return Signal(channels=function(other, self._channels, out=sumpf_internal.allocate_array(self.shape())), sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def __invert__(self): """Rededicated operator overload for inverting this spectrum. The inverse of a spectrum is simply ``1 / spectrum``. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.divide(1.0, self._channels, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def _algebra_function(self, other, function, other_pivot, label): """Protected helper function that implements the broadcasting of spectrums or arrays with different shapes when using the overloaded math operators. :param other: the object "on the other side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :param other_pivot: a default value, that is used as first operand for samples, where only the other object has data (e.g. when the two spectrums don't overlap due to different lengths). If ``other_pivot`` is ``None``, the data from the other object is copied. :param label: the string label for the computed channels :returns: a :class:`~sumpf.Spectrum` instance """ if isinstance(other, Spectrum): if len(self) == 1: channels = sumpf_internal.allocate_array(shape=(len(other), self._length), dtype=numpy.complex128) function(self._channels[0], other.channels(), out=channels) elif len(other) == 1: channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) function(self._channels, other.channels()[0], out=channels) elif len(self) < len(other): channels = sumpf_internal.allocate_array(shape=(len(other), self._length), dtype=numpy.complex128) function(self._channels, other.channels()[0:len(self)], out=channels[0:len(self)]) if other_pivot is None: channels[len(self):] = other.channels()[len(self):] else: function(other_pivot, other.channels()[len(self):], out=channels[len(self):]) else: channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) function(self._channels[0:len(other)], other.channels(), out=channels[0:len(other)]) channels[len(other):] = self._channels[len(other):] return Spectrum(channels=channels, resolution=self.__resolution, labels=(label,) * len(channels)) else: # other is an array or a number try: return Spectrum(channels=function(self._channels, other, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels) except TypeError: return NotImplemented
def __invert__(self): """Rededicated operator overload for inverting this spectrum. The inverse of a spectrum is simply ``1 / spectrum``. :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=numpy.divide( 1.0, self._channels, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def __neg__(self): """Operator for inverting the phase of the spectrogram with ``-spectrogram``. :returns: a :class:`sumpf.Spectrogram` instance """ channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) numpy.negative(self._channels, out=channels) return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def _algebra_function_right(self, other, function): """Protected helper function for overloading the right hand side operators. :param other: the object "on the left side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=function(other, self._channels, out=sumpf_internal.allocate_array(self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def conjugate(self): """Returns a spectrogram with the complex conjugate of this spectrogram's channels. :returns: a :class:`sumpf.Spectrogram` """ channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) numpy.conjugate(self._channels, out=channels) return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def __algebra_function_different_type(self, other, function): channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) try: function(self._channels, other, out=channels) except TypeError: return NotImplemented else: return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def __init__(self, start_frequency=20.0, stop_frequency=20000.0, phase=0.0, interval=(0, 1.0), sampling_rate=48000.0, length=2**16): """ :param start_frequency: the start frequency in Hz :param stop_frequency: the stop frequency in Hz :param phase: a phase offset in radians (e.g. pass pi/2 for a cosine sweep) :param interval: a tuple, list or array of two numbers, that specify the indices of the samples, at which the start and the stop frequencies shall be excited. The sweep will continue outside this interval. This functionality is useful, if the sweep shall be faded in or out and the range between the start and the stop frequency shall not be affected by the fading. The indices can be specified as integer indices or as floats between 0.0 and 1.0, which are mapped to 0 and ``length``. Negative indices (also possible with floats) are relative to the end of the signal. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sweep """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) # generate the sweep _, _, t, _, sweep_offset, l, a = exponential_sweep_parameters( start_frequency, stop_frequency, interval, sampling_rate, length) if length <= 8192 or not numexpr: # for short sweeps NumExpr is slower than NumPy array = t array /= l numpy.expm1(array, out=array) array *= a array += phase numpy.sin(array, out=channels[0, :]) else: numexpr.evaluate(ex="sin(a * expm1(t / l) + phase)", out=channels[0, :], optimization="moderate") # store the values and parameters BaseExponentialSweep.__init__( self, channels=channels, sampling_rate=sampling_rate, offset=0, function=lambda tau: start_frequency * math.exp( (tau - sweep_offset) / l), l=l)
def __init__(self, value=0.0, resolution=1.0, length=1): """ :param value: the complex value of the samples :param resolution: the frequency resolution of the spectrum as a float :param length: the number of samples of the spectrum """ channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.complex128) channels[:] = value Spectrum.__init__(self, channels=channels, resolution=resolution, labels=("Constant", ))
def inverse_short_time_fourier_transform(self, window=4096, overlap=0.5, pad=True): """Computes a :class:`~sumpf.Signal` from this spectrogram. This method requires :mod:`scipy` to be installed. :param window: the window function, that was used to compute this spectrogram. It can be passed as a :class:`~sumpf.Signal`, as an iterable or an integer window length. See :func:`~sumpf._internal._functions.get_window` for details. :param overlap: the overlap of the windowed segments. It can be passed as an integer number of samples or a float fraction of the window's length. Negative numbers will be added to the window's length. :param pad: True, if the signal was padded with zeros to fit an integer number of segments. False, if the samples at the end of the signal, that did not fit a full segment, were ignored. The results when setting this to False may be unexpected... """ import scipy.signal # create some necessary objects window = sumpf_internal.get_window(window=window, overlap=overlap, symmetric=False, sampling_rate=self.__sampling_rate) window_length = window.length() overlap = sumpf_internal.index(overlap, window_length) step = window_length - overlap # compute the STFT if len(window) == 1: istft = scipy.signal.istft(self._channels, fs=self.__sampling_rate, window=window.channels()[0], nperseg=window_length, noverlap=overlap, boundary=pad)[1] else: istft = [] for c, w in zip(self._channels, window.channels()): istft.append(scipy.signal.istft(c, fs=self.__sampling_rate, window=w, nperseg=window_length, noverlap=overlap, boundary=pad)[1]) channels = sumpf_internal.allocate_array(shape=numpy.shape(istft)) channels[:] = istft # return the spectrogram return sumpf.Signal(channels=channels, sampling_rate=self.__sampling_rate * step, offset=self.__offset * step, labels=self._labels)
def spectrum(self, resolution, length): """Samples the transfer functions with the given resolution and given number of samples and returns the result as a spectrum. :param resolution: the frequency resolution of the resulting spectrum :param length: the number of samples per channel of the resulting spectrum :returns: a :class:`~sumpf.Spectrum` instance """ frequencies = numpy.linspace(0.0, (length - 1) * resolution, length) s = S(frequencies) channels = sumpf_internal.allocate_array(shape=(len(self.__transfer_functions), length), dtype=numpy.complex128) for tf, c in zip(self.__transfer_functions, channels): tf(s, out=c) return sumpf.Spectrum(channels=channels, resolution=resolution, labels=self.__labels)
def __init__(self, impulse_response): """ :param impulse_response: the impulse response :class:`~sumpf.Signal`, from which this energy decay curve shall be computed. """ channels = sumpf_internal.allocate_array( shape=impulse_response.shape()) numpy.square(impulse_response.channels(), out=channels) numpy.cumsum(channels[:, ::-1], axis=1, out=channels[:, ::-1]) Signal.__init__(self, channels=channels, sampling_rate=impulse_response.sampling_rate(), offset=impulse_response.offset(), labels=impulse_response.labels())
def __algebra_function_different_shapes(self, other, function, other_pivot, label): # allocate the channels array start = min(self.__offset, other.offset()) stop = max(self.__offset + self._length, other.offset() + other.length()) length = stop - start channelcount = max(len(self), len(other)) shape = (channelcount, length) channels = sumpf_internal.allocate_array(shape) # copy the two signals channels[:] = 0.0 channels[0:len(self), self.__offset - start:self.__offset + self._length - start] = self._channels if len(self) == 1: for c in range(1, len(channels)): channels[c, self.__offset - start:self.__offset + self._length - start] = self._channels[0] if other_pivot is None: channels[0:len(other), other.offset() - start:other.offset() + other.length() - start] = other.channels() if len(other) == 1: for c in range(1, len(channels)): channels[c, other.offset() - start:other.offset() + other.length() - start] = other.channels()[0] else: channel_slice = slice(other.offset() - start, other.offset() + other.length() - start) function(other_pivot, other.channels(), out=channels[0:len(other), channel_slice]) if len(other) == 1: for c in range(1, len(channels)): channels[c, channel_slice] = channels[0, channel_slice] # apply the function to the overlapping parts of the two signals overlap_start = max(self.__offset, other.offset()) overlap_stop = min(self.__offset + self._length, other.offset() + other.length()) if overlap_start < overlap_stop: if len(self._channels) == 1: function(self._channels[0, overlap_start - self.__offset:overlap_stop - self.__offset], other.channels()[:, overlap_start - other.offset():overlap_stop - other.offset()], out=channels[:, overlap_start - start:overlap_stop - start]) elif len(other.channels()) == 1: function(self._channels[:, overlap_start - self.__offset:overlap_stop - self.__offset], other.channels()[0, overlap_start - other.offset():overlap_stop - other.offset()], out=channels[:, overlap_start - start:overlap_stop - start]) else: overlap_channelcount = min(len(self), len(other)) function(self._channels[0:overlap_channelcount, overlap_start - self.__offset:overlap_stop - self.__offset], other.channels()[0:overlap_channelcount, overlap_start - other.offset():overlap_stop - other.offset()], out=channels[0:overlap_channelcount, overlap_start - start:overlap_stop - start]) # assemble and return the result signal return Signal(channels=channels, sampling_rate=self.__sampling_rate, offset=start, labels=(label,) * channelcount)
def __invert__(self): """Rededicated operator overload for inverting this signal. Convolving a signal with its inverse results in a unit impulse. The inverse is computing in the frequency domain: ``ifft(1 / fft(signal))``. :returns: a :class:`~sumpf.Signal` instance """ channels = sumpf_internal.allocate_array(shape=self.shape()) spectrum = numpy.fft.rfft(self._channels) inverse = numpy.divide(1.0, spectrum) channels[:] = numpy.fft.irfft(inverse) return Signal(channels=channels, sampling_rate=self.__sampling_rate, offset=-self.__offset, labels=self._labels)
def __init__(self, value=0.0, sampling_rate=48000.0, length=2**16): """ :param value: the float value of the samples :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the signal """ channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) channels[:] = value Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=("Constant", ))
def __init__(self, start_frequency=20.0, stop_frequency=20000.0, phase=0.0, interval=(0, 1.0), sampling_rate=48000.0, length=2 ** 16): """ :param start_frequency: the start frequency in Hz :param stop_frequency: the stop frequency in Hz :param phase: a phase offset in radians (e.g. pass pi/2 for a cosine sweep) :param interval: a tuple, list or array of two numbers, that specify the indices of the samples, at which the start and the stop frequencies shall be excited. The sweep will continue outside this interval. This functionality is useful, if the sweep shall be faded in or out and the range between the start and the stop frequency shall not be affected by the fading. The indices can be specified as integer indices or as floats between 0.0 and 1.0, which are mapped to 0 and ``length``. Negative indices (also possible with floats) are relative to the end of the signal. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sweep """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) # generate the sweep start, stop, t, T, sweep_offset, a, b, k = linear_sweep_parameters(start_frequency=start_frequency, stop_frequency=stop_frequency, interval=interval, sampling_rate=sampling_rate, length=length) t *= -1.0 t += T s = 2.0 / (stop - start) # a scaling factor, so that the convolution of the sweep and the inverse sweep results in a unit impulse if length <= 8192 or not numexpr: # for short sweeps NumExpr is slower than NumPy array = t * t array *= b array += a * t array += phase + 0.0 numpy.sin(array, out=channels[0, :]) channels *= s else: numexpr.evaluate(ex="s * sin(phase + a * t + b * (t**2))", out=channels[0, :], optimization="moderate") # store the values and parameters Sweep.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=-stop - start, function=lambda tau: stop_frequency - k * (tau - sweep_offset))
def __init__(self, start_frequency=20.0, stop_frequency=20000.0, phase=0.0, interval=(0, 1.0), sampling_rate=48000.0, length=2 ** 16): """ :param start_frequency: the start frequency in Hz :param stop_frequency: the stop frequency in Hz :param phase: a phase offset in radians (e.g. pass pi/2 for a cosine sweep) :param interval: a tuple, list or array of two numbers, that specify the indices of the samples, at which the start and the stop frequencies shall be excited. The sweep will continue outside this interval. This functionality is useful, if the sweep shall be faded in or out and the range between the start and the stop frequency shall not be affected by the fading. The indices can be specified as integer indices or as floats between 0.0 and 1.0, which are mapped to 0 and ``length``. Negative indices (also possible with floats) are relative to the end of the signal. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sweep """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) # generate the sweep _, _, t, _, sweep_offset, l, a = exponential_sweep_parameters(start_frequency, stop_frequency, interval, sampling_rate, length) if length <= 8192 or not numexpr: # for short sweeps NumExpr is slower than NumPy array = t array /= l numpy.expm1(array, out=array) array *= a array += phase numpy.sin(array, out=channels[0, :]) else: numexpr.evaluate(ex="sin(a * expm1(t / l) + phase)", out=channels[0, :], optimization="moderate") # store the values and parameters BaseExponentialSweep.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, function=lambda tau: start_frequency * math.exp((tau - sweep_offset) / l), l=l)
def _algebra_function_right(self, other, function): """Protected helper function for overloading the right hand side operators. :param other: the object "on the left side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :returns: a :class:`~sumpf.Signal` instance """ channels = sumpf_internal.allocate_array(shape=self.shape(), dtype=numpy.complex128) try: function(other, self._channels, out=channels) except TypeError: return NotImplemented else: return Spectrogram(channels=channels, resolution=self.__resolution, sampling_rate=self.__sampling_rate, offset=self.__offset, labels=self._labels)
def _algebra_function_right(self, other, function, other_pivot, label): """Protected helper function for overloading the right hand side operators. :param other: the object "on the left side of the operator" :param function: a function, that implements the computation for arrays (e.g. numpy.add) :param other_pivot: a default value, that is used as first operand for samples, where only the other object has data (e.g. when the two spectrums don't overlap due to different lengths). If ``other_pivot`` is ``None``, the data from the other object is copied. :param label: the string label for the computed channels :returns: a :class:`~sumpf.Spectrum` instance """ return Spectrum(channels=function(other, self._channels, out=sumpf_internal.allocate_array( self.shape(), numpy.complex128)), resolution=self.__resolution, labels=self._labels)
def __init__(self, frequency=1000.0, phase=0.0, sampling_rate=48000.0, length=2 ** 16): """ :param frequency: the sine wave's frequency in Hz :param phase: a phase offset in radians (e.g. pass pi/2 for a cosine signal) :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the sine wave """ channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) duration = (length - 1) / sampling_rate omega_t_plus_phase = numpy.linspace(phase, 2.0 * math.pi * frequency * duration + phase, length) numpy.sin(omega_t_plus_phase, out=channels[0]) Wave.__init__(self, frequency=frequency, channels=channels, sampling_rate=sampling_rate, offset=0, labels=("Sine",))
def spectrum(self, resolution=48000.0 / 4096, length=2049): """Samples the transfer functions with the given resolution and given number of samples and returns the result as a spectrum. :param resolution: the frequency resolution of the resulting spectrum :param length: the number of samples per channel of the resulting spectrum :returns: a :class:`~sumpf.Spectrum` instance """ frequencies = numpy.linspace(0.0, (length - 1) * resolution, length) s = S(frequencies) channels = sumpf_internal.allocate_array(shape=(len( self.__transfer_functions), length), dtype=numpy.complex128) for tf, c in zip(self.__transfer_functions, channels): tf(s, out=c) return sumpf.Spectrum(channels=channels, resolution=resolution, labels=self.__labels)
def inverse_fourier_transform(self): """Computes the channel-wise inverse Fourier transform of this spectrum. :returns: a :class:`~sumpf.Signal` instance """ if self._length == 0: return sumpf.Signal(channels=numpy.empty(shape=(len(self), 0)), sampling_rate=0.0, offset=0, labels=self._labels) length = max(1, (self._length - 1) * 2) sampling_rate = self.__resolution * length channels = sumpf_internal.allocate_array(shape=(len(self._channels), length)) channels[:, :] = numpy.fft.irfft(self._channels, n=length) return sumpf.Signal(channels=channels, sampling_rate=sampling_rate, offset=0, labels=self._labels)
def __init__(self, plateau=0, sampling_rate=48000.0, length=8192, symmetric=True): """ :param plateau: an integer length or a float factor for the signal length, that specifies, how many samples in the middle of the signal shall be 1.0 without being affected by the window's fade in and fade out. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the window function :param symmetric: True, if the window's last sample shall be the same as its first sample. False, if the window's last sample shall be the same as its second sample. The latter is often beneficial in segmentation applications, as it makes it easier to meet the "constant overlap-add"-constraint. """ self.__symmetric = symmetric self.__plateau = sumpf_internal.index(plateau, length) channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) if self.__plateau == 0: if symmetric: channels[0, :] = self._function(length) else: channels[0, :] = self._function(length + 1)[0:-1] else: fade_length = (length - self.__plateau ) // 2 # the length of the fade in or the fade out if symmetric: fade = self._function(2 * fade_length) else: fade = self._function(2 * fade_length + 1) channels[0, 0:fade_length] = fade[0:fade_length] channels[0, fade_length:length - fade_length] = 1.0 channels[0, length - fade_length:] = fade[fade_length:2 * fade_length] Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=(self._label(), ))
def pad(self, length, value=0.0): """Returns a spectrum with the given length, which is achieved by either cropping or appending the given value. :param length: the length of the resulting spectrum :param value: the value with which the spectrum shall be padded :returns: a padded or cropped :class:`~sumpf.Spectrum` """ if length == self._length: return self else: channels = sumpf_internal.allocate_array(shape=(len(self), length), dtype=numpy.complex128) if length < self._length: channels[:] = self._channels[:, 0:length] else: channels[:, 0:self._length] = self._channels channels[:, self._length:] = value return Spectrum(channels=channels, resolution=self.__resolution, labels=self._labels)
def __init__(self, start_frequency=0.0, stop_frequency=None, resolution=1.0, length=1): """ :param start_frequency: the start frequency in Hz. All frequencies below this will have zero magnitude. :param stop_frequency: the stop frequency in Hz or None to take the spectrum's maximum frequency. All frequencies above this will have zero magnitude. :param resolution: the resolution of the spectrum as a float :param length: the length of the spectrum """ # interpret the start and stop frequency offset = min(int(round(start_frequency / resolution)), length) if stop_frequency is None: sequence_length = length - offset else: sequence_length = min(int(round(stop_frequency / resolution)), length) - offset # generate the channel channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.complex128) channel = channels[0] channel[0:offset] = 0.0 if offset < length and sequence_length > 0: channel[offset] = 1.0 if offset + 1 < length and sequence_length > 1: channel[offset + 1] = 1.0 i = 2 while i < sequence_length: j = min(i // 2, sequence_length - i) channel[offset + i:offset + i + j] = channel[offset:offset + j] if i + i // 2 < sequence_length: j = min(i, sequence_length - i) channel[offset + i + i // 2:offset + i + j] = -channel[offset + i // 2:offset + j] i *= 2 channel[offset + sequence_length:] = 0.0 # store the parameters Spectrum.__init__(self, channels=channels, resolution=resolution, labels=("Noise",)) self.__sequence_length = sequence_length
def __init__(self, value=0.0, resolution=24000.0 / 2048, sampling_rate=48000.0 / 4096, number_of_frequencies=2049, length=32): """ :param value: the complex value of the samples :param resolution: the frequency resolution of the spectrogram as a float :param sampling_rate: the sampling rate of the spectrogram in Hz as an integer or a float :param number_of_frequencies: the number of frequency bins of the spectrogram :param length: the number of time bins of the spectrogram """ channels = sumpf_internal.allocate_array(shape=(1, number_of_frequencies, length), dtype=numpy.complex128) channels[:] = value Spectrogram.__init__(self, channels=channels, resolution=resolution, sampling_rate=sampling_rate, offset=0, labels=("Constant", ))
def output(self): """Computes the merged data set and returns it. :returns: a data set, that contains all channels of the added data sets. """ if not self.__data: raise RuntimeError("Nothing to merge") if len(self.__data) == 1: return next(iter(self.__data.values()) ) # simply return the first and only data set else: first = next(iter(self.__data.values())) number_of_channels = sum(len(s) for s in self.__data.values()) labels = [""] * number_of_channels if isinstance(first, sumpf.Signal): merged_offset = min(s.offset() for s in self.__data.values()) length = max(s.offset() + s.length() for s in self.__data.values()) - merged_offset channels = sumpf_internal.allocate_array( shape=(number_of_channels, length)) for index, channel, offset, label in zip( self.__indices(), (c for d in self.__data.values() for c in d.channels()), (d.offset() for d in self.__data.values() for l in d.channels()), # pylint: disable=line-too-long (l for d in self.__data.values() for l in d.labels())): start = offset - merged_offset stop = start + len(channel) channels[index, 0:start] = 0.0 channels[index, start:stop] = channel channels[index, stop:] = 0.0 labels[index] = label return sumpf.Signal(channels=channels, sampling_rate=first.sampling_rate(), offset=merged_offset, labels=labels) elif isinstance(first, sumpf.Spectrum): length = max(s.length() for s in self.__data.values()) channels = sumpf_internal.allocate_array( shape=(number_of_channels, length), dtype=numpy.complex128) for index, channel, label in zip( self.__indices(), (c for d in self.__data.values() for c in d.channels()), (l for d in self.__data.values() for l in d.labels())): channels[index, 0:len(channel)] = channel channels[index, len(channel):] = 0.0 labels[index] = label return sumpf.Spectrum(channels=channels, resolution=first.resolution(), labels=labels) elif isinstance(first, sumpf.Spectrogram): merged_offset = min(s.offset() for s in self.__data.values()) length = max(s.offset() + s.length() for s in self.__data.values()) - merged_offset number_of_frequencies = max(s.number_of_frequencies() for s in self.__data.values()) channels = sumpf_internal.allocate_array( shape=(number_of_channels, number_of_frequencies, length), dtype=numpy.complex128) channels[:] = 0.0 for index, channel, offset, label in zip( self.__indices(), (c for d in self.__data.values() for c in d.channels()), (d.offset() for d in self.__data.values() for l in d.channels()), # pylint: disable=line-too-long (l for d in self.__data.values() for l in d.labels())): channel_frequencies, channel_length = channel.shape start = offset - merged_offset stop = start + channel_length channels[index, 0:channel_frequencies, start:stop] = channel labels[index] = label return sumpf.Spectrogram(channels=channels, resolution=first.resolution(), sampling_rate=first.sampling_rate(), offset=merged_offset, labels=labels) elif isinstance(first, sumpf.Filter): transfer_functions = [None] * number_of_channels for index, transfer_function, label in zip( self.__indices(), (tf for d in self.__data.values() for tf in d.transfer_functions()), # pylint: disable=line-too-long (l for d in self.__data.values() for l in d.labels())): transfer_functions[index] = transfer_function labels[index] = label return sumpf.Filter(transfer_functions=transfer_functions, labels=labels) else: raise ValueError( f"Cannot merge data sets of type {type(first)}")
def output( self ): # noqa: C901; it's either a complex method or a lot of duplicated code # pylint: disable=too-many-branches,too-many-statements; """Computes the concatenated signal and returns it :returns: a Signal instance """ if not self.__data: raise RuntimeError("Nothing to concatenate") if len(self.__data) == 1: return next(iter(self.__data.values()) ) # simply return the first and only data set else: first = next(iter(self.__data.values())) # find the start and stop samples of the data sets index = 0 indices = [] number_of_channels = 0 for s in self.__data.values(): start = index + s.offset() index = start + s.length() indices.append((start, index, s.channels())) number_of_channels = max(number_of_channels, len(s)) indices.sort(key=lambda t: (t[0], t[1])) # allocate an array for the concatenated channels and copy the first data set offset = min(i[0] for i in indices) length = max(i[1] for i in indices) - offset start, stop, dataset_channels = indices[0] start -= offset stop -= offset if len(dataset_channels.shape) == 2: signal = True channels = sumpf_internal.allocate_array( shape=(number_of_channels, length), dtype=dataset_channels.dtype) channels[0:len(dataset_channels), start:stop] = dataset_channels if len(dataset_channels) < number_of_channels: channels[len(dataset_channels):, start:stop] = 0.0 else: signal = False number_of_frequencies = max(i[2].shape[1] for i in indices) channels = sumpf_internal.allocate_array( shape=(number_of_channels, number_of_frequencies, length), dtype=dataset_channels.dtype) l, f = dataset_channels.shape[0:2] channels[0:l, 0:f, start:stop] = dataset_channels if f < number_of_frequencies: channels[0:l, f:, start:stop] = 0.0 if l < number_of_channels: channels[l:, :, start:stop] = 0.0 # copy the other data sets last_index = 0 # the maximum sample index, at which there is already data in the channels array for index, previous in zip(indices[1:], indices[0:-1]): start, stop, dataset_channels = index start -= offset stop -= offset l, f = dataset_channels.shape[0:2] last_index = max(last_index, previous[1] - offset) if start < last_index: # the current data set overlaps with the previous one, add the samples in the overlapping region if last_index >= stop: # pylint: disable=no-else-continue stop = start + dataset_channels.shape[-1] if signal: channels[0:l, start:stop] += dataset_channels else: channels[0:l, 0:f, start:stop] += dataset_channels continue else: if signal: channels[ 0:l, start: last_index] += dataset_channels[:, 0:last_index - start] dataset_channels = dataset_channels[:, last_index - start:] else: channels[ 0:l, 0:f, start: last_index] += dataset_channels[:, :, 0:last_index - start] dataset_channels = dataset_channels[:, :, last_index - start:] start = last_index elif start > last_index: # a gap between the current data set and the previous one, fill it with zeros if signal: channels[:, last_index:start] = 0.0 else: channels[:, :, last_index:start] = 0.0 if signal: channels[0:l, start:stop] = dataset_channels if len( dataset_channels ) < number_of_channels: # if the current signal has less channels than the other, fill the missing channels with zeros channels[l:, start:stop] = 0.0 else: channels[0:l, 0:f, start:stop] = dataset_channels if f < number_of_frequencies: channels[0:l, f:, start:stop] = 0.0 if len( dataset_channels ) < number_of_channels: # if the current signal has less channels than the other, fill the missing channels with zeros channels[l:, :, start:stop] = 0.0 # create and return the result if signal: return sumpf.Signal( channels=channels, sampling_rate=first.sampling_rate(), offset=offset, labels=tuple(f"Concatenation {i}" for i in range(1, number_of_channels + 1))) else: return sumpf.Spectrogram( channels=channels, resolution=first.resolution(), sampling_rate=first.sampling_rate(), offset=offset, labels=tuple(f"Concatenation {i}" for i in range(1, number_of_channels + 1)))
def __init__(self, start_frequency=20.0, stop_frequency=20000.0, phase=0.0, interval=(0, 1.0), sampling_rate=48000.0, length=2**16): """ :param start_frequency: the start frequency in Hz (this is usually the lowest frequency, that is excited at the end of an inverse sweep, but at the beginning of a regular sweep) :param start_frequency: the stop frequency in Hz (this is usually the highest frequency, that is excited at the beginning of an inverse sweep, but at the end of a regular sweep) :param phase: a phase offset in radians (e.g. pass pi/2 for a inverse cosine sweep) :param interval: a tuple, list or array of two numbers, that specify the indices of the samples, at which the start and the stop frequencies shall be excited. The sweep will continue outside this interval. This functionality is useful, if the sweep shall be faded in or out and the range between the start and the stop frequency shall not be affected by the fading. The indices can be specified as integer indices or as floats between 0.0 and 1.0, which are mapped to 0 and ``length``. Negative indices (also possible with floats) are relative to the end of the signal. :param sampling_rate: the sampling rate of the resulting signal in Hz as an integer or a float :param length: the number of samples of the inverse sweep """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) # generate the sweep start, stop, t, T, sweep_offset, l, a = exponential_sweep_parameters( start_frequency, stop_frequency, interval, sampling_rate, length) s = 2.0 * start_frequency / l / ( stop_frequency - start_frequency ) / sampling_rate # a scaling factor, so that the convolution of the sweep and the inverse sweep results in a unit impulse if length <= 8192 or not numexpr: # for short sweeps NumExpr is slower than NumPy exponent = numpy.subtract(T, t, out=t) exponent /= l # compute the envelope envelope = numpy.exp(exponent) envelope *= s # compute the sweep sweep = numpy.expm1(exponent, out=exponent) sweep *= a sweep += phase numpy.sin(sweep, out=sweep) # combine the envelope and the sweep numpy.multiply(envelope, sweep, out=channels[0, :]) else: envelope = "s * exp((T-t) / l)" sweep = "sin(a * expm1((T-t) / l) + phase)" numexpr.evaluate(ex=f"{envelope} * {sweep}", out=channels[0, :], optimization="moderate") # store the values and parameters BaseExponentialSweep.__init__( self, channels=channels, sampling_rate=sampling_rate, offset=-stop - start, function=lambda tau: stop_frequency * math.exp( (-tau + sweep_offset) / l), l=l)
def harmonic_impulse_response(self, impulse_response, harmonic, length=None): """Cuts out the impulse response of the given harmonic from an impulse response of a system, that has been measured with this sweep. :param impulse_response: the :class:`~sumpf.Signal` instance with the complete impulse response of the system. The signal's offset shall point to the beginning of the linear impulse response. :param harmonic: the integer order of the harmonic, that shall be cut out. ``1`` is the linear part, while the harmonics with an order of ``2`` and higher come from nonlinearities in the system :param length: the integer length, that the harmonic impulse response shall have. This will be achieved by cropping or zero padding. If this is None, the length will be determined automatically and it will vary for the different orders of harmonics. """ offset = impulse_response.offset() sampling_rate = impulse_response.sampling_rate() # cut out the harmonic impulse response from the signal if harmonic == 1: channels = impulse_response.channels()[:, -offset:] remaining_delay = None else: delay = math.log(harmonic) * self.__l shift = math.ceil(delay * sampling_rate) start = -int(shift) - offset remaining_delay = shift / sampling_rate - delay if harmonic == 2: stop = -offset else: stop = -int( math.floor( math.log(harmonic - 1) * self.__l * sampling_rate)) - offset if stop == 0 or (start < 0 and stop >= 0): # pylint: disable=chained-comparison; can't see what's wrong with this stop = None channels = impulse_response.channels()[:, start:stop] # zero-pad or crop the impulse response, so it has the desired length if length is not None: channel_count, harmonic_length = channels.shape if harmonic_length < length: new_channels = sumpf_internal.allocate_array( shape=(channel_count, length)) new_channels[:, 0:harmonic_length] = channels new_channels[:, harmonic_length:] = 0.0 if remaining_delay is not None: apply_delay(channels=new_channels, sampling_rate=sampling_rate, delay=remaining_delay, out=new_channels) channels = new_channels elif harmonic_length > channel_count: if remaining_delay is not None: new_channels = sumpf_internal.allocate_array( shape=(channel_count, length)) apply_delay(channels=channels, sampling_rate=sampling_rate, delay=remaining_delay, out=new_channels) channels = new_channels else: channels = channels[:, 0:length] elif remaining_delay is not None: apply_delay(channels=channels, sampling_rate=sampling_rate, delay=remaining_delay, out=channels) # return the impulse response of the harmonic h = sumpf_internal.counting_number(harmonic) return Signal( channels=channels, sampling_rate=sampling_rate, offset=0, labels=[f"{l} ({h} harmonic)" for l in impulse_response.labels()])
def __init__(self, function=numpy.blackman, rise_interval=None, fall_interval=None, sampling_rate=48000.0, length=2**16): """ :param function: a function, that takes an integer length and returns the samples of a fade in, followed by a fade out, which have in total as many samples as the given length. :mod:`numpy`'s window functions can be used for this parameter. :param rise_interval: a tuple of sample indices between which the fade in shall be done. The sample indices can be given as integers or as floats between 0.0 and 1.0, that are multiplied with the given length of the signal. Negative numbers will be counted from the back of the signal. Combining a float with an integer is possible. :param fall_interval: a tuple of sample indices between which the fade out shall be done. The sample indices can be given as integers or as floats between 0.0 and 1.0, that are multiplied with the given length of the signal. Negative numbers will be counted from the back of the signal. Combining a float with an integer is possible. :param sampling_rate: the sampling rate of the signal as a float or integer :param length: the number of samples of the fade signal """ # allocate shared memory for the channels channels = sumpf_internal.allocate_array(shape=(1, length), dtype=numpy.float64) channel = channels[0] # parse the raise and fall intervals if rise_interval is None: rise = None elif isinstance(rise_interval, collections.abc.Iterable): rise = sumpf_internal.index(rise_interval, length=length) else: rise = (0, sumpf_internal.index(rise_interval, length=length)) if fall_interval is None: fall = None elif isinstance(fall_interval, collections.abc.Iterable): fall = sumpf_internal.index(fall_interval, length=length) else: fall = (sumpf_internal.index(fall_interval, length=length), length) # initialize the channels if rise is None: # only fade out if fall is None: channel[:] = 1.0 label = "Fade" else: a, b = fall fall_length = b - a channel[0:a] = 1.0 channel[a:b] = function(2 * fall_length)[fall_length:] channel[b:] = 0.0 label = "Fade out" elif fall is None: # only fade in a, b = rise rise_length = b - a channel[0:a] = 0.0 channel[a:b] = function(2 * rise_length)[0:rise_length] channel[b:] = 1.0 label = "Fade in" elif (rise[0] < fall[0]) or ((rise[0] == rise[1] == fall[0] != fall[1])): # fade in and then out a, b = rise c, d = fall start, stop = min(a, c), max(b, d) channel[0:start] = 0.0 fade_in_out(function, rise_stop=b - start, fall_start=c - start, fall_length=d - c, out=channel[start:stop]) channel[stop:] = 0.0 label = "Fade in-out" else: # fade out and then in a, b = fall c, d = rise start, stop = min(a, c), max(b, d) channel[0:start] = 1.0 fade_out_in(function, fall_stop=b - a, rise_start=c - a, rise_length=d - c, out=channel[min(a, c):max(b, d)]) channel[stop:] = 1.0 label = "Fade out-in" # initialize the signal Signal.__init__(self, channels=channels, sampling_rate=sampling_rate, offset=0, labels=(label, ))