いぬおさんのおもしろ数学実験室

おいしい紅茶でも飲みながら数学、物理、工学、プログラミング、そして読書を楽しみましょう

肉声を電話の声に変換する

 この記事に直接関連した内容の書籍をブログ管理人が出版しました。記事をリファインして、極力詳しく解説しています。見ていただけるとうれしいです(2022年5月15日(日))。↓

 電話ので人の声を聞くと、ザラついていると言うか、いかにも「機械を通した声」と言うか、うまい表現の方法がこれ以上見つかりませんがそんな感じに聞こえます。とにかく直に聞いたのとは違いますよね。今回は肉声を電話を通したときの声に変換してみましょう。
 人間は大体20Hz~20000Hzの音を聞き取るそうです(可聴域)。身の回りの音には様々な周波数の成分が混ざっています。このうち、可聴域の外の周波数の音はあってもなくても人間には変わりなく聞こえます(一応……)。電話は通す音の周波数を300Hz~3400Hzに制限しているのだそうです。可聴域に比べるとずっと狭い。肉声と違って聞こえるのはこれが原因なんでしょうが、人の声に含まれる周波数の主要部分は大体この辺だそうで、十分に伝わるわけです。この周波数帯の外をプログラムでカットしてみます。電話の声に近いものが得られるはずです。例によってPythonを使いました。

import sys
import numpy as np
import scipy as sp
from scipy.fftpack import fft
import wave
N = 8192 #FFTするサイズ。2の累乗。
#
#★ window = np.hamming(N)
#--------------------------------
#サウンドデータの高域成分をカットする
#
#databuf: サウンドデータの入ったバッファ
#num1: 第num1次高調波から第num2高調波までを残して消す
def editsound(databuf, num1, num2):
    databuf1 = np.zeros(databuf.size)
    #databuf1 = databuf
    for i in range(int(databuf.size / N)):
        #Nフレームずつ処理
        #★ databuf2 = window * databuf[N * i: N * (i+1)]
        databuf2 = databuf[N * i: N * (i+1)] #★★
        yf = sp.fftpack.fft(databuf2)
        for j in range(1, num1):
            yf[j] = 0 #低周波成分を0に
            yf[N - j] = 0
        for j in range(num2+1, int(N/2)):
            yf[j] = 0 #高周波成分を0に
            yf[N - j] = 0
        #ifftの結果は複素数。もとが実データなので結果の虚数部分は0。
        #だがコピー先は複素数の配列ではないので実数部分だけをコピーする。
        databuf1[N * i: N * (i+1)] = sp.fftpack.ifft(yf).real
    return databuf1
#--------------------------------
wavefile = wave.open('人の声.wav',"rb")
buf = wavefile.readframes(wavefile.getnframes())#サウンドデータ部分の読み取り
buf = np.frombuffer(buf, dtype= "int16")#16bit符号付き整数に変換
wavefile.close()
print(wavefile.getparams())
sounddata = buf.copy() #サウンドデータ用バッファ
sounddata[0::2] = editsound(sounddata[0::2], 56, 631) #左の音を編集
sounddata[1::2] = editsound(sounddata[1::2], 56, 631) #右の音を編集
writewave = wave.Wave_write("電話の音に.wav") #保存先ファイル名
writewave.setparams(wavefile.getparams()) #保存先ファイルにもとと同じパラメータをセット
writewave.writeframes(sounddata) #編集したサウンドデータを保存
writewave.close()
sys.exit()

人の声の入ったwavファイルを用意し(「人の声.wav」)、プログラムで読み込みます。FFTで周波数成分を調べ、300Hz未満、3400Hzを超える周波数に対応する高調波を全てカットします。そしてIFFTで波形を合成し、「電話の声.wav」に保存しています。実際に聞いてみると……確かに電話の音っぽい!!
 プログラムでは指定の高調波のみを残す関数を書き(editsound(databuf, num1, num2))、第56次高調波から第361高調波までを残して他をカットしています。例えば300Hzは何次の高調波なのかは単純な比例計算で分かります。今回はFFTをN=8192データずつ実行しているので、FFTの性質から第8192次高調波がサウンドデータのサンプリング周波数です。ぼくが使った音声データのサンプリング周波数は44100Hzだったので、カットする限界の周波数、300Hzが第何次の高調波なのかは 300:44100=n:8192 とおいてnを求めれば分かります。計算するとn=55.7……なので、少し多めに切り取る考えで第56次高調波以上を残しました。上の方も同じように計算できます。なお、FFTの基本的な性質については過去の記事があります。必要に応じてご参照ください。
www.omoshiro-suugaku.com

 一応電話の声にはなったんですが、注意して聞くと定期的に小さい音が「プチ、プチ、……」と入っていることが分かります。「??」と思っていろいろ考えました。これ、多分8192データ毎の音です。カンですがそもそもFFTは「相手にするデータは周期的である」という前提で実行するものです。今回の音声データは周期的にはなっていないので、どこかで変なことが起こっても不思議はありません。念のため8192*4データ毎にFFTを実行すると「プチ、プチ」も間隔が延びました。ことによると「窓関数を使えば解決か!?」と思ってコードの★の部分を生かし、★★の部分をコメントアウトしてみたら8192データ毎に音量が小→大→小となる感じで、目的は果たせませんでした。このブログではまだ窓関数については書いていませんが、これはもともと周期的でもなく8192データ(今回の例)毎の継ぎ目も連続でない音声データを相手にFFTを実行するために起こる不具合を解消するためのものです。いずれまた書きましょう。