import sox
import random
import os
import warnings
import jams
from collections import namedtuple
import logging
import tempfile
import numpy as np
import shutil
import pandas as pd
from .scaper_exceptions import ScaperError
from .scaper_warnings import ScaperWarning
from .util import _close_temp_files
from .util import _set_temp_logging_level
from .util import _get_sorted_files
from .util import _validate_folder_path
from .util import _populate_label_list
from .util import _trunc_norm
from .util import max_polyphony
from .util import polyphony_gini
from .util import is_real_number, is_real_array
from .audio import get_integrated_lufs

SUPPORTED_DIST = {"const": lambda x: x,
                  "choose": lambda x: random.choice(x),
                  "uniform": random.uniform,
                  "normal": random.normalvariate,
                  "truncnorm": _trunc_norm}

# Define single event spec as namedtuple
EventSpec = namedtuple(
    'EventSpec',
    ['label', 'source_file', 'source_time', 'event_time', 'event_duration',
     'snr', 'role', 'pitch_shift', 'time_stretch'], verbose=False)
'''
Container for storing event specifications, either probabilistic (i.e. using
distribution tuples to specify possible values) or instantiated (i.e. storing
constants directly).
'''


def generate_from_jams(jams_infile, audio_outfile, fg_path=None, bg_path=None,
                       jams_outfile=None):
    '''
    Generate a soundscape based on an existing scaper JAMS file and save to
    disk.

    Parameters
    ----------
    jams_infile : str
        Path to JAMS file (must be a file previously generated by Scaper).
    audio_outfile : str
        Path for saving the generated soundscape audio.
    fg_path : str or None
        Specifies a different path for foreground audio than the one stored in
        the input jams file. For the reconstruction to be successful the folder
        and file structure inside this path must be identical the one that was
        used to create the input jams file. If None (default), the fg_path from
        the input jams file will be used.
    bg_path : str or None
        Specifies a different path for background audio than the one stored in
        the input jams file. For the reconstruction to be successful the folder
        and file structure inside this path must be identical the one that was
        used to create the input jams file. If None (default), the bg_path from
        the input jams file will be used.
    jams_outfile : str or None
        Path for saving new JAMS file, if None (default) a new JAMS is not
        saved. Useful when either fg_path or bg_path is not None, as it saves
        a new JAMS files where the source file paths match the new fg_path
        and/or bg_path.

    Raises
    ------
    ScaperError
        If jams_infile does not point to a valid JAMS file that was previously
        generated by Scaper and contains an annotation of the sound_event
        namespace.

    '''
    jam = jams.load(jams_infile)
    anns = jam.search(namespace='sound_event')

    if len(anns) == 0:
        raise ScaperError(
            'JAMS file does not contain any annotation with namespace '
            'sound_event.')

    ann = anns[0]

    # Update paths
    if fg_path is None:
        new_fg_path = ann.sandbox.scaper['fg_path']
    else:
        new_fg_path = os.path.expanduser(fg_path)
        # Update source files
        for idx in ann.data.index:
            if ann.data.loc[idx, 'value']['role'] == 'foreground':
                sourcefile = ann.data.loc[idx, 'value']['source_file']
                sourcefilename = os.path.basename(sourcefile)
                parent = os.path.dirname(sourcefile)
                parentname = os.path.basename(parent)
                newsourcefile = os.path.join(
                    new_fg_path, parentname, sourcefilename)
                ann.data.loc[idx, 'value']['source_file'] = newsourcefile
        # Update sandbox
        ann.sandbox.scaper['fg_path'] = new_fg_path

    if bg_path is None:
        new_bg_path = ann.sandbox.scaper['bg_path']
    else:
        new_bg_path = os.path.expanduser(bg_path)
        # Update source files
        for idx in ann.data.index:
            if ann.data.loc[idx, 'value']['role'] == 'background':
                sourcefile = ann.data.loc[idx, 'value']['source_file']
                sourcefilename = os.path.basename(sourcefile)
                parent = os.path.dirname(sourcefile)
                parentname = os.path.basename(parent)
                newsourcefile = os.path.join(
                    new_bg_path, parentname, sourcefilename)
                ann.data.loc[idx, 'value']['source_file'] = newsourcefile
        # Update sandbox
        ann.sandbox.scaper['bg_path'] = new_bg_path

    # Create scaper object
    duration = ann.sandbox.scaper['duration']
    protected_labels = ann.sandbox.scaper['protected_labels']
    sc = Scaper(duration, new_fg_path, new_bg_path, protected_labels)

    # Set synthesis parameters
    sc.ref_db = ann.sandbox.scaper['ref_db']
    sc.n_channels = ann.sandbox.scaper['n_channels']
    sc.fade_in_len = ann.sandbox.scaper['fade_in_len']
    sc.fade_out_len = ann.sandbox.scaper['fade_out_len']

    # Generate audio and save to disk
    reverb = ann.sandbox.scaper['reverb']
    sc._generate_audio(audio_outfile, ann, reverb=reverb,
                       disable_sox_warnings=True)

    # If there are slice (trim) operations, need to perform them!
    if 'slice' in ann.sandbox.keys():
        for sliceop in ann.sandbox['slice']:
            # must use temp file in order to save to same file
            tmpfiles = []
            with _close_temp_files(tmpfiles):
                # Create tmp file
                tmpfiles.append(
                    tempfile.NamedTemporaryFile(suffix='.wav', delete=True))
                # Save trimmed result to temp file
                tfm = sox.Transformer()
                tfm.trim(sliceop['slice_start'], sliceop['slice_end'])
                tfm.build(audio_outfile, tmpfiles[-1].name)
                # Copy result back to original file
                shutil.copyfile(tmpfiles[-1].name, audio_outfile)

    # Optionally save new jams file
    if jams_outfile is not None:
        jam.save(jams_outfile)


def trim(audio_infile, jams_infile, audio_outfile, jams_outfile, start_time,
         end_time, no_audio=False):
    '''
    Trim and audio file and corresponding Scaper JAMS file and save to disk.

    Given an input audio file and corresponding jams file, trim both the audio
    and all annotations in the jams file to the time range ``[start_time,
    end_time]`` and save the result to ``audio_outfile`` and ``jams_outfile``
    respectively. This function uses ``jams.slice()`` for trimming the jams
    file while ensuring the start times of the jam's annotations and
    observations they contain match the trimmed audio file.

    Parameters
    ----------
    audio_infile : str
        Path to input audio file
    jams_infile : str
        Path to input jams file
    audio_outfile : str
        Path to output trimmed audio file
    jams_outfile : str
        Path to output trimmed jams file
    start_time : float
        Start time for trimmed audio/jams
    end_time : float
        End time for trimmed audio/jams
    no_audio : bool
        If true, operates on the jams only. Audio input and output paths
        don't have to point to valid files.

    '''
    # First trim jams (might raise an error)
    jam = jams.load(jams_infile)
    jam_sliced = jam.slice(start_time, end_time, strict=False)

    # Special work for annotations of the scaper 'sound_event' namespace
    for ann in jam_sliced.annotations:
        if ann.namespace == 'sound_event':

            # DON'T MODIFY event's value dict! Keeps original instantiated
            # values for reconstruction / reproducibility.
            # Count number of FG events
            n_events = 0
            for idx, line in ann.data.iterrows():
                if line.value['role'] == 'foreground':
                    n_events += 1

            # Re-compute max polyphony
            poly = max_polyphony(ann)

            # Re-compute polyphony gini
            gini = polyphony_gini(ann)

            # Update specs in sandbox
            ann.sandbox.scaper['n_events'] = n_events
            ann.sandbox.scaper['polyphony_max'] = poly
            ann.sandbox.scaper['polyphony_gini'] = gini
            ann.sandbox.scaper['duration'] = ann.duration

    # Save result to output jams file
    jam_sliced.save(jams_outfile)

    # Next, trim audio
    if not no_audio:
        tfm = sox.Transformer()
        tfm.trim(start_time, end_time)
        if audio_outfile != audio_infile:
            tfm.build(audio_infile, audio_outfile)
        else:
            # must use temp file in order to save to same file
            tmpfiles = []
            with _close_temp_files(tmpfiles):
                # Create tmp file
                tmpfiles.append(
                    tempfile.NamedTemporaryFile(
                        suffix='.wav', delete=True))
                # Save trimmed result to temp file
                tfm.build(audio_infile, tmpfiles[-1].name)
                # Copy result back to original file
                shutil.copyfile(tmpfiles[-1].name, audio_outfile)


def _get_value_from_dist(dist_tuple):
    '''
    Sample a value from the provided distribution tuple.

    Given a distribution tuple, validate its format/values and then sample
    and return a single value from the distribution specified by the tuple.

    Parameters
    ----------
    dist_tuple : tuple
        Distribution tuple to be validated. See ``Scaper.add_event`` for
        details about the expected format for the distribution tuple.

    Returns
    -------
    value
        A value from the specified distribution.

    See Also
    --------
    Scaper.add_event :  Add a foreground sound event to the foreground
    specification.
    _validate_distribution : Check whether a tuple specifying a parameter
    distribution has a valid format, if not raise an error.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(dist_tuple)
    return SUPPORTED_DIST[dist_tuple[0]](*dist_tuple[1:])


def _validate_distribution(dist_tuple):
    '''
    Check whether a tuple specifying a parameter distribution has a valid
    format, if not raise an error.

    Parameters
    ----------
    dist_tuple : tuple
        Tuple specifying a distribution to sample from. See Scaper.add_event
        for details about the expected format of the tuple and allowed values.

    Raises
    ------
    ScaperError
        If the tuple does not have a valid format.

    See Also
    --------
    Scaper.add_event : Add a foreground sound event to the foreground
    specification.
    '''
    # Make sure it's a tuple
    if not isinstance(dist_tuple, tuple):
        raise ScaperError('Distribution tuple must be of type tuple.')

    # Make sure the tuple contains at least 2 items
    if len(dist_tuple) < 2:
        raise ScaperError('Distribution tuple must be at least of length 2.')

    # Make sure the first item is one of the supported distribution names
    if dist_tuple[0] not in SUPPORTED_DIST.keys():
        raise ScaperError(
            "Unsupported distribution name: {:s}".format(dist_tuple[0]))

    # If it's a constant distribution, tuple must be of length 2
    if dist_tuple[0] == 'const':
        if len(dist_tuple) != 2:
            raise ScaperError('"const" distribution tuple must be of length 2')
    # If it's a choose, tuple must be of length 2 and second item of type list
    elif dist_tuple[0] == 'choose':
        if len(dist_tuple) != 2 or not isinstance(dist_tuple[1], list):
            raise ScaperError(
                'The "choose" distribution tuple must be of length 2 where '
                'the second item is a list.')
    # If it's a uniform distribution, tuple must be of length 3, 2nd item must
    # be a real number and 3rd item must be real and greater/equal to the 2nd.
    elif dist_tuple[0] == 'uniform':
        if (len(dist_tuple) != 3 or
                not is_real_number(dist_tuple[1]) or
                not is_real_number(dist_tuple[2]) or
                dist_tuple[1] > dist_tuple[2]):
            raise ScaperError(
                'The "uniform" distribution tuple be of length 2, where the '
                '2nd item is a real number and the 3rd item is a real number '
                'and greater/equal to the 2nd item.')
    # If it's a normal distribution, tuple must be of length 3, 2nd item must
    # be a real number and 3rd item must be a non-negative real
    elif dist_tuple[0] == 'normal':
        if (len(dist_tuple) != 3 or
                not is_real_number(dist_tuple[1]) or
                not is_real_number(dist_tuple[2]) or
                dist_tuple[2] < 0):
            raise ScaperError(
                'The "normal" distribution tuple must be of length 3, where '
                'the 2nd item (mean) is a real number and the 3rd item (std '
                'dev) is real and non-negative.')
    elif dist_tuple[0] == 'truncnorm':
        if (len(dist_tuple) != 5 or
                not is_real_number(dist_tuple[1]) or
                not is_real_number(dist_tuple[2]) or
                not is_real_number(dist_tuple[3]) or
                not is_real_number(dist_tuple[4]) or
                dist_tuple[2] < 0 or
                dist_tuple[4] < dist_tuple[3]):
            raise ScaperError(
                'The "truncnorm" distribution tuple must be of length 5, '
                'where the 2nd item (mean) is a real number, the 3rd item '
                '(std dev) is real and non-negative, the 4th item (trunc_min) '
                'is a real number and the 5th item (trun_max) is a real '
                'number that is equal to or greater than trunc_min.')


def _validate_label(label, allowed_labels):
    '''
    Validate that a label tuple is in the right format and that it's values
    are valid.

    Parameters
    ----------
    label : tuple
        Label tuple (see ```Scaper.add_event``` for required format).
    allowed_labels : list
        List of allowed labels.

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(label)

    # Make sure it's one of the allowed distributions for a label and that the
    # label value is one of the allowed labels.
    if label[0] == "const":
        if not label[1] in allowed_labels:
            raise ScaperError(
                'Label value must match one of the available labels: '
                '{:s}'.format(str(allowed_labels)))
    elif label[0] == "choose":
        if label[1]:  # list is not empty
            if not set(label[1]).issubset(set(allowed_labels)):
                raise ScaperError(
                    'Label list provided must be a subset of the available '
                    'labels: {:s}'.format(str(allowed_labels)))
    else:
        raise ScaperError(
            'Label must be specified using a "const" or "choose" tuple.')


def _validate_source_file(source_file_tuple, label_tuple):
    '''
    Validate that a source_file tuple is in the right format a that it's values
    are valid.

    Parameters
    ----------
    source_file : tuple
        Source file tuple (see ```Scaper.add_event``` for required format).
    label : str
        Label tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(source_file_tuple)
    _validate_distribution(label_tuple)

    # If source file is specified explicitly
    if source_file_tuple[0] == "const":
        # 1. the filepath must point to an existing file
        if not os.path.isfile(source_file_tuple[1]):
            raise ScaperError(
                "Source file not found: {:s}".format(source_file_tuple[1]))
        # 2. the label must match the file's parent folder name
        parent_name = os.path.basename(os.path.dirname(source_file_tuple[1]))
        if label_tuple[0] != "const" or label_tuple[1] != parent_name:
            raise ScaperError(
                "Source file's parent folder name does not match label.")
    # Otherwise it must be specified using "choose"
    elif source_file_tuple[0] == "choose":
        if source_file_tuple[1]:  # list is not empty
            if not all(os.path.isfile(x) for x in source_file_tuple[1]):
                raise ScaperError(
                    'Source file list must either be empty or all paths in '
                    'the list must point to valid files.')
    else:
        raise ScaperError(
            'Source file must be specified using a "const" or "choose" tuple.')


def _validate_time(time_tuple):
    '''
    Validate that a time tuple has the right format and that the
    specified distribution cannot result in a negative time.

    Parameters
    ----------
    time_tuple : tuple
        Time tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(time_tuple)

    # Ensure the values are valid for time
    if time_tuple[0] == "const":
        if (time_tuple[1] is None or
                not is_real_number(time_tuple[1]) or
                time_tuple[1] < 0):
            raise ScaperError(
                'Time must be a real non-negative number.')
    elif time_tuple[0] == "choose":
        if (not time_tuple[1] or
                not is_real_array(time_tuple[1]) or
                not all(x is not None for x in time_tuple[1]) or
                not all(x >= 0 for x in time_tuple[1])):
            raise ScaperError(
                'Time list must be a non-empty list of non-negative real '
                'numbers.')
    elif time_tuple[0] == "uniform":
        if time_tuple[1] < 0:
            raise ScaperError(
                'A "uniform" distribution tuple for time must have '
                'min_value >= 0')
    elif time_tuple[0] == "normal":
        warnings.warn(
            'A "normal" distribution tuple for time 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!',
            ScaperWarning)
    elif time_tuple[0] == "truncnorm":
        if time_tuple[3] < 0:
            raise ScaperError(
                'A "truncnorm" distirbution tuple for time must specify a non-'
                'negative trunc_min value.')


def _validate_duration(duration_tuple):
    '''
    Validate that a duration tuple has the right format and that the
    specified distribution cannot result in a negative or zero value.

    Parameters
    ----------
    duration : tuple
        Duration tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(duration_tuple)

    # Ensure the values are valid for duration
    if duration_tuple[0] == "const":
        if (not is_real_number(duration_tuple[1]) or
                duration_tuple[1] <= 0):
            raise ScaperError(
                'Duration must be a real number greater than zero.')
    elif duration_tuple[0] == "choose":
        if (not duration_tuple[1] or
                not is_real_array(duration_tuple[1]) or
                not all(x > 0 for x in duration_tuple[1])):
            raise ScaperError(
                'Duration list must be a non-empty list of positive real '
                'numbers.')
    elif duration_tuple[0] == "uniform":
        if duration_tuple[1] <= 0:
            raise ScaperError(
                'A "uniform" distribution tuple for duration must have '
                'min_value > 0')
    elif duration_tuple[0] == "normal":
        warnings.warn(
            'A "normal" distribution tuple for duration can result in '
            'non-positives values, in which case the distribution will be '
            're-sampled until a positive value is returned: this can result '
            'in an infinite loop!',
            ScaperWarning)
    elif duration_tuple[0] == "truncnorm":
        if duration_tuple[3] <= 0:
            raise ScaperError(
                'A "truncnorm" distirbution tuple for time must specify a '
                'positive trunc_min value.')


def _validate_snr(snr_tuple):
    '''
    Validate that an snr distribution tuple has the right format.

    Parameters
    ----------
    snr : tuple
        SNR tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # Make sure it's a valid distribution tuple
    _validate_distribution(snr_tuple)

    # Ensure the values are valid for SNR
    if snr_tuple[0] == "const":
        if not is_real_number(snr_tuple[1]):
            raise ScaperError(
                'SNR must be a real number.')
    elif snr_tuple[0] == "choose":
        if (not snr_tuple[1] or
                not is_real_array(snr_tuple[1])):
            raise ScaperError(
                'SNR list must be a non-empty list of real numbers.')

    # No need to check for "uniform" and "normal" since they must produce a
    # real number and technically speaking any real number is a valid SNR.
    # TODO: do we want to impose limits on the possible SNR values?


def _validate_pitch_shift(pitch_shift_tuple):
    '''
    Validate that a pitch_shift distribution tuple has the right format.

    Parameters
    ----------
    pitch_shift_tuple : tuple
        Pitch shift tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # If the tuple is none then it's valid
    if pitch_shift_tuple is not None:
        # Make sure it's a valid distribution tuple
        _validate_distribution(pitch_shift_tuple)

        # Ensure the values are valid for pitch shift
        if pitch_shift_tuple[0] == "const":
            if not is_real_number(pitch_shift_tuple[1]):
                raise ScaperError(
                    'Pitch shift must be a real number.')
        elif pitch_shift_tuple[0] == "choose":
            if (not pitch_shift_tuple[1] or
                    not is_real_array(pitch_shift_tuple[1])):
                raise ScaperError(
                    'Pitch shift list must be a non-empty list of real '
                    'numbers.')

        # No need to check for "uniform" and "normal" since they must produce a
        # real number and technically speaking any real number is a valid pitch
        # shift
        # TODO: do we want to impose limits on the possible pitch shift values?


def _validate_time_stretch(time_stretch_tuple):
    '''
    Validate that a time_stretch distribution tuple has the right format.

    Parameters
    ----------
    time_stretch_tuple: tuple
        Time stretch tuple (see ```Scaper.add_event``` for required format).

    Raises
    ------
    ScaperError
        If the validation fails.

    '''
    # if the tuple is none then its valid
    if time_stretch_tuple is not None:
        # Make sure it's a valid distribution tuple
        _validate_distribution(time_stretch_tuple)

        # Ensure the values are valid for time stretch
        if time_stretch_tuple[0] == "const":
            if (not is_real_number(time_stretch_tuple[1]) or
                    time_stretch_tuple[1] <= 0):
                raise ScaperError(
                    'Time stretch must be a real number greater than zero.')
        elif time_stretch_tuple[0] == "choose":
            if (not time_stretch_tuple[1] or
                    not is_real_array(time_stretch_tuple[1]) or
                    not all(x > 0 for x in time_stretch_tuple[1])):
                raise ScaperError(
                    'Time stretch list must be a non-empty list of positive '
                    'real numbers.')
        elif time_stretch_tuple[0] == "uniform":
            if time_stretch_tuple[1] <= 0:
                raise ScaperError(
                    'A "uniform" distribution tuple for time stretch must have '
                    'min_value > 0')
        elif time_stretch_tuple[0] == "normal":
            warnings.warn(
                'A "normal" distribution tuple for time stretch can result in '
                'non-positives values, in which case the distribution will be '
                're-sampled until a positive value is returned: this can '
                'result in an infinite loop!',
                ScaperWarning)
        elif time_stretch_tuple[0] == "truncnorm":
            if time_stretch_tuple[3] <= 0:
                raise ScaperError(
                    'A "truncnorm" distirbution tuple for time stretch must '
                    'specify a positive trunc_min value.')

        # TODO: do we want to impose limits on the possible time stretch
        # values?


def _validate_event(label, source_file, source_time, event_time,
                    event_duration, snr, allowed_labels, pitch_shift,
                    time_stretch):
    '''
    Check that event parameter values are valid.

    Parameters
    ----------
    label : tuple
    source_file : tuple
    source_time : tuple
    event_time : tuple
    event_duration : tuple
    snr : tuple
    allowed_labels : list
        List of allowed labels for the event.
    pitch_shift : tuple or None
    time_stretch: tuple or None

    Raises
    ------
    ScaperError :
        If any of the input parameters has an invalid format or value.

    See Also
    --------
    Scaper.add_event : Add a foreground sound event to the foreground
    specification.
    '''
    # allowed_labels must be a list. All other parameters will be validated
    # individually.
    if not isinstance(allowed_labels, list):
        raise ScaperError('allowed_labels must be of type list.')

    # SOURCE FILE
    _validate_source_file(source_file, label)

    # LABEL
    _validate_label(label, allowed_labels)

    # SOURCE TIME
    _validate_time(source_time)

    # EVENT TIME
    _validate_time(event_time)

    # EVENT DURATION
    _validate_duration(event_duration)

    # SNR
    _validate_snr(snr)

    # Pitch shift
    _validate_pitch_shift(pitch_shift)

    # Time stretch
    _validate_time_stretch(time_stretch)


class Scaper(object):
    '''
    Create a Scaper object.

    Parameters
    ----------
    duration : float
        Duration of the soundscape, in seconds.
    fg_path : str
        Path to foreground folder.
    bg_path : str
        Path to background folder.
    protected_labels : list
        Provide a list of protected foreground labels. When a foreground
        label is in the protected list it means that when a sound event
        matching the label gets added to a soundscape instantiation the
        duration of the source audio file cannot be altered, and the
        duration value that was provided in the specification will be
        ignored.

        Adding labels to the protected list is useful for sound events
        whose semantic validity would be lost if the sound were trimmed
        before the sound event ends, for example an animal vocalization
        such as a dog bark.

    '''

    def __init__(self, duration, fg_path, bg_path, protected_labels=[]):
        '''
        Create a Scaper object.

        Parameters
        ----------
        duration : float
            Duration of the soundscape, in seconds.
        fg_path : str
            Path to foreground folder.
        bg_path : str
            Path to background folder.
        protected_labels : list
            Provide a list of protected foreground labels. When a foreground
            label is in the protected list it means that when a sound event
            matching the label gets added to a soundscape instantiation the
            duration of the source audio file cannot be altered, and the
            duration value that was provided in the specification will be
            ignored.

            Adding labels to the protected list is useful for sound events
            whose semantic validity would be lost if the sound were trimmed
            before the sound event ends, for example an animal vocalization
            such as a dog bark.

        '''
        # Duration must be a positive real number
        if np.isrealobj(duration) and duration > 0:
            self.duration = duration
        else:
            raise ScaperError('Duration must be a positive real value')

        # Initialize parameters
        self.sr = 44100
        self.ref_db = -12
        self.n_channels = 1
        self.fade_in_len = 0.01  # 10 ms
        self.fade_out_len = 0.01  # 10 ms

        # Start with empty specifications
        self.fg_spec = []
        self.bg_spec = []

        # Validate paths and set
        expanded_fg_path = os.path.expanduser(fg_path)
        expanded_bg_path = os.path.expanduser(bg_path)
        _validate_folder_path(expanded_fg_path)
        _validate_folder_path(expanded_bg_path)
        self.fg_path = expanded_fg_path
        self.bg_path = expanded_bg_path

        # Populate label lists from folder paths
        self.fg_labels = []
        self.bg_labels = []
        _populate_label_list(self.fg_path, self.fg_labels)
        _populate_label_list(self.bg_path, self.bg_labels)

        # Copy list of protected labels
        self.protected_labels = protected_labels[:]

    def add_background(self, label, source_file, source_time):
        '''
        Add a background recording to the background specification.

        The background duration will be equal to the duration of the
        soundscape ``Scaper.duration`` specified when initializing the Scaper
        object. If the source file is shorter than this duration then it will
        be concatenated to itself as many times as necessary to produce the
        specified duration when calling ``Scaper.generate``.

        Parameters
        ----------
        label : tuple
            Specifies the label of the background. See Notes below for the
            expected format of this tuple and the allowed values.
            NOTE: The label specified by this tuple must match one
            of the labels in the Scaper's background label list
            ``Scaper.bg_labels``. Furthermore, if ``source_file`` is
            specified using "const" (see Notes), then ``label`` must also be
            specified using "const" and its ``value `` (see Notes) must
            match the source file's parent folder's name.
        source_file : tuple
            Specifies the audio file to use as the source. See Notes below for
            the expected format of this tuple and the allowed values.
            NOTE: If ``source_file`` is specified using "const" (see Notes),
            then ``label`` must also be specified using "const" and its
            ``value`` (see Notes) must match the source file's parent
            folder's name.
        source_time : tuple
            Specifies the desired start time in the source file. See Notes
            below for the expected format of this tuple and the allowed values.
            NOTE: the source time specified by this tuple should be equal to or
            smaller than ``<source file duration> - <soundscape duration>``.
            Larger values will be automatically changed to fulfill this
            requirement when calling ``Scaper.generate``.

        Notes
        -----
        Each parameter of this function is set by passing a distribution
        tuple, whose first item is always the distribution name and subsequent
        items are distribution specific. The supported distribution tuples are:
            * ``("const", value)`` : a constant, given by ``value``.
            * ``("choose", valuelist)`` : choose a value from
              ``valuelist`` at random (uniformly). The ``label`` and
              ``source_file`` parameters also support providing an empty
              ``valuelist`` i.e. ``("choose", [])``, in which case the
              value will be chosen at random from all available labels or files
              as determined automatically by Scaper by examining the file
              structure of ``bg_path`` provided during initialization.
            * ``("uniform", min_value, max_value)`` : sample a random
              value from a uniform distribution between ``min_value``
              and ``max_value``.
            * ``("normal", mean, stddev)`` : sample a random value from a
              normal distribution defined by its mean ``mean`` and
              standard deviation ``stddev``.

        IMPORTANT: not all parameters support all distribution tuples. In
        particular, ``label`` and ``source_file`` only support ``"const"`` and
        ``"choose"``, whereas ``source_time`` supports all distribution tuples.
        As noted above, only ``label`` and ``source_file`` support providing an
        empty ``valuelist`` with ``"choose"``.

        '''
        # These values are fixed for the background sound
        event_time = ("const", 0)
        event_duration = ("const", self.duration)
        snr = ("const", 0)
        role = 'background'
        pitch_shift = None
        time_stretch = None

        # Validate parameter format and values
        _validate_event(label, source_file, source_time, event_time,
                        event_duration, snr, self.bg_labels, None, None)

        # Create background sound event
        bg_event = EventSpec(label=label,
                             source_file=source_file,
                             source_time=source_time,
                             event_time=event_time,
                             event_duration=event_duration,
                             snr=snr,
                             role=role,
                             pitch_shift=pitch_shift,
                             time_stretch=time_stretch)

        # Add event to background spec
        self.bg_spec.append(bg_event)

    def add_event(self, label, source_file, source_time, event_time,
                  event_duration, snr, pitch_shift, time_stretch):
        '''
        Add a foreground sound event to the foreground specification.

        Parameters
        ----------
        label : tuple
            Specifies the label of the sound event. See Notes below for the
            expected format of this tuple and the allowed values.
            NOTE: The label specified by this tuple must match one
            of the labels in the Scaper's foreground label list
            ``Scaper.fg_labels``. Furthermore, if ``source_file`` is
            specified using "const" (see Notes), then ``label`` must also be
            specified using "const" and its ``value `` (see Notes) must
            match the source file's parent folder's name.
        source_file : tuple
            Specifies the audio file to use as the source. See Notes below for
            the expected format of this tuple and the allowed values.
            NOTE: If ``source_file`` is specified using "const" (see Notes),
            then ``label`` must also be specified using "const" and its
            ``value`` (see Notes) must match the source file's parent
            folder's name.
        source_time : tuple
            Specifies the desired start time in the source file. See Notes
            below for the expected format of this tuple and the allowed values.
            NOTE: the source time specified by this tuple should be equal to or
            smaller than ``<source file duration> - event_duration``. Larger
            values will be automatically changed to fulfill this requirement
            when calling ``Scaper.generate``.
        event_time : tuple
            Specifies the desired start time of the event in the soundscape.
            See Notes below for the expected format of this tuple and the
            allowed values.
            NOTE: The value specified by this tuple should be equal to or
            smaller than ``<soundscapes duration> - event_duration``, and
            larger values will be automatically changed to fulfill this
            requirement when calling ``Scaper.generate``.
        event_duration : tuple
            Specifies the desired duration of the event. See Notes below for
            the expected format of this tuple and the allowed values.
            NOTE: The value specified by this tuple should be equal to or
            smaller than the source file's duration, and larger values will be
            automatically changed to fulfill this requirement when calling
            ``Scaper.generate``.
        snr : tuple
            Specifies the desired signal to noise ratio (SNR) between the event
            and the background. See Notes below for the expected format of
            this tuple and the allowed values.
        pitch_shift : tuple
            Specifies the number of semitones to shift the event by. None means
            no pitch shift.
        time_stretch: tuple
            Specifies the time stretch factor (value>1 will make it slower and
            longer, value<1 will makes it faster and shorter).

        Notes
        -----
        Each parameter of this function is set by passing a distribution
        tuple, whose first item is always the distribution name and subsequent
        items are distribution specific. The supported distribution tuples are:
            * ``("const", value)`` : a constant, given by ``value``.
            * ``("choose", valuelist)`` : choose a value from
              ``valuelist`` at random (uniformly). The ``label`` and
              ``source_file`` parameters also support providing an empty
              ``valuelist`` i.e. ``("choose", [])``, in which case the
              value will be chosen at random from all available labels or
              source files as determined automatically by Scaper by examining
              the file structure of ``fg_path`` provided during
              initialization.
            * ``("uniform", min_value, max_value)`` : sample a random
              value from a uniform distribution between ``min_value``
              and ``max_value`` (including ``max_value``).
            * ``("normal", mean, stddev)`` : sample a random value from a
              normal distribution defined by its mean ``mean`` and
              standard deviation ``stddev``.

        IMPORTANT: not all parameters support all distribution tuples. In
        particular, ``label`` and ``source_file`` only support ``"const"`` and
        ``"choose"``, whereas the remaining parameters support all distribution
        tuples. As noted above, only ``label`` and ``source_file`` support
        providing an empty ``valuelist`` with ``"choose"``.

        See Also
        --------
        _validate_event : Check that event parameter values are valid.

        Scaper.generate : Generate a soundscape based on the current
        specification and save to disk as both an audio file and a JAMS file
        describing the soundscape.

        '''

        # SAFETY CHECKS
        _validate_event(label, source_file, source_time, event_time,
                        event_duration, snr, self.fg_labels, pitch_shift,
                        time_stretch)

        # Create event
        event = EventSpec(label=label,
                          source_file=source_file,
                          source_time=source_time,
                          event_time=event_time,
                          event_duration=event_duration,
                          snr=snr,
                          role='foreground',
                          pitch_shift=pitch_shift,
                          time_stretch=time_stretch)

        # Add event to foreground specification
        self.fg_spec.append(event)

    def _instantiate_event(self, event, isbackground=False,
                           allow_repeated_label=True,
                           allow_repeated_source=True,
                           used_labels=[],
                           used_source_files=[],
                           disable_instantiation_warnings=False):
        '''
        Instantiate an event specification.

        Given an event specification containing distribution tuples,
        instantiate the event, i.e. samples values for the label, source_file,
        source_time, event_time, event_duration and snr from their respective
        distribution tuples, and return the sampled values in as a new event
        specification.

        Parameters
        ----------
        event : EventSpec
            Event specification containing distribution tuples.
        isbackground : bool
            Flag indicating whether the event to instantiate is a background
            event or not (False implies it is a foreground event).
        allow_repeated_label : bool
            When True (default) any label can be used, including a label that
            has already been used for another event. When False, only a label
            that is not already in ``used_labels`` can be selected.
        allow_repeated_source : bool
            When True (default) any source file matching the selected label can
            be used, including a source file that has already been used for
            another event. When False, only a source file that is not already
            in ``used_source_files`` can be selected.
        used_labels : list
            List labels that have already been used in the current soundscape
            instantiation. The label selected for instantiating the event will
            be appended to this list unless its already in it.
        used_source_files : list
            List of full paths to source files that have already been used in
            the current soundscape instantiation. The source file selected for
            instantiating the event will be appended to this list unless its
            already in it.
        disable_instantiation_warnings : bool
            When True (default is False), warnings stemming from event
            instantiation (primarily about automatic duration adjustments) are
            disabled. Not recommended other than for testing purposes.

        Returns
        -------
        instantiated_event : EventSpec
            Event specification containing values sampled from the distribution
            tuples of the input event specification.

        Raises
        ------
        ScaperError
            If allow_repeated_source is False and there is no valid source file
            to select.

        '''
        # set paths and labels depending on whether its a foreground/background
        # event
        if isbackground:
            file_path = self.bg_path
            allowed_labels = self.bg_labels
        else:
            file_path = self.fg_path
            allowed_labels = self.fg_labels

        # determine label
        if event.label[0] == "choose" and not event.label[1]:
            label_tuple = list(event.label)
            label_tuple[1] = allowed_labels
            label_tuple = tuple(label_tuple)
        else:
            label_tuple = event.label
        label = _get_value_from_dist(label_tuple)

        # Make sure we can use this label
        if (not allow_repeated_label) and (label in used_labels):
            if (len(allowed_labels) == len(used_labels) or
                    label_tuple[0] == "const"):
                raise ScaperError(
                    "Cannot instantiate event {:s}: all available labels "
                    "have already been used and "
                    "allow_repeated_label=False.".format(label))
            else:
                while label in used_labels:
                    label = _get_value_from_dist(label_tuple)

        # Update the used labels list
        if label not in used_labels:
            used_labels.append(label)

        # determine source file
        if event.source_file[0] == "choose" and not event.source_file[1]:
            source_files = _get_sorted_files(
                os.path.join(file_path, label))
            source_file_tuple = list(event.source_file)
            source_file_tuple[1] = source_files
            source_file_tuple = tuple(source_file_tuple)
        else:
            source_file_tuple = event.source_file

        source_file = _get_value_from_dist(source_file_tuple)

        # Make sure we can use this source file
        if (not allow_repeated_source) and (source_file in used_source_files):
            source_files = _get_sorted_files(os.path.join(file_path, label))
            if (len(source_files) == len(used_source_files) or
                    source_file_tuple[0] == "const"):
                raise ScaperError(
                    "Cannot instantiate event {:s}: all available source "
                    "files have already been used and "
                    "allow_repeated_source=False.".format(label))
            else:
                while source_file in used_source_files:
                    source_file = _get_value_from_dist(source_file_tuple)

        # Update the used source files list
        if source_file not in used_source_files:
            used_source_files.append(source_file)

        # Get the duration of the source audio file
        source_duration = sox.file_info.duration(source_file)

        # If the foreground event's label is in the protected list, use the
        # source file's duration without modification.
        if label in self.protected_labels:
            event_duration = source_duration
        else:
            # determine event duration
            # For background events the duration is fixed to self.duration
            # (which must be > 0), but for foreground events it could
            # potentially be non-positive, hence the loop.
            event_duration = -np.Inf
            while event_duration <= 0:
                event_duration = _get_value_from_dist(event.event_duration)

        # Check if chosen event duration is longer than the duration of the
        # selected source file, if so adjust the event duration.
        if (event_duration > source_duration):
            old_duration = event_duration  # for warning
            event_duration = source_duration
            if not disable_instantiation_warnings:
                warnings.warn(
                    "{:s} event duration ({:.2f}) is greater that source "
                    "duration ({:.2f}), changing to {:.2f}".format(
                        label, old_duration, source_duration, event_duration),
                    ScaperWarning)

        # Get time stretch value
        if event.time_stretch is None:
            time_stretch = None
            event_duration_stretched = event_duration
        else:
            time_stretch = -np.Inf
            while time_stretch <= 0:
                time_stretch = _get_value_from_dist(event.time_stretch)
            # compute duration after stretching
            event_duration_stretched = event_duration * time_stretch

        # If the event duration is longer than the soundscape we can trim it
        # without losing validity (since the event will end when the soundscape
        # ends).
        if time_stretch is None:
            if (event_duration > self.duration):
                old_duration = event_duration  # for warning
                event_duration = self.duration
                if not disable_instantiation_warnings:
                    warnings.warn(
                        "{:s} event duration ({:.2f}) is greater than the "
                        "soundscape duration ({:.2f}), changing to "
                        "{:.2f}".format(
                            label, old_duration, self.duration, self.duration),
                        ScaperWarning)
        else:
            if (event_duration_stretched > self.duration):
                old_duration = event_duration  # for warning
                event_duration = self.duration / float(time_stretch)
                if not disable_instantiation_warnings:
                    warnings.warn(
                        "{:s} event duration ({:.2f}) with stretch factor "
                        "{:.2f} gives {:.2f} which is greater than the "
                        "soundscape duration ({:.2f}), changing to "
                        "{:.2f}".format(
                            label, old_duration, time_stretch,
                            event_duration_stretched, self.duration,
                            event_duration),
                        ScaperWarning)

        # determine source time
        source_time = -np.Inf
        while source_time < 0:
            source_time = _get_value_from_dist(event.source_time)

        # Make sure source time + event duration is not greater than the
        # source duration, if it is, adjust the source time (i.e. duration
        # takes precedences over start time).
        if source_time + event_duration > source_duration:
            old_source_time = source_time
            source_time = source_duration - event_duration
            if not disable_instantiation_warnings:
                warnings.warn(
                    '{:s} source time ({:.2f}) is too great given event '
                    'duration ({:.2f}) and source duration ({:.2f}), changed '
                    'to {:.2f}.'.format(
                        label, old_source_time, event_duration,
                        source_duration, source_time),
                    ScaperWarning)

        # determine event time
        # for background events the event time is fixed to 0, but for
        # foreground events it's not.
        event_time = -np.Inf
        while event_time < 0:
            event_time = _get_value_from_dist(event.event_time)

        # Make sure the selected event time + event duration are is not greater
        # than the total duration of the soundscape, if it is adjust the event
        # time. This means event duration takes precedence over the event
        # start time.
        if time_stretch is None:
            if event_time + event_duration > self.duration:
                old_event_time = event_time
                event_time = self.duration - event_duration
                if not disable_instantiation_warnings:
                    warnings.warn(
                        '{:s} event time ({:.2f}) is too great given event '
                        'duration ({:.2f}) and soundscape duration ({:.2f}), '
                        'changed to {:.2f}.'.format(
                            label, old_event_time, event_duration,
                            self.duration, event_time),
                        ScaperWarning)
        else:
            if event_time + event_duration_stretched > self.duration:
                old_event_time = event_time
                event_time = self.duration - event_duration_stretched
                if not disable_instantiation_warnings:
                    warnings.warn(
                        '{:s} event time ({:.2f}) is too great given '
                        'stretched event duration ({:.2f}) and soundscape '
                        'duration ({:.2f}), changed to {:.2f}.'.format(
                            label, old_event_time, event_duration_stretched,
                            self.duration, event_time),
                        ScaperWarning)

        # determine snr
        snr = _get_value_from_dist(event.snr)

        # get role (which can only take "foreground" or "background" and
        # is set internally, not by the user).
        role = event.role

        # determine pitch_shift
        if event.pitch_shift is not None:
            pitch_shift = _get_value_from_dist(event.pitch_shift)
        else:
            pitch_shift = None

        # pack up instantiated values in an EventSpec
        instantiated_event = EventSpec(label=label,
                                       source_file=source_file,
                                       source_time=source_time,
                                       event_time=event_time,
                                       event_duration=event_duration,
                                       snr=snr,
                                       role=role,
                                       pitch_shift=pitch_shift,
                                       time_stretch=time_stretch)
        # Return
        return instantiated_event

    def _instantiate(self, allow_repeated_label=True,
                     allow_repeated_source=True, reverb=None,
                     disable_instantiation_warnings=False):
        '''
        Instantiate a specific soundscape in JAMS format based on the current
        specification.

        Any non-deterministic event values (i.e. distribution tuples) will be
        sampled randomly from based on the distribution parameters.

        Parameters
        ----------
        allow_repeated_label : bool
            When True (default) the same label can be used more than once
            in a soundscape instantiation. When False every label can
            only be used once.
        allow_repeated_source : bool
            When True (default) the same source file can be used more than once
            in a soundscape instantiation. When False every source file can
            only be used once.
        reverb : float or None
            Has no effect on this function other than being documented in the
            instantiated annotation's sandbox. Passed by ``Scaper.generate``.
        disable_instantiation_warnings : bool
            When True (default is False), warnings stemming from event
            instantiation (primarily about automatic duration adjustments) are
            disabled. Not recommended other than for testing purposes.

        Returns
        -------
        jam : JAMS object
            A JAMS object containing a sound_event annotation representing the
            instantiated soundscape.

        See Also
        --------
        Scaper.generate

        '''
        jam = jams.JAMS()
        ann = jams.Annotation(namespace='sound_event')

        # Set annotation duration (might be changed later due to cropping)
        ann.duration = self.duration

        # INSTANTIATE BACKGROUND AND FOREGROUND EVENTS AND ADD TO ANNOTATION
        # NOTE: logic for instantiating bg and fg events is NOT the same.

        # Add background sounds
        bg_labels = []
        bg_source_files = []
        for event in self.bg_spec:
            value = self._instantiate_event(
                event,
                isbackground=True,
                allow_repeated_label=allow_repeated_label,
                allow_repeated_source=allow_repeated_source,
                used_labels=bg_labels,
                used_source_files=bg_source_files,
                disable_instantiation_warnings=disable_instantiation_warnings)

            # Note: add_background doesn't allow to set a time_stretch, i.e.
            # it's hardcoded to time_stretch=None, so we don't need to check
            # if value.time_stretch is not None, since it always will be.
            ann.append(time=value.event_time,
                       duration=value.event_duration,
                       value=value._asdict(),
                       confidence=1.0)

        # Add foreground events
        fg_labels = []
        fg_source_files = []
        for event in self.fg_spec:
            value = self._instantiate_event(
                event,
                isbackground=False,
                allow_repeated_label=allow_repeated_label,
                allow_repeated_source=allow_repeated_source,
                used_labels=fg_labels,
                used_source_files=fg_source_files,
                disable_instantiation_warnings=disable_instantiation_warnings)

            if value.time_stretch is not None:
                event_duration_stretched = (
                    value.event_duration * value.time_stretch)
            else:
                event_duration_stretched = value.event_duration

            ann.append(time=value.event_time,
                       duration=event_duration_stretched,
                       value=value._asdict(),
                       confidence=1.0)

        # Compute max polyphony
        poly = max_polyphony(ann)

        # Compute the number of foreground events
        n_events = len(self.fg_spec)

        # Compute gini
        gini = polyphony_gini(ann)

        # Add specs and other info to sandbox
        ann.sandbox.scaper = jams.Sandbox(
            duration=self.duration,
            fg_path=self.fg_path,
            bg_path=self.bg_path,
            fg_spec=self.fg_spec,
            bg_spec=self.bg_spec,
            fg_labels=self.fg_labels,
            bg_labels=self.bg_labels,
            protected_labels=self.protected_labels,
            ref_db=self.ref_db,
            n_channels=self.n_channels,
            fade_in_len=self.fade_in_len,
            fade_out_len=self.fade_out_len,
            n_events=n_events,
            polyphony_max=poly,
            polyphony_gini=gini,
            allow_repeated_label=allow_repeated_label,
            allow_repeated_source=allow_repeated_source,
            reverb=reverb)

        # Add annotation to jams
        jam.annotations.append(ann)

        # Set jam metadata
        jam.file_metadata.duration = ann.duration

        # Return
        return jam

    def _generate_audio(self, audio_path, ann, reverb=None,
                        disable_sox_warnings=True):
        '''
        Generate audio based on a sound_event annotation and save to disk.

        Parameters
        ----------
        audio_path : str
            Path for saving soundscape audio file.
        ann : jams.Annotation
            Annotation of the sound_event namespace.
        reverb : float or None
            Amount of reverb to apply to the generated soundscape between 0
            (no reverberation) and 1 (maximum reverberation). Use None
            (default) to prevent the soundscape from going through the reverb
            module at all.
        disable_sox_warnings : bool
            When True (default), warnings from the pysox module are suppressed
            unless their level is ``'CRITICAL'``.

        Raises
        ------
        ScaperError
            If annotation is not of the sound_event namespace.

        See Also
        --------
        Scaper.generate

        '''
        if ann.namespace != 'sound_event':
            raise ScaperError(
                'Annotation namespace must be sound_event, found: {:s}'.format(
                    ann.namespace))

        # disable sox warnings
        if disable_sox_warnings:
            temp_logging_level = 'CRITICAL'  # only critical messages please
        else:
            temp_logging_level = logging.getLogger().level

        with _set_temp_logging_level(temp_logging_level):

            # Array for storing all tmp files (one for every event)
            tmpfiles = []
            with _close_temp_files(tmpfiles):

                for event in ann.data.iterrows():

                    # first item is index, second is event dictionary
                    e = event[1]

                    if e.value['role'] == 'background':
                        # Concatenate background if necessary. Right now we
                        # always concatenate the background at least once,
                        # since the pysox combiner raises an error if you try
                        # to call build using an input_file_list with less than
                        # 2 elements. In the future if the combiner is updated
                        # to accept a list of length 1, then the max(..., 2)
                        # statement can be removed from the calculation of
                        # ntiles.
                        source_duration = (
                            sox.file_info.duration(e.value['source_file']))
                        ntiles = int(
                            max(self.duration // source_duration + 1, 2))

                        # Create combiner
                        cmb = sox.Combiner()
                        # Ensure consistent sampling rate and channels
                        cmb.convert(samplerate=self.sr,
                                    n_channels=self.n_channels,
                                    bitdepth=None)
                        # Then trim
                        cmb.trim(e.value['source_time'],
                                 e.value['source_time'] +
                                 e.value['event_duration'])

                        # After trimming, normalize background to reference DB.
                        bg_lufs = get_integrated_lufs(e.value['source_file'])
                        gain = self.ref_db - bg_lufs
                        cmb.gain(gain_db=gain, normalize=False)

                        # Prepare tmp file for output
                        tmpfiles.append(
                            tempfile.NamedTemporaryFile(
                                suffix='.wav', delete=True))

                        cmb.build(
                            [e.value['source_file']] * ntiles,
                            tmpfiles[-1].name, 'concatenate')

                    elif e.value['role'] == 'foreground':
                        # Create transformer
                        tfm = sox.Transformer()
                        # Ensure consistent sampling rate and channels
                        tfm.convert(samplerate=self.sr,
                                    n_channels=self.n_channels,
                                    bitdepth=None)
                        # Trim
                        tfm.trim(e.value['source_time'],
                                 e.value['source_time'] +
                                 e.value['event_duration'])

                        # Pitch shift
                        if e.value['pitch_shift'] is not None:
                            tfm.pitch(e.value['pitch_shift'])

                        # Time stretch
                        if e.value['time_stretch'] is not None:
                            tfm.tempo(1.0 / float(e.value['time_stretch']))

                        # Apply very short fade in and out
                        # (avoid unnatural sound onsets/offsets)
                        tfm.fade(fade_in_len=self.fade_in_len,
                                 fade_out_len=self.fade_out_len)

                        # Normalize to specified SNR with respect to
                        # self.ref_db
                        fg_lufs = get_integrated_lufs(e.value['source_file'])
                        gain = self.ref_db + e.value['snr'] - fg_lufs
                        tfm.gain(gain_db=gain, normalize=False)

                        # Pad with silence before/after event to match the
                        # soundscape duration
                        prepad = e.value['event_time']
                        if e.value['time_stretch'] is None:
                            postpad = max(
                                0, self.duration - (e.value['event_time'] +
                                                    e.value['event_duration']))
                        else:
                            postpad = max(
                                0, self.duration - (e.value['event_time'] +
                                                    e.value['event_duration'] *
                                                    e.value['time_stretch']))
                        tfm.pad(prepad, postpad)

                        # Finally save result to a tmp file
                        tmpfiles.append(
                            tempfile.NamedTemporaryFile(
                                suffix='.wav', delete=True))
                        tfm.build(e.value['source_file'], tmpfiles[-1].name)

                    else:
                        raise ScaperError(
                            'Unsupported event role: {:s}'.format(
                                e.value['role']))

                # Finally combine all the files and optionally apply reverb
                # If we have more than one tempfile (i.e.g background + at
                # least one foreground event, we need a combiner. If there's
                # only the background track, then we need a transformer!
                if len(tmpfiles) == 0:
                    warnings.warn(
                        "No events to synthesize (silent soundscape), no audio "
                        "saved to disk.", ScaperWarning)
                elif len(tmpfiles) == 1:
                    tfm = sox.Transformer()
                    if reverb is not None:
                        tfm.reverb(reverberance=reverb * 100)
                    # TODO: do we want to normalize the final output?
                    tfm.build(tmpfiles[0].name, audio_path)
                else:
                    cmb = sox.Combiner()
                    if reverb is not None:
                        cmb.reverb(reverberance=reverb * 100)
                    # TODO: do we want to normalize the final output?
                    cmb.build([t.name for t in tmpfiles], audio_path, 'mix')

    def generate(self, audio_path, jams_path, allow_repeated_label=True,
                 allow_repeated_source=True,
                 reverb=None, disable_sox_warnings=True, no_audio=False,
                 txt_path=None, disable_instantiation_warnings=False):
        '''
        Generate a soundscape based on the current specification and save to
        disk as both an audio file and a JAMS file describing the soundscape.

        Parameters
        ----------
        audio_path : str
            Path for saving soundscape audio
        jams_path : str
            Path for saving soundscape jams
        allow_repeated_label : bool
            When True (default) the same label can be used more than once
            in a soundscape instantiation. When False every label can
            only be used once.
        allow_repeated_source : bool
            When True (default) the same source file can be used more than once
            in a soundscape instantiation. When False every source file can
            only be used once.
        reverb : float or None
            Amount of reverb to apply to the generated soundscape between 0
            (no reverberation) and 1 (maximum reverberation). Use None
            (default) to prevent the soundscape from going through the reverb
            module at all.
        disable_sox_warnings : bool
            When True (default), warnings from the pysox module are suppressed
            unless their level is ``'CRITICAL'``.
        no_audio : bool
            If true only generates a JAMS file and no audio is saved to disk.
        txt_path: str or None
            If not None, in addition to the JAMS file output a simplified
            annotation in a space separated format [onset  offset  label],
            saved to the provided path (good for loading labels in audacity).
        disable_instantiation_warnings : bool
            When True (default is False), warnings stemming from event
            instantiation (primarily about automatic duration adjustments) are
            disabled. Not recommended other than for testing purposes.

        Raises
        ------
        ScaperError
            If the reverb parameter is passed an invalid value.

        See Also
        --------
        Scaper._instantiate
        Scaper._generate_audio

        '''
        # Check parameter validity
        if reverb is not None:
            if not (0 <= reverb <= 1):
                raise ScaperError(
                    'Invalid value for reverb: must be in range [0, 1] or '
                    'None.')

        # Create specific instance of a soundscape based on the spec
        jam = self._instantiate(
            allow_repeated_label=allow_repeated_label,
            allow_repeated_source=allow_repeated_source,
            reverb=reverb,
            disable_instantiation_warnings=disable_instantiation_warnings)
        ann = jam.annotations.search(namespace='sound_event')[0]

        # Generate the audio and save to disk
        if not no_audio:
            self._generate_audio(audio_path, ann, reverb=reverb,
                                 disable_sox_warnings=disable_sox_warnings)

        # Finally save JAMS to disk too
        jam.save(jams_path)

        # Optionally save to CSV as well
        if txt_path is not None:

            df = pd.DataFrame(columns=['onset', 'offset', 'label'])

            for idx, row in ann.data.iterrows():
                if row.value['role'] == 'foreground':
                    newrow = ([row.time.total_seconds(),
                               row.time.total_seconds() +
                               row.duration.total_seconds(),
                               row.value['label']])
                    df.loc[len(df)] = newrow

            # sort events by onset time
            df = df.sort_values('onset')
            df.reset_index(inplace=True, drop=True)
            df.to_csv(txt_path, index=False, header=False, sep=' ')
