Esempio n. 1
0
def create_target(estimator, bass_boost_gain, bass_boost_fc, bass_boost_q, tilt):
    """Creates target frequency response with bass boost, tilt and high pass at 20 Hz"""
    target = FrequencyResponse(
        name='bass_and_tilt',
        frequency=FrequencyResponse.generate_frequencies(f_min=10, f_max=estimator.fs / 2, f_step=1.01)
    )
    target.raw = target.create_target(
        bass_boost_gain=bass_boost_gain,
        bass_boost_fc=bass_boost_fc,
        bass_boost_q=bass_boost_q,
        tilt=tilt
    )
    high_pass = FrequencyResponse(
        name='high_pass',
        frequency=[10, 18, 19, 20, 21, 22, 20000],
        raw=[-80, -5, -1.6, -0.6, -0.2, 0, 0]
    )
    high_pass.interpolate(f_min=10, f_max=estimator.fs / 2, f_step=1.01)
    target.raw += high_pass.raw
    return target
Esempio n. 2
0
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
Esempio n. 3
0
def main(dir_path=None,
         test_signal=None,
         room_target=None,
         room_mic_calibration=None,
         fs=None,
         plot=False,
         channel_balance=None,
         decay=None,
         target_level=None,
         fr_combination_method='average',
         specific_limit=20000,
         generic_limit=1000,
         bass_boost_gain=0.0,
         bass_boost_fc=105,
         bass_boost_q=0.76,
         tilt=0.0,
         do_room_correction=True,
         do_headphone_compensation=True,
         do_equalization=True):
    """"""
    if dir_path is None or not os.path.isdir(dir_path):
        raise NotADirectoryError(f'Given dir path "{dir_path}"" is not a directory.')

    # Dir path as absolute
    dir_path = os.path.abspath(dir_path)

    # Impulse response estimator
    print('Creating impulse response estimator...')
    estimator = open_impulse_response_estimator(dir_path, file_path=test_signal)

    # Room correction frequency responses
    room_frs = None
    if do_room_correction:
        print('Running room correction...')
        _, room_frs = room_correction(
            estimator, dir_path,
            target=room_target,
            mic_calibration=room_mic_calibration,
            fr_combination_method=fr_combination_method,
            specific_limit=specific_limit,
            generic_limit=generic_limit,
            plot=plot
        )

    # Headphone compensation frequency responses
    hp_left, hp_right = None, None
    if do_headphone_compensation:
        print('Running headphone compensation...')
        hp_left, hp_right = headphone_compensation(estimator, dir_path)

    # Equalization
    eq_left, eq_right = None, None
    if do_equalization:
        print('Creating headphone equalization...')
        eq_left, eq_right = equalization(estimator, dir_path)

    # Bass boost and tilt
    print('Creating frequency response target...')
    target = create_target(estimator, bass_boost_gain, bass_boost_fc, bass_boost_q, tilt)

    # HRIR measurements
    print('Opening binaural measurements...')
    hrir = open_binaural_measurements(estimator, dir_path)

    # Write info and stats in readme
    write_readme(os.path.join(dir_path, 'README.md'), hrir, fs)

    if plot:
        # Plot graphs pre processing
        os.makedirs(os.path.join(dir_path, 'plots', 'pre'), exist_ok=True)
        print('Plotting BRIR graphs before processing...')
        hrir.plot(dir_path=os.path.join(dir_path, 'plots', 'pre'))

    # Crop noise and harmonics from the beginning
    print('Cropping impulse responses...')
    hrir.crop_heads()

    # Crop noise from the tail
    hrir.crop_tails()

    # Write multi-channel WAV file with sine sweeps for debugging
    hrir.write_wav(os.path.join(dir_path, 'responses.wav'))

    # Equalize all
    if do_headphone_compensation or do_room_correction or do_equalization:
        print('Equalizing...')
        for speaker, pair in hrir.irs.items():
            for side, ir in pair.items():
                fr = FrequencyResponse(
                    name=f'{speaker}-{side} eq',
                    frequency=FrequencyResponse.generate_frequencies(f_step=1.01, f_min=10, f_max=estimator.fs / 2),
                    raw=0, error=0
                )

                if room_frs is not None and speaker in room_frs and side in room_frs[speaker]:
                    # Room correction
                    fr.error += room_frs[speaker][side].error

                hp_eq = hp_left if side == 'left' else hp_right
                if hp_eq is not None:
                    # Headphone compensation
                    fr.error += hp_eq.error

                eq = eq_left if side == 'left' else eq_right
                if eq is not None and type(eq) == FrequencyResponse:
                    # Equalization
                    fr.error += eq.error

                # Remove bass and tilt target from the error
                fr.error -= target.raw

                # Smoothen and equalize
                fr.smoothen_heavy_light()
                fr.equalize(max_gain=40, treble_f_lower=10000, treble_f_upper=estimator.fs / 2)

                # Create FIR filter and equalize
                fir = fr.minimum_phase_impulse_response(fs=estimator.fs, normalize=False, f_res=5)
                ir.equalize(fir)

    # Adjust decay time
    if decay:
        print('Adjusting decay time...')
        for speaker, pair in hrir.irs.items():
            for side, ir in pair.items():
                if speaker in decay:
                    ir.adjust_decay(decay[speaker])

    # Correct channel balance
    if channel_balance is not None:
        print('Correcting channel balance...')
        hrir.correct_channel_balance(channel_balance)

    # Normalize gain
    print('Normalizing gain...')
    hrir.normalize(peak_target=None if target_level is not None else -0.1, avg_target=target_level)

    if plot:
        print('Plotting BRIR graphs after processing...')
        # Convolve test signal, re-plot waveform and spectrogram
        for speaker, pair in hrir.irs.items():
            for side, ir in pair.items():
                ir.recording = ir.convolve(estimator.test_signal)
        # Plot post processing
        hrir.plot(os.path.join(dir_path, 'plots', 'post'))

    # Plot results, always
    print('Plotting results...')
    hrir.plot_result(os.path.join(dir_path, 'plots'))

    # Re-sample
    if fs is not None and fs != hrir.fs:
        print(f'Resampling BRIR to {fs} Hz')
        hrir.resample(fs)
        hrir.normalize(peak_target=None if target_level is not None else -0.1, avg_target=target_level)

    # Write multi-channel WAV file with standard track order
    print('Writing BRIRs...')
    hrir.write_wav(os.path.join(dir_path, 'hrir.wav'))

    # Write multi-channel WAV file with HeSuVi track order
    hrir.write_wav(os.path.join(dir_path, 'hesuvi.wav'), track_order=HESUVI_TRACK_ORDER)