You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1416 lines
40 KiB
1416 lines
40 KiB
2 years ago
|
"""
|
||
|
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
|