9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リアルタイム再生するボイスチェンジャーを作ろう!

Last updated at Posted at 2025-11-09

🎙️はじめに

SNS通話で緊張した経験、ありませんか?
「自分の声を聴かれるのが恥ずかしい…」
「もっと自然に話したいけど声が気になってしゃべれない…」

そんな悩みを少しでも軽くできたらと思い、自作ボイスチェンジャーを開発しました!
この記事では、「声の仕組み」から「声を変える技術」までをわかりやすく解説します。

⚠️免責事項

本記事の目的は声を加工して通話を楽しむことです。
声を偽装して他人を欺くなど、悪用は絶対におやめください。
本記事の内容を悪用して発生したいかなる損害にも、作者は一切の責任を負いません。

📚目次

  1. 声の仕組み
  2. 声を変える仕組み
  3. 実装方法
  4. 次回予告

🧠声の仕組み

声は、大きく分けて「声帯」と「声道」の2つの要素で作られます。

声帯(音の高さ=ピッチを決める部分)

  • 声帯が振動して、「ブー」という基本的な音を作る
  • この時の振動の速さ=周波数が声の高さに対応する
    👉男性は低い(100Hz前後)
    👉女性は高い(200Hz前後)

声道(音色=フォルマントを決める部分)

  • 声帯で作られた音が、口・喉・鼻腔を通ることで共鳴する
  • この共鳴周波数が「声の質感」を決める
    👉同じ高さでも、男性と女性で声が違うのはこのため。

声が出る仕組み

  1. 肺から空気を吐く
  2. 声帯が閉じて空気が振動を起こす
  3. その声が口や喉、鼻腔で共鳴して声として出る

🎛️声を変える仕組み

ざっくりいうと、

「声の高さ(ピッチ)」と「音色(フォルマント)」を操作して、声を変換するということです。

音の高さの変換

・音の高さは周波数をn半音分だけ変えることで調整できます。

半音とは隣り合う鍵盤の音の差
周波数比でいうと1半音=2^(1/12)

例えば、440Hzの2半音上=440*2^(2/12)≒493.88Hz

音の高さを上げると波が早く振動するため、再生速度が速くなる。逆もまた然り。
なので、音声の高さを変換した後は再生速度を元に戻す必要があります。

👉そんな時に使われるのが、逆フーリエ変換(IFFT)です。波形を一度周波数の世界に変換してから、時間軸に戻します。

🎨音色を変える(フォルマントシフト)

  1. フーリエ変換(FFT)で音を周波数領域に変換
    →音を「どの周波数の成分がどれだけ含まれているか 」に分解する。

  2. 周波数ごとに増幅・減衰をかけて声質を調整
    →声道の共鳴(フォルマント)をシフトして、声質を操作。
    (例:フォルマントを上げる=女性っぽく、下げる=男性っぽく)

  3. 逆フーリエ変換で時間領域の音に戻す
    →加工後のスペクトルから再び音声波形を復元する。

:hammer_pick:実装方法

  • 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実装に取り掛かりたいと思います。

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?