Attention to speech modulates distortion product otoacoustic emissions evoked by speech-derived stimuli in humans.
The 4 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
- [1] § Materials and methods › Stimuli for eliciting speech-like DPOAEs ↔ stimulus_generation.py, lines 124–148 · score 0.75 · bandpass filter, standard deviations, fundamental waveform, fundamental frequency, signal, Stimuli
- [2] § Materials and methods › Stimuli for eliciting speech-like DPOAEs ↔ stimulus_generation.py, lines 150–183 · score 0.70 · Hilbert transform, harmonic overtones, fundamental waveform, Stimuli
- [3] § Materials and methods › Analysis of speech-like DPOAEs ↔ DPOAE_analysis.py, lines 664–728 · score 0.66 · imaginary part, complex cross correlation, microphone recording, envelope, waveforms, DPOAEs
- [4] § Materials and methods › Analysis of speech-like DPOAEs ↔ cross_correlation.py, the whole file · a weak match · score 0.56 · imaginary part, complex cross correlation, envelope, signal
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 661 lines · 30 KB · MIT · 2 matches
- import numpy as np
- import librosa
- from pathlib import Path
- import os
- import re
- import soundfile as sf
- import pandas as pd
- import scipy.signal as sig
- import matplotlib.pyplot as plt
- from collections.abc import Iterable
- from plot_stimulus_correlations import stimuli_correlation
- from butterworth_filter import butter_filter
- class stimulus_generator:
- """Generates stimuli for speech-DPOAE measurements. Stimuli follow the specified harmonic numbers n1 and n2.
- Distortion product is calculated as 2*n1 - n2 and can be changed via distortion_product method.
- Parameters
- ----------
- n1 : list or int
- The lower harmonic number(s) for the stimuli.
- n2 : list or int
- The upper harmonic number(s) for the stimuli.
- voice : str
- The voice type of the source audio, setting the fundamental frequency range.
- Can either be 'male' or 'female'.
- source_dir : str or Path
- The directory containing the source audio files.
- target_dir : str or Path
- The directory where the generated stimuli will be saved.
- fs : int, optional
- The sampling frequency of the audio files, by default 44100.
- """
- def __init__(self, n1, n2, voice, source_dir, target_dir, fs=44100):
- self.fs = fs
- self.n1 = np.array(n1)
- self.n2 = np.array(n2)
- if len(self.n1) != len(self.n2):
- raise ValueError("n1 and n2 must have the same length.")
- self.dp = self.distortion_product(self.n1, self.n2)
- self.source_dir = Path(source_dir)
- self.target_dir = Path(target_dir)
- if not os.path.exists(self.target_dir):
- os.makedirs(self.target_dir)
- self.voice = voice
- if self.voice == 'female':
- self.f0_min, self.f0_max = 80, 300
- elif self.voice == 'male':
- self.f0_min, self.f0_max = 50, 200
- else:
- raise ValueError('Unknown voice.')
- #####################################################################
- def distortion_product(self, n1, n2):
- """Calculate the distortion product based on the provided harmonic numbers."""
- if isinstance(n1, Iterable) and isinstance(n2, Iterable):
- return 2 * np.array(n1) - np.array(n2)
- else:
- return 2 * n1 - n2
- def generate_sine_wave(self, frequency, duration, nrm=0.01):
- """Generate a sine wave of a given frequency and duration."""
- t = np.linspace(0, duration, int(self.fs * duration), endpoint=False)
- wave = nrm * np.sin(2 * np.pi * frequency * t)
- return wave
- def save_wave(self, wave, filename):
- """Save the generated waveform to a file."""
- if not os.path.exists(os.path.dirname(filename)):
- os.makedirs(os.path.dirname(filename))
- sf.write(filename, wave, self.fs)
- def zscore_norm(self, data):
- """Normalize the data using z-score normalization."""
- mean = np.mean(data)
- std = np.std(data)
- return (data - mean) / std
- ###################################################################
- def load_source_audio(self):
- """Load all audio files from the source directory."""
- self.source_files = sorted(list(self.source_dir.glob('*.wav')))
- if not self.source_files:
- raise FileNotFoundError("No .wav files found in the source directory.")
- return
- def determine_f0(self, audio_array, harm):
- """Estimate the fundamental frequency (f0) of the audio signal using the pyin algorithm.
- Parameters
- ----------
- audio_array : np.ndarray
- The audio signal from which to estimate the f0.
- harm : int
- The harmonic number for which to estimate the f0.
- Returns
- -------
- f0_mean : float
- The mean of the estimated f0 values.
- f0_std : float
- The standard deviation of the estimated f0 values.
- """
- # Parameters for pyin algorithm
- fr_length = 4000
- win_length = 3300
- hop_length = 1250
- # Estimate the fundamental frequency (f0)
- f0, voiced_flag, _ = librosa.pyin(audio_array, sr=self.fs, fmin=(harm*self.f0_min), fmax=(harm*self.f0_max), frame_length=fr_length, win_length=win_length, hop_length=hop_length)
- # Calculate mean and standard deviation of f0
- f0_mean = np.mean(f0[voiced_flag])
- f0_std = np.std(f0[voiced_flag])
- return f0_mean, f0_std
- #######################################################################
- def gen_fundamental_waveform(self, x, f0_mean, f0_std):
- """Generate the fundamental waveform of the audio signal x by bandpass filtering around the fundamental frequency of the signal.
- Filter bandwidth is defined as f0_mean +/- f0_std/2.
- Parameters
- ----------
- x : np.ndarray
- The audio signal from which to generate the fundamental waveform.
- f0_mean : float
- The mean of the estimated fundamental frequency.
- f0_std : float
- The standard deviation of the estimated fundamental frequency.
- Returns
- -------
- fund_wf : np.ndarray
- The generated fundamental waveform.
- """
- lowcut = np.round(f0_mean - f0_std/2, 1)
- highcut = np.round(f0_mean + f0_std/2, 1)
- fund_wf = butter_filter(x, [lowcut, highcut], 'bandpass', fs=self.fs)
- fund_wf = self.zscore_norm(fund_wf)
- return fund_wf
- def gen_stimuli_hilbert(self, n, fund_wf, nrm=0.01):
- """Generate the stimulus waveform for the nth harmonic overtone using the Hilbert transform of the fundamental waveform.
- Parameters
- ----------
- n : int
- The harmonic number for which to generate the stimulus waveform.
- fund_wf : np.ndarray
- The fundamental waveform of the audio signal.
- nrm : float, optional
- The normalization factor for the generated waveform, by default 0.01.
- Returns
- -------
- stim : np.ndarray
- The generated stimulus waveform for the nth harmonic overtone.
- """
- fund_wf_orig = fund_wf.copy()
- # remove nans from fund_wf and replace with zeros
- fund_wf = np.nan_to_num(fund_wf_orig)
- # as the hilbert transform expects a periodic signal, concatenate the waveform with itself
- fund_wf = np.concatenate((fund_wf[::-1], fund_wf))
- # apply the hilbert transform to get the amplitude and phase
- wf_hilbert = sig.hilbert(fund_wf)
- amp = np.abs(wf_hilbert[len(fund_wf_orig):])
- phase = np.unwrap(np.angle(wf_hilbert[len(fund_wf_orig):])) # halbe Länge, weil wir ja concatenated hatten
- stim = nrm * np.nan_to_num(amp * np.cos(phase * n))
- stim = stim - np.mean(stim) # remove DC offset
- return stim
- ####################################################################
- def create_pure_tone_stimuli(self, f1, f2, duration=120):
- """Creates pure tone stimuli based on the specified frequencies.
- Parameters
- ----------
- f1 : float
- The frequency of the first pure tone in Hz.
- f2 : float
- The frequency of the second pure tone in Hz.
- duration : float, optional
- The duration of the stimuli in seconds, by default 120 seconds.
- """
- pt_dp = self.distortion_product(f1, f2)
- stim1 = self.generate_sine_wave(f1, duration)
- stim2 = self.generate_sine_wave(f2, duration)
- stimdp = self.generate_sine_wave(pt_dp, duration)
- stereo_stim = np.vstack((stim1, stim2)).T
- self.save_wave(stereo_stim, self.target_dir.parent / f'stereo_pt.wav')
- self.save_wave(stimdp, self.target_dir.parent / f'dp_pt.wav')
- return
- def create_stimuli(self):
- """Create stimuli based on the source audio files and the specified harmonic numbers."""
- # Load source audio files
- self.load_source_audio()
- for audio_file in self.source_files:
- audio_name = audio_file.stem
- audio_array, _ = librosa.load(audio_file, sr=self.fs)
- audio_array = audio_array / np.max(np.abs(audio_array)) # normalize audio array
- #audio_array = audio_array - np.mean(audio_array) # remove DC offset
- stim_columns = ['filename', 'f0_mean', 'f0_std', 'harm', 'harm_freq [Hz]', 'harm_std [Hz]']
- csv_stimuli = []
- f0_mean, f0_std = self.determine_f0(audio_array, 1)
- # Generate the fundamental waveform
- fund_wf = self.gen_fundamental_waveform(audio_array, f0_mean, f0_std)
- # Generate and save stimulus waveforms
- for harms in [self.n1, self.n2, self.dp]:
- if isinstance(harms, Iterable):
- for n in harms:
- wf = self.gen_stimuli_hilbert(n, fund_wf)
- freq_stim, std_stim = self.determine_f0(wf, n)
- filename = f"{self.target_dir}/mono_stimuli/{audio_name}_n{n}.wav"
- self.save_wave(wf, filename)
- csv_stimuli.append([audio_name, f0_mean, f0_std, n, freq_stim, std_stim])
- else:
- n = harms
- wf = self.gen_stimuli_hilbert(n, fund_wf)
- freq_stim, std_stim = self.determine_f0(wf, n)
- filename = f"{self.target_dir}/mono_stimuli/{audio_name}_n{n}.wav" # das passt noch nicht
- self.save_wave(wf, filename)
- csv_stimuli.append([audio_name, f0_mean, f0_std, n, freq_stim, std_stim])
- self.determine_stimulus_correlations(audio_name)
- csv_stimuli.append(['-' * 10, '-' * 10, '-' * 10, '-' * 10, '-' * 10, '-' * 10]) # separator for different audio files
- csv_stimuli_df = pd.DataFrame(csv_stimuli, columns=stim_columns)
- csv_path = self.target_dir / f'stimuli_info.csv'
- write_header = not os.path.exists(csv_path)
- csv_stimuli_df.to_csv(csv_path, mode='a', header=write_header, index=False)
- def determine_stimulus_correlations(self, audio_name):
- """Determine the correlations between the generated stimuli and the distortion product waveform."""
- sig_columns = ['filename', 'n1', 'n2', 'dp', 'sig_peak']
- csv_correlations = []
- stim_path = self.target_dir / 'mono_stimuli/'
- plot_path = self.target_dir / 'mono_stimuli/Plots/'
- # determine stimulus correlations
- if isinstance(self.n1, Iterable):
- for n1, n2, dp in zip(self.n1, self.n2, self.dp):
- stim_n1 = librosa.load(stim_path / f'{audio_name}_n{n1}.wav', sr=self.fs)[0]
- stim_n2 = librosa.load(stim_path / f'{audio_name}_n{n2}.wav', sr=self.fs)[0]
- stim_dp = librosa.load(stim_path / f'{audio_name}_n{dp}.wav', sr=self.fs)[0]
- sig_peak = stimuli_correlation(dp, stim_dp, stim_n1, stim_n2, filename=plot_path / f"corr_{audio_name}_n{n1}n{n2}_dp{dp}.png", fs=self.fs)
- csv_correlations.append([audio_name, n1, n2, dp, sig_peak])
- else:
- # if n1, n2, dp are not lists, just use them directly
- stim_n1 = librosa.load(stim_path / f'{audio_name}_n{self.n1}.wav', sr=self.fs)[0]
- stim_n2 = librosa.load(stim_path / f'{audio_name}_n{self.n2}.wav', sr=self.fs)[0]
- stim_dp = librosa.load(stim_path / f'{audio_name}_n{self.dp}.wav', sr=self.fs)[0]
- sig_peak = stimuli_correlation(dp, stim_dp, stim_n1, stim_n2, filename=plot_path / f"corr_{audio_name}_n{self.n1}n{self.n2}_dp{self.dp}.png", fs=self.fs)
- csv_correlations.append([audio_name, self.n1, self.n2, self.dp, sig_peak])
- sig_df = pd.DataFrame(csv_correlations, columns=sig_columns)
- csv_path = self.target_dir / f'stimuli_correlations.csv'
- write_header = not os.path.exists(csv_path)
- sig_df.to_csv(csv_path, mode='a', header=write_header, index=False)
- return sig_peak
- def create_stereo_stimuli(self):
- """Create stereo stimuli by stacking the mono stimuli."""
- mono_stim_path = self.target_dir / 'mono_stimuli/'
- out_path = self.target_dir / 'stereo_stimuli/'
- self.load_source_audio()
- for audio_file in self.source_files:
- audio_name = audio_file.stem
- if isinstance(self.n1, list):
- for n1, n2 in zip(self.n1, self.n2):
- stim_n1 = librosa.load(mono_stim_path / f'{audio_name}_n{n1}.wav', sr=self.fs)[0]
- stim_n2 = librosa.load(mono_stim_path / f'{audio_name}_n{n2}.wav', sr=self.fs)[0]
- # create stereo stimulus
- stereo_stim = np.vstack((stim_n1, stim_n2)).T
- filename = out_path / f'{audio_name}_n{n1}n{n2}.wav'
- self.save_wave(stereo_stim, filename)
- else:
- stim_n1 = librosa.load(mono_stim_path / f'{audio_name}_n{self.n1}.wav', sr=self.fs)[0]
- stim_n2 = librosa.load(mono_stim_path / f'{audio_name}_n{self.n2}.wav', sr=self.fs)[0]
- # create stereo stimulus
- stereo_stim = np.vstack((stim_n1, stim_n2)).T
- filename = out_path / f'{audio_name}_n{self.n1}n{self.n2}.wav'
- self.save_wave(stereo_stim, filename)
- return
- class multiband_generator:
- def __init__(self, stim_directory, bookA, bookB=None, fs=44100):
- """
- Initialize the multiband generator with the directory containing stimuli and the books to be combined.
- Parameters
- ----------
- stim_directory : str or Path
- The directory where the stimulus files are located.
- bookA : str
- The name of the folder containing the mono stimuli for book A.
- Typically, it should follow the format 'Name_Voice'.
- bookB : str
- The name of the folder containing the mono stimuli for book B.
- Typically, it should follow the format 'Name_Voice'.
- If not provided, only book A will be used.
- fs : int, optional
- The sampling frequency of the audio files, by default 44100.
- """
- self.stim_directory = Path(stim_directory)
- self.bookA = bookA
- self.bookB = bookB
- self.fs = fs
- def distorion_product(self, n1, n2):
- """
- Calculate the distortion product based on the provided harmonic numbers.
- Parameters
- ----------
- n1 : int or list of int
- The first harmonic number(s).
- n2 : int or list of int
- The second harmonic number(s).
- Returns
- -------
- np.ndarray
- The distortion product waveform.
- """
- if isinstance(n1, Iterable) and isinstance(n2, Iterable):
- return 2 * np.array(n1) - np.array(n2)
- else:
- return 2 * n1 - n2
- def save_wave(self, wave, filename):
- """Save the generated waveform to a file."""
- if not os.path.exists(os.path.dirname(filename)):
- os.makedirs(os.path.dirname(filename))
- sf.write(filename, wave, self.fs)
- def extract_chapter_number(self, filename):
- """Extract the chapter number from the filename."""
- base = re.sub(r'_n\d+$', '', filename) # remove the n1 or n2 suffix
- match = re.findall(r'(?<!\d)(\d{2})(?!\d)', base) # find two-digit numbers
- return match[0] if match else None
- def load_stim_files(self, book):
- """Load all audio files for the respective book from the stimulus directory."""
- mono_stim_path = self.stim_directory / book / 'mono_stimuli'
- stim_files = sorted(list(mono_stim_path.glob('*.wav')))
- if not stim_files:
- raise FileNotFoundError("No .wav files found in the stimulus directory.")
- return stim_files
- def create_multiband_singlespeaker(self, all_n1, all_n2, book, nrm=0.01):
- """Create multiband stimuli by combining the mono stimuli from book A and book B.
- The resulting stimuli will be saved in a new directory named 'multiband_stimuli'.
- Parameters
- ----------
- all_n1 : list of int
- The harmonic numbers for the first band that will be combined.
- all_n2 : list of int
- The harmonic numbers for the second band that will be combined.
- book : str
- The name of the book for which the multiband stimuli will be created.
- nrm : float, optional
- The normalization factor for the generated waveform, by default 0.01.
- """
- # Create target directory for multiband stimuli
- target_dir = self.stim_directory / book / 'multiband_stimuli'
- if not os.path.exists(target_dir):
- os.makedirs(target_dir)
- # Load stimuli for book A only
- mono_stimuli = self.load_stim_files(book)
- chapters = sorted(set(self.extract_chapter_number(file.stem) for file in mono_stimuli if 'single' in file.stem))
- for chapter in chapters:
- chapter_files = [file for file in mono_stimuli if self.extract_chapter_number(file.stem) == chapter and 'single' in file.stem]
- base_name = '_'.join(chapter_files[0].stem.split('_')[:3])
- harms_name = ''.join([f'n{n1}' for n1 in all_n1]) + '_' + ''.join([f'n{n2}' for n2 in all_n2])
- multiband_n1 = np.array([])
- multiband_n2 = np.array([])
- n1_files = [file for file in chapter_files if any(f'n{n1}' in file.stem for n1 in all_n1)]
- n2_files = [file for file in chapter_files if any(f'n{n2}' in file.stem for n2 in all_n2)]
- if len(n1_files) != len(n2_files):
- raise ValueError(f"Number of n1 and n2 files do not match for chapter {chapter} in book {book}.")
- for n1_file, n2_file in zip(n1_files, n2_files):
- # Load the audio files
- stim_n1, _ = librosa.load(n1_file, sr=self.fs)
- stim_n2, _ = librosa.load(n2_file, sr=self.fs)
- if multiband_n1.shape[0] == 0 and multiband_n2.shape[0] == 0:
- multiband_n1 = stim_n1
- multiband_n2 = stim_n2
- else:
- multiband_n1 += stim_n1
- multiband_n2 += stim_n2
- # Normalize the multiband stimuli
- multiband_n1 = nrm * multiband_n1 / np.max(np.abs(multiband_n1))
- multiband_n2 = nrm * multiband_n2 / np.max(np.abs(multiband_n2))
- # remove DC offset
- multiband_n1 = multiband_n1 - np.mean(multiband_n1)
- multiband_n2 = multiband_n2 - np.mean(multiband_n2)
- # save them as stereo stimuli
- multiband_stim = np.vstack((multiband_n1, multiband_n2)).T
- filename = target_dir / f'{base_name}_{harms_name}.wav'
- self.save_wave(multiband_stim, filename)
- # calculate correlation between dp waveforms and multiband stimuli
- dp = self.distorion_product(all_n1, all_n2)
- dp_files = [file for file in chapter_files if any(f'n{d}' in file.stem for d in dp)]
- sig_peak = []
- for ndp, dp_file in zip(dp, dp_files):
- wf_dp, _ = librosa.load(dp_file, sr=self.fs)
- sig_peak.append(stimuli_correlation(ndp, wf_dp, multiband_n1, multiband_n2, filename=target_dir / f"corr_{base_name}_{harms_name}_dp{ndp}.png", fs=self.fs))
- # Save the correlation results to a CSV file
- multiband_columns = ['filename', 'n1', 'n2', 'dp', 'sig_peak']
- multiband_data = np.array([[base_name for i in range(len(sig_peak))], ['n'.join(map(str, all_n1)) for i in range(len(sig_peak))], ['n'.join(map(str, all_n2)) for i in range(len(sig_peak))], list(dp), sig_peak]).T
- multiband_csv = pd.DataFrame(multiband_data, columns=multiband_columns)
- csv_path = self.stim_directory / book / f'multiband_correlations_{book}_{harms_name}.csv'
- write_header = not os.path.exists(csv_path)
- multiband_csv.to_csv(csv_path, mode='a', header=write_header, index=False)
- return
- def create_multiband_competingspeaker(self, bookA, bookB, A_n1, A_n2, B_n1, B_n2, nrm=0.01):
- """Create multiband stimuli by combining the mono stimuli from book A and book B.
- The resulting stimuli will be saved in a new directory named 'competing_stimuli_{bookA}_{bookB}'.
- Parameters
- ----------
- bookA : str
- The name of the folder containing the mono stimuli for book A.
- bookB : str
- The name of the folder containing the mono stimuli for book B.
- A_n1 : list of int
- The harmonic numbers for the first band in book A that will be combined.
- A_n2 : list of int
- The harmonic numbers for the second band in book A that will be combined.
- B_n1 : list of int
- The harmonic numbers for the first band in book B that will be combined.
- B_n2 : list of int
- The harmonic numbers for the second band in book B that will be combined.
- nrm : float, optional
- The normalization factor for the generated waveform, by default 0.01.
- """
- target_dir = self.stim_directory / f'competing_stimuli_{self.bookA}_{self.bookB}'
- if not os.path.exists(target_dir):
- os.makedirs(target_dir)
- stim_bookA = self.load_stim_files(bookA)
- stim_bookB = self.load_stim_files(bookB)
- # remove potential single speaker files
- stim_bookA = [file for file in stim_bookA if 'single' not in file.stem]
- stim_bookB = [file for file in stim_bookB if 'single' not in file.stem]
- chapters_A = sorted(set(self.extract_chapter_number(file.stem) for file in stim_bookA))
- chapters_B = sorted(set(self.extract_chapter_number(file.stem) for file in stim_bookB))
- if chapters_A != chapters_B:
- raise ValueError("Chapters in book A and book B do not match.")
- for chapter in chapters_A:
- chapter_files_A = [file for file in stim_bookA if self.extract_chapter_number(file.stem) == chapter]
- chapter_files_B = [file for file in stim_bookB if self.extract_chapter_number(file.stem) == chapter]
- base_name = 'Ch_' + chapter
- harms_A = ''.join([f'n{n1}' for n1 in A_n1]) + '_' + ''.join([f'n{n2}' for n2 in A_n2])
- harms_B = ''.join([f'n{n1}' for n1 in B_n1]) + '_' + ''.join([f'n{n2}' for n2 in B_n2])
- multiband_n1 = np.array([])
- multiband_n2 = np.array([])
- n1_files_A = [file for file in chapter_files_A if any(f'n{n1}' in file.stem for n1 in A_n1)]
- n2_files_A = [file for file in chapter_files_A if any(f'n{n2}' in file.stem for n2 in A_n2)]
- n1_files_B = [file for file in chapter_files_B if any(f'n{n1}' in file.stem for n1 in B_n1)]
- n2_files_B = [file for file in chapter_files_B if any(f'n{n2}' in file.stem for n2 in B_n2)]
- if len(n1_files_A) != len(n2_files_A) or len(n1_files_B) != len(n2_files_B):
- raise ValueError(f"Number of n1 and n2 files do not match for chapter {chapter}.")
- for n1_file_A, n1_file_B, n2_file_A, n2_file_B in zip(n1_files_A, n1_files_B, n2_files_A, n2_files_B):
- # Load the audio files
- stim_n1_A, _ = librosa.load(n1_file_A, sr=self.fs)
- stim_n2_A, _ = librosa.load(n2_file_A, sr=self.fs)
- stim_n1_B, _ = librosa.load(n1_file_B, sr=self.fs)
- stim_n2_B, _ = librosa.load(n2_file_B, sr=self.fs)
- if len(stim_n1_A) > len(stim_n1_B):
- stim_n1_B = np.pad(stim_n1_B, (0, len(stim_n1_A) - len(stim_n1_B)), 'constant')
- elif len(stim_n1_B) > len(stim_n1_A):
- stim_n1_A = np.pad(stim_n1_A, (0, len(stim_n1_B) - len(stim_n1_A)), 'constant')
- if len(stim_n2_A) > len(stim_n2_B):
- stim_n2_B = np.pad(stim_n2_B, (0, len(stim_n2_A) - len(stim_n2_B)), 'constant')
- elif len(stim_n2_B) > len(stim_n2_A):
- stim_n2_A = np.pad(stim_n2_A, (0, len(stim_n2_B) - len(stim_n2_A)), 'constant')
- if multiband_n1.shape[0] == 0 and multiband_n2.shape[0] == 0:
- multiband_n1 = stim_n1_A + stim_n1_B
- multiband_n2 = stim_n2_A + stim_n2_B
- else:
- multiband_n1 += stim_n1_A + stim_n1_B
- multiband_n2 += stim_n2_A + stim_n2_B
- # Normalize the multiband stimuli
- multiband_n1 = nrm * multiband_n1 / np.max(np.abs(multiband_n1))
- multiband_n2 = nrm * multiband_n2 / np.max(np.abs(multiband_n2))
- # save them as stereo stimuli
- multiband_stim = np.vstack((multiband_n1, multiband_n2)).T
- filename = target_dir / f'{base_name}_{bookA}_{harms_A}_{bookB}_{harms_B}.wav'
- self.save_wave(multiband_stim, filename)
- # calculate correlation between dp waveforms and multiband stimuli
- dp_A = self.distorion_product(A_n1, A_n2)
- dp_B = self.distorion_product(B_n1, B_n2)
- dp_files_A = [file for file in chapter_files_A if any(f'n{d}' in file.stem for d in dp_A)]
- dp_files_B = [file for file in chapter_files_B if any(f'n{d}' in file.stem for d in dp_B)]
- sig_peak = []
- for i, (dp_file_A, dp_file_B) in enumerate(zip(dp_files_A, dp_files_B)):
- wf_dp_A, _ = librosa.load(dp_file_A, sr=self.fs)
- wf_dp_B, _ = librosa.load(dp_file_B, sr=self.fs)
- res_A = stimuli_correlation(dp_A[i], wf_dp_A, multiband_n1, multiband_n2, filename=target_dir / f"corr_{base_name}_{bookA}_dp{dp_A[i]}.png", fs=self.fs)
- res_B = stimuli_correlation(dp_B[i], wf_dp_B, multiband_n1, multiband_n2, filename=target_dir / f"corr_{base_name}_{bookB}_dp{dp_B[i]}.png", fs=self.fs)
- sig_peak.append((res_A, res_B))
- # Save the correlation results to a CSV file
- multiband_columns = ['filename', 'book A', 'book B', 'n1 (book A)', 'n2 (book A)', 'dp (book A)', 'n1 (book B)', 'n2 (book B)', 'dp (book B)', 'sig_peak_A', 'sig_peak_B']
- multiband_data = np.array([[base_name for i in range(len(sig_peak))], [bookA for i in range(len(sig_peak))], [bookB for i in range(len(sig_peak))], ['n'.join(map(str, A_n1)) for i in range(len(sig_peak))], ['n'.join(map(str, A_n2)) for i in range(len(sig_peak))], list(dp_A), ['n'.join(map(str, B_n1)) for i in range(len(sig_peak))], ['n'.join(map(str, B_n2)) for i in range(len(sig_peak))], list(dp_B),[res[0] for res in sig_peak], [res[1] for res in sig_peak]]).T
- multiband_csv = pd.DataFrame(multiband_data, columns=multiband_columns)
- csv_path = self.stim_directory / f'multiband_correlations_{bookA}_{harms_A}_{bookB}_{harms_B}.csv'
- write_header = not os.path.exists(csv_path)
- multiband_csv.to_csv(csv_path, mode='a', header=write_header, index=False)
- return
- def mix_books(self, audio_path, bookA, bookB, nrm=0.02):
- """Mix the audio files from book A and book B into stereo stimuli.
- The resulting stimuli will be saved in a new directory named 'competing_audios_{bookA}_{bookB}'.
- Parameters
- ----------
- audio_path : str or Path
- The directory where the source audio files are located.
- bookA : str
- The name of the folder containing the audio files for book A.
- bookB : str
- The name of the folder containing the audio files for book B.
- nrm : float, optional
- The normalization factor for the generated waveform, by default 0.02.
- """
- audio_path = Path(audio_path)
- single_dir = audio_path.parent / 'single_audios/'
- if not os.path.exists(single_dir):
- os.makedirs(single_dir)
- target_dir = audio_path.parent / f'competing_audios_{self.bookA}_{self.bookB}'
- if not os.path.exists(target_dir):
- os.makedirs(target_dir)
- path_bookA = audio_path / bookA
- path_bookB = audio_path / bookB
- source_files_A = sorted(list(path_bookA.glob('*.wav')))
- source_files_B = sorted(list(path_bookB.glob('*.wav')))
- # extract potential single speaker files
- source_files_single = [file for file in source_files_A + source_files_B if 'single' in file.stem]
- # save single speaker files separately
- for file in source_files_single:
- single_audio = librosa.load(file, sr=self.fs)[0]
- single_audio = nrm * single_audio / np.max(np.abs(single_audio))
- self.save_wave(single_audio, single_dir / file.name)
- source_files_A = [file for file in source_files_A if file not in source_files_single]
- source_files_B = [file for file in source_files_B if file not in source_files_single]
- if len(source_files_A) != len(source_files_B):
- raise ValueError(f"Number of audio files in {bookA} and {bookB} do not match.")
- # mix remaining chapters together
- chapters_A = sorted(set(self.extract_chapter_number(file.stem) for file in source_files_A))
- chapters_B = sorted(set(self.extract_chapter_number(file.stem) for file in source_files_B))
- if chapters_A != chapters_B:
- raise ValueError("Chapters in book A and book B do not match.")
- for chapter in chapters_A:
- chapter_files_A = [file for file in source_files_A if self.extract_chapter_number(file.stem) == chapter]
- chapter_files_B = [file for file in source_files_B if self.extract_chapter_number(file.stem) == chapter]
- base_name = 'Ch_' + chapter
- audio_A = librosa.load(chapter_files_A[0], sr=self.fs)[0]
- audio_B = librosa.load(chapter_files_B[0], sr=self.fs)[0]
- audio_A = nrm * audio_A / np.max(np.abs(audio_A))
- audio_B = nrm * audio_B / np.max(np.abs(audio_B))
- if len(audio_A) > len(audio_B):
- audio_B = np.pad(audio_B, (0, len(audio_A) - len(audio_B)), 'constant')
- elif len(audio_B) > len(audio_A):
- audio_A = np.pad(audio_A, (0, len(audio_B) - len(audio_A)), 'constant')
- mixed_audio = np.vstack((audio_A, audio_B)).T
- filename = target_dir / f'{base_name}_{bookA}_{bookB}.wav'
- self.save_wave(mixed_audio, filename)
- return
stimulus_generation.py at commit 026e5b7, under MIT · at the source
Overview
Abstract
Humans are remarkably skilled at understanding speech in noisy environments. While segregation of different audio streams is mostly accomplished in the auditory cortex, neural feedback connections run from the cortex to the brainstem and to the cochlea. The latter organ not only houses the mechanosensitive hair cells, but also possesses an active process enabling it to amplify sound in a frequency-dependent manner. A physiological correlate of the active process are distortion-product otoacoustic emissions (DPOAEs) that can be measured non-invasively from the ear canal. Here we employed speech-like DPOAEs, measured in response to stimuli derived from natural human speech and thus reflecting the harmonic spectral structure of voiced speech. We show that these emissions are modulated by selective attention to one of two competing voices, as well as by intermodal attention. Specifically, speech-like DPOAEs evoked by stimuli related to resolved harmonics of a voice were significantly reduced when that voice was attended compared to when it was ignored. No such effect was observed for stimuli related to unresolved harmonics of the target voice when the competing voice's harmonics in that range were unresolved as well, indicating that attentional modulation is specific to those components of voiced speech that are spectrally resolved. Our findings support the hypothesis that the cochlea's active process already shapes selective attention to speech in noise. Moreover, the speech-like DPOAEs that we developed open up further possibilities for investigating the contribution of the cochlear active process to auditory scene analysis in naturalistic settings.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.
janna-stb/dpoae_attention_study
026e5b711d983c50894d168c83b8fadc4f918fa5, 1 December 2025Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
14 files
- DPOAE_analysis.py, Python, 969 lines, 1 match
- butterworth_filter.py, Python, 59 lines
- cross_correlation.py, Python, 48 lines, 1 match
- plot_statistics.py, Python, 1,316 lines
- plot_stimulus_correlatio
ns.py , Python, 83 lines - run_DPOAE_analysis.py, Python, 17 lines
- run_plot_statistics.py, Python, 15 lines
- run_statistical_analysis
.py , Python, 20 lines - run_stimulus_generation.
py , Python, 56 lines - running_mean.py, Python, 21 lines
- statistical_analysis.py, Python, 1,217 lines
- stimulus_generation.py, Python, 661 lines, 2 matches
- LICENSE, License, 21 lines
- README.md, Text, 84 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 12 scripts, each with its path and the digest of its content;
- 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data availability statement
All custom code used for stimulus generation, speech-like DPOAE analysis via cross-correlation, and statistical evaluation is openly available on GitHub at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 30 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 2 authors, 7 keywords, 1 funder, 60 references.
Cite
This paper
Steinebach, J., & Reichenbach, T. (2026). Attention to speech modulates distortion product otoacoustic emissions evoked by speech-derived stimuli in humans. Frontiers in neuroscience, 20, 1756386. https://
BibTeX
@article{steinebach2026a
author = {Steinebach, Janna and Reichenbach, Tobias},
title = {{Attention to speech modulates distortion product otoacoustic emissions evoked by speech-derived stimuli in humans}},
journal = {Frontiers in neuroscience},
year = {2026},
month = mar,
volume = {20},
pages = {1756386},
publisher = {Frontiers Media SA},
issn = {1662-4548},
doi = {10.3389/
url = {https://
pmid = {41847227},
pmcid = {PMC12989502}
}
RIS
TY - JOUR
AU - Steinebach, Janna
AU - Reichenbach, Tobias
TI - Attention to speech modulates distortion product otoacoustic emissions evoked by speech-derived stimuli in humans
T2 - Frontiers in neuroscience
J2 - Front Neurosci
PY - 2026
DA - 2026/
VL - 20
SP - 1756386
SN - 1662-4548
PB - Frontiers Media SA
DO - 10.3389/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.3389/
"type": "article-journal",
"title": "Attention to speech modulates distortion product otoacoustic emissions evoked by speech-derived stimuli in humans",
"container-title": "Frontiers in neuroscience",
"author": [
{
"family": "Steinebach",
"given": "Janna"
},
{
"family": "Reichenbach",
"given": "Tobias"
}
],
"container-title-short":
"volume": "20",
"page": "1756386",
"DOI": "10.3389/
"PMID": "41847227",
"PMCID": "PMC12989502",
"ISSN": "1662-4548",
"publisher": "Frontiers Media SA",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
2
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1038/s41562-026-02414-7 [code]
- Optimized feature gains explain and predict successes and failures of human selective listening.Journal: Nature human behaviourIn common: statsmodels, seaborn, pandas, 3 other tools, cognitive, 4 references
- [2] doi:10.7554/elife.100056 [code]
- Multi-talker speech comprehension at different temporal scales in listeners with normal and impaired hearing.Journal: eLifeIn common: pandas, SciPy, NumPy, cognitive, 4 references
- [3] doi:10.1523/eneuro.0041-26.2026 [code]
- Ocular Speech Tracking Persists in Blindness, but Its Dynamics and Oculo-Cerebral Connectivity Depend on Visual Status.Journal: eNeuroIn common: statsmodels, seaborn, pandas, 3 other tools, cognitive, 1 reference
- [4] doi:10.1111/psyp.70397 [code]
- Cortical Contributions to Attentional Orienting and Response Cancellation in Action Stopping.Journal: PsychophysiologyIn common: statsmodels, seaborn, pandas, 3 other tools, cognitive, 1 reference
- [5] doi:10.1016/j.ibneur.2026.05.013 [code]
- Anticipatory slow potentials before auditory feedback show posterior predominance but limited condition effects in speech-in-noise.Journal: IBRO neuroscience reportsIn common: statsmodels, pandas, SciPy, 2 other tools, cognitive, 1 reference
- [6] doi:10.1162/imag.a.1286 [code]
- Behavioral imitation with artificial neural networks leads to personalized models of brain dynamics during videogame play.Journal: Imaging neuroscience (Cambridge, Mass.)In common: statsmodels, seaborn, pandas, 3 other tools, 1 reference
- [7] doi:10.1162/imag.a.1256 [code]
- Gamer in the scanner: Event-related analysis of fMRI activity during retro videogame play guided by automated annotations of game content.Journal: Imaging neuroscience (Cambridge, Mass.)In common: statsmodels, seaborn, pandas, 3 other tools, 1 reference
- [8] doi:10.34133/csbj.0042 [code]
- Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &
lt;i& gt;CRB1& lt;/ i& gt;: Implications for Clinical Trials. Journal: Computational and structural biotechnology journalIn common: statsmodels, seaborn, pandas, 3 other tools, 1 reference - [9] doi:10.1117/1.nph.13.2.025001 [code]
- Surface-based image reconstruction optimization for high-density functional near-infrared spectroscopy.Journal: NeurophotonicsIn common: statsmodels, seaborn, pandas, 3 other tools, 1 reference
- [10] doi:10.1073/pnas.2536563123 [code]
- Temporally structured motor and auditory representations in covert syllable production.Journal: Proceedings of the National Academy of Sciences of the United States of AmericaIn common: statsmodels, pandas, SciPy, 2 other tools, cognitive, 1 reference
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 12 scripts, and 4 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:de9228c9019fb9ab…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
