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.
1415 lines
40 KiB
1415 lines
40 KiB
""" |
|
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
|
|
|