예제 #1
0
    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.'
            )
예제 #2
0
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')
예제 #3
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')
예제 #4
0
    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.')
예제 #5
0
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]')
예제 #6
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.')
예제 #7
0
        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()
예제 #8
0
    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]
예제 #9
0
    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.'
            )
예제 #10
0
    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
예제 #11
0
    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))
예제 #12
0
    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.'
            )
예제 #13
0
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
예제 #14
0
    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.'
            )
예제 #15
0
    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.'
            )
예제 #16
0
    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
예제 #17
0
    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)
예제 #18
0
def _validate_ambisonics_angle(angle):
    if (not is_real_number(angle)):
            raise AmbiScaperError(
                'Ambisonics angle must be a number')
예제 #19
0
    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')
예제 #20
0
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
예제 #21
0
 def __validate_reverb_name_types(reverb_name):
     if not isinstance(reverb_name, str):
         raise AmbiScaperError(
             'reverb_config: reverb name must be a string')