"""
Tools for generating auditory stimuli. 
"""
from __future__ import division
import numpy as np
import scipy
import scipy.io.wavfile
import resampy


def create(type, **kwds):
    """ Create a Sound instance using a key returned by Sound.key().
    """
    cls = globals()[type]
    return cls(**kwds)


class Sound(object):
    """
    Base class for all sound stimulus generators.
    """

    def __init__(self, duration, rate=100e3, **kwds):
        """
        Parameters
        ----------
        duration: float (no default):
            duration of the stimulus, in seconds
        
        rate : float (default: 100000.)
            sample rate for sound generation
        
        """
        self.opts = {"rate": rate, "duration": duration}
        self.opts.update(kwds)
        self._time = None
        self._sound = None

    @property
    def sound(self):
        """ 
        :obj:`array`: The generated sound array, expressed in Pascals.
        """
        if self._sound is None:
            self._sound = self.generate()
        return self._sound

    @property
    def time(self):
        """
        :obj:`array`: The array of time values, expressed in seconds.
        """
        if self._time is None:
            self._time = np.linspace(0, self.opts["duration"], self.num_samples)
        return self._time

    @property
    def num_samples(self):
        """ 
        int: The number of samples in the sound array. 
        """
        return 1 + int(self.opts["duration"] * self.opts["rate"])

    @property
    def dt(self):
        """
        float: the sample period (time step between samples).
        """
        return 1.0 / self.opts["rate"]

    @property
    def duration(self):
        """
        float: The duration of the sound
        """
        return self.opts["duration"]

    def key(self):
        """ 
        The sound can be recreated using ``create(**key)``.
        :obj:`dict`: Return dict of parameters needed to completely describe this sound.
        """
        k = self.opts.copy()
        k["type"] = self.__class__.__name__
        return k

    def measure_dbspl(self, tstart, tend):
        """ 
        Measure the sound pressure for the waveform in a window of time
        
        Parameters
        ----------
        tstart :
            time to start spl measurement (seconds).
        
        tend :
            ending time for spl measurement (seconds).
        
        Returns
        -------
        float : The measured amplitude (dBSPL) of the sound from tstart to tend

        """
        istart = int(tstart * self.opts["rate"])
        iend = int(tend * self.opts["rate"])
        return pa_to_dbspl(self.sound[istart:iend].std())

    def generate(self):
        """
        Generate and return the sound output. This method is defined by subclasses.
        """
        raise NotImplementedError()

    def __getattr__(self, name):
        if "opts" not in self.__dict__:
            raise AttributeError(name)
        if name in self.opts:
            return self.opts[name]
        else:
            return object.__getattr__(self, name)


class TonePip(Sound):
    """ Create one or more tone pips with cosine-ramped edges.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sound
    f0 : float or array-like
        Tone frequency in Hz. Must be less than half of the sample rate.
    dbspl : float
        Maximum amplitude of tone in dB SPL. 
    pip_duration : float
        Duration of each pip including ramp time. Must be at least 
        2 * ramp_duration.
    pip_start : array-like
        Start times of each pip
    ramp_duration : float
        Duration of a single ramp period (from minimum to maximum). 
        This may not be more than half of pip_duration.
    
    """

    def __init__(self, **kwds):
        reqdWords = [
            "rate",
            "duration",
            "f0",
            "dbspl",
            "pip_duration",
            "pip_start",
            "ramp_duration",
        ]
        for k in reqdWords:
            if k not in kwds.keys():
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["f0"] > kwds["rate"] * 0.5:
            raise ValueError("f0 must be less than (0.5 * rate).")
        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to compute the tone pips
        
        Returns
        -------
        array : 
            generated waveform
        
        """
        o = self.opts
        return piptone(
            self.time,
            o["ramp_duration"],
            o["rate"],
            o["f0"],
            o["dbspl"],
            o["pip_duration"],
            o["pip_start"],
        )


class FMSweep(Sound):
    """ Create an FM sweep with either linear or logarithmic rates, 
    of a specified duration between two frequencies.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sweep
    start : float
        t times of each pip
    freqs : list 
        [f0, f1]: the start and stop times for the sweep
    ramp : string
        valid input for type of sweep (linear, logarithmic, etc)
    dbspl : float
        Maximum amplitude of pip in dB SPL.
    """

    def __init__(self, **kwds):
        for k in ["rate", "duration", "start", "freqs", "ramp", "dbspl"]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to actually compute the the FM sweep
                
        Returns
        -------
        array : 
            generated waveform
        
        """
        o = self.opts
        return fmsweep(
            self.time, o["start"], o["duration"], o["freqs"], o["ramp"], o["dbspl"]
        )


class NoisePip(Sound):
    """ One or more noise pips with cosine-ramped edges.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sound
    seed : int >= 0
        Random seed
    dbspl : float
        Maximum amplitude of tone in dB SPL. 
    pip_duration : float
        Duration of each pip including ramp time. Must be at least 
        2 * ramp_duration.
    pip_start : array-like
        Start times of each pip
    ramp_duration : float
        Duration of a single ramp period (from minimum to maximum). 
        This may not be more than half of pip_duration.
        
    """

    def __init__(self, **kwds):
        for k in [
            "rate",
            "duration",
            "dbspl",
            "pip_duration",
            "pip_start",
            "ramp_duration",
            "seed",
        ]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["seed"] < 0:
            raise ValueError("Random seed must be integer > 0")

        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to compute the noise pips
        
        Returns
        -------
        array : 
            generated waveform
        
        """
        o = self.opts
        return pipnoise(
            self.time,
            o["ramp_duration"],
            o["rate"],
            o["dbspl"],
            o["pip_duration"],
            o["pip_start"],
            o["seed"],
        )


class ClickTrain(Sound):
    """ One or more clicks (rectangular pulses).
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    dbspl : float
        Maximum amplitude of click in dB SPL. 
    click_duration : float
        Duration of each click including ramp time. Must be at least 
        1/rate.
    click_starts : array-like
        Start times of each click
    """

    def __init__(self, **kwds):
        for k in ["rate", "duration", "dbspl", "click_duration", "click_starts"]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["click_duration"] < 1.0 / kwds["rate"]:
            raise ValueError("click_duration must be greater than sample rate.")

        Sound.__init__(self, **kwds)

    def generate(self):
        o = self.opts
        return clicks(
            self.time, o["rate"], o["dbspl"], o["click_duration"], o["click_starts"]
        )


class SAMNoise(Sound):
    """ One or more gaussian noise pips with cosine-ramped edges.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sound
    seed : int >= 0
        Random seed
    dbspl : float
        Maximum amplitude of pip in dB SPL. 
    pip_duration : float
        Duration of each pip including ramp time. Must be at least 
        2 * ramp_duration.
    pip_start : array-like
        Start times of each pip
    ramp_duration : float
        Duration of a single ramp period (from minimum to maximum). 
        This may not be more than half of pip_duration.
    fmod : float
        SAM modulation frequency
    dmod : float
        Modulation depth
    """

    def __init__(self, **kwds):
        parms = [
            "rate",
            "duration",
            "seed",
            "pip_duration",
            "pip_start",
            "ramp_duration",
            "fmod",
            "dmod",
            "seed",
        ]
        for k in parms:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["seed"] < 0:
            raise ValueError("Random seed must be integer > 0")

        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to compute the SAM noise
        
        Returns
        -------
        array : 
            generated waveform
        
        """
        o = self.opts
        o["phaseshift"] = 0.0
        return modnoise(
            self.time,
            o["ramp_duration"],
            o["rate"],
            o["f0"],
            o["pip_duration"],
            o["pip_start"],
            o["dbspl"],
            o["fmod"],
            o["dmod"],
            0.0,
            o["seed"],
        )


class SAMTone(Sound):
    """ SAM tones with cosine-ramped edges.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sound
    f0 : float or array-like
        Tone frequency in Hz. Must be less than half of the sample rate.
    dbspl : float
        Maximum amplitude of tone in dB SPL. 
    pip_duration : float
        Duration of each pip including ramp time. Must be at least 
        2 * ramp_duration.
    pip_start : array-like
        Start times of each pip
    ramp_duration : float
        Duration of a single ramp period (from minimum to maximum). 
        This may not be more than half of pip_duration.
    fmod : float
        SAM modulation frequency, Hz
    dmod : float
        Modulation depth, %
        
    """

    def __init__(self, **kwds):

        for k in [
            "rate",
            "duration",
            "f0",
            "dbspl",
            "pip_duration",
            "pip_start",
            "ramp_duration",
            "fmod",
            "dmod",
        ]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["f0"] > kwds["rate"] * 0.5:
            raise ValueError("f0 must be less than (0.5 * rate).")

        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to compute a SAM tone
        
        Returns
        -------
        array : 
            generated waveform
        
        """
        o = self.opts
        basetone = piptone(
            self.time,
            o["ramp_duration"],
            o["rate"],
            o["f0"],
            o["dbspl"],
            o["pip_duration"],
            o["pip_start"],
        )
        return sinusoidal_modulation(
            self.time, basetone, o["pip_start"], o["fmod"], o["dmod"], 0.0
        )


def pa_to_dbspl(pa, ref=20e-6):
    """ Convert Pascals (rms) to dBSPL. By default, the reference pressure is
    20 uPa.
    """
    return 20 * np.log10(pa / ref)


def dbspl_to_pa(dbspl, ref=20e-6):
    """ Convert dBSPL to Pascals (rms). By default, the reference pressure is
    20 uPa.
    """
    return ref * 10 ** (dbspl / 20.0)


class SAMNoise(Sound):
    """ One or more gaussian noise pips with cosine-ramped edges, sinusoidally modulated.
    
    Parameters
    ----------
    rate : float
        Sample rate in Hz
    duration : float
        Total duration of the sound
    seed : int >= 0
        Random seed
    dbspl : float
        Maximum amplitude of pip in dB SPL. 
    pip_duration : float
        Duration of each pip including ramp time. Must be at least 
        2 * ramp_duration.
    pip_start : array-like
        Start times of each pip
    ramp_duration : float
        Duration of a single ramp period (from minimum to maximum). 
        This may not be more than half of pip_duration.
    fmod : float
        SAM modulation frequency
    dmod : float
        Modulation depth
    
    Returns
    -------
    array :
        waveform
    
    """

    def __init__(self, **kwds):
        for k in [
            "rate",
            "duration",
            "seed",
            "pip_duration",
            "pip_start",
            "ramp_duration",
            "fmod",
            "dmod",
        ]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["seed"] < 0:
            raise ValueError("Random seed must be integer > 0")

        Sound.__init__(self, **kwds)

    def generate(self):
        o = self.opts
        basenoise = pipnoise(
            self.time,
            o["ramp_duration"],
            o["rate"],
            o["dbspl"],
            o["pip_duration"],
            o["pip_start"],
            o["seed"],
        )
        return sinusoidal_modulation(
            self.time, basenoise, o["pip_start"], o["fmod"], o["dmod"], 0.0
        )


class ComodulationMasking(Sound):
    """
    Make a stimulus for comodulation masking release.
    Note the parameter names are shortened so that the SGC generated filename
    in cnmodel fits within system limits.
    
    Parameters
    ----------
    rate : float
        sample rate, in Hz
    
    dur : float
        entire waveform duration in seconds
    
    pipst : float
        time to start the test tone pips (seconds)
        array, such as [0.25, 0.35, 0.45]
    
    pipdu : float
        duration of the test (target) tone pips

    maskst : float
        time to start the masker tones pips (seconds)
        array, such as [0.1]
    
    maskdu : float
        duration of the masker tone pips

    rf : float
        rise/fall of the pips 
        
    f0 : float (kHz)
        Center frequency for the target tone, in kHz
    
    db : float
        on-target masker and flankinb band intensity
        In dB SPL (re 0.00002 dynes/cm2)

    s2n : float
        signal re masker, in dbspl
    
    fmod : float
        amplitude modulation frequency, in Hz
    
    dmod : float
        amplitude modulation depth, in %
   
    fltype : string
        Flanking type:
        One of:
            'MultiTone' : multiple tones, with phase, spacing and # of bands specified as below
            'NBNoise' : the flanking stimulus is made up of narrow band noises (not implemented)
            'None' : no flanking sounds (just on-target stimuli)
    
    flspc : float
        Spacing of flanking bands in octaves from the center frequency, f0
    
    flgap : int
        gap around the cf in flspc (e.g., 1 would skip the first adjacent band)
    
    flph : string
        One of:
            'Comodulated': all of the flanking tones are comodulated in phase 
                with the target at the same frequency and depth
            'Codgh' : 'Grose and Hall codeviant':
                The flanking bands have the same amplitude and frequency
                as the target, but the phase of each band is different. Phases are
                calculated so that all the bands wrap around 2*pi
            'Codvw' : 'Verhey and Winter codeviant':
                The flanking bands have the same amplitude and frequency
                as the target, but are out of phase with the on-frequency masker
            'Random': The phases are selected at random. This is probably best only
                used when there are a large number of flanking bands.
    
    flspl : float
        Flanking signal, in dbspl. 
    
    flN : int
        Number of flanking bands on either side of f0, spaced accordingly.
    
    Returns
    -------
    array :
        waveform
    
    """

    def __init__(self, **kwds):
        # print (kwds)
        for k in [
            "rate",
            "duration",
            "pipdu",
            "pipst",
            "rf",
            "maskst",
            "maskdu",
            # general:
            "f0",
            "dbspl",
            "s2n",
            "fmod",
            "dmod",
            # flankers:
            "flgap",
            "fltype",
            "flspc",
            "flph",
            "flN",
        ]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if "flspl" not in kwds:
            kwds["flspl"] = kwds["dbspl"]
        # if 'mask_spl' not in kwds:
        #      kwds['mask_spl'] = kwds['dbspl']
        # if kwds['mask_spl'] is None:
        #     kwds['mask_spl'] = 0.
        if kwds["flspl"] is None:
            raise ValueError()

        Sound.__init__(self, **kwds)

    def generate(self):

        o = self.opts
        # start with center tone
        onfreqmasker = piptone(
            self.time, o["rf"], o["rate"], o["f0"], o["flspl"], o["maskdu"], o["maskst"]
        )
        tardelay = (
            0.0
        )  # 1.5/o['fmod']  # delay by one and one half cycles (no target in first dip)
        target = piptone(
            self.time,
            o["rf"],
            o["rate"],
            o["f0"],
            o["dbspl"] + o["s2n"],
            o["pipdu"] - tardelay,
            [p + tardelay for p in o["pipst"]],
        )
        # target = shape_signal(onfreqmasker, self.time, o['rf'], o['rate'], o['f0'],
        #                o['dbspl']+o['s2n'], o['pipdu']-tardelay, [p + tardelay for p in o['pipst']])
        if (o["dbspl"] + o["s2n"]) <= 0.0:
            target = np.zeros_like(target)
        onfreqmasker = sinusoidal_modulation(
            self.time, onfreqmasker, o["maskst"], o["fmod"], o["dmod"], 0.0
        )
        # print("o['dbspl']+o['s2n']: ", o['dbspl']+o['s2n'])
        # print('np.max(target): ', np.max(target))

        # target = sinusoidal_modulation(self.time, target, [p + tardelay for p in o['pip_start']],
        #                o['fmod'], o['dmod'], 0.)
        self.onmask = onfreqmasker
        self.target = target
        if o["fltype"] not in ["None", "Ref", "NBN", "Tone"]:
            raise ValueError("Unknown flanking_type: %s" % o["fltype"])
        if o["fltype"] in ["NBN"]:
            raise ValueError('Flanking type "NBNoise" is not yet implemented')
        elif o["fltype"] in ["None"]:
            return (onfreqmasker + target) / 2.0  # scaling...
        elif o["fltype"] in ["Tone"]:
            nband = int(o["flN"])
            gap = int(o["flgap"])
            octspace = o["flspc"]
            f0 = o["f0"]
            flankfs = [f0 * (2 ** (octspace * (k + 1 + gap))) for k in range(nband)]
            flankfs.extend(
                [f0 / ((2 ** (octspace * (k + 1 + gap)))) for k in range(nband)]
            )
            flankfs = sorted(flankfs)
            flanktone = [[]] * len(flankfs)
            for i, fs in enumerate(flankfs):
                flanktone[i] = piptone(
                    self.time,
                    o["rf"],
                    o["rate"],
                    flankfs[i],
                    o["flspl"],
                    o["maskdu"],
                    o["maskst"],
                )
        elif o["fltype"] in ["None", "Ref"]:
            return (onfreqmasker + target) / 2.0  # scaling...
        if o["fltype"] == "NBN":
            raise ValueError("Flanking type nbnoise not yet implemented")

        ph = 0.0
        if o["flph"] == "Comod":
            ph = np.zeros(len(flankfs))
        elif o["flph"] == "Codvw":  # verhey and winter: just 180 out of phase
            ph = np.pi * np.ones(len(flankfs))
        elif o["flph"] in ["Codgh", "Codev"]:  # with precession: Grose and Hall 89
            ph = 2.0 * np.pi * np.arange(-o["flN"], o["flN"] + 1, 1) / o["flN"]
        elif o["flph"] == "Random":
            ph = 2.0 * np.pi * np.arange(-o["flN"], o["flN"] + 1, 1) / o["flN"]
            raise ValueError("Random flanking phases not implemented")
        else:
            raise ValueError(
                "Masker Phase pattern of type %s is not implemented" % o["flph"]
            )
        # print(('flanking phases: ', ph))
        # print (len(flanktone))
        # print(('flanking freqs: ', flankfs))
        for i, fs in enumerate(flankfs):
            flanktone[i] = sinusoidal_modulation(
                self.time, flanktone[i], o["maskst"], o["fmod"], o["dmod"], ph[i]
            )
            if i == 0:
                maskers = flanktone[i]
            else:
                maskers = maskers + flanktone[i]
        signal = (onfreqmasker + maskers + target) / (o["flN"] + 2)
        return signal


class DynamicRipple(Sound):
    def __init__(self, **kwds):
        for k in ["rate", "duration"]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        # if kwds['pip_duration'] < kwds['ramp_duration'] * 2:
        #     raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        import DMR

        self.dmr = DMR.DMR()
        Sound.__init__(self, **kwds)

    def generate(self):
        """
        Call to compute a dynamic ripple stimulus
        
        Returns
        -------
        array :
           
           generated waveform
        """
        o = self.opts
        self.dmr.set_params(Fs=o["rate"], duration=o["duration"] + 1.0 / o["rate"])
        self.dmr.make_waveform()
        self._time = self.dmr.vTime  # get time from the generator, not linspace
        return self.dmr.vStim


class SpeechShapedNoise(Sound):
    """
    Adapted from http://www.srmathias.com/speech-shaped-noise/
    """

    def __init__(self, **kwds):
        for k in ["rate", "duration", "waveform", "samplingrate"]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        # if kwds['pip_duration'] < kwds['ramp_duration'] * 2:
        #     raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        Sound.__init__(self, **kwds)

    def generate(self):
        o = self.opts
        print("opts: ", o)
        ssn, t = make_ssn(
            o["rate"], o["duration"], o["waveform"].sound, o["samplingrate"]
        )
        self._time = t  # override time array because we read a wave file
        # if self.opts['duration'] == 0:
        #     self.opts['duration'] = np.max(t) - 1./o['rate']
        return ssn


class RandomSpectrumShape(Sound):
    """
    Random Spectral Shape stimuli
    log-spaced tones
    Amplitudes adjusted in groups of 4 or 8 (amp_group_size)
    Amplitude SD (amp_sd)
    Frequency range (octaves above and below f0) (octaves)
    spacing (fraction of octave: e.g, 1/8 or 1/64 as 8 or 64) (spacing)
    
    Generates one sample
    
    Young and Calhoun, 2005
    Yu and Young, 2000
    """

    def __init__(self, **kwds):
        for k in [
            "rate",
            "duration",
            "f0",
            "dbspl",
            "pip_duration",
            "pip_start",
            "ramp_duration",
            "amp_group_size",
            "amp_sd",
            "spacing",
            "octaves",
        ]:
            if k not in kwds:
                raise TypeError("Missing required argument '%s'" % k)
        if kwds["pip_duration"] < kwds["ramp_duration"] * 2:
            raise ValueError("pip_duration must be greater than (2 * ramp_duration).")
        if kwds["f0"] > kwds["rate"] * 0.5:
            raise ValueError("f0 must be less than (0.5 * rate).")

        Sound.__init__(self, **kwds)

    def generate(self):
        o = self.opts
        octaves = o["octaves"]
        lowf = o["f0"] / octaves
        highf = o["f0"] * octaves
        freqlist = np.logspace(
            np.log2(lowf),
            np.log2(highf),
            num=o["spacing"] * octaves * 2,
            endpoint=True,
            base=2,
        )
        amplist = np.zeros_like(freqlist)
        db = o["dbspl"]
        # assign amplitudes
        if db == None:
            db = 80.0
        groupsize = o["amp_group_size"]
        for i in range(0, len(freqlist), groupsize):
            if o["amp_sd"] > 0.0:
                a = np.random.normal(scale=o["amp_sd"])
            else:
                a = 0.0
            amplist[i : i + groupsize] = 20.0 * np.log10(a + db)
        for i in range(len(freqlist)):
            wave = piptone(
                self.time,
                o["ramp_duration"],
                o["rate"],
                freqlist[i],
                amplist[i],
                o["pip_duration"],
                o["pip_start"],
                pip_phase=np.pi * 2.0 * np.random.rand(),
            )
            if i == 0:
                result = wave
            else:
                result = result + wave
        return result / (np.sqrt(np.mean(result ** 2.0)))  # scale by rms level


class ReadWavefile(Sound):
    """ Read a .wav file from disk, possibly converting the sample rate and the scale
    for use in driving the auditory nerve fiber model.
    
    Parameters
    ----------
    wavefile : str
        name of the .wav file to read
    rate : float
        Sample rate in Hz (waveform will be resampled to this rate)
    channel: int (default: 0)
        If wavefile has 2 channels, select 0 or 1 for the channel to read
    dbspl : float or None
        If specified, the wave file is scaled such that its overall dBSPL
        (measured from RMS of the entire waveform) is equal to this value.
        Either ``dbspl`` or ``scale`` must be specified.
    scale : float or None
        If specified, the wave data is multiplied by this value to yield values in dBSPL. 
        Either ``dbspl`` or ``scale`` must be specified.
    delay: float (default: 0.)
        Silent delay time to start sound, in s. Allows anmodel and cells to run to steady-state
    maxdur : float or None (default: None)
        If specified, maxdur defines the total duration of generated waveform to return (in seconds).
        If None, the generated waveform duration will be the sum of any delay value and
        the duration of the waveform from the wavefile.
    
    Returns
    -------
    array :
        waveform
    
    """

    def __init__(
        self, wavefile, rate, channel=0, dbspl=None, scale=None, delay=0.0, maxdur=None
    ):
        if dbspl is not None and scale is not None:
            raise ValueError('Only one of "dbspl" or "scale" can be set')
        duration = (
            0.0
        )  # forced because of the way num_samples has to be calculated first
        if delay < 0.0:
            raise ValueError("ReadWavefile: delay must be > 0., got: %f" % delay)
        if maxdur is not None and maxdur < 0:
            raise ValueError(
                "ReadWavefile: maxdur must be None or > 0., got: %f" % maxdur
            )
        Sound.__init__(
            self,
            duration,
            rate,
            wavefile=wavefile,
            channel=channel,
            dbspl=dbspl,
            scale=scale,
            maxdur=maxdur,
            delay=delay,
        )

    def generate(self):
        """
        Read the wave file from disk, clip duration, resample if necessary, and scale
        
        Returns
        -------
        array :   generated waveform
        """
        [fs_wav, stimulus] = scipy.io.wavfile.read(
            self.opts["wavefile"]
        )  # raw is a numpy array of integer, representing the samples
        if len(stimulus.shape) > 1 and stimulus.shape[1] > 0:
            stimulus = stimulus[:, self.opts["channel"]]  # just use selected channel
        fs_wav = float(fs_wav)
        maxdur = self.opts["maxdur"]
        delay = self.opts["delay"]
        delay_array = np.zeros(
            int(delay * fs_wav)
        )  # build delay array (may have 0 length)
        if maxdur is None:
            maxdur = delay + len(stimulus) / fs_wav  # true total length
        maxpts = int(maxdur * fs_wav)
        stimulus = np.hstack((delay_array, stimulus))[:maxpts]

        if self.opts["rate"] != fs_wav:
            stimulus = resampy.resample(stimulus, fs_wav, self.opts["rate"])
        self.opts["duration"] = (stimulus.shape[0] - 1) / self.opts[
            "rate"
        ]  # compute the duration, match for linspace calculation used in time.
        self._time = None
        self.time  # requesting time should cause recalulation of the time
        if self.opts["dbspl"] is not None:
            rms = np.sqrt(np.mean(stimulus ** 2.0))  # find rms of the waveform
            stimulus = (
                dbspl_to_pa(self.opts["dbspl"]) * stimulus / rms
            )  # scale into Pascals
        if self.opts["scale"] is not None:
            stimulus = stimulus * self.opts["scale"]
        return stimulus


def sinusoidal_modulation(t, basestim, tstart, fmod, dmod, phaseshift):
    """
    Impose a sinusoidal amplitude-modulation on the input waveform.
    For dmod=100%, the envelope max is 2, the min is 0; for dmod = 0, the max and min are 1
    maintains equal energy for all modulation depths.
    Equation from Rhode and Greenberg, J. Neurophys, 1994 (adding missing parenthesis) and
    Sayles et al. J. Physiol. 2013
    The envelope can be phase shifted (useful for co-deviant stimuli).
    
    Parameters
    ----------
    t : array
        array of waveform time values (seconds)
    basestim : array
        array of waveform values that will be subject to sinulsoidal envelope modulation
    tstart : float
        time at which the base sound starts (modulation starts then, with 0 phase crossing)
        (seconds)
    fmod : float
        modulation frequency (Hz)
    dmod : float
        modulation depth (percent)
    phaseshift : float
        modulation phase shift (starting phase, radians)
    
    """

    env = 1.0 + (dmod / 100.0) * np.sin(
        (2.0 * np.pi * fmod * (t - tstart)) + phaseshift - np.pi / 2
    )  # envelope...
    return basestim * env


def modnoise(t, rt, Fs, F0, dur, start, dBSPL, FMod, DMod, phaseshift, seed):
    """
    Generate an amplitude-modulated noise with linear ramps.
    
    Parameters
    ----------
    t : array
        array of waveform time values
    rt : float
        ramp duration
    Fs : float
        sample rate
    F0 : float
        tone frequency
    dur : float
        duration of noise
    start : float
        start time for noise
    dBSPL : float
        sound pressure of stimulus
    FMod : float
        modulation frequency
    DMod : float
        modulation depth percent
    phaseshift : float
        modulation phase
    seed : int
        seed for random number generator
    
    Returns
    -------
    array :
        waveform
    
    """
    irpts = int(rt * Fs)
    mxpts = len(t) + 1
    pin = pipnoise(t, rt, Fs, dBSPL, dur, start, seed)
    env = 1 + (DMod / 100.0) * np.sin(
        (2 * np.pi * FMod * t) - np.pi / 2 + phaseshift
    )  # envelope...

    pin = linearramp(pin, mxpts, irpts)
    env = linearramp(env, mxpts, irpts)
    return pin * env


def linearramp(pin, mxpts, irpts):
    """
    Apply linear ramps to *pin*.
    
    Parameters
    ----------
    pin : array
        input waveform to apply ramp to

    mxpts : int
        point in array to start ramp down
    
    irpts : int
        duration of the ramp

    Returns
    -------
    array :
        waveform
    
    
    Original (adapted from Manis; makeANF_CF_RI.m)::
    
        function [out] = ramp(pin, mxpts, irpts)
            out = pin;
            out(1:irpts)=pin(1:irpts).*(0:(irpts-1))/irpts;
            out((mxpts-irpts):mxpts)=pin((mxpts-irpts):mxpts).*(irpts:-1:0)/irpts;
            return;
        end
    """
    out = pin.copy()
    r = np.linspace(0, 1, irpts)
    irpts = int(irpts)
    # print 'irpts: ', irpts
    # print len(out)
    out[:irpts] = out[:irpts] * r
    # print  out[mxpts-irpts:mxpts].shape
    # print r[::-1].shape
    out[mxpts - irpts - 1 : mxpts] = out[mxpts - irpts - 1 : mxpts] * r[::-1]
    return out


def pipnoise(t, rt, Fs, dBSPL, pip_dur, pip_start, seed):
    """
    Create a waveform with multiple sine-ramped noise pips. Output is in 
    Pascals.
    
    Parameters
    ----------
    t : array
        array of time values
    rt : float
        ramp duration 
    Fs : float
        sample rate
    dBSPL : float
        maximum sound pressure level of pip
    pip_dur : float
        duration of pip including ramps
    pip_start : float
        list of starting times for multiple pips
    seed : int
        random seed

    Returns
    -------
    array :
        waveform

    """
    rng = np.random.RandomState(seed)
    pin = np.zeros(t.size)
    for start in pip_start:
        # make pip template
        pip_pts = int(pip_dur * Fs) + 1
        pip = dbspl_to_pa(dBSPL) * rng.randn(pip_pts)  # unramped stimulus

        # add ramp
        ramp_pts = int(rt * Fs) + 1
        ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2
        pip[:ramp_pts] *= ramp
        pip[-ramp_pts:] *= ramp[::-1]

        ts = int(np.floor(start * Fs))
        pin[ts : ts + pip.size] += pip

    return pin


def piptone(t, rt, Fs, F0, dBSPL, pip_dur, pip_start):
    """
    Create a waveform with multiple sine-ramped tone pips. Output is in 
    Pascals.
    
    Parameters
    ----------
    t : array
        array of time values
    rt : float
        ramp duration 
    Fs : float
        sample rate
    F0 : float
        pip frequency
    dBSPL : float
        maximum sound pressure level of pip
    pip_dur : float
        duration of pip including ramps
    pip_start : float
        list of starting times for multiple pips

    Returns
    -------
    array :
        waveform

    """
    # make pip template
    pip_pts = int(pip_dur * Fs) + 1
    pip_t = np.linspace(0, pip_dur, pip_pts)
    pip = (
        np.sqrt(2) * dbspl_to_pa(dBSPL) * np.sin(2 * np.pi * F0 * pip_t)
    )  # unramped stimulus

    # add ramp
    ramp_pts = int(rt * Fs) + 1
    ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2
    pip[:ramp_pts] *= ramp
    pip[-ramp_pts:] *= ramp[::-1]

    # apply template to waveform
    pin = np.zeros(t.size)
    ps = pip_start
    if ~isinstance(ps, list):
        ps = [ps]
    for start in pip_start:
        ts = int(np.floor(start * Fs))
        pin[ts : ts + pip.size] += pip

    return pin


def shape_signal(signal, t, rt, Fs, F0, dBSPL, pip_dur, pip_start):
    """
    Create a waveform with multiple sine-ramped tone pips, based on the 
    signal (so output is *always* in phase with the reference signal waveform)
    Output is in Pascals.
    
    Parameters
    ----------
    t : array
        array of time values
    rt : float
        ramp duration (risetime)
    Fs : float
        sample rate
    F0 : float
        pip frequency
    dBSPL : float
        maximum sound pressure level of pip
    pip_dur : float
        duration of pip including ramps
    pip_start : float
        list of starting times for multiple pips

    Returns
    -------
    array :
        waveform

    """
    pip_t = t
    pip_pts = pip_t.shape[0]
    pip = np.sqrt(2) * dbspl_to_pa(dBSPL) * signal  # referencestimulus

    # make envelope with cos2 rise-fall
    ramp_pts = int(rt * Fs) + 1
    env_pts = int(pip_dur * Fs)
    envelope = np.ones(env_pts)
    ramp = np.sin(np.linspace(0, np.pi / 2.0, ramp_pts)) ** 2
    envelope[:ramp_pts] *= ramp
    envelope[-ramp_pts:] *= ramp[::-1]

    # apply envelope template to waveform
    pin = np.zeros(t.size)
    ps = pip_start
    if ~isinstance(ps, list):
        ps = [ps]
    for start in pip_start:
        ts = int(np.floor(start * Fs))
        pin[ts : ts + envelope.size] += pip[ts : ts + envelope.size] * envelope

    return pin


def clicks(t, Fs, dBSPL, click_duration, click_starts):
    """
    Create a waveform with multiple retangular clicks. Output is in 
    Pascals.
    
    Parameters
    ----------
    t : array
        array of time values
    Fs : float
        sample frequency (Hz)
    click_start : float (seconds)
        delay to first click in train 
    click_duration : float (seconds)
        duration of each click
    click_interval : float (seconds)
        interval between click starts
    nclicks : int
        number of clicks in the click train
    dspl : float
        maximum sound pressure level of pip

    Returns
    -------
    array :
        waveform

    """
    swave = np.zeros(t.size)
    amp = dbspl_to_pa(dBSPL)
    td = int(np.floor(click_duration * Fs))
    nclicks = len(click_starts)
    for n in range(nclicks):
        t0s = click_starts[n]  # time for nth click
        t0 = int(np.floor(t0s * Fs))  # index
        if t0 + td > t.size:
            raise ValueError("Clicks: train duration exceeds waveform duration")
        swave[t0 : t0 + td] = amp
    return swave


def fmsweep(t, start, duration, freqs, ramp, dBSPL):
    """
    Create a waveform for an FM sweep over time. Output is in 
    Pascals.

    Parameters
    ----------
    t : array
        time array for waveform 
    start : float (seconds)
        start time for sweep
    duration : float (seconds)
        duration of sweep
    freqs : array (Hz)
        Two-element array specifying the start and end frequency of the sweep
    ramp : str
        The shape of time course of the sweep (linear, logarithmic)
    dBSPL : float
        maximum sound pressure level of sweep
    
    Returns
    -------
    array :
        waveform
    
    
    """

    sw = scipy.signal.chirp(
        t, freqs[0], duration, freqs[1], method=ramp, phi=0, vertex_zero=True
    )
    sw = np.sqrt(2) * dbspl_to_pa(dBSPL) * sw
    return sw


def make_ssn(rate, duration, sig, samplingrate):
    """
        Speech-shaped noise
        Adapted from http://www.srmathias.com/speech-shaped-noise/
        Created on Thu Jun 26 12:42:08 2014
        @author: smathias
        """
    # note rate is currently ignored...
    sig = np.array(sig).astype("float64")
    if (
        rate != samplingrate
    ):  # interpolate to the current system sampling rate from the original rate
        sig = np.interp(
            np.arange(0, len(sig) / rate, 1.0 / rate),
            np.arange(0, len(sig) / samplingrate),
            1.0 / samplingrate,
        )
    sig = 2 * sig / np.max(sig)
    z, t = noise_from_signal(sig, rate, keep_env=True)
    return z, t


def noise_from_signal(x, fs=40000, keep_env=True):
    """Create a noise with same spectrum as the input signal.
    Parameters
    ----------
    x : array_like
        Input signal.
    fs : int
         Sampling frequency of the input signal. (Default value = 40000)
    keep_env : bool
         Apply the envelope of the original signal to the noise. (Default
         value = False)
    Returns
    -------
    ndarray
        Noise signal.
    """
    x = np.asarray(x)
    n_x = x.shape[-1]
    n_fft = next_pow_2(n_x)
    X = np.fft.rfft(x, next_pow_2(n_fft))
    # Randomize phase.
    noise_mag = np.abs(X) * np.exp(2.0 * np.pi * 1j * np.random.random(X.shape[-1]))
    noise = np.real(np.fft.irfft(noise_mag, n_fft))
    out = noise[:n_x]
    if keep_env:
        env = np.abs(scipy.signal.hilbert(x))
        [bb, aa] = scipy.signal.butter(6.0, 50.0 / (fs / 2.0))  # 50 Hz LP filter
        env = scipy.signal.filtfilt(bb, aa, env)
        out *= env
    t = np.arange(0, (len(out)) / fs, 1.0 / fs)
    return out, t
