Эксперимент для сотрудника с нарушением слуха, ч. 2, проверка на себе

от автора

Привет! В первой части мы рассказали, зачем вообще решили заняться этим вопросом, а также поделились переводом статьи, ставшей для нас отправной точкой для собственных изысканий. Теперь хотим рассказать, как мы доработали идею под нашего сотрудника.

Отдельное спасибо комментаторам, которые отметились в комментариях к первой части. Устройства с костной проводимостью, программные решения вроде Equalizer APO 1.2.1, слуховые устройства с поддержкой Bluetooth — мы собрали и передали все ваши идеи. Может быть, что-то из этого и выйдет. Но мы расскажем о своём варианте. Возможно, он тоже кому-то будет полезен.

Подготовка

Для проверки нужно построить свои HL-аудиграммы (‘Hearing Levels’). Сгенерируем wav-образцы на ключевых частотах с постепенно возрастающей громкостью с помощью gen_hl_samples.py, прослушаем их и запишем время, когда стало слышно каждый из образцов. Далее по «линейке» сопоставим время с уровнем громкости и получим свою аудиограмму в виде набора «частота» — «уровень слышимости частоты».

В авторский код gen_hl_samples.py были внесены некоторые изменения:

  • во-первых, в дополнение к wav-файлам вместо вывода в консоль параметров «время — дБ» стали выводить своеобразную «линейку»: текстовый файлик со шкалой, который помогает по моменту времени определить уровень слышимости (одинаковый для всех ключевых частот)

    Пример линейки

    MIN      0:00        0:01        0:02        0:03        0:04        0:05        0:06        0:07        0:08        0:09        0:10     MAX ──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼──────────    volume │ -90.00 dB │ -80.00 dB │ -70.00 dB │ -60.00 dB │ -50.00 dB │ -40.00 dB │ -30.00 dB │ -20.00 dB │ -10.00 dB │   0.00 dB │    volume amplitude │   0.00003 │   0.00010 │   0.00032 │   0.00100 │   0.00316 │   0.01000 │   0.03162 │   0.10000 │   0.31623 │   1.00000 │ amplitude 
  • во-вторых, сменили направление изменения громкости на возрастание, дабы сразу не оглохнуть, начав прослушивать образец. В оригинале громкость образцов убывала.

gen_hl_samples.py

Генератор образцов для построения своей HL-аудиограммы: генерирует для каждой частоты из списка [125, 250, 500, 1000, 2000, 4000, 6000] wav-файл, громкость нарастает от vol_min до vol_max со скоростью vol_delta дБ/сек

import numpy as np from scipy.io import wavfile   def db_to_amplitude(level_dB):  # преобразование дБ в амлитуду     return np.power(10.0, level_dB / 20.0)   def amplitude_to_db(amplitude):  # преобразование амлитуты в дБ     return 20 * np.log10(amplitude)   def line_aka(time, frequency, volume, time_str='', scale_str='', vol_str='', amp_str=''):     if len(time_str) == 0 or len(scale_str) == 0 or len(vol_str) == 0 or len(amp_str) == 0:         time_str = f"MIN      {time // 60:0>1.0f}:{time % 60:0>2.0f}"  # временные отметки для шкалы         scale_str = f"──────────┼"  # шкала         # vol_str = f"  {frequency: >4} Hz │"  # отметки уровня тона         vol_str = f"   volume │"  # отметки уровня тона         amp_str = f"amplitude │"     else:         time_str += f"{' ' * 8}{time // 60:0>1.0f}:{time % 60:0>2.0f}"         scale_str += f"{'─' * 11}┼"         vol_str += f"{volume: >7.2f} dB │"         amp_str += f" {db_to_amplitude(volume): >9.5f} │"      return time_str, scale_str, vol_str, amp_str   if __name__ == "__main__":     sampleRate = 44100     duration = 1  # сек, длительность звучания тона в образце на текущем уровнем громкости      sampleCount = sampleRate * duration      s = ''  # шкала время/уровень громкости тона     for freq in [125, 250, 500, 1000, 2000, 4000, 6000]:  # Гц, частота тонов образцов         samples = np.array([])          # amp = amp_delta = 0.01  # начальная амлитуда и шаг изменения амплитуды         # amp_max = 0.1           # конечная амплитуда         vol_min = -90           # дБ, начальный уровень громкости         vol_max = 0             # дБ, конечный уровень громкости         vol_delta = 1           # дБ, шаг         t = 0                   # сек, начальный момент времени          s_time, s_scale, s_dB, s_amp = line_aka(t, freq, None)          for vol_dB in range(vol_min, vol_max + vol_delta, vol_delta):             time_points_array = np.linspace(t, t + duration, sampleCount)              samples_new = np.sin(2 * np.pi * freq * time_points_array)             samples_new *= db_to_amplitude(vol_dB)             samples = np.append(samples, samples_new)              t += duration             s_time, s_scale, s_dB, s_amp = line_aka(t, freq, vol_dB, s_time, s_scale, s_dB, s_amp)          # сохранение частотного образца в файл         wavfile.write(f"./wav_files/sine_{freq}.wav", sampleRate, samples)         print(f"{freq: >4} Hz wav file done!")      s = f"{s_time}     MAX\n{s_scale}──────────\n{s_dB}    volume\n{s_amp} amplitude"     # print(s)      with open(f"./wav_files/sine_hearing_levels_scale.txt", 'w', encoding='utf-8') as file_handle:         file_handle.write(s) 
 125 Hz wav file done!  250 Hz wav file done!  500 Hz wav file done! 1000 Hz wav file done! 2000 Hz wav file done! 4000 Hz wav file done! 6000 Hz wav file done! 

Аудиограммы готовы. Визуализируем данные с помощью matplotlib и numpy.

image_builder.py

# https://blog.demofox.org/2015/04/14/decibels-db-and-amplitude/ #   0 дБ означает полный уровень сигнала: амплитуда == 1.0, т.е. 100% громкость #   каждые 6 дБ сигнал изменяется в ~2 раза #       +n дБ - увеличение громкости #       -n дЬ - уменьшение громкости  import matplotlib.pyplot as plt from matplotlib.ticker import EngFormatter, FuncFormatter, PercentFormatter import numpy as np import matplotlib as mpl from mpl_toolkits.axes_grid1 import make_axes_locatable   def db_to_amplitude(level_db):  # преобразование дБ в амлитуду     return np.power(10.0, level_db / 20.0)   def amplitude_to_db(amplitude):  # преобразование амлитуты в дБ     return 20 * np.log10(amplitude)   audiograms = {  # decibel-frequency audiograms     1: np.array([  # An_         [125, -48.0],         [250, -42.0],         [500, -52.0],         [1000, -36.0],         [2000, -23.0],         [4000, -29.0],         [6000, -19.0],     ]),     2: np.array([  # Mi_         [125, -76.0],         [250, -68.0],         [500, -72.0],         [1000, -73.0],         [2000, -77.0],         [4000, -83.0],         [6000, -78.0],     ])}  for k, a in audiograms.items():     min_y, max_y = a[:, 1].min() * 1.1, 0     img_width, img_height = 16, 9      fig, ax = plt.subplots(1, 1, figsize=(img_width, img_height))     plt.xticks(rotation='vertical')     fig.suptitle(f"Аудиограмма {k} сотрудника Cloud4y")  # , fontsize=14)     fig.patch.set_facecolor('white')      ax.plot(a[:, 0], a[:, 1], '-o')     ax.set_xscale('log')      ax.set_xlabel(r'Частота сигнала')     ax.set_ylabel(r'лучше   <<<   Уровень слуха (слышимость сигнала)   >>>   хуже')     ax.set_ylim(min_y, max_y)     ax.xaxis.set_major_formatter(EngFormatter(unit='Гц'))     ax.yaxis.set_major_formatter(EngFormatter(unit='дБ'))     ax.xaxis.set_ticks(a[:, 0])     ax.yaxis.set_major_locator(plt.MultipleLocator(10))     ax.yaxis.set_minor_locator(plt.MultipleLocator(2))     ax.grid(axis='y')      # дополнительные вертикальные оси - способ 1     y2 = ax.secondary_yaxis('right', functions=(db_to_amplitude, amplitude_to_db))     y2.set_yscale('log')     y2.set_ylabel(r'Амплитуда сигнала')     y2.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x: .3f}"))      y3 = ax.secondary_yaxis(1.1, functions=(db_to_amplitude, amplitude_to_db))     y3.set_yscale('log')     y3.set_ylabel(r'Громкость сигнала')     # y3.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x * 100:5.1f} %"))     y3.yaxis.set_major_formatter(PercentFormatter(xmax=1, decimals=1, symbol=' %', is_latex=False))      # # дополнительные вертикальные оси - способ 2 - происходит наложение 2го графика на 1й вместо добавления Y-оси     # #     чтобы графики совпадали, нужно подправлять пределы     # ax2 = ax.twinx()     # ax2.set_yscale('log')     # ax2.plot(df[:, 0], db_to_amplitude(df[:, 1]), 'y:x')     # ax2.set_ylabel(r'Signal Amplitude')     # ax2.set_ylim(db_to_amplitude(min_y), db_to_amplitude(max_y))     # ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x:.3f}"))     # ax2.xaxis.set_ticks(df[:, 0])      ax.axhline(y=a[:, 1].max() + 1, linestyle="-", color='C2')   # demo_1_audible_normally     ax.axhline(y=np.median(a[:, 1]), linestyle="-", color='C1')  # demo_2_audible_partially     ax.axhline(y=a[:, 1].min() - 1, linestyle="-", color='C3')   # demo_3_not_audible      labels = [         "HL-Аудиограмма (Hearing Level)",         f"demo_1: {a[:, 1].max() + 1: .0f} дБ ≡ {db_to_amplitude(a[:, 1].max() + 1): .5f}, корректировка не потребуется",         f"demo_2: {np.median(a[:, 1]): .0f} дБ ≡ {db_to_amplitude(np.median(a[:, 1])): .5f}, для частичной корректировки",         f"demo_3: {a[:, 1].min() - 1: .0f} дБ ≡ {db_to_amplitude(a[:, 1].min() - 1): .5f}, для полной корректировки по аудиограмме",     ]     plt.legend(labels=labels)      divider = make_axes_locatable(ax)     cax = divider.append_axes("left", size="0.7%", pad=-.09)     cax.set_ylim(min_y, max_y)     norm = mpl.colors.Normalize(vmin=min_y, vmax=max_y)     cmap = mpl.cm.ScalarMappable(norm=norm, cmap='RdYlGn_r', )     fig.colorbar(cmap, cax=cax)     cax.yaxis.set_major_locator(plt.MultipleLocator(10))     cax.yaxis.set_minor_locator(plt.MultipleLocator(2))     cax.set_yticklabels([])      fig.tight_layout()     plt.show() 

На каждой аудиограмме отмечены три уровня громкости, ‘нормально слышно’, ‘частично слышно’, ‘совсем не слышно’, на которых мы сгенерируем с помощью gen_continuous_sample.py демки, которые затем обработаем их алгоритмом, чтобы понять, насколько хорошо проходит обработка.

gen_continuous_sample.py

Генерация образцов с тремя разными постоянными уровнями громкости, частота меняется от freq_start до freq_end со скоростью freq_inc/time_inc Гц/сек.

from scipy.io import wavfile import numpy as np   def db_to_amplitude(level_db):  # преобразование дБ в амлитуду     return np.power(10.0, level_db / 20.0)   def amplitude_to_db(amplitude):  # преобразование амлитуты в дБ     return 20 * np.log10(amplitude)   def generate_samples(sample_rate, level=-20.0):     amp = db_to_amplitude(level)  # коэффициент амлитуды (уровня громкости) сигнала (1 ≡ 0 dB)      freq_start = 125  # Гц     freq_end = 8000  # Гц      freq_inc = 25  # Гц, шаг увеличения тональности     time_inc = .25  # сек, длительность сигнала перед увеличением тональности      print(f"amplitude {amp: .5f} ≡ {np.round(20 * np.log10(amp), 2)} dB")      inc_sample_count = int(sample_rate * time_inc)     sample = np.array([])      freq = freq_start     t = 0.0     while freq < freq_end:         time_points_array = np.linspace(t, t + time_inc, inc_sample_count, endpoint=False)          new_samples = np.sin(2 * np.pi * freq * time_points_array)         new_samples *= amp          sample = np.append(sample, new_samples)          freq += freq_inc         t += time_inc      return sample   if __name__ == '__main__':     levels_a = {'demo_1_audible_normally': -18.0, 'demo_2_audible_partially': -36.0, 'demo_3_not_audible': -53.0}  # An_     levels_m = {'demo_1_audible_normally': -67.0, 'demo_2_audible_partially': -76.0, 'demo_3_not_audible': -84.0}  # Mi_     levels = {1: levels_a, 2: levels_m}     sampleRate = 44100      for k, l in levels.items():         for name, level in l.items():             generated_samples = generate_samples(sampleRate, level=level)             wavfile.write(f"./wav_files/{k}_{name}.wav", sampleRate, generated_samples) 
amplitude  0.12589 ≡ -18.0 dB amplitude  0.01585 ≡ -36.0 dB amplitude  0.00224 ≡ -53.0 dB amplitude  0.00045 ≡ -67.0 dB amplitude  0.00016 ≡ -76.0 dB amplitude  0.00006 ≡ -84.0 dB 

Демо-wav готовы для обработки с помощью gain.py.

gain.py

from scipy.io import wavfile  import numpy as np import time import os.path   # находит и возвращает главную частоту в заданном окне и уровень сигнала def get_dominant_freq(sample_rate, window):     # область частот окна     yf = np.fft.fft(window)     yf = np.abs(2 * yf / len(window))      window_size = len(window)     window_half = len(window) // 2      max_amp = yf[:window_half].max()     max_amp_idx = yf[:window_half].argmax()      # поиск частоты по её индексу     frequency = sample_rate * max_amp_idx / window_size  # frequency = (sample_rate/2) * max_amp_idx / (window_size/2)      return frequency, amplitude_to_db(max_amp)   def get_gain(freq, dB, audiogram):  # возвращает необходимый сдвиг уровня для сигнала для указанной частоты согласно аудиограмме     threshold = None      if freq < audiogram[0][0]:         threshold = audiogram[0][1]     else:         for i in range(len(audiogram) - 1):             if freq >= audiogram[i + 1][0]:                 continue             threshold = (audiogram[i + 1][1] - audiogram[i][1]) * \                         (freq - audiogram[i][0]) / (audiogram[i + 1][0] - audiogram[i][0]) + audiogram[i][1]             break          if threshold is None:             threshold = audiogram[-1][1]      if threshold <= dB:  # уровень сигнала уже в зоне слышимости         return 0.0     else:  # сдвиг уровня сигнала в зону слышимости         return threshold - dB + 3   def db_to_amplitude(dB):  # преобразование дБ в амлитуду     return np.power(10.0, dB / 20.0)   def amplitude_to_db(amp):  # преобразование амлитуты в дБ     return 20 * np.log10(amp)   if __name__ == "__main__":     start_time = time.time()      df_1 = np.array([  # An_         [125, -48.0],         [250, -42.0],         [500, -52.0],         [1000, -36.0],         [2000, -23.0],         [4000, -29.0],         [6000, -19.0],     ])     df_2 = np.array([  # Mi_         [125, -76.0],         [250, -68.0],         [500, -72.0],         [1000, -73.0],         [2000, -77.0],         [4000, -83.0],         [6000, -78.0],     ])      # считывание демо-файлов     wavfiles = [         ['1_demo_1_audible_normally.wav', '1_demo_2_audible_partially.wav', '1_demo_3_not_audible.wav'],         ['2_demo_1_audible_normally.wav', '2_demo_2_audible_partially.wav', '2_demo_3_not_audible.wav']     ]      for files, audiogram in zip(wavfiles, [df_1, df_2]):         for file in files:             file_name = file.split('.')[0]             sampleRate, samples = wavfile.read(os.path.join(os.getcwd(), 'wav_files', file), mmap=True)             outSamples = np.array([])  # набор обработанных участков для нового файла             windowSize = 512             sampleIdx = 0             while sampleIdx < len(samples):                 window = samples[sampleIdx:sampleIdx + windowSize]                  freq, dB = get_dominant_freq(sampleRate, window)                 gain = get_gain(freq, dB, audiogram=audiogram)                  startSec = np.round(sampleIdx / sampleRate, 2)                 endSec = np.round((sampleIdx + windowSize) / sampleRate, 2)                  if gain > 0.0:                     # print(f"{file_name}\t{startSec: >6.3f}..{endSec: <6.3f}s\tGAINED\t{freq: >8.2f} Hz\tvol {dB: >6.2f} dB\tgain: {gain: >6.2f} dB")                     amp = db_to_amplitude(gain)                     amplified = window * amp                 else:                     # print(f"{file_name}\t{startSec: >6.3f}..{endSec: <6.3f}s\tSTOCK\t{freq: >8.2f} Hz\tvol {dB: >6.2f} dB\tgain: {gain: >6.2f} dB")                     amplified = window                  outSamples = np.append(outSamples, amplified)                  sampleIdx += windowSize              processed_file = os.path.join(os.getcwd(), 'wav_files', f"{file_name}_processed.wav")             wavfile.write(processed_file, sampleRate, outSamples)             print('ready', os.path.relpath(processed_file))      print(f"{(time.time() - start_time)} seconds") 
ready wav_files\1_demo_1_audible_normally_processed.wav ready wav_files\1_demo_2_audible_partially_processed.wav ready wav_files\1_demo_3_not_audible_processed.wav ready wav_files\2_demo_1_audible_normally_processed.wav ready wav_files\2_demo_2_audible_partially_processed.wav ready wav_files\2_demo_3_not_audible_processed.wav 180.6359121799469 seconds 

Всё готово! Можно послушать демки, оценить обработку, попробовать обработать реальные аудиофайлы, наконец. Результат не идеален, но вполне рабочий, демки стало слышно.

Ради интереса и объективного сравнения можно даже посмотреть файлы «до» и «после» в Praat. Становится видно изъян алгоритма: усиливаются те частоты, которые заведомо находятся в зоне слышимости и усиливаться не должны. Это особенно заметно на первой демке, которая была сгенерирована заведомо слышимой на всем частотном диапазоне:

Спасибо за внимание и будьте здоровы!


Что ещё интересного есть в блоге Cloud4Y

→ Изучаем своё железо: сброс паролей BIOS на ноутбуках

→ Частые ошибки в настройках Nginx, из-за которых веб-сервер становится уязвимым

→ Хомяк торгует криптовалютой не хуже, чем трейдер

→ Облачная кухня: готовим данные для мониторинга с помощью vCloud API и скороварки

→ Эксперимент для сотрудника с нарушением слуха, ч. 1

Подписывайтесь на наш Telegram-канал, чтобы не пропустить очередную статью. Пишем не чаще двух раз в неделю и только по делу.

ссылка на оригинал статьи https://habr.com/ru/company/cloud4y/blog/567318/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *