def _validate_reverb_name(self, reverb_name_tuple): # Make sure that type matches def __validate_reverb_name_types(reverb_name): if not isinstance(reverb_name, str): raise AmbiScaperError( 'reverb_config: reverb name must be a string') # Make sure that the sofa file exists and is valid def __validate_reverb_name_configuration(reverb_name): reverb_full_path = self.sofa_reverb_folder_path + '/' + reverb_name # The provided name should exist in sofa_reverb_folder_path if not os.path.exists(os.path.expanduser(reverb_full_path)): raise AmbiScaperError('reverb_config: file does not exist: ' + reverb_full_path) # TODO # The file should be a valid AmbisonicsDRIR file sofa_file = pysofaconventions.SOFAAmbisonicsDRIR( reverb_full_path, 'r') if not sofa_file.isValid(): sofa_file.close() raise AmbiScaperError( 'reverb_config: file is not a valid AmbisonicsDRIR SOFA file: ' + reverb_name) sofa_file.close() # Make sure it's a valid distribution tuple _validate_distribution(reverb_name_tuple) # If reverb name is specified explicitly if reverb_name_tuple[0] == "const": # reverb name: allowed string if reverb_name_tuple[1] is None: raise AmbiScaperError('reverb_config: reverb name is None') __validate_reverb_name_types(reverb_name_tuple[1]) __validate_reverb_name_configuration(reverb_name_tuple[1]) # Otherwise it must be specified using "choose" # Empty list is allowed, meaning all avaiable IRS elif reverb_name_tuple[0] == "choose": # Empty list [ __validate_reverb_name_types(name) for name in reverb_name_tuple[1] ] [ __validate_reverb_name_configuration(name) for name in reverb_name_tuple[1] ] # No other labels allowed" else: raise AmbiScaperError( 'Reverb name must be specified using a "const" or "choose" tuple.' )
def _validate_ambisonics_order(order): if (not isinstance(order,int)): raise AmbiScaperError( 'Ambisonics order must be an integer') if (order<0): raise AmbiScaperError( 'Ambisonics order must be bigger than 0')
def _validate_ambisonics_degree(degree, order): _validate_ambisonics_order(order) if (not isinstance(degree,int)): raise AmbiScaperError( 'Ambisonics degree must be an integer') if (np.abs(degree) > order): raise AmbiScaperError( 'Ambisonics degree modulus must be minor or equal to ambisonics order')
def _validate_room_dimensions(self, room_dimensions_tuple): ''' TODO :param room_dimensions_tuple: :return: ''' def _valid_room_dimensions_values(room_dimensions): # room_dimensions: list of 3 Numbers if (room_dimensions is None or not isinstance(room_dimensions, list) or not all( isinstance(dim, Number) for dim in room_dimensions) or len(room_dimensions) is not 3): return False else: return True # Make sure it's a valid distribution tuple _validate_distribution(room_dimensions_tuple) # If room_dimensions is specified explicitly if room_dimensions_tuple[0] == "const": if not _valid_room_dimensions_values(room_dimensions_tuple[1]): raise AmbiScaperError( 'reverb_config: room dimensions must be a list of 3 elements' ) elif room_dimensions_tuple[0] == "choose": if not room_dimensions_tuple[1]: # list is empty raise AmbiScaperError( 'reverb_config: room_dimensions_tuple list empty') elif not all( _valid_room_dimensions_values(room_dimensions) for room_dimensions in room_dimensions_tuple[1]): raise AmbiScaperError( 'reverb_config: room dimensions must be a list of 3 elements' ) elif room_dimensions_tuple[0] == "uniform": if room_dimensions_tuple[1] < 0: raise AmbiScaperError( 'A "uniform" distribution tuple for room dimensions must have ' 'min_value >= 0') elif room_dimensions_tuple[0] == "normal": warnings.warn( 'A "normal" distribution tuple for room dimensions can result in ' 'negative values, in which case the distribution will be ' 're-sampled until a positive value is returned: this can result ' 'in an infinite loop!', AmbiScaperWarning) elif room_dimensions_tuple[0] == "truncnorm": if room_dimensions_tuple[3] < 0: raise AmbiScaperError( 'A "truncnorm" distirbution tuple for room dimensions must specify a non-' 'negative trunc_min value.')
def _validate_spread_coef(alpha): ''' Must be a real number between 0.0 and 1.0 :param alpha: :return: ''' if not is_real_number(alpha): raise AmbiScaperError( 'Ambisonics spread coef must be a real number') if (not 0.0 <= alpha <= 1.0): raise AmbiScaperError( 'Ambisonics spread coef must be in the range [0.0, 1.0]')
def _validate_t60(self, t60_tuple): ''' TODO :param beta_tuple: :return: ''' def _valid_t60_values(t60): # t60: float bigger than 0 if (t60 is None or not isinstance(t60, float) or t60 <= 0): return False else: return True # Make sure it's a valid distribution tuple _validate_distribution(t60_tuple) # If t60 is specified explicitly if t60_tuple[0] == "const": if not _valid_t60_values(t60_tuple[1]): raise AmbiScaperError('reverb_config: t60 must be a float >0') elif t60_tuple[0] == "choose": if not t60_tuple[1]: # list is empty raise AmbiScaperError('reverb_config: t60_tuple list empty') elif not all(_valid_t60_values(t60) for t60 in t60_tuple[1]): raise AmbiScaperError('reverb_config: t60 must be a float >0') elif t60_tuple[0] == "uniform": if t60_tuple[1] < 0: raise AmbiScaperError( 'A "uniform" distribution tuple for t60 must have ' 'min_value >= 0') elif t60_tuple[0] == "normal": warnings.warn( 'A "normal" distribution tuple for t60 can result in ' 'negative values, in which case the distribution will be ' 're-sampled until a positive value is returned: this can result ' 'in an infinite loop!', AmbiScaperWarning) elif t60_tuple[0] == "truncnorm": if t60_tuple[3] < 0: raise AmbiScaperError( 'A "truncnorm" distirbution tuple for t60 must specify a non-' 'negative trunc_min value.')
def __validate_reverb_name_configuration(reverb_name): reverb_full_path = self.sofa_reverb_folder_path + '/' + reverb_name # The provided name should exist in sofa_reverb_folder_path if not os.path.exists(os.path.expanduser(reverb_full_path)): raise AmbiScaperError('reverb_config: file does not exist: ' + reverb_full_path) # TODO # The file should be a valid AmbisonicsDRIR file sofa_file = pysofaconventions.SOFAAmbisonicsDRIR( reverb_full_path, 'r') if not sofa_file.isValid(): sofa_file.close() raise AmbiScaperError( 'reverb_config: file is not a valid AmbisonicsDRIR SOFA file: ' + reverb_name) sofa_file.close()
def get_receiver_position(room_dimensions): ''' TODO: for the moment just the center :param room_dimensions: :return: ''' if not isinstance(room_dimensions, list) or len(room_dimensions) != 3: raise AmbiScaperError('Incorrect room dimensions') return [float(l) / 2.0 for l in room_dimensions]
def _validate_IR_length(self, IRlenght_tuple): ''' TODO :param IRlenght_tuple: :return: ''' # Make sure it's a valid distribution tuple _validate_distribution(IRlenght_tuple) def __valid_IR_length_values(IRlength): if (not isinstance(IRlength, int) or IRlength <= 0): return False else: return True # If IR length is specified explicitly if IRlenght_tuple[0] == "const": # IR length: positive integer if IRlenght_tuple[1] is None: raise AmbiScaperError('reverb_config: IR length is None') elif not __valid_IR_length_values(IRlenght_tuple[1]): raise AmbiScaperError( 'reverb_config: IR length must be a positive integer') # Otherwise it must be specified using "choose" elif IRlenght_tuple[0] == "choose": if not IRlenght_tuple[1]: # list is empty raise AmbiScaperError('reverb_config: IR length list empty') elif not all( __valid_IR_length_values(length) for length in IRlenght_tuple[1]): raise AmbiScaperError( 'reverb_config: IR length must be a positive integer') # No other labels allowed" else: raise AmbiScaperError( 'IR length must be specified using a "const" or "choose" tuple.' )
def set_sofa_reverb_folder_path(self, path): """ Set the base path where to find the AmbisonicsDRIR SOFA files :param path: The path to the folder :raises: AmbiScaperError If the provided path does not exist or is not a folder """ if not os.path.exists(path): raise AmbiScaperError( "The provided SOFA reverb folder path does not exist! " + path) if not os.path.isdir(path): raise AmbiScaperError( "The provided SOFA reverb path is not a folder! " + path) self.sofa_reverb_folder_path = path
def generate_sofa_file_full_path(self, sofa_reverb_name): ''' Return full path to a SOFA Ambisonics reverb given a reverb name :param sofa_reverb_name: string referencing to a valid recorded reverb name :raises: AmbiScaper Error if reverb name is not valid, or if sofa path is not specified ''' if not isinstance(sofa_reverb_name, str): raise AmbiScaperError('Not valid reverb name type') elif not self.sofa_reverb_folder_path: raise AmbiScaperError("SOFA reverb folder path is not specified!") elif find_element_in_list( sofa_reverb_name, self.retrieve_available_sofa_reverb_files()) is None: raise AmbiScaperError('Reverb name does not exist: ', sofa_reverb_name) return os.path.expanduser( os.path.join(self.sofa_reverb_folder_path, sofa_reverb_name))
def _validate_microphone_type(self, mic_type_tuple): ''' Validate that a mic_type tuple is in the right format and that it's values are valid. Parameters ---------- mic_type_tuple : tuple Label tuple (see ```AmbiScaper.add_event``` for required format). Raises ------ AmbiScaperError If the validation fails. ''' # Make sure it's a valid distribution tuple _validate_distribution(mic_type_tuple) # Make sure it's one of the allowed distributions for a mic_type and that the # mic_type value is one of the allowed labels. if mic_type_tuple[0] == "const": if not mic_type_tuple[1] in self.supported_virtual_mics.keys(): raise AmbiScaperError( 'Microphone type value must match one of the available labels: ' '{:s}'.format(str(self.supported_virtual_mics.keys()))) elif mic_type_tuple[0] == "choose": if mic_type_tuple[1]: # list is not empty if not set(mic_type_tuple[1]).issubset( set(self.supported_virtual_mics.keys())): raise AmbiScaperError( 'Microphone type provided must be a subset of the available ' 'labels: {:s}'.format( str(self.supported_virtual_mics.keys()))) else: raise AmbiScaperError( 'Microphone type must be specified using a "const" or "choose" tuple.' )
def change_channel_ordering_fuma_2_acn(fuma_array): ''' TODO :param fuma_array: :return: ''' # Input must be a numpy array if not isinstance(fuma_array,np.ndarray): raise AmbiScaperError( 'Error: ACN conversion: input array not a numpy ndarray') # Method only valid for 1st order elif np.shape(fuma_array)[1] is not 4: raise AmbiScaperError( 'Error: ACN conversion: input array is not order 1') # Create new array with same shape acn_array = np.ndarray(shape=np.shape(fuma_array)) # Copy them one by one for i in range(4): acn_array[:, FUMA_2_ACN_BFORMAT_CHANNEL_ORDERING_DICT[i]] = fuma_array[:, i] return acn_array
def _validate_source_type(self, source_type_tuple): ''' Validate that a source_type tuple is in the right format and that it's values are valid. Parameters ---------- source_type_tuple : tuple Label tuple (see ```AmbiScaper.add_event``` for required format). Raises ------ AmbiScaperError If the validation fails. ''' # Make sure it's a valid distribution tuple _validate_distribution(source_type_tuple) # Make sure it's one of the allowed distributions for a source_type and that the # source_type value is one of the allowed labels. if source_type_tuple[0] == "const": if not source_type_tuple[1] in SMIR_ALLOWED_SOURCE_TYPES: raise AmbiScaperError( 'Source type value must match one of the available labels: ' '{:s}'.format(str(SMIR_ALLOWED_SOURCE_TYPES))) elif source_type_tuple[0] == "choose": if source_type_tuple[1]: # list is not empty if not set(source_type_tuple[1]).issubset( set(SMIR_ALLOWED_SOURCE_TYPES)): raise AmbiScaperError( 'Source type provided must be a subset of the available ' 'labels: {:s}'.format(str(SMIR_ALLOWED_SOURCE_TYPES))) else: raise AmbiScaperError( 'Source type must be specified using a "const" or "choose" tuple.' )
def _validate_reverb_wrap(self, reverb_wrap_tuple): ''' :param reverb_wrap: :return: ''' # Make sure it's a valid distribution tuple _validate_distribution(reverb_wrap_tuple) # Make sure that type matches def __valid_reverb_wrap_types(reverb_wrap): if (not isinstance(reverb_wrap, str)): return False else: return True def __valid_reverb_wrap_values(reverb_wrap): if reverb_wrap not in self.valid_wrap_values: return False else: return True # If reverb wrap is specified explicitly if reverb_wrap_tuple[0] == "const": # reverb wrap: allowed string if reverb_wrap_tuple[1] is None: raise AmbiScaperError('reverb_config: reverb wrap is None') elif not __valid_reverb_wrap_types(reverb_wrap_tuple[1]): raise AmbiScaperError( 'reverb_config: reverb wrap must be a string') elif not __valid_reverb_wrap_values(reverb_wrap_tuple[1]): raise AmbiScaperError('reverb_config: reverb wrap not valid:' + reverb_wrap_tuple[1]) # Otherwise it must be specified using "choose" # Empty list is allowed, meaning all avaiable IRS elif reverb_wrap_tuple[0] == "choose": if not all( __valid_reverb_wrap_types(length) for length in reverb_wrap_tuple[1]): raise AmbiScaperError( 'reverb_config: reverb wrap must be a string') elif not all( __valid_reverb_wrap_values(name) for name in reverb_wrap_tuple[1]): raise AmbiScaperError( 'reverb_config: reverb names not valid: ' + str(reverb_wrap_tuple[1])) # No other labels allowed" else: raise AmbiScaperError( 'Reverb wrap must be specified using a "const" or "choose" tuple.' )
def retrieve_available_sofa_reverb_files(self): ''' Get a list of the existing SOFA files at the current SOFA path (recursively). :return: An array containing all available sofa files (not tested for validity) :raises: AmbiScaper error If the sofa folder is not specified ''' if not self.sofa_reverb_folder_path: raise AmbiScaperError("SOFA reverb folder path is not specified!") available_sofa_files = [] # Iterate recursively over all files for (dirpath, dirnames, filenames) in os.walk(self.sofa_reverb_folder_path): for file in filenames: # if file is sofa if os.path.splitext(file)[-1] == '.sofa': # Get the subpath relative to `self.sofa_reverb_folder_path` if dirpath == self.sofa_reverb_folder_path: # File is at the hierarchy top: no path prepend available_sofa_files.append(file) else: # We are not at the hierarchy top, so prepend the relative path relative_path = dirpath.replace( self.sofa_reverb_folder_path, '') available_sofa_files.append( os.path.join(relative_path, file)) return available_sofa_files
def _validate_smir_reverb_spec(self, IRlength, room_dimensions, t60, reflectivity, source_type, microphone_type): # TODO ''' Check that event parameter values are valid. Parameters ---------- label : tuple source_file : tuple source_time : tuple event_time : tuple event_duration : tuple event_azimuth : tuple event_elevation : tuple event_spread : tuple snr : tuple allowed_labels : list List of allowed labels for the event. pitch_shift : tuple or None time_stretch: tuple or None Raises ------ AmbiScaperError : If any of the input parameters has an invalid format or value. See Also -------- AmbiScaper.add_event : Add a foreground sound event to the foreground specification. ''' # IR LENGTH self._validate_IR_length(IRlength) # ROOM DIMENSIONS self._validate_room_dimensions(room_dimensions) # We must define either t60 or reflectivity, but not none # If both are defined, just raise a warning if reflectivity is None: if t60 is None: raise AmbiScaperError( 'reverb_config: Neither t60 nor reflectivity defined!') else: # T60 self._validate_t60(t60) elif t60 is None: # REFLECTIVITY self._validate_wall_reflectivity(reflectivity) else: # T60 self._validate_t60(t60) raise AmbiScaperWarning( 'reverb_config: Both t60 and reflectivity defined!' + 'Using t60 by default') # SOURCE TYPE self._validate_source_type(source_type) # MYCROPHONE TYPE self._validate_microphone_type(microphone_type)
def _validate_ambisonics_angle(angle): if (not is_real_number(angle)): raise AmbiScaperError( 'Ambisonics angle must be a number')
def _validate_wall_reflectivity(self, wall_reflectivity_tuple): ''' TODO :param wall_reflectivity_tuple: :return: ''' def _valid_wall_reflectivity_values(wall_reflectivity): # wall_reflectivity: list of 6 floats in the range [0,1] if (wall_reflectivity is None or not isinstance(wall_reflectivity, list) or len(wall_reflectivity) is not 6 or not all( isinstance(r, float) and 0 <= r <= 1 for r in wall_reflectivity)): return False else: return True # Make sure it's a valid distribution tuple _validate_distribution(wall_reflectivity_tuple) # If wall_reflectivity is specified explicitly if wall_reflectivity_tuple[0] == "const": if not _valid_wall_reflectivity_values(wall_reflectivity_tuple[1]): raise AmbiScaperError( 'reverb_config: wall_reflectivity must be a list of 6 floats between 0 and 1' ) elif wall_reflectivity_tuple[0] == "choose": if not wall_reflectivity_tuple[1]: # list is empty raise AmbiScaperError( 'reverb_config: wall_reflectivity_tuple list empty') elif not all( _valid_wall_reflectivity_values(wall_reflectivity) for wall_reflectivity in wall_reflectivity_tuple[1]): raise AmbiScaperError( 'reverb_config: wall_reflectivity must be a list of 6 floats between 0 and 1' ) elif wall_reflectivity_tuple[0] == "uniform": if wall_reflectivity_tuple[1] < 0: raise AmbiScaperError( 'A "uniform" distribution tuple for wall_reflectivity must have ' 'min_value >= 0') elif wall_reflectivity_tuple[2] > 1: raise AmbiScaperError( 'A "uniform" distribution tuple for wall_reflectivity must have ' 'max_value <= 1') elif wall_reflectivity_tuple[0] == "normal": warnings.warn( 'A "normal" distribution tuple for wall_reflectivity can result in ' 'values outside [0,1], in which case the distribution will be ' 're-sampled until a positive value is returned: this can result ' 'in an infinite loop!', AmbiScaperWarning) elif wall_reflectivity_tuple[0] == "truncnorm": if wall_reflectivity_tuple[3] < 0: raise AmbiScaperError( 'A "uniform" distribution tuple for wall_reflectivity must have ' 'min_value >= 0') elif wall_reflectivity_tuple[4] > 1: raise AmbiScaperError( 'A "uniform" distribution tuple for wall_reflectivity must have ' 'max_value <= 1')
def change_normalization_fuma_2_sn3d(fuma_array): ''' :param fuma_array: :return: ''' # Input must be a numpy array if not isinstance(fuma_array,np.ndarray): raise AmbiScaperError( 'Error: SN3D conversion: input array not a numpy ndarray') # Method only valid for 1st order elif np.shape(fuma_array)[1] is not 4: raise AmbiScaperError( 'Error: SN3D conversion: input array is not order 1') # Create new array with same shape sn3d_array = np.ndarray(shape=np.shape(fuma_array)) # W channel: multiply by sqrt(2) sn3d_array[:, 0] = fuma_array[:, 0] * np.sqrt(2) # All 1st order channels remain same for i in range(1,4): sn3d_array[:, i] = fuma_array[:, i] return sn3d_array ################################################ # This is the old implementation up to order 3, # with explicit equations taken from from # D. Malham, 'Higher order Ambisonic systems' # https://www.york.ac.uk/inst/mustech/3d_audio/higher_order_ambisonics.pdf # notice that phi and theta are switched from standard... # # left here just in case for convenience ################################################ # # def get_ambisonics_coefs(azimuth,elevation,order): # # _validate_angle(azimuth) # _validate_angle(elevation) # _validate_ambisonics_order(order) # # # azimuth and elevation values are fine as long as they are real numbers... # # coefs = np.zeros(get_number_of_ambisonics_channels(order)) # # a = azimuth # usually phi # e = elevation # usually theta # # if (order >= 0): # coefs[0] = 1. # W # # if (order >= 1): # coefs[1] = sin(a) * cos(e) # Y # coefs[2] = sin(e) # Z # coefs[3] = cos(a) * cos(e) # X # # if (order >= 2): # coefs[4] = (sqrt(3)/2.) * sin(2*a) * pow(cos(e),2) # V # coefs[5] = (sqrt(3)/2.) * sin(a) * sin(2*e) # T # coefs[6] = 0.5 * ( 3*pow(sin(e),2)-1 ) # R # coefs[7] = (sqrt(3)/2.) * cos(a) * sin(2*e) # S # coefs[8] = (sqrt(3)/2.) * cos(2*a) * pow(cos(e),2) # U # # if (order >= 3): # coefs[9] = (sqrt(5./8.)) * sin(3*a) * pow(cos(e),3) # Q # coefs[10] = (sqrt(15)/2.) * sin(2*a) * sin(e) * pow(cos(e),2) # O # coefs[11] = (sqrt(3./8.)) * sin(a) * cos(e) * (5*(pow(sin(e),2))-1) # M # coefs[12] = 0.5 * ( sin(e) * (5*(pow(sin(e),2))-3) ) # K # coefs[13] = (sqrt(3./8.)) * cos(a) * cos(e) * ((5*pow(sin(e),2))-1) # L # coefs[14] = (sqrt(15)/2.) * cos(2*a) * sin(e) * pow(cos(e),2) # N # coefs[15] = (sqrt(5./8.)) * cos(3*a) * pow(cos(e),3) # P # # return coefs
def __validate_reverb_name_types(reverb_name): if not isinstance(reverb_name, str): raise AmbiScaperError( 'reverb_config: reverb name must be a string')