diff --git a/muda/deformers/__init__.py b/muda/deformers/__init__.py index 49f2821..385d1bc 100644 --- a/muda/deformers/__init__.py +++ b/muda/deformers/__init__.py @@ -10,4 +10,7 @@ from .util import * from .colorednoise import * from .ir import * + +from .filter import * from .clipping import * + diff --git a/muda/deformers/filter.py b/muda/deformers/filter.py new file mode 100644 index 0000000..0e736ad --- /dev/null +++ b/muda/deformers/filter.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# CREATED:2020-07-11 by Han Han +"""Filtering (low/band/high-pass) algorithms""" + +import numpy as np +from scipy import signal +from copy import deepcopy +import numpy as np +import librosa + +from ..base import BaseTransformer, _get_rng + +__all__ = ["Filter", "RandomLPFilter", "RandomHPFilter","RandomBPFilter"] + + +def checkfreqinband(freq,state,datatype): + """check if a given frequency falls into the passband + + + Parameters + ---------- + freq: int, float or string + If int, frequency is a midi number + If float, frequency is in hz + If string, frequency is the pitch class + + state: state of the current filter deformer + + datatype: "midi" or "hz" or "pitchclass", specifying unit of frequency, one of midi, + + Returns + ------- + Frequency: freq or None + frequency if it falls under passband, or None otherwise + Bool : + True if the frequency falls under passband, False otherwise + """ + + #convert frequency to hertz + if datatype == "midi": + frequency = librosa.midi_to_hz(freq) + elif datatype == "hz": + frequency = freq + elif datatype == "pitchclass": + frequency = librosa.note_to_hz(freq) + + #check if it falls into the passband + if state["btype"] == "bandpass": + low,high = state["cut_off"] + elif state["btype"] == "low": + high = state["cut_off"] + low = 0 + else: + high = state["nyquist"] + low = state["cut_off"] + + #if frequency out of passband do not create new annotations and designate frequency as None, voiced as False + if frequency <=low: + return None,False + elif frequency >= high: + return None,False + else: + return freq, True + + + +class AbstractFilter(BaseTransformer): + """Abstract base class for Filtering transformations""" + + + def __init__(self): + """Abstract base class for Filtering Transformations. + + This implements the deformations, but does not manage state. + """ + + BaseTransformer.__init__(self) + + # Build the annotation mapping + self._register("pitch_contour", self.filter_contour) + self._register("pitch_hz|note_hz", self.filter_hz) + self._register("pitch_midi|note_midi", self.filter_midi) + self._register("pitch_class", self.filter_class) + + + def states(self,jam): + mudabox = jam.sandbox.muda + state = dict( + nyquist=mudabox._audio["sr"]/2 + ) + yield state + + @staticmethod + def audio(mudabox, state): + + if state["btype"] == "bandpass": + low,high = state["cut_off"] + sos = signal.cheby2( + state["order"], + state["attenuation"]/2, + [low,high], + btype=state["btype"], + output='sos', + fs=mudabox._audio["sr"]) + else: + sos = signal.cheby2( + state["order"], + state["attenuation"]/2, + state["cut_off"], + btype=state["btype"], + output='sos', + fs=mudabox._audio["sr"]) + + mudabox._audio["y"] = signal.sosfiltfilt(sos, mudabox._audio["y"]) + + + + @staticmethod + def filter_contour(annotation, state): + #same length after modification + for obs in annotation.pop_data(): + new_freq,voice = checkfreqinband(obs.value["frequency"],state,datatype="hz") + annotation.append( + time=obs.time, + duration=obs.duration, + confidence=obs.confidence, + value={ + "index": obs.value["index"], + "frequency": new_freq, + "voiced": voice, + }, + ) + + @staticmethod + def filter_hz(annotation, state): + #non-existent pitch removed + for obs in annotation.pop_data(): + new_freq,voice = checkfreqinband(obs.value,state,datatype="hz") + if voice: + annotation.append( + time=obs.time, + duration=obs.duration, + confidence=obs.confidence, + value=new_freq, + ) + + @staticmethod + def filter_midi(annotation, state): + #non-existent pitch removed + for obs in annotation.pop_data(): + new_midi,voice = checkfreqinband(obs.value,state,datatype="midi") + if voice: + annotation.append( + time=obs.time, + duration=obs.duration, + confidence=obs.confidence, + value=new_midi, + ) + + @staticmethod + def filter_class(annotation, state): + #non-existent pitch removed + for obs in annotation.pop_data(): + value = deepcopy(obs.value) + + new_freq, voice = checkfreqinband(value["tonic"]+str(value["pitch"]),state,"pitchclass") + if voice: + value["tonic"] = new_freq[:-1] + value["pitch"] = int(new_freq[-1]) + annotation.append( + time=obs.time, + duration=obs.duration, + confidence=obs.confidence, + value=value, + ) + + + + +class Filter(AbstractFilter): + """ Filtering by cheby2 iir filter + + This transformation affects the following attributes: + + - Annotations + - pitch_contour, pitch_hz, pitch_midi, pitch_class + - note_hz, note_midi + - Audio + + Attributes + ---------- + type: "low" or "high" or "bandpass" + order: int > 0 + order of the filter + attenutation: float > 0 + The minimum attenuation required in the stop band. + Specified in decibels, as a positive number. + + cutoff: in hz + can be float, list of float, or list of tuples in the case of bandpass filter + + make one or more filters of the same type, but customized cutoff frequencies + + See Also + -------- + RandomFilter + + Examples + -------- + >>> # Filter the signal at the passband frequency with a + chebyshev type 2 filter of certain order and attenuation + >>> D = muda.deformers.Filter(btype,order,attenuation,cutoff) + """ + + def __init__(self,btype="low", attenuation=60.0, cutoff=4000): + AbstractFilter.__init__(self) + self.btype = btype + #self.order = order + self.attenuation = attenuation + if self.btype == "bandpass": + if isinstance(cutoff,tuple): + self.cutoff = [cutoff] + elif isinstance(cutoff,list): + if all(isinstance(i,tuple) for i in cutoff): + self.cutoff = cutoff + elif all(isinstance(i,list) for i in cutoff): + if all(len(i) == 2 for i in cutoff): # [[a,b],[c,d]] + self.cutoff = [tuple(c) for c in cutoff] + else: + raise ValueError("bandpass filter cutoff must be tuple or list of tuples") + else: + raise ValueError("bandpass filter cutoff must be tuple or list of tuples") + else: + raise ValueError("bandpass filter cutoff must be tuple or list of tuples") + + else: + if isinstance(cutoff,tuple): + raise ValueError("low/high pass filter cutoff must be float or list of floats") + elif isinstance(cutoff,list) and isinstance(cutoff[0],tuple): + raise ValueError("low/high pass filter cutoff must be float or list of floats") + + else: + self.cutoff = np.atleast_1d(cutoff).flatten().tolist() + + + def states(self, jam): + mudabox = jam.sandbox.muda + fs = mudabox._audio["sr"] + for state in AbstractFilter.states(self, jam): + if self.btype == "bandpass": + for low,high in self.cutoff: + if low > high: + raise ValueError("cutoff_low must be smaller than cutoff_high") + else: + state["cut_off"] = (low,high) + state["order"] = signal.cheb2ord([low,high], [low-fs/10,high+fs/10], 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + state["btype"] = self.btype + yield state + elif self.btype == "low": + for freq in self.cutoff: + if freq <= 0: + raise ValueError("cutoff frequency for lowpass filter must be strictly positive") + else: + state["cut_off"] = freq + state["order"] = signal.cheb2ord(freq, freq+fs/10, 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + state["btype"] = self.btype + yield state + elif self.btype == "high": + for freq in self.cutoff: + if freq <= 0 or freq >= state["nyquist"]: + raise ValueError("cutoff frequency for high pass filter must be strictly positive and smaller than nyquist frequency") + else: + state["cut_off"] = freq + state["order"] = signal.cheb2ord(freq, freq-fs/10, 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + state["btype"] = self.btype + yield state + + + +class RandomLPFilter(AbstractFilter): + """ Filtering by cheby2 iir filter + + This transformation affects the following attributes: + + - Annotations + - pitch_contour, pitch_hz, pitch_midi, pitch_class + - note_hz, note_midi + - Audio + + Attributes + ---------- + n_samples : int > 0 + The number of samples to generate per input + + order: int > 0 + order of the filter + attenuation: float > 0 + The minimum attenuation required in the stop band. + Specified in decibels, as a positive number. + cutoff: float in hz + low pass cutoff frequency + + + sigma : float > 0 + The parameters of the normal distribution for sampling + pitch shifts + + rng : None, int, or np.random.RandomState + The random number generator state. + + If `None`, then `np.random` is used. + + If `int`, then `rng` becomes the seed for the random state. + + See Also + -------- + Filter + + Examples + -------- + >>> # Apply n_samples of low pass filtering, + where the cutoff frequency is randomly extracted + from a normal distribution + >>> D = muda.deformers.RandomLPFilter(n_samples,order,attenuation,cutoff,sigma) + """ + + def __init__(self, n_samples=3, attenuation=60.0, cutoff=8000,sigma=1.0,rng=0): + AbstractFilter.__init__(self) + if sigma <= 0: + raise ValueError("sigma must be strictly positive") + + if n_samples <= 0: + raise ValueError("n_samples must be None or positive") + + if isinstance(cutoff,float) and cutoff<=0: + raise ValueError("cutoff frequency must be None or positive") + + if isinstance(cutoff,list) and sum(np.array(cutoff)<=0)>0: + raise ValueError("cutoff frequency must be None or positive") + + if attenuation <= 0: + raise ValueError("attenuation must be None or positive") + + + + self.n_samples = n_samples + #self.order = order + self.attenuation = attenuation + self.sigma = float(sigma) + self.rng = rng + self._rng = _get_rng(rng) + self.cutoff = float(cutoff) + + + + #specify and stores the type/parameters of the augmentation + def states(self, jam): + mudabox = jam.sandbox.muda + fs = mudabox._audio["sr"] + for state in AbstractFilter.states(self, jam): + for _ in range(self.n_samples): + state["btype"] = "low" + state["order"] = signal.cheb2ord(freq, freq+fs/10, 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + state["cut_off"] = self._rng.normal( + loc=self.cutoff, scale=self.sigma, size=None + ) + + yield state + + + + +class RandomHPFilter(AbstractFilter): + """ Filtering by cheby2 iir filter + + This transformation affects the following attributes: + + - Annotations + - pitch_contour, pitch_hz, pitch_midi, pitch_class + - note_hz, note_midi + - Audio + + Attributes + ---------- + n_samples : int > 0 + The number of samples to generate per input + + order: int > 0 + order of the filter + attenuation: float > 0 + The minimum attenuation required in the stop band. + Specified in decibels, as a positive number. + + cutoff: float in hz + high pass cutoff frequency + + sigma : float > 0 + The parameters of the normal distribution for sampling + pitch shifts + + rng : None, int, or np.random.RandomState + The random number generator state. + + If `None`, then `np.random` is used. + + If `int`, then `rng` becomes the seed for the random state. + + + See Also + -------- + Filter + + Examples + -------- + >>> # Apply n_samples of high pass filtering, + where the cutoff frequency is randomly extracted + from a normal distribution + >>> D = muda.deformers.RandomHPFilter(m_samples,order,attenuation,cutoff,sigma) + """ + + def __init__(self, n_samples=3, attenuation=60.0, cutoff=8000,sigma=1.0,rng=0): + AbstractFilter.__init__(self) + if sigma <= 0: + raise ValueError("sigma must be strictly positive") + + if n_samples <= 0: + raise ValueError("n_samples must be None or positive") + + if attenuation <= 0: + raise ValueError("attenuation must be None or positive") + + if isinstance(cutoff,list) or cutoff<=0: + raise ValueError("high pass cutoff frequency must be strictly positive and lower than nyquist frequency") + + + self.n_samples = n_samples + #self.order = order + self.attenuation = attenuation + self.sigma = float(sigma) + self.rng = rng + self._rng = _get_rng(rng) + self.cutoff = float(cutoff) + + + + #specify and stores the type/parameters of the augmentation + + def states(self, jam): + for state in AbstractFilter.states(self, jam): + for _ in range(self.n_samples): + state["btype"] = "high" + state["order"] = signal.cheb2ord(freq, freq-fs/10, 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + state["cut_off"] = self._rng.normal( + loc=self.cutoff, scale=self.sigma, size=None + ) + + yield state + + +class RandomBPFilter(AbstractFilter): + """ Filtering by cheby2 iir filter + + This transformation affects the following attributes: + + - Annotations + - pitch_contour, pitch_hz, pitch_midi, pitch_class + - note_hz, note_midi + - Audio + + Attributes + ---------- + n_samples : int > 0 + The number of samples to generate per input + + order: int > 0 + order of the filter + attenuation: float > 0 + The minimum attenuation required in the stop band. + Specified in decibels, as a positive number. + + cutoff: float in hz + high pass cutoff frequency + + + + sigma : float > 0 + The parameters of the normal distribution for sampling + pitch shifts + + rng : None, int, or np.random.RandomState + The random number generator state. + + If `None`, then `np.random` is used. + + If `int`, then `rng` becomes the seed for the random state. + + + See Also + -------- + Filter, RandomHPFilter, RandomLPFilter + + Examples + -------- + >>> # Apply n_samples of band pass filtering, where the + low and high cutoff frequencies are randomly selected from two + normal distributions centered around some specified lowerbound + and upperbound + >>> D = muda.deformers.RandomBPFilter(n_samples,order,attenuation,cutoff_low,cutoff_high,sigma) + """ + + def __init__(self, n_samples=3, attenuation=60.0, cutoff_low=4000, cutoff_high=8000,sigma=1.0,rng=0): + AbstractFilter.__init__(self) + if sigma is not None and sigma <= 0: + raise ValueError("sigma must be strictly positive") + + if n_samples is not None and n_samples <= 0: + raise ValueError("n_samples must be None or positive") + + if attenuation is not None and attenuation <= 0: + raise ValueError("attenuation must be None or positive") + + if cutoff_low >= cutoff_high: + raise ValueError("band pass higher cutoff frequency must be strictly greater than lower cutoff frequency") + if cutoff_low<=0 or cutoff_high <=0: + raise ValueError("band pass cutoff frequency must be strictly greater than zero") + + self.n_samples = n_samples + #self.order = order + self.attenuation = attenuation + self.sigma = float(sigma) + self.rng = rng + self._rng = _get_rng(rng) + self.cutoff_low = float(cutoff_low) + self.cutoff_high = float(cutoff_high) + + + #specify and stores the type/parameters of the augmentation + + def states(self, jam): + for state in AbstractFilter.states(self, jam): + for _ in range(self.n_samples): + + #make sure higher bound is lower than lower bound + high = self._rng.normal( + loc=self.cutoff_high, scale=self.sigma, size=None + ) + low = self._rng.normal( + loc=self.cutoff_low, scale=self.sigma, size=None + ) + while high <= low: + high = self._rng.normal( + loc=self.cutoff_high, scale=self.sigma, size=None + ) + low = self._rng.normal( + loc=self.cutoff_low, scale=self.sigma, size=None + ) + state["btype"] = "bandpass" + state["cut_off"] = (low,high) + state["order"] = signal.cheb2ord([low,high], [low-fs/10,high+fs/10], 3, self.attenuation, fs=fs)[0] + state["attenuation"] = self.attenuation + + + + yield state + + diff --git a/tests/data/ir_file_delayed.wav b/tests/data/ir_file_delayed.wav index 23d4706..085f416 100644 Binary files a/tests/data/ir_file_delayed.wav and b/tests/data/ir_file_delayed.wav differ diff --git a/tests/data/mixture.jams b/tests/data/mixture.jams new file mode 100644 index 0000000..c5d5f87 --- /dev/null +++ b/tests/data/mixture.jams @@ -0,0 +1,381 @@ +{ + "annotations": [ + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "pitch_contour", + "data": { + "time": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "duration": [ + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5 + ], + "value": [ + { + "index": 0, + "frequency": 50.0, + "voiced": true + }, + { + "index": 0, + "frequency": 100.0, + "voiced": true + }, + { + "index": 0, + "frequency": 400.0, + "voiced": true + }, + { + "index": 0, + "frequency": 800.0, + "voiced": true + }, + { + "index": 0, + "frequency": 1000.0, + "voiced": true + }, + { + "index": 0, + "frequency": 1200.0, + "voiced": true + } + ], + "confidence": [ + null, + null, + null, + null, + null, + null + ] + }, + "sandbox": {}, + "time": 0, + "duration": null + }, + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "note_hz", + "data": [ + { + "time": 0.0, + "duration": 1.5, + "value": 50.0, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 100.0, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 400.0, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 800.0, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 1000.0, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 1200.0, + "confidence": null + } + ], + "sandbox": {}, + "time": 0, + "duration": null + }, + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "note_midi", + "data": [ + { + "time": 0.0, + "duration": 1.5, + "value": 31.349957715000777, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 43.34995771500078, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 67.34995771500078, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 79.34995771500078, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 83.21309485364912, + "confidence": null + }, + { + "time": 0.0, + "duration": 1.5, + "value": 86.36950772365465, + "confidence": null + } + ], + "sandbox": {}, + "time": 0, + "duration": null + }, + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "pitch_class", + "data": { + "time": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "duration": [ + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5 + ], + "value": [ + { + "tonic": "G", + "pitch": 1 + }, + { + "tonic": "G", + "pitch": 2 + }, + { + "tonic": "G", + "pitch": 4 + }, + { + "tonic": "G", + "pitch": 5 + }, + { + "tonic": "B", + "pitch": 5 + }, + { + "tonic": "D", + "pitch": 6 + } + ], + "confidence": [ + null, + null, + null, + null, + null, + null + ] + }, + "sandbox": {}, + "time": 0, + "duration": null + }, + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "pitch_hz", + "data": { + "time": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "duration": [ + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5 + ], + "value": [ + 50.0, + 100.0, + 400.0, + 800.0, + 1000.0, + 1200.0 + ], + "confidence": [ + null, + null, + null, + null, + null, + null + ] + }, + "sandbox": {}, + "time": 0, + "duration": null + }, + { + "annotation_metadata": { + "curator": { + "name": "", + "email": "" + }, + "annotator": {}, + "version": "", + "corpus": "", + "annotation_tools": "", + "annotation_rules": "", + "validation": "", + "data_source": "synth" + }, + "namespace": "pitch_midi", + "data": { + "time": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "duration": [ + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5 + ], + "value": [ + 31.349957715000777, + 43.34995771500078, + 67.34995771500078, + 79.34995771500078, + 83.21309485364912, + 86.36950772365465 + ], + "confidence": [ + null, + null, + null, + null, + null, + null + ] + }, + "sandbox": {}, + "time": 0, + "duration": null + } + ], + "file_metadata": { + "title": "", + "artist": "", + "release": "", + "duration": 1.5, + "identifiers": {}, + "jams_version": "0.3.4" + }, + "sandbox": {} +} \ No newline at end of file diff --git a/tests/data/mixture.wav b/tests/data/mixture.wav new file mode 100644 index 0000000..5e343b6 Binary files /dev/null and b/tests/data/mixture.wav differ diff --git a/tests/test_deformers.py b/tests/test_deformers.py index 7c16749..581cae7 100644 --- a/tests/test_deformers.py +++ b/tests/test_deformers.py @@ -11,6 +11,8 @@ import librosa import muda +from scipy import fft +from scipy import signal import pytest import scipy @@ -21,7 +23,6 @@ def does_not_raise(): yield - def ap_(a, b, msg=None, rtol=1e-5, atol=1e-5): """Shorthand for 'assert np.allclose(a, b, rtol, atol), "%r != %r" % (a, b) """ @@ -35,6 +36,34 @@ def jam_fixture(): return muda.load_jam_audio('tests/data/fixture.jams', 'tests/data/fixture.wav') +@pytest.fixture(scope='module') +def jam_mixture(): + return muda.load_jam_audio('tests/data/mixture.jams', + 'tests/data/mixture.wav') + + +@pytest.fixture(scope='module') +def jam_impulse(): + sr=22050 + + impulse = np.zeros(round(1.5*sr)) + impulse[len(impulse)//2]= 1.0 + + #make jam object for this audio - for testing purposes + freq_dict = { + 50.0: [(0.0,0.6),(0.8,1.2)], + 100.0: [(0.0,0.6),(1.0,1.1)], + 400.0: [(0.0,0.9),(1.0,1.1)], + 800.0: [(0.5,0.9),(1.2,1.5)], + 1200.0: [(1.2,1.5)] + } + + jam = make_jam(freq_dict,sr,1.5) + + if jam.file_metadata.duration is None: + jam.file_metadata.duration = 1.5 + + return muda.jam_pack(jam, _audio=dict(y=impulse, sr=sr)) @pytest.fixture(scope='module') def jam_raw(): @@ -47,6 +76,55 @@ def test_raw(jam_raw): D = muda.deformers.TimeStretch(rate=2.0) six.next(D.transform(jam_raw)) +def make_jam(freq_dict,sr,track_duration): + """ + this function creates a jam according to a dictionary that specifies + each frequency's presence + + dict: keys are frequencies + values are list of tuples (start_time, duration) of that frequency + """ + jam = jams.JAMS() + + # Store the track duration + jam.file_metadata.duration = track_duration + + pitch_co = jams.Annotation(namespace='pitch_contour') + note_h = jams.Annotation(namespace='note_hz') + note_m = jams.Annotation(namespace='note_midi') + pitch_cl = jams.Annotation(namespace='pitch_class') + pitch_h = jams.Annotation(namespace='pitch_hz') + pitch_m = jams.Annotation(namespace='pitch_midi') + + pitch_co.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + note_h.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + note_m.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + pitch_cl.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + pitch_h.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + pitch_m.annotation_metadata = jams.AnnotationMetadata(data_source='synth') + + + #assign frequencies to each start_time + freqs = freq_dict.keys() + for f in freqs: + time_dur = freq_dict[f] #list of tuples (start_time,duration) + for t, dur in time_dur: + pitch_co.append(time=t, duration=dur, value={"index":0,"frequency":f,"voiced":True}) + note_h.append(time=t, duration=dur,value=f) + note_m.append(time=t, duration=dur, value=librosa.hz_to_midi(f)) + pclass = librosa.hz_to_note(f) + pitch_cl.append(time=t, duration=dur,value={"tonic":pclass[:-1],"pitch":int(pclass[-1])}) + pitch_h.append(time=t, duration=dur,value=f) + pitch_m.append(time=t, duration=dur, value=librosa.hz_to_midi(f)) + # Store the new annotation in the jam + jam.annotations.append(pitch_co) + jam.annotations.append(note_h) + jam.annotations.append(note_m) + jam.annotations.append(pitch_cl) + jam.annotations.append(pitch_h) + jam.annotations.append(pitch_m) + + return jam """ Helper functions -- used across deformers """ def __test_time(jam_orig, jam_new, rate): @@ -768,7 +846,459 @@ def test_base_transformer(): six.next(D.transform(jam_fixture)) + + +"""Deformer: Filtering""" +# Helper function + +def __test_tonic_filter(ann_orig, ann_new, cutoff): + # raise error if original note now out of range is still included in annotation + for obs in ann_new: + v_new = librosa.note_to_hz(obs.value["tonic"]+str(obs.value['pitch'])) + assert cutoff[0] < v_new < cutoff[1] + + # ensure number of new annotations is less than or equal to the original + assert len(ann_new) <= len(ann_orig) + + + + + + +def __test_contour_filter(ann_orig, ann_new, cutoff): + + for obs1,obs2 in zip(ann_orig,ann_new): + v_orig = obs1.value['frequency'] + v_new = obs2.value['frequency'] + if cutoff[0] < v_orig and cutoff[1] > v_orig: + ap_(v_orig,v_new) + else: + assert v_new == None + + + + + +def __test_hz_filter(ann_orig, ann_new, cutoff): + + for obs in ann_new: + v_new = obs.value + assert cutoff[0] < v_new < cutoff[1] + assert len(ann_new) <= len(ann_orig) + + + +def __test_midi_filter(ann_orig, ann_new, cutoff): + + for obs in ann_new: + v_new = librosa.midi_to_hz(obs.value) + assert cutoff[0] < v_new < cutoff[1] + assert len(ann_new) <= len(ann_orig) + + +def __test_pitch_filter(jam_orig, jam_new, cutoff): + + + # Test each annotation + for ann_orig, ann_new in zip(jam_orig.annotations, jam_new.annotations): + #assert len(ann_orig) == len(ann_new) + + + if ann_orig.namespace == 'pitch_class': + __test_tonic_filter(ann_orig, ann_new, cutoff) + elif ann_orig.namespace == 'pitch_contour': + assert len(ann_orig) == len(ann_new) + __test_contour_filter(ann_orig, ann_new, cutoff) + elif ann_orig.namespace in ['pitch_hz','note_hz']: + __test_hz_filter(ann_orig, ann_new, cutoff) + elif ann_orig.namespace in ['pitch_midi','note_midi']: + __test_midi_filter(ann_orig, ann_new, cutoff) + +def __testsound(attenuation,cutoff_freq,audio_new,audio_orig,sr,btype): + #this attenuation should be the ultimate one + N = len(audio_orig) + T = 1.0 / sr + if btype == "bandpass": + low,high = cutoff_freq + + elif btype == "low": + low = 0 + high = cutoff_freq + else: + low = cutoff_freq + high = sr/2 + + #bin number of cutoff frequencies + idx_low = round(low // (sr/N)) # bin number of the passband + idx_high = round(high // (sr/N)) + + yf_orig = fft(audio_orig) + yf_filt = fft(audio_new) + + #db of fft coefficients + db_orig = 20 * np.log10(2.0/N * np.abs(yf_orig)) + db_filt = 20 * np.log10(2.0/N * np.abs(yf_filt)) + + #check passband (if number of bins greater than threshold is equal) + + + if btype == "low": + stop_filt = db_filt[idx_high:N//2] + pass_filt = db_filt[:idx_low] + pass_orig = db_orig[:idx_low] + stop_orig = db_orig[idx_high:N//2] + elif btype == "high": + stop_filt = db_filt[:idx_low] + pass_filt = db_filt[idx_high:N//2] + pass_orig = db_orig[idx_high:N//2] + stop_orig = db_orig[:idx_low] + else: + stop_filt = np.array(list(db_filt[:idx_low]) + list(db_filt[idx_high:N//2])) + pass_filt = db_filt[idx_low:idx_high] + pass_orig = db_orig[idx_low:idx_high] + stop_orig = np.array(list(db_orig[:idx_low]) + list(db_orig[idx_high:N//2])) + + #check passband + assert sum(pass_orig > -attenuation) == sum(pass_filt > -attenuation) + + #check stopband + assert sum(stop_filt > -attenuation) == 0 + + + +#test filtering +@pytest.mark.parametrize('btype,cutoff', + [("high",20.0), + ("high",100.0), + ("high",500.0), + ("low",1000.0), + ("low",500.0), + ("low",[100.0,300.0,900.0]), + ("bandpass",[(25.0,45.0),(30.0,500.0),(200.0,1000.0),(900.0,2000)]), + ("bandpass",(1300,1500.0)), + + + pytest.mark.xfail(("bandpass", [[40,100],[100,200,300]]), raises=ValueError), + pytest.mark.xfail(("bandpass", [[100,40],[100,200]]), raises=ValueError), + pytest.mark.xfail(("high", -50), raises=ValueError), + pytest.mark.xfail(("low", 0.0), raises=ValueError), + pytest.mark.xfail(("low", (20,100)), raises=ValueError), + pytest.mark.xfail(("low", [(20,50),(40,100)]), raises=ValueError), + pytest.mark.xfail(("bandpass", 1.0), raises=ValueError), + pytest.mark.xfail(("bandpass", -30.0), raises=ValueError), + pytest.mark.xfail(("bandpass", [40, 100]), raises=ValueError) + ]) + + +@pytest.mark.parametrize('attenuation', #input is the ultimate one + [10.0,30.0,80.0, + + pytest.mark.xfail(-20.0, raises=ValueError) + ]) + + + +def test_filtering(btype, cutoff, jam_impulse,attenuation): + + D = muda.deformers.Filter(btype=btype, + order=4, + attenuation=attenuation, + cutoff=cutoff) + + + jam_orig = deepcopy(jam_impulse) + + + for jam_new in D.transform(jam_orig): + # Verify that the original jam reference hasn't changed + assert jam_new is not jam_orig + assert not np.allclose(jam_orig.sandbox.muda['_audio']['y'], + jam_new.sandbox.muda['_audio']['y']) + + d_state = jam_new.sandbox.muda.history[-1]['state'] + nyquist = d_state["nyquist"] + order = d_state['order'] + atten = d_state['attenuation'] #this is the halfed one for the filter parameter + + + if btype == "bandpass": + low,high = d_state['cut_off'] + elif btype == "low": + low = 0 + high = d_state['cut_off'] + else: + low = d_state['cut_off'] + high = nyquist + + + assert order > 0 + assert isinstance(order,int) + assert atten > 0 + + if btype == "bandpass": + assert 0 < low < high < nyquist + + else: + assert 0 <= low < high <= nyquist + + + __test_pitch_filter(jam_orig, jam_new, [low,high]) + + #test sound + __testsound(attenuation, #this is the ultimate one + d_state['cut_off'], + jam_new.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['sr'], + btype) + + # Serialization test + D2 = muda.deserialize(muda.serialize(D)) + __test_params(D, D2) + + +#test random lowpass +@pytest.mark.parametrize('cutoff', + [50.0,100.0,400.0,1000.0, + + pytest.mark.xfail(-50.0, raises=ValueError), + pytest.mark.xfail([0.0,-20], raises=ValueError), + pytest.mark.xfail([(50.0,1000.0),(1000.0,2000.0)], raises=ValueError) + ]) + + + +@pytest.mark.parametrize('attenuation', #input is the ultimate one + [10.0,30.0,80.0, + + pytest.mark.xfail(-20.0, raises=ValueError) + ]) + +@pytest.mark.parametrize('n_samples', #input is the ultimate one + [1,3,5, + pytest.mark.xfail(0, raises=ValueError), + pytest.mark.xfail(-1, raises=ValueError) + ]) + + +def test_randomlpfiltering(n_samples,cutoff, jam_impulse,attenuation): + + D = muda.deformers.RandomLPFilter(n_samples=n_samples, + order=4, + attenuation=attenuation, + cutoff=cutoff, + sigma=1.0, + rng=0) + + + jam_orig = deepcopy(jam_impulse) + orig_duration = librosa.get_duration(**jam_orig.sandbox.muda['_audio']) + n_out = 0 + for jam_new in D.transform(jam_orig): + # Verify that the original jam reference hasn't changed + assert jam_new is not jam_orig + + # Verify that the state and history objects are intact + __test_deformer_history(D, jam_new.sandbox.muda.history[-1]) + + d_state = jam_new.sandbox.muda.history[-1]['state'] + nyquist = d_state["nyquist"] + low = 0 + high = d_state['cut_off'] + order = d_state['order'] + atten = d_state['attenuation'] + + assert order > 0 + assert isinstance(order,int) + assert atten > 0 + + + assert 0 <= high <= nyquist + + __test_pitch_filter(jam_orig, jam_new, [low,high]) + #test sound + __testsound(attenuation, + d_state['cut_off'], + jam_new.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['sr'], + "low") + n_out += 1 + + assert n_samples == n_out + + # Serialization test + D2 = muda.deserialize(muda.serialize(D)) + __test_params(D, D2) + + + +@pytest.mark.parametrize('cutoff', + [90.0,300.0,1000.0,1300.0, + + pytest.mark.xfail(-50, raises=ValueError), + pytest.mark.xfail([10.0,70.0,700.0], raises=ValueError), + pytest.mark.xfail([0.0,-20], raises=ValueError), + pytest.mark.xfail([(50.0,1000.0),(1000.0,2000.0)], raises=ValueError) + ]) + + + +@pytest.mark.parametrize('attenuation', #input is the ultimate one + [10.0,30.0,80.0, + + pytest.mark.xfail(-20.0, raises=ValueError) + ]) + +@pytest.mark.parametrize('n_samples', #input is the ultimate one + [1,3,5, + pytest.mark.xfail(0, raises=ValueError), + pytest.mark.xfail(-1, raises=ValueError) + ]) + +def test_randomhpfiltering(cutoff, n_samples,jam_impulse,attenuation): + + D = muda.deformers.RandomHPFilter(n_samples=n_samples, + order=4, + attenuation=attenuation, + cutoff=cutoff, + sigma=1.0, + rng=0) + + + jam_orig = deepcopy(jam_impulse) + orig_duration = librosa.get_duration(**jam_orig.sandbox.muda['_audio']) + + n_out = 0 + for jam_new in D.transform(jam_orig): + # Verify that the original jam reference hasn't changed + assert jam_new is not jam_orig + + + # Verify that the state and history objects are intact + __test_deformer_history(D, jam_new.sandbox.muda.history[-1]) + + d_state = jam_new.sandbox.muda.history[-1]['state'] + nyquist = d_state["nyquist"] + low = d_state['cut_off'] + high = nyquist + + order = d_state['order'] + atten = d_state['attenuation'] + + assert order > 0 + assert isinstance(order,int) + assert atten > 0 + + + assert 0 < low < nyquist + + __test_pitch_filter(jam_orig, jam_new, [low,high]) + #test sound + __testsound(attenuation, + d_state['cut_off'], + jam_new.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['sr'], + "high") + + n_out += 1 + + assert n_samples == n_out + + # Serialization test + D2 = muda.deserialize(muda.serialize(D)) + __test_params(D, D2) + + + + +@pytest.mark.parametrize('cutoff', + [(50.0,300.0),(900.0,1300.0), + + pytest.mark.xfail(-50, raises=ValueError), + pytest.mark.xfail([700.0,600.0], raises=ValueError), + pytest.mark.xfail([0.0,-20], raises=ValueError), + pytest.mark.xfail([(50.0,1000.0),(1000.0,2000.0)], raises=ValueError) + ]) + + + +@pytest.mark.parametrize('attenuation', #input is the ultimate one + [10.0,30.0,80.0, + + pytest.mark.xfail(-20.0, raises=ValueError) + ]) + +@pytest.mark.parametrize('n_samples', #input is the ultimate one + [1,3,5, + pytest.mark.xfail(0, raises=ValueError), + pytest.mark.xfail(-1, raises=ValueError) + ]) + + +def test_randombpfiltering(cutoff, n_samples, jam_impulse,attenuation): + if type(cutoff) != tuple: + raise ValueError("cut off frequency for random bandpass filter must be a tuple") + else: + low,high = cutoff + + D = muda.deformers.RandomBPFilter(n_samples=n_samples, + order=4, + attenuation=attenuation, + cutoff_low=low, + cutoff_high = high, + sigma=1.0, + rng=0) + + + jam_orig = deepcopy(jam_impulse) + orig_duration = librosa.get_duration(**jam_orig.sandbox.muda['_audio']) + + n_out = 0 + for jam_new in D.transform(jam_orig): + # Verify that the original jam reference hasn't changed + assert jam_new is not jam_orig + + + # Verify that the state and history objects are intact + __test_deformer_history(D, jam_new.sandbox.muda.history[-1]) + + d_state = jam_new.sandbox.muda.history[-1]['state'] + low,high = d_state['cut_off'] + nyquist = d_state["nyquist"] + order = d_state['order'] + atten = d_state['attenuation'] + + assert order > 0 + assert isinstance(order,int) + assert atten > 0 + + assert 0 < low < high < nyquist + + __test_pitch_filter(jam_orig, jam_new, [low,high]) + #test sound + __testsound(attenuation, + d_state['cut_off'], + jam_new.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['y'], + jam_orig.sandbox.muda['_audio']['sr'], + "bandpass") + + n_out += 1 + + assert n_samples == n_out + + # Serialization test + D2 = muda.deserialize(muda.serialize(D)) + __test_params(D, D2) + + + + + + """ Deformer: Clipping """ # Helper function def __test_clipped_buffer(jam_orig, jam_new, clip_limit): @@ -824,7 +1354,6 @@ def test_clipping(clip_limit, expectation, jam_fixture): D2 = muda.deserialize(muda.serialize(D)) __test_params(D, D2) - # LinearClipping @pytest.mark.parametrize('lower, upper', [(0.3, 0.5), (0.1, 0.9), @@ -869,7 +1398,6 @@ def test_linear_clipping(n_samples, lower, upper, jam_fixture): D2 = muda.deserialize(muda.serialize(D)) __test_params(D, D2) - # RandomClipping @pytest.mark.parametrize('a, b', [(0.5, 0.5), (5.0, 1.0), (1.0, 3.0), @@ -895,8 +1423,7 @@ def test_random_clipping(n_samples, a, b, jam_fixture): for jam_new in D.transform(jam_orig): # Verify that the original jam reference hasn't changed assert jam_new is not jam_orig - __test_time(jam_orig, jam_fixture, 1.0) - + # Verify that the state and history objects are intact __test_deformer_history(D, jam_new.sandbox.muda.history[-1]) @@ -912,3 +1439,5 @@ def test_random_clipping(n_samples, a, b, jam_fixture): # Serialization test D2 = muda.deserialize(muda.serialize(D)) __test_params(D, D2) + +