マイクで音声を録音した時、録音環境によっては不快なノイズが入ってしまいます。Pythonの外部ライブラリであるnoisereduceは難しいコードなしに簡単にノイズキャンセルをかけることができます。実波形を例にその特徴を解説します。
こんにちは。wat(@watlablog)です。今回はライブラリの力を借りてノイズキャンセルのコードを検証してみます!
noisereduceでノイズキャンセルするPythonコード
noisereduceについて
今回はnoisereduceという外部ライブラリを使ってノイズキャンセルを行います。noisereduceの公式ドキュメントは以下のGitHubをご確認ください。
GitHub:https://github.com/timsainb/noisereduce
信号処理技術におけるノイズ除去は古くから歴史があり、スペクトル・サブトラクションやウィーナーフィルタといった様々な方法があります。
今回紹介するnoisereduceは、公式ページからするとNoise Gateという手法を使っているとのことです。
この方法は信号の入力レベルにより閾値を設け、閾値を超えた信号をPassするもの…と読み取れますが、まだ詳しく学んでいないので詳細は以下のWikipediaに丸投げします。
Wikipedia:Noise Gate
この記事では単純にnoisereduceを使ってみた結果を紹介します。まずはpip install noisereduceでライブラリをインストールしましょう。
動作環境
本記事のPython環境は以下のとおりです。フーリエ変換で周波数成分を確認するためにSciPy等も使っています。
Python | Python 3.9.6 |
---|---|
PyCharm (IDE) | PyCharm CE 2020.1 |
Numpy | 1.21.1 |
Scipy | 1.4.1 |
matplotlib | 3.4.3 |
PySoundFile | 0.9.0.post1 |
noisereduce | 2.0.1 |
サンプルのwavファイル
noisereduceは録音された信号波形とは別に、背景ノイズの波形も必要です。コード内で使用するサンプルのwavファイルを用意しました。ダウンロードしてお使いください。
ピアノの音
Piano-A5.wavは筆者所有の電子ピアノの音をケーブルを使ってMacbookに取り込んだ音声です。5番目のA(ラ)の音を打鍵しました。
※再生ボタンクリックで音が出ます。
ハムノイズ
Ham-Noise.wavは打鍵していない時の背景ノイズです。ブーという音が鳴っています。
※再生ボタンクリックで音が出ます。
電子ピアノからケーブルでPCに接続していますが、どうやら電源ノイズ(いわゆるハムノイズ)が入っているようです。通常はそのようなノイズが入らないように録音するのが良いのですが、今回は良いサンプルとして使いましょう。
サンプル音声の波形観察
二つの音声を比較します。波形全体を観察すると、Piano-A5.wavは暗ノイズに比べて十分大きな振幅を持った減衰自由振動をしていることがわかります。
しかし無音区間を拡大すると、特徴的な時間波形になっていることがわかりました。二つのファイルは別々に録音されたものなので、位相はずれています。
周波数領域で見るとノイズは50[Hz]の倍数で綺麗に高周波帯域まで存在していることがわかります。これがブー音の正体ですね。
A5音の基本周波数である440[Hz]近傍がこちら。noisereduceでどこまで消せるのか、試してみましょう。
noisereduceのサンプルコード
noisereduceはノイズを除去したい信号yとノイズのみの信号y_noise、サンプリングレートsrを指定して実行します。
今回は定常のノイズであるため、stationary=Falseにする必要があります(デフォルトのTrueだと非定常のノイズ除去になる)。
1 2 |
# ノイズキャンセル wave = nr.reduce_noise(y=wave_signal, y_noise=wave_noise, sr=samplerate, stationary=False) |
この辺は公式ページを参考にしてください。
波形プロットやwavファイルの入出力を追加した全コードを以下に示します。
このコードを実行すると、読み込んだ波形や周波数分析の結果がプロットされていき、プログラム実行ディレクトリにNC-Piano-A5.wavという新しい音声ファイルが作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
import numpy as np from scipy import fftpack import soundfile as sf import noisereduce as nr from matplotlib import pyplot as plt def create_waves(file1, file2): ''' 原信号とノイズ信号を生成 ''' # wavファイルの読み込み wave_sn, samplerate = sf.read(file1) wave_noise, samplerate = sf.read(file2) dt = 1 / samplerate t = np.arange(0, len(wave_sn) * dt, dt) # 波形確認 plot([t, t], [wave_sn, wave_noise], ['Piano-A5.wav', 'Ham-Noise.wav'], 'Time [s]', 'Amplitude', (8, 4), [0, 0], [0, 0], 0, 0) return wave_sn, wave_noise, samplerate def noise_cancel(wave_signal, wave_noise, samplerate, filename_out): ''' Noise波形を使ってSignal+Noise波形からNoiseを低減 ''' # ノイズキャンセル wave = nr.reduce_noise(y=wave_signal, y_noise=wave_noise, sr=samplerate, stationary=False) # 音声を保存 sf.write(filename_out, wave, samplerate) # 波形確認 dt = 1 / samplerate t = np.arange(0, len(wave) * dt, dt) plot([t, t], [wave_signal, wave], ['Original', 'Noise Cancel'], 'Time [s]', 'Amplitude', (8, 4), [0, 0], [0, 0], 0, 0) return wave def calc_fft(data, samplerate): ''' フーリエ変換 ''' spectrum = fftpack.fft(data) amp = np.sqrt((spectrum.real ** 2) + (spectrum.imag ** 2)) amp = amp / (len(data) / 2) phase = np.arctan2(spectrum.imag, spectrum.real) phase = np.degrees(phase) freq = np.linspace(0, samplerate, len(data)) return amp, freq def plot(t, x, label, xlabel, ylabel, figsize, xlim, ylim, xlog, ylog): ''' 汎用プロット関数(1プロット重ね書き) ''' # フォントの種類とサイズを設定する。 plt.rcParams['font.size'] = 14 plt.rcParams['font.family'] = 'Times New Roman' # 目盛を内側にする。 plt.rcParams['xtick.direction'] = 'in' plt.rcParams['ytick.direction'] = 'in' # Subplot設定とグラフの上下左右に目盛線を付ける。 fig = plt.figure(figsize=figsize) ax1 = fig.add_subplot(111) ax1.yaxis.set_ticks_position('both') ax1.xaxis.set_ticks_position('both') # 軸のラベルを設定する。 ax1.set_xlabel(xlabel) ax1.set_ylabel(ylabel) # スケールを設定する。 if xlim != [0, 0]: ax1.set_xlim(xlim[0], xlim[1]) if ylim != [0, 0]: ax1.set_ylim(ylim[0], ylim[1]) # 対数スケール if xlog == 1: ax1.set_xscale('log') if ylog == 1: ax1.set_yscale('log') # プロットを行う。 for i in range(len(x)): ax1.plot(t[i], x[i], label=label[i], lw=1) ax1.legend() # レイアウト設定 fig.tight_layout() # グラフを表示する。 plt.show() plt.close() return if __name__ == '__main__': ''' メイン処理 ''' # サンプル音声を用意 file1 = 'Piano-A5.wav' file2 = 'Ham-Noise.wav' wave_sn, wave_noise, samplerate = create_waves(file1, file2) # ノイズキャンセル file_out = 'NC-Piano-A5.wav' wave = noise_cancel(wave_sn, wave_noise, samplerate, file_out) # 周波数分析 fft_signal, freq = calc_fft(wave_sn, samplerate) fft_canceled, freq = calc_fft(wave, samplerate) fft_noise, freq = calc_fft(wave_noise, samplerate) # 波形確認 plot([freq, freq], [fft_signal, fft_canceled], ['Original', 'Noise Cancel'], 'Frequency [Hz]', 'Amplitude', (8, 4), [0, 5000], [0, 0], 0, 1) |
実行結果
下図が元音声とnoisereduceによるノイズキャンセル後の音声を比較した周波数波形です。
拡大するとこちら。見事50[Hz]の倍数成分の消失を確認できます…が、ちょっと気になる点として、振幅成分が大きく変化している部分を挙げることができます。
時間波形を見ると明らかです。ノイズを強力に除去することはできたものの、原音声から遠ざかってしまう結果となりました。
ノイズキャンセル後の音声を再生できるようにしてみました。減衰が大きく、すぐに音が消えてしまうという結果です。
※再生ボタンクリックで音が出ます。
時定数time_constant_sを5等に増やしたり、prop_decreaseを0.5等に減らすと減衰度合いは減ってきますが、ノイズ低減効果も薄れてしまうようです。
まとめ
この記事ではnoisereduceというPythonの外部ライブラリを使ってノイズキャンセルをしてみました。元音声とノイズ音声を使うというのは直感的にわかりやすく、強力なノイズ低減の効果を確認することができました。
サンプルで用意した音声は録音時によく問題となる電源ノイズが混入したファイルでしたが、noisereduceによって完全に消すことができました。
しかし減衰が大きく元音声の特徴も崩してしまうこともわかりました。
ここは設定の仕方で改善できるかも知れませんが、何かしらフィルタをかけるというのはそういうことなのかも知れません。
例えば会議のレコーディングノイズを除去するといった場合にこのライブラリはかなり活用できそうです。
しかし、原音をできる限り維持させつつノイズだけ消したいといった目的には別の手法を考えた方が良いでしょう(その場合はやはりハード的な対策しかないかも)。
ノイズ除去の分野は奥が深そうですが、1行で使えるノイズ低減コードを手に入れることができました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!