Pythonで女性の声を音声変換してみた!フーリエ変換による音声データmp3,wavの編集とwavへの出力

Python

この記事の内容

この記事では,Pythonを用いて音声データを編集(声を低くしたり,高くしたり,大きくしたり,小さくしたりなど)する方法を書きます.

環境は,Windowsです.

以降で説明するソースコードで,以下のように,元の音声データを低くしたり,高くしました.

編集前の音声

編集後の音声

(低くした音声)

(高くした音声)

雑音が入っていて,かなり聞きづらい感じになっていますが,声は低く,もしくは高くなっていることが分かります.音声の精度は編集者の腕次第ということで,今回は編集方法のみを紹介します.

手順としては,

1.Pythonで音声データ(形式はmp3もしくはwav)を取り込み,

2.フーリエ変換を用いて編集した後,

3.逆フーリエ変換で時系列データに戻して,

4.音声データを取り出す(wav形式)

という感じです.

音声の取り込みはffmpegでサポートされている,PythonモジュールPydubで取り込めるものならOKですが,出力にはscipy.io.wavfileを用いるのでとりあえずwav限定です.

他の音声出力形式が欲しい場合は,wavを他のソフトなどでmp3などに変換するか,他のモジュールを探すなどが必要です.

※素人が行ったものなので,至らぬ点があると思いますが,その場合はコメント欄にてご指摘いただけると幸いです.

もう少しきれいに変換出来たら,再度本記事を書き直します.

準備

実行するには,以下の準備が必要です.

Pythonのインストール

ffmpegのインストール(音声データモジュールPydub使用取り込みのため)

Pythonで音声データをフーリエ変換,編集する方法

ライブラリのインポート

# 必要なモジュールをインポート
from pydub import AudioSegment #音声データの取り込みのため
import matplotlib.pyplot as plt #グラフ可視化のため
import numpy as np #色々な計算に使う
from scipy import fftpack #フーリエ変換に使う
from scipy.io.wavfile import write #音声データ出力のため
import copy #編集のとき,元データを取っておくために使う

音声データ(時系列データ)を取り込む

以下のaudio.mp3を実行ファイル(pythonファイル)と同じディレクトリに置きます.この音声データは,こちらのサイトで取得しました.

音声データを取り込みます.

# ファイルの読み込み
sourceAudio = AudioSegment.from_mp3("audio.mp3")
#sourceAudio = AudioSegment.from_wav("audio.wav")

wavファイルを取り込む場合は,コメントアウトの方を使用ください.

音声データを取り込めれば,以下で各種パラメータを取得できます.

#動画の長さを取得
AudioLength = sourceAudio.duration_seconds
print('音声データの秒数', AudioLength, 'sec')

#音声のフレームレート
FrameRate = sourceAudio.frame_rate
print('フレームレート', FrameRate, 'Hz')

ただし,sourceAudioのままではデータを加工できませんから,時系列のリストとして変数にいれます.低いレベルでデータを編集するなら,ここが大事です.

# 音声データをリストで抽出
wave = sourceAudio.get_array_of_samples()

グラフに表示してみると,こんな感じです.

# リストをグラフ化
plt.plot(wave)
plt.show()
plt.close()

あとは,後で使用する音声に関するパラメータを計算しておきます.

N = len(wave) #音声データのデータ個数

dt = 1/FrameRate/2 # = AudioLength/N データ間隔(sec)

高速フーリエ変換(FFT)する

FFTは,Pythonならモジュールを使って簡単にできます.今回は,scipy の fftpackを使用します.

# FFT処理
fft = fftpack.fft(wave)                # FFT(実部と虚部)

たったこれだけで,音声データwaveをFFTしたデータfftが取得できます.

FFTは,各要素が複素数のリストとなっています.

あとで可視化できるように,振幅(絶対値)と周波数のリストを用意しておきましょう.

fft_amp = np.abs(fft / (N / 2))             # 振幅成分を計算
samplerate = N / AudioLength
fft_axis = np.linspace(0, samplerate, N)    # 周波数軸を作成

とりあえず,そのまま逆フーリエ変換してみる

FFTとIFFT(逆高速フーリエ変換)が正しくできているかを確認します.

IFFTは,以下でできます.

# IFFT処理
ifft_time = fftpack.ifft(fft) #この時点ではまだ複素数

グラフに可視化してみます.グラフを表示する関数PLOTを以下とします.

表示,出力するIFFT後のデータは実数部分だけでOKです.

#グラフを表示する関数
def PLOT():
    # フォントの種類とサイズを設定する。
    plt.rcParams['font.size'] = 14
    plt.rcParams['font.family'] = 'Times New Roman'

    # 目盛を内側にする。
    plt.rcParams['xtick.direction'] = 'in'
    plt.rcParams['ytick.direction'] = 'in'

    # グラフの上下左右に目盛線を付ける。
    fig = plt.figure()
    ax1 = fig.add_subplot(211)
    ax1.yaxis.set_ticks_position('both')
    ax1.xaxis.set_ticks_position('both')
    ax2 = fig.add_subplot(212)
    ax2.yaxis.set_ticks_position('both')
    ax2.xaxis.set_ticks_position('both')

    # 軸のラベルを設定する。
    ax1.set_xlabel('Frequency [Hz]')
    ax1.set_ylabel('y')
    ax2.set_xlabel('Time [s]')
    ax2.set_ylabel('y')

    # データの範囲と刻み目盛を明示する。
    ax1.set_xlim(0, int(max(fft_axis)/2))

    # 時間軸生成
    t = np.arange(0, AudioLength, dt)

    # データプロットの準備とともに、ラベルと線の太さ、凡例の設置を行う。
    ax1.plot(fft_axis, fft_amp, label='signal', lw=1)
    ax2.plot(t, wave, label='original', lw=5)
    ax2.plot(t, ifft_time.real, label='ifft', lw=1)

    fig.tight_layout()
    #plt.legend()

    # グラフを表示する。
    plt.show()
    plt.close()
#グラフ表示
print("グラフ表示中…")
PLOT()

FFT(上段)の方は,リストの半分だけ表示しています(FFTを計算すると,これと対称なデータも生成されます).

下段は元データとIFFTデータを表示したグラフですが,上手くIFFTできていることがわかります.これで,安心してデータを編集できます.

以降では,このFFTデータ(上段)をいじってIFFTすることにより,元の音声データを編集していきます.

周波数空間で音声データを編集する

さて,ここからが本題です.

FFTした周波数空間でのデータを加工することにより,それをIFFTした音声を編集します.

どのように加工するかですが,例えば以下の考え方でやっていきます.

・FTTデータを高い方向にシフトさせれば,それをIFFTした音声は高くなる

・FTTデータを低い方向にシフトさせれば,それをIFFTした音声は低くなる

・FTTデータの振幅を小さくすれば,それをIFFTした音声も小さくなる

・FTTのある周波数の振幅を小さくすれば,その周波数の音声は消える.例えば,高い周波数成分を削れば,IFFTした音声からは高周波数の雑音が消える

今回は,とりあえず周波数をシフトさせて音声を低くしたり,高くします.

また,シフトさせて余った部分はゼロにするため,IFFTした音声は元データよりも振幅が小さくなるため,振幅を大きくする操作も行います.

 男性の話し声は500Hz, 女性の話し声は1,000Hzなので500Hzシフトさせれば音声変換できるはずですが,500Hzではイマイチ分かりにくかったので1,000~1,500Hzくらいシフトさせます.

shift_frequencyを正の値にすれば低く,負の値にすれば高くなります.

# 元データを保管
fft_original = copy.copy(fft)

# 周波数をシフト
# shift_frequencyがプラスで周波数が低く,マイナスで高くなる
shift_frequency = 1500 # シフトさせる周波数(Hz)
shift = int(shift_frequency*len(fft)/FrameRate) #周波数→データインデックスにスケール変換
for f in range(0, int(len(fft)/2)):
    if( (f+shift > 0) and (f+shift < int(len(fft)/2)) ):
        fft[f] = fft_original[f+shift]
        fft[-1*f] = fft_original[-1*f-shift]
    else:
        fft[f] = 0
        fft[-1*f] = 0

改めて振幅を計算します.

fft_amp = np.abs(fft / (N / 2))             # 振幅成分を計算

逆高速フーリエ変換(IFFT)して音声データを時系列に戻す

編集したデータをIFFTします.

# IFFT処理
ifft_time = fftpack.ifft(fft) #この時点ではまだ複素数

グラフをプロットします.

#グラフ表示
print("グラフ表示中…")
PLOT()

FFTデータが左にシフトしていることが分かると思いますが,振幅は削られているのでそれをIFFTしたデータの振幅も元データよりも小さくなっています.

そのため,出力される音声データは小さくなりますから,振幅を大きくしましょう.

以下のような関数を作成します.

# 自動的に増幅する振幅を計算する関数
def Auto_amp_coefficient(original_data, edited_data):

    amp = max(original_data)/max(edited_data)

    return amp

やっていることは単純で,小さくなったIFFTを何倍大きくするかを決定する関数です.

元データと編集データがおおよそ相似であると仮定して,元データと編集データの一番大きな値の比を計算します.

(本当はいくつかサンプリングしてその比の平均値を計算したかったのですが,なんかうまくいかなかったので単純化しました)

求まったampを,編集データIFFTにかけます.

# 音量調整
print('音量調節中…')
amp = Auto_amp_coefficient(wave, ifft_time.real)
ifft_time.real *= amp

これをグラフにすると,編集した音声が元のデータと同じくらいになっていることがわかります.

#グラフ表示
print("グラフ表示中…")
PLOT()

音声データをwavファイルとして出力

最後に,編集した音声データリストをwavファイルとして出力します.

# 編集後のデータを改めて定義
audio_data = ifft_time.real

# 以下,音声データ出力
print('データ生成中…')
data = [[0 for x in range(2)] for i in range(len(audio_data))]
for i in range(len(ifft_time)):
    data[i][0] = audio_data[i]
    data[i][1] = audio_data[i]

data = np.array(data, dtype='int16')

write('out.wav', 2*FrameRate, data)

print('データ出力完了')

scipy.io.wavfileを用いているためデータ整形にひと手間必要ですが,Pydubで出力する方法がなさそうだったのでこのモジュールを使用しました.もっと簡単にリストをmp3やwavに出力できる方法があれば,教えてください( ´∀` )

出力された音声データ

まとめコード

# 音声編集ソースコード

# 必要なモジュールをインポート
from pydub import AudioSegment
import matplotlib.pyplot as plt
import numpy as np
from scipy import fftpack
from scipy.io.wavfile import write
import copy

# 自動的に増幅する振幅を計算する関数
def Auto_amp_coefficient(original_data, edited_data):
    sampling_num = 10000 #サンプル数.増やすと精度が上がる
    sample = np.random.randint(0, len(original_data), sampling_num)
    '''
    amp_sum = 0
    for s in sample:
        amp_sum += abs(original_data[s])/abs(edited_data[s])
    amp = amp_sum/sampling_num
    '''
    amp = max(original_data)/max(edited_data)

    return amp

#グラフを表示する関数
def PLOT():
    # フォントの種類とサイズを設定する。
    plt.rcParams['font.size'] = 14
    plt.rcParams['font.family'] = 'Times New Roman'

    # 目盛を内側にする。
    plt.rcParams['xtick.direction'] = 'in'
    plt.rcParams['ytick.direction'] = 'in'

    # グラフの上下左右に目盛線を付ける。
    fig = plt.figure()
    ax1 = fig.add_subplot(211)
    ax1.yaxis.set_ticks_position('both')
    ax1.xaxis.set_ticks_position('both')
    ax2 = fig.add_subplot(212)
    ax2.yaxis.set_ticks_position('both')
    ax2.xaxis.set_ticks_position('both')

    # 軸のラベルを設定する。
    ax1.set_xlabel('Frequency [Hz]')
    ax1.set_ylabel('y')
    ax2.set_xlabel('Time [s]')
    ax2.set_ylabel('y')

    # データの範囲と刻み目盛を明示する。
    ax1.set_xlim(0, int(max(fft_axis)/2))

    # 時間軸生成
    t = np.arange(0, AudioLength, dt)

    # データプロットの準備とともに、ラベルと線の太さ、凡例の設置を行う。
    ax1.plot(fft_axis, fft_amp, label='signal', lw=1)
    ax2.plot(t, wave, label='original', lw=5)
    ax2.plot(t, ifft_time.real, label='ifft', lw=1)

    fig.tight_layout()
    plt.legend()

    # グラフを表示する。
    plt.show()
    plt.close()

# ファイルの読み込み
sourceAudio = AudioSegment.from_mp3("audio.mp3")
#sourceAudio = AudioSegment.from_wav("audio.wav")

#動画の長さを取得
AudioLength = sourceAudio.duration_seconds
print('音声データの秒数', AudioLength, 'sec')

#音声のフレームレート
FrameRate = sourceAudio.frame_rate
print('フレームレート', FrameRate, 'Hz')

# 音声データをリストで抽出
wave = sourceAudio.get_array_of_samples()

N = len(wave)
print("データ個数", N)

dt = 1/FrameRate/2 # = AudioLength/N
print('dt', dt, 'sec')

# リストをグラフ化
#plt.plot(wave)
#plt.grid()
#plt.show()
#plt.close()

# FFT処理
fft = fftpack.fft(wave)                # FFT(実部と虚部)


#編集 -------------------------------------
print('編集中…')

# 元データを保管
fft_original = copy.copy(fft)

# shiftがプラスで周波数が低くなる
shift_frequency = 1500
shift = int(shift_frequency*len(fft)/FrameRate)
#編集
for f in range(0, int(len(fft)/2)):
    if( (f+shift > 0) and (f+shift < int(len(fft)/2)) ):
        fft[f] = fft_original[f+shift]
        fft[-1*f] = fft_original[-1*f-shift]
    else:
        fft[f] = 0
        fft[-1*f] = 0

# -----------------------------------------

fft_amp = np.abs(fft / (N / 2))             # 振幅成分を計算
samplerate = N / AudioLength
fft_axis = np.linspace(0, samplerate, N)    # 周波数軸を作成

# IFFT処理
ifft_time = fftpack.ifft(fft) #この時点ではまだ複素数


# 音量調整
print('音量調節中…')
amp = Auto_amp_coefficient(wave, ifft_time.real)
ifft_time.real *= amp


#グラフ表示
print("グラフ表示中…")
PLOT()

# 編集後のデータを改めて定義
audio_data = ifft_time.real

# 以下,音声データ出力
print('データ生成中…')
data = [[0 for x in range(2)] for i in range(len(audio_data))]
for i in range(len(ifft_time)):
    data[i][0] = audio_data[i]
    data[i][1] = audio_data[i]

data = np.array(data, dtype='int16')

write('out.wav', 2*FrameRate, data)

print('データ出力完了')

コメント

タイトルとURLをコピーしました