def main(): test_signal = os.path.join(DIR_PATH, 'sweep-6.15s-48000Hz-32bit-2.93Hz-24000Hz.pkl') estimator = ImpulseResponseEstimator.from_pickle(test_signal) for group in ['volume2', 'volume2-48-52', 'objective2', 'None']: fig, ax = plt.subplots() fig.set_size_inches(12, 9) config_fr_axis(ax) ax.set_title(group) files = sorted( list(glob(os.path.join(DIR_PATH, group, 'headphones*.wav'))), key=lambda x: float(re.search(r'\d+', os.path.split(x)[1])[0])) for file_path in files: hp = HRIR(estimator) hp.open_recording(file_path, ['FL', 'FR']) left = hp.irs['FL']['left'].frequency_response() right = hp.irs['FR']['right'].frequency_response() ax.plot(left.frequency, right.raw - left.raw, label=os.path.split(file_path)[1].replace('.wav', ''), linewidth=0.5) ax.legend() ax.set_ylim([-5, 5]) plt.show() save_fig_as_png(os.path.join(DIR_PATH, f'{group}.png'), fig)
def equalization(estimator, dir_path): """Reads equalization FIR filter or CSV settings Args: estimator: ImpulseResponseEstimator dir_path: Path to directory Returns: - Left side FIR as Numpy array or FrequencyResponse or None - Right side FIR as Numpy array or FrequencyResponse or None """ if os.path.isfile(os.path.join(dir_path, 'eq.wav')): print('eq.wav is no longer supported, use eq.csv!') # Default for both sides eq_path = os.path.join(dir_path, 'eq.csv') eq_fr = None if os.path.isfile(eq_path): eq_fr = FrequencyResponse.read_from_csv(eq_path) # Left left_path = os.path.join(dir_path, 'eq-left.csv') left_fr = None if os.path.isfile(left_path): left_fr = FrequencyResponse.read_from_csv(left_path) elif eq_fr is not None: left_fr = eq_fr if left_fr is not None: left_fr.interpolate(f_step=1.01, f_min=10, f_max=estimator.fs / 2) # Right right_path = os.path.join(dir_path, 'eq-right.csv') right_fr = None if os.path.isfile(right_path): right_fr = FrequencyResponse.read_from_csv(right_path) elif eq_fr is not None: right_fr = eq_fr if right_fr is not None and right_fr != left_fr: right_fr.interpolate(f_step=1.01, f_min=10, f_max=estimator.fs / 2) # Plot if left_fr is not None or right_fr is not None: if left_fr == right_fr: # Both are the same, plot only one graph fig, ax = plt.subplots() fig.set_size_inches(12, 9) left_fr.plot_graph(fig=fig, ax=ax, show=False) else: # Left and right are different, plot two graphs in the same figure fig, ax = plt.subplots(1, 2) fig.set_size_inches(22, 9) if left_fr is not None: left_fr.plot_graph(fig=fig, ax=ax[0], show=False) if right_fr is not None: right_fr.plot_graph(fig=fig, ax=ax[1], show=False) save_fig_as_png(os.path.join(dir_path, 'plots', 'eq.png'), fig) return left_fr, right_fr
def open_generic_room_measurement(estimator, dir_path, mic_calibration, target, method='average', limit=1000, plot=False): """Opens generic room measurment file Args: estimator: ImpulseResponseEstimator instance dir_path: Path to directory mic_calibration: Measurement microphone calibration FrequencyResponse target: Room response target FrequencyResponse method: Combination method. "average" or "conservative" limit: Upper limit in Hertz for equalization. Gain will ramp down to 0 dB in the octave leading to this. 0 disables limit. plot: Plot frequency response? Returns: Generic room measurement FrequencyResponse """ file_path = os.path.join(dir_path, 'room.wav') if not os.path.isfile(file_path): return None # Read the file fs, data = read_wav(file_path, expand=True) if fs != estimator.fs: raise ValueError(f'Sampling rate of "{file_path}" doesn\'t match!') # Average frequency responses of all tracks of the generic room measurement file irs = [] for track in data: n_cols = int( round((len(track) / estimator.fs - 2) / (estimator.duration + 2))) for i in range(n_cols): # Starts at 2 seconds in the beginning plus previous sweeps and their tails start = int(2 * estimator.fs + i * (2 * estimator.fs + len(estimator))) # Ends at start plus one more (current) sweep end = int(start + 2 * estimator.fs + len(estimator)) end = min(end, len(track)) # Select current sweep sweep = track[start:end] # Deconvolve as impulse response ir = ImpulseResponse(estimator.estimate(sweep), estimator.fs, sweep) # Crop harmonic distortion from the head # Noise in the tail should not affect frequency response so it doesn't have to be cropped ir.crop_head(head_ms=1) irs.append(ir) # Frequency response for the generic room measurement room_fr = FrequencyResponse( name='generic_room', frequency=FrequencyResponse.generate_frequencies(f_min=10, f_max=estimator.fs / 2, f_step=1.01), raw=0, error=0, target=target.raw) # Calculate and stack errors raws = [] errors = [] for ir in irs: fr = ir.frequency_response() if mic_calibration is not None: fr.raw -= mic_calibration.raw fr.center([100, 10000]) room_fr.raw += fr.raw raws.append(fr.copy()) fr.compensate(target, min_mean_error=True) if method == 'conservative' and len(irs) > 1: fr.smoothen_fractional_octave(window_size=1 / 3, treble_window_size=1 / 3) errors.append(fr.error_smoothed) else: errors.append(fr.error) room_fr.raw /= len(irs) errors = np.vstack(errors) if errors.shape[0] > 1: # Combine errors if method == 'conservative': # Conservative error curve is zero everywhere else but on indexes where both have the same sign, # at these indexes the smaller absolute value is selected. # This ensures that no curve will be adjusted to the other side of zero mask = np.mean(errors > 0, axis=0) # Average from boolean values per column positive = mask == 1 # Mask for columns with only positive values negative = mask == 0 # Mask for columns with only negative values # Minimum value for columns with only positive values room_fr.error[positive] = np.min(errors[:, positive], axis=0) # Maximum value for columns with only negative values (minimum absolute value) room_fr.error[negative] = np.max(errors[:, negative], axis=0) # Smoothen out kinks room_fr.smoothen_fractional_octave(window_size=1 / 6, treble_window_size=1 / 6) room_fr.error = room_fr.error_smoothed.copy() elif method == 'average': room_fr.error = np.mean(errors, axis=0) room_fr.smoothen_fractional_octave(window_size=1 / 3, treble_window_size=1 / 3) else: raise ValueError( f'Invalid value "{method}" for method. Supported values are "conservative" and "average"' ) else: room_fr.error = errors[0, :] room_fr.smoothen_fractional_octave(window_size=1 / 3, treble_window_size=1 / 3) if limit > 0: # Zero error above limit start = np.argmax(room_fr.frequency > limit / 2) end = np.argmax(room_fr.frequency > limit) mask = np.concatenate([ np.ones(start if start > 0 else 0), signal.windows.hann(end - start), np.zeros(len(room_fr.frequency) - end) ]) room_fr.error *= mask room_fr.error_smoothed *= mask if plot: # Create dir room_plots_dir = os.path.join(dir_path, 'plots', 'room') os.makedirs(room_plots_dir, exist_ok=True) # Create generic FR plot fr = room_fr.copy() fr.name = 'Generic room measurement' fr.raw = fr.smoothed.copy() fr.error = fr.error_smoothed.copy() # Create figure and axes fig, ax = plt.subplots() fig.set_size_inches(15, 9) config_fr_axis(ax) ax.set_title('Generic room measurement') # Plot target, raw and error ax.plot(fr.frequency, fr.target, color=COLORS['lightpurple'], linewidth=5, label='Target') for raw in raws: raw.smoothen_fractional_octave(window_size=1 / 3, treble_window_size=1 / 3) ax.plot(raw.frequency, raw.smoothed, color='grey', linewidth=0.5) ax.plot(fr.frequency, fr.raw, color=COLORS['blue'], label='Raw smoothed') ax.plot(fr.frequency, fr.error, color=COLORS['red'], label='Error smoothed') ax.legend() # Set y limits sl = np.logical_and(fr.frequency >= 20, fr.frequency <= 20000) stack = np.vstack([fr.raw[sl], fr.error[sl], fr.target[sl]]) ax.set_ylim(get_ylim(stack, padding=0.1)) # Save FR figure save_fig_as_png(os.path.join(room_plots_dir, 'room.png'), fig) plt.close(fig) return room_fr
def room_correction(estimator, dir_path, target=None, mic_calibration=None, fr_combination_method='average', specific_limit=20000, generic_limit=1000, plot=False): """Corrects room acoustics Args: estimator: ImpulseResponseEstimator dir_path: Path to directory target: Path to room target response CSV file mic_calibration: Path to room measurement microphone calibration file fr_combination_method: Method for combining generic room measurment frequency responses. "average" or "conservative" specific_limit: Upper limit in Hertz for equalization of specific room eq. 0 disables limit. generic_limit: Upper limit in Hertz for equalization of generic room eq. 0 disables limit. plot: Plot graphs? Returns: - Room Impulse Responses as HRIR or None - Equalization frequency responses as dict of dicts (similar to HRIR) or None """ # Open files target = open_room_target(estimator, dir_path, target=target) mic_calibration = open_mic_calibration(estimator, dir_path, mic_calibration=mic_calibration) rir = open_room_measurements(estimator, dir_path) missing = [ch for ch in SPEAKER_NAMES if ch not in rir.irs] room_fr = open_generic_room_measurement(estimator, dir_path, mic_calibration, target, method=fr_combination_method, limit=generic_limit, plot=plot) if not len(rir.irs) and room_fr is None: # No room recording files found return None, None frs = dict() fr_axes = [] figs = None if len(rir.irs): # Crop heads and tails from room impulse responses for speaker, pair in rir.irs.items(): for side, ir in pair.items(): ir.crop_head() rir.crop_tails() rir.write_wav(os.path.join(dir_path, 'room-responses.wav')) if plot: # Plot all but frequency response plot_dir = os.path.join(dir_path, 'plots', 'room') os.makedirs(plot_dir, exist_ok=True) figs = rir.plot(plot_fr=False, close_plots=False) # Create equalization frequency responses reference_gain = None for speaker, pair in rir.irs.items(): frs[speaker] = dict() for side, ir in pair.items(): # Create frequency response fr = ir.frequency_response() if mic_calibration is not None: # Calibrate frequency response fr.raw -= mic_calibration.raw # Sync gains if reference_gain is None: reference_gain = fr.center( [100, 10000]) # Shifted (up) by this many dB else: fr.raw += reference_gain # Adjust target level with the (negative) gain caused by speaker-ear distance in reverberant room target_adjusted = target.copy() target_adjusted.raw += IR_ROOM_SPL[speaker][side] # Compensate with the adjusted room target fr.compensate(target_adjusted, min_mean_error=False) # Zero error above limit if specific_limit > 0: start = np.argmax(fr.frequency > specific_limit / 2) end = np.argmax(fr.frequency > specific_limit) mask = np.concatenate([ np.ones(start if start > 0 else 0), signal.windows.hann(end - start), np.zeros(len(fr.frequency) - end) ]) fr.error *= mask # Add frequency response frs[speaker][side] = fr if plot: file_path = os.path.join(dir_path, 'plots', 'room', f'{speaker}-{side}.png') fr = fr.copy() fr.smoothen_fractional_octave(window_size=1 / 3, treble_window_size=1 / 3) _, fr_ax = ir.plot_fr(fr=fr, fig=figs[speaker][side], ax=figs[speaker][side].get_axes()[4], plot_raw=False, plot_error=False, plot_file_path=file_path, fix_ylim=True) fr_axes.append(fr_ax) if len(missing) > 0 and room_fr is not None: # Use generic measurement for speakers that don't have specific measurements for speaker in missing: frs[speaker] = {'left': room_fr.copy(), 'right': room_fr.copy()} if plot and figs is not None: room_plots_dir = os.path.join(dir_path, 'plots', 'room') os.makedirs(room_plots_dir, exist_ok=True) # Sync FR plot axes sync_axes(fr_axes) # Save specific fR figures for speaker, pair in figs.items(): for side, fig in pair.items(): save_fig_as_png( os.path.join(room_plots_dir, f'{speaker}-{side}.png'), fig) plt.close(fig) return rir, frs
def headphone_compensation(estimator, dir_path): """Equalizes HRIR tracks with headphone compensation measurement. Args: estimator: ImpulseResponseEstimator instance dir_path: Path to output directory Returns: None """ # Read WAV file hp_irs = HRIR(estimator) hp_irs.open_recording(os.path.join(dir_path, 'headphones.wav'), speakers=['FL', 'FR']) hp_irs.write_wav(os.path.join(dir_path, 'headphone-responses.wav')) # Frequency responses left = hp_irs.irs['FL']['left'].frequency_response() right = hp_irs.irs['FR']['right'].frequency_response() # Center by left channel gain = left.center([100, 10000]) right.raw += gain # Compensate zero = FrequencyResponse(name='zero', frequency=left.frequency, raw=np.zeros(len(left.frequency))) left.compensate(zero, min_mean_error=False) right.compensate(zero, min_mean_error=False) # Headphone plots fig = plt.figure() gs = fig.add_gridspec(2, 3) fig.set_size_inches(22, 10) fig.suptitle('Headphones') # Left axl = fig.add_subplot(gs[0, 0]) left.plot_graph(fig=fig, ax=axl, show=False) axl.set_title('Left') # Right axr = fig.add_subplot(gs[1, 0]) right.plot_graph(fig=fig, ax=axr, show=False) axr.set_title('Right') # Sync axes sync_axes([axl, axr]) # Combined _left = left.copy() _right = right.copy() gain_l = _left.center([100, 10000]) gain_r = _right.center([100, 10000]) ax = fig.add_subplot(gs[:, 1:]) ax.plot(_left.frequency, _left.raw, linewidth=1, color='#1f77b4') ax.plot(_right.frequency, _right.raw, linewidth=1, color='#d62728') ax.plot(_left.frequency, _left.raw - _right.raw, linewidth=1, color='#680fb9') sl = np.logical_and(_left.frequency > 20, _left.frequency < 20000) stack = np.vstack([_left.raw[sl], _right.raw[sl], _left.raw[sl] - _right.raw[sl]]) ax.set_ylim([np.min(stack) * 1.1, np.max(stack) * 1.1]) axl.set_ylim([np.min(stack) * 1.1, np.max(stack) * 1.1]) axr.set_ylim([np.min(stack) * 1.1, np.max(stack) * 1.1]) ax.set_title('Comparison') ax.legend([f'Left raw {gain_l:+.1f} dB', f'Right raw {gain_r:+.1f} dB', 'Difference'], fontsize=8) ax.set_xlabel('Frequency (Hz)') ax.semilogx() ax.set_xlim([20, 20000]) ax.set_ylabel('Amplitude (dBr)') ax.grid(True, which='major') ax.grid(True, which='minor') ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:.0f}')) # Save headphone plots file_path = os.path.join(dir_path, 'plots', 'headphones.png') os.makedirs(os.path.split(file_path)[0], exist_ok=True) save_fig_as_png(file_path, fig) plt.close(fig) return left, right
def main(): estimator = ImpulseResponseEstimator.from_pickle(TEST_SIGNAL) # Open feedback measurement feedback = HRIR(estimator) feedback.open_recording(os.path.join(DIR_PATH, 'headphones-FL,FR.wav'), speakers=['FL', 'FR']) feedback.crop_heads() feedback.crop_tails() # Open feedforward measurement # Only FL-left and FR-right are needed here feedforward = HRIR(estimator) feedforward.open_recording(os.path.join(DIR_PATH, 'headphones.wav'), speakers=['FL', 'FR']) ffl = feedforward.irs['FL']['left'].frequency_response() ff_gain = ffl.center([100, 10000]) zero = FrequencyResponse(name='zero', frequency=ffl.frequency, raw=np.zeros(ffl.frequency.shape)) ffl.compensate(zero) ffl.smoothen_heavy_light() ffr = feedforward.irs['FR']['right'].frequency_response() ffr.raw += ff_gain ffr.compensate(zero) ffr.smoothen_heavy_light() feedforward_errors = {'left': ffl, 'right': ffr} # Open HRIR measurement hrir = HRIR(estimator) hrir.open_recording(os.path.join(DIR_PATH, 'FL,FR.wav'), speakers=['FL', 'FR']) hrir.crop_heads() hrir.crop_tails() fllfr = hrir.irs['FL']['left'].frequency_response() gain = fllfr.center([100, 10000]) # Feedback vs HRIR fig, ax = plt.subplots(3, 2) fig.set_size_inches(18, 12) fig.suptitle('Feedback Compensation') i = 0 feedback_errors = {'left': None, 'right': None} for speaker, pair in feedback.irs.items(): j = 0 for side, ir in pair.items(): # HRIR is the target target = hrir.irs[speaker][side].frequency_response() target.raw += gain target.smoothen_fractional_octave(window_size=1/3, treble_window_size=1/3) # Frequency response of the headphone feedback measurement fr = ir.frequency_response() fr.raw += gain fr.error = fr.raw - target.raw fr.smoothen_heavy_light() # Add to this side average if feedback_errors[side] is None: feedback_errors[side] = fr.error_smoothed else: feedback_errors[side] += fr.error_smoothed # Plot ir.plot_fr(fr=fr, fig=fig, ax=ax[i, j]) ax[i, j].set_title(f'{speaker}-{side}') ax[i, j].set_ylim([np.min(fr.error_smoothed), np.max(fr.error_smoothed)]) j += 1 i += 1 for i, side in enumerate(['left', 'right']): feedback_errors[side] = FrequencyResponse( name=side, frequency=fllfr.frequency.copy(), error=feedback_errors[side] / 2 ) feedback_errors[side].plot_graph(fig=fig, ax=ax[2, i], show=False) sync_axes([ax[i, j] for i in range(ax.shape[0]) for j in range(ax.shape[1])]) save_fig_as_png(os.path.join(DIR_PATH, 'feedback.png'), fig) # Feedforward fig, ax = plt.subplots(1, 2) fig.set_size_inches(18, 9) fig.suptitle('Feedforward Compensation') ffl.plot_graph(fig=fig, ax=ax[0], show=False) ffr.plot_graph(fig=fig, ax=ax[1], show=False) save_fig_as_png(os.path.join(DIR_PATH, 'feedforward.png'), fig) # Feedback compensation vs Feedforward compensation feedback_errors['left'].raw = feedback_errors['left'].error fbg = feedback_errors['left'].center([200, 2000]) feedback_errors['left'].error = feedback_errors['left'].raw feedback_errors['left'].raw = [] feedback_errors['right'].error += fbg feedforward_errors['left'].raw = feedforward_errors['left'].error_smoothed ffg = feedforward_errors['left'].center([200, 2000]) feedforward_errors['left'].error_smoothed = feedforward_errors['left'].raw feedforward_errors['left'].raw = [] feedforward_errors['right'].error_smoothed += ffg fig, ax = plt.subplots(1, 2) fig.set_size_inches(18, 9) fig.suptitle('Feedback vs Feedforward') sl = np.logical_and(feedback_errors['left'].frequency > 20, feedback_errors['left'].frequency < 20000) stack = [ feedback_errors['left'].error[sl], feedback_errors['right'].error[sl], feedforward_errors['left'].error_smoothed[sl], feedforward_errors['right'].error_smoothed[sl], ] for i, side in enumerate(['left', 'right']): config_fr_axis(ax[i]) ax[i].plot(feedback_errors[side].frequency, feedback_errors[side].error) ax[i].plot(feedforward_errors[side].frequency, feedforward_errors[side].error_smoothed) difference = feedback_errors[side].error - feedforward_errors[side].error_smoothed stack.append(difference[sl]) ax[i].plot(feedback_errors[side].frequency, difference, color=COLORS['red']) ax[i].set_title(side) ax[i].legend(['Feedback', 'Feedforward', 'Difference']) stack = np.concatenate(stack) ax[0].set_ylim([np.min(stack), np.max(stack)]) ax[1].set_ylim([np.min(stack), np.max(stack)]) save_fig_as_png(os.path.join(DIR_PATH, 'comparison.png'), fig) plt.show()