🎙️はじめに
SNS通話で緊張した経験、ありませんか?
「自分の声を聴かれるのが恥ずかしい…」
「もっと自然に話したいけど声が気になってしゃべれない…」
そんな悩みを少しでも軽くできたらと思い、自作ボイスチェンジャーを開発しました!
この記事では、「声の仕組み」から「声を変える技術」までをわかりやすく解説します。
⚠️免責事項
本記事の目的は声を加工して通話を楽しむことです。
声を偽装して他人を欺くなど、悪用は絶対におやめください。
本記事の内容を悪用して発生したいかなる損害にも、作者は一切の責任を負いません。
📚目次
- 声の仕組み
- 声を変える仕組み
- 実装方法
- 次回予告
🧠声の仕組み
声は、大きく分けて「声帯」と「声道」の2つの要素で作られます。
声帯(音の高さ=ピッチを決める部分)
- 声帯が振動して、「ブー」という基本的な音を作る
- この時の振動の速さ=周波数が声の高さに対応する
👉男性は低い(100Hz前後)
👉女性は高い(200Hz前後)
声道(音色=フォルマントを決める部分)
- 声帯で作られた音が、口・喉・鼻腔を通ることで共鳴する
- この共鳴周波数が「声の質感」を決める
👉同じ高さでも、男性と女性で声が違うのはこのため。
声が出る仕組み
- 肺から空気を吐く
- 声帯が閉じて空気が振動を起こす
- その声が口や喉、鼻腔で共鳴して声として出る
🎛️声を変える仕組み
ざっくりいうと、
「声の高さ(ピッチ)」と「音色(フォルマント)」を操作して、声を変換するということです。
音の高さの変換
・音の高さは周波数をn半音分だけ変えることで調整できます。
半音とは隣り合う鍵盤の音の差
周波数比でいうと1半音=2^(1/12)
例えば、440Hzの2半音上=440*2^(2/12)≒493.88Hz
音の高さを上げると波が早く振動するため、再生速度が速くなる。逆もまた然り。
なので、音声の高さを変換した後は再生速度を元に戻す必要があります。
👉そんな時に使われるのが、逆フーリエ変換(IFFT)です。波形を一度周波数の世界に変換してから、時間軸に戻します。
🎨音色を変える(フォルマントシフト)
-
フーリエ変換(FFT)で音を周波数領域に変換
→音を「どの周波数の成分がどれだけ含まれているか 」に分解する。 -
周波数ごとに増幅・減衰をかけて声質を調整
→声道の共鳴(フォルマント)をシフトして、声質を操作。
(例:フォルマントを上げる=女性っぽく、下げる=男性っぽく) -
逆フーリエ変換で時間領域の音に戻す
→加工後のスペクトルから再び音声波形を復元する。
実装方法
- Python×librosa×sounddeviceを使ったリアルタイム変換を実現しました
以下のコードでは、ピッチ(音の高さ)とフォルマント(声質)を調整して、リアルタイム変換を行います。
import sounddevice as sd
import numpy as np
import librosa
from functools import partial
from scipy import signal
from collections import deque
def change_voice(audio, sample_rate, formant_shift, pitch_semitones):
"""
ボイスチェンジャーのメイン処理
audio : 入力音声データ(numpy配列)
sample_rate : サンプリング周波数(例:44100Hz)
formant_shift : 声質の変化倍率(1.0で変化なし)
pitch_semitones : ピッチ変化量(単位:半音)
"""
# 1. 基本的なピッチシフト
# 音の高さを変える(例:+5で5半音上げる)
if pitch_semitones != 0:
shifted = librosa.effects.pitch_shift(audio, sr=sample_rate, n_steps=pitch_semitones)
else:
shifted = audio.copy()
# 2. フォルマントシフト
# 声道の共鳴周波数(声質)をシフトさせる
if formant_shift != 1.0:
# フーリエ変換で周波数領域へ
spectrum = np.fft.rfft(shifted)
freqs = np.fft.rfftfreq(len(shifted), 1 / sample_rate)
# 高周波成分を強調/抑制して声質を変える
gain = 1 + (freqs / (sample_rate / 4)) * (formant_shift - 1)
gain = np.clip(gain, 1, formant_shift)
# 逆フーリエ変換で時間領域に戻す
shifted = np.fft.irfft(spectrum * gain).real
# 長さ調整
# ピッチ変換によって波形の長さが変わることがあるため、元の長さにそろえる
if len(shifted) < len(audio):
shifted = np.pad(shifted, (0, len(audio) - len(shifted)))
else:
shifted = shifted[:len(audio)]
return shifted.astype(np.float32)
🔍 技術ポイント
-
librosa.effects.pitch_shift()
→高品質なピッチ変換を簡単に実現できます。
音の高さだけを変えて、話すスピードは変えません。 -
np.fft.rfft()/np.fft.irfft()
→音声を時間領域⇔周波数領域に変換します。
これにより「どの周波数成分を強調・抑制するか」を直接操作できます。 -
np.pad()
→ピッチ変換によって波形が短くなったり長くなったりするため、長さを調整して整合性を取ります。
📣呼び出し側
Python と音声ライブラリ(sounddevice, librosa, numpy)を用いて、マイク入力 → ピッチ・フォルマント変換 → スピーカー出力 をリアルタイム処理します。
def callback(indata, outdata, frames, time, status, *, sample_rate, formant_shift, pitch_semitones):
if status:
print(status)
audio = indata[:, 0].astype(np.float32)
processed = change_voice(audio, sample_rate, formant_shift, pitch_semitones)
outdata[:, 0] = processed
def dev_play(input_device, output_device, sample_rate, block_size, formant_shift, pitch_semitones):
stream = None
cb = partial(callback,
sample_rate=sample_rate,
formant_shift=formant_shift,
pitch_semitones=pitch_semitones
)
input_info = sd.query_devices(input_device)
output_info = sd.query_devices(output_device)
print(f"入力デバイス: {input_info['name']} / 出力デバイス: {output_info['name']}")
input_hostapi = sd.query_hostapis()[input_info['hostapi']]['name']
output_hostapi = sd.query_hostapis()[output_info['hostapi']]['name']
extra_settings = None
stream = sd.Stream(
samplerate=sample_rate,
blocksize=block_size,
channels=1,
dtype='float32',
callback=cb,
device=(input_device, output_device),
extra_settings=extra_settings,
latency='low',
)
stream.start()
print("✅ ストリーム開始成功")
🧠 実行例
if __name__ == "__main__":
input_device = 1 # マイクデバイス番号
output_device = 3 # スピーカーデバイス番号
sample_rate = 44100
block_size = 1024
# 声質パラメータ
formant_shift = 1.2 # 1.0=変化なし / 1.5で明るく / 0.8で落ち着いた声
pitch_semitones = 4.0 # +4で高く / -4で低く
dev_play(input_device, output_device, sample_rate, block_size, formant_shift, pitch_semitones)
💬コツ
block_size:小さくすると遅延が減るがCPU負荷が増える
latency='low':リアルタイム性を重視する設定
formant_shift/pitch_semitones:両者のバランスで性別・年齢感を自然に調整可能
次回予告
今回はCUIによる音声変換を実現させましたが、ゆくゆくは配布していきたいと思うのでfletによるGUI実装に取り掛かりたいと思います。