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)
示例#2
0
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
示例#3
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
示例#4
0
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
示例#5
0
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()