トップへ(mam-mam.net/)

「ボイスチェンジャー」アプリケーションを作成(マイクからの音をピッチシフトしてスピーカーで鳴らす) ~Delphiソースコード集

「ボイスチェンジャー」アプリケーションを作成(マイクからの音をピッチシフトしてスピーカーで鳴らす) ~Delphiソースコード集

Delphiでボイスチェンジャーを作成します。
「waveInAddBuffer」でマイクからの音をデータとして取得し、FFT(高速フーリエ変換)で周波数成分に分解を行った後、 ピッチ(音程)をシフトして、IFFT(逆高速フーリエ変換)で音声データに戻し、waveOutWriteで音を鳴らしています。
高い音や低い音に変換してスピーカーから鳴らします。

プロジェクトを作成する

Delphiを起動し、メニューから「ファイル」⇒「新規作成」⇒「Windows VCLアプリケーション -Delphi(W)」 をクリックする。
TComboboxをフォームにドラッグ&ドロップします。

ソースコードの入力

キーボードの「F12」キーを押してソースコードエディタに切り替え、以下のソースコードをコピー&ペーストします。
キーボードの「F12」キーを押してデザインモードに切り替え、
Form1のOnCreateイベントに「FormCreate」を設定し、Form1のOnCloseに「FormClose」を設定します。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,Vcl.Controls, Vcl.Forms,
  MMSystem, System.Math,
  Vcl.ComCtrls, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private 宣言 }
    WaveFmt:TWaveFormatEx;
    BufIn:array of TBytes;
  public
    { Public 宣言 }
    WaveInHandle:HWAVEIN;
    WaveOutHandle:HWAVEOUT;
    BufNumId:Integer;
    WaveHeader:array of TWaveHdr;
  end;

  TFFTData=array of Double;


var
  Form1: TForm1;
const
  //サンプリングバッファの数
  BufNum:Integer=6;


implementation

{$R *.dfm}

//FFT関数(高速フーリエ変換)
procedure FFT(var ReArr,ImArr:TFFTData);
var n:Integer;
    i:integer;
    ct1,ct2,ct3:integer;
    TmpRe,TmpIm:Double;
    nfft:array[0..3] of integer;
    fcos,fsin:TFFTData;
    tmp:extended;
    noblk:integer;
    cntb:array[0..1] of integer;
begin
  n:=Length(ReArr);

  ct2:=1;
  for ct1 := 1 to length(ImArr)-2 do
  begin
    TmpRe:=0;
    TmpIm:=0;
    if ct1<ct2 then
    begin
      TmpRe:=ReArr[ct1-1];
      ReArr[ct1-1]:=ReArr[ct2-1];
      ReArr[ct2-1]:=TmpRe;
      TmpIm:=ImArr[ct1-1];
      ImArr[ct1-1]:=ImArr[ct2-1];
      ImArr[ct2-1]:=TmpIm;
    end;
    ct3:=length(ImArr) div 2;
    while ct3<ct2 do
    begin
      ct2:=ct2-ct3;
      ct3:=ct3 div 2;
    end;
    ct2:=ct2+ct3;
  end;
                                            //誤差調整
  nfft[0]:=floor(Log2(length(ImArr))/Log2(2)+0.0000000001);
  SetLength(fcos,n);
  SetLength(fsin,n);
  fcos[0]:=1;
  fsin[0]:=0;

  for ct1 := 1 to nfft[0] do
  begin
    nfft[2]:=floor(Power(2,ct1));
    nfft[1]:=n div nfft[2];
    nfft[3]:=nfft[2] div 2;
    for ct2 := 1 to nfft[3] do
    begin
      tmp:=-Pi/nfft[3]*ct2;
      fcos[ct2]:=cos(tmp);
      fsin[ct2]:=sin(tmp);
    end;
    for ct2 := 1 to nfft[1] do
    begin
      noblk:=nfft[2]*(ct2-1);
      for ct3 := 0 to nfft[3]-1 do
      begin
        cntb[0]:=noblk+ct3;
        cntb[1]:=cntb[0]+nfft[3];
        TmpRe:=ReArr[cntb[1]]*fcos[ct3]-ImArr[cntb[1]]*fsin[ct3];
        TmpIm:=ImArr[cntb[1]]*fcos[ct3]+ReArr[cntb[1]]*fsin[ct3];
        ReArr[cntb[1]]:=ReArr[cntb[0]]-TmpRe;
        ImArr[cntb[1]]:=ImArr[cntb[0]]-TmpIm;
        ReArr[cntb[0]]:=ReArr[cntb[0]]+TmpRe;
        ImArr[cntb[0]]:=ImArr[cntb[0]]+TmpIm;
      end;
    end;
  end;
end;

//逆FFT関数
procedure IFFT(var ReArr, ImArr: TFFTData);
var i,n: Integer;
begin
  n:=Length(ReArr);
  for i := 0 to n-1 do
    ImArr[i] := -ImArr[i];
  FFT(ReArr, ImArr);
  for i := 0 to n-1 do
  begin
    ReArr[i] := ReArr[i] / n;
    ImArr[i] := -ImArr[i] / n;
  end;
end;

//ShiftFactor を 1未満に設定すると低くなり、1以上に定すると高くなる
procedure PitchShift(var ReArr, ImArr: TFFTData; ShiftFactor: Double);
var i, n, NewIdx, halfN: Integer;
    ShiftedRe, ShiftedIm:TFFTData;
begin
  n:=Length(ReArr);
  SetLength(ShiftedRe, n);
  SetLength(ShiftedIm, n);
  halfN := n div 2;

  // 配列の初期化
  for i := 0 to n - 1 do
  begin
    ShiftedRe[i] := 0;
    ShiftedIm[i] := 0;
  end;
  // 周波数を ShiftFactor 倍にシフト
  for i:=0 to halfN-1 do
  begin
    NewIdx := Round(i*ShiftFactor);
    if (NewIdx < halfN) then
    begin
      //値をシフト
      ShiftedRe[NewIdx] := ShiftedRe[NewIdx] + ReArr[i];
      ShiftedIm[NewIdx] := ShiftedIm[NewIdx] + ImArr[i];
      // 鏡像の値もシフト
      if (NewIdx > 0) then
      begin
        ShiftedRe[n-NewIdx] := ShiftedRe[n-NewIdx] + ReArr[n-i];
        ShiftedIm[n-NewIdx] := ShiftedIm[n-NewIdx] + ImArr[n-i];
      end;
    end;
  end;
  for i:=0 to n-1 do
  begin
    ReArr[i] := ShiftedRe[i];
    ImArr[i] := ShiftedIm[i];
  end;
end;


//コールバック関数
procedure WaveInCallBackFunc(hW:HWAVEOUT;uMsg:Cardinal;
  dwInstance,dwParam1,dwParam2:UINT_PTR);stdcall;
type
  PInt16=^Int16;
var OldBufId:Integer;
    i,n:Integer;
    re,im:TFFTData;
    pi16:PInt16;
begin
  if uMsg=MM_WIM_DATA then
  begin
    OldBufId:=Form1.BufNumId;
    inc(Form1.BufNumId);
    if Form1.BufNumId>=BufNum then Form1.BufNumId:=0;
    waveInAddBuffer(
      Form1.WaveInHandle,
      @Form1.WaveHeader[Form1.BufNumId],SizeOf(TWaveHdr)
    );

    n:=Length(Form1.BufIn[OldBufId]) div 2;
    SetLength(re, n);
    SetLength(im, n);

    pi16:=@Form1.BufIn[OldBufId][0];
    for i := 0 to n-1 do
    begin
      re[i]:=Double(pi16^)/32768;
      im[i]:=0;
      inc(pi16);
    end;
    //高速フーリエ変換
    FFT(re,im);
    //ピッチをシフトする
    PitchShift(re,im,0.6+Form1.ComboBox1.ItemIndex/5);
    //逆高速フーリエ変換
    IFFT(re,im);
    pi16:=@Form1.BufIn[OldBufId][0];
    for i := 0 to n-1 do
    begin
      pi16^:=Int16(Trunc(re[i]*32767));
      inc(pi16);
    end;
    //ピッチをシフトした音を鳴らす
    waveOutWrite(
      Form1.WaveOutHandle,@Form1.WaveHeader[OldBufId],SizeOf(TWaveHdr));
  end;
end;


procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var i:Integer;
begin
  Action:=caFree;
  waveInStop(WaveInHandle);
  for i := 0 to BufNum-1 do
  begin
    waveInUnprepareHeader(WaveInHandle,@WaveHeader[i],SizeOf(TWaveHdr));
    waveOutUnprepareHeader(WaveOutHandle,@WaveHeader[i],SizeOf(TWaveHdr));
  end;
  waveInClose(WaveInHandle);
  waveOutClose(WaveOutHandle);
end;

procedure TForm1.FormCreate(Sender: TObject);
var i:Integer;
begin
  ComboBox1.Style:=csDropDownList;
  ComboBox1.Items.Add('低い');
  ComboBox1.Items.Add('少し低い');
  ComboBox1.Items.Add('普通');
  ComboBox1.Items.Add('少し高め');
  ComboBox1.Items.Add('高め');
  ComboBox1.Items.Add('高い');
  ComboBox1.Items.Add('かなり高い');
  ComboBox1.ItemIndex:=6;

  WaveFmt.wFormatTag:=WAVE_FORMAT_PCM;
  //1チャンネル(モノラル)
  WaveFmt.nChannels:=1;
  //サンプリング周波数 11025 又は 22050 又は 44100
  WaveFmt.nSamplesPerSec:=11025;
  //1サンプル当たりのビット数を指定(符号付き16Bit整数 -32768~32767の範囲)
  WaveFmt.wBitsPerSample:=16;
  //1ブロックのバイト数
  WaveFmt.nBlockAlign:=WaveFmt.nChannels*WaveFmt.wBitsPerSample div 8;
  //1秒当たりの平均バイト数
  WaveFmt.nAvgBytesPerSec:=WaveFmt.nBlockAlign*WaveFmt.nSamplesPerSec;
  WaveFmt.cbSize:=0;//必ず0

  waveInOpen(
    @WaveInHandle,WAVE_MAPPER,@WaveFmt,NativeUInt(@WaveInCallBackFunc),
    0,CALLBACK_FUNCTION+WAVE_ALLOWSYNC);
  waveOutOpen(
    @WaveOutHandle,WAVE_MAPPER,@WaveFmt,0,0,
    CALLBACK_FUNCTION+WAVE_ALLOWSYNC);

  SetLength(WaveHeader,BufNum);
  SetLength(BufIn,BufNum);
  for i := 0 to BufNum-1 do
  begin
    //バッファサイズの設定
    SetLength(BufIn[i],1024);
    //バッファの設定
    WaveHeader[i].lpData:=PAnsiChar(BufIn[i]);
    WaveHeader[i].dwBufferLength:=Length(BufIn[i]);
    WaveHeader[i].dwBytesRecorded:=0;
    WaveHeader[i].dwUser:=i;
    WaveHeader[i].dwFlags:=0;
    WaveHeader[i].dwLoops:=0;
    WaveHeader[i].lpNext:=nil;
    WaveHeader[i].reserved:=0;
    waveInPrepareHeader(WaveInHandle,@WaveHeader[i],SizeOf(TWaveHdr));
    waveOutPrepareHeader(WaveOutHandle,@WaveHeader[i],SizeOf(TWaveHdr));
  end;
  BufNumId:=0;
  waveInAddBuffer(WaveInHandle,@WaveHeader[BufNumId],SizeOf(TWaveHdr));
  waveInStart(WaveInHandle);
end;

end.

実行

実行時には以下の注意点があります。以下の注意点を踏まえたうえで実行してください。

※注意1 Windows10 の場合「マイクブースト」の機能を切る必要があります
(マイクブースト機能対応デバイスで、この機能を使用している場合)
ツールバーの右側にある「スピーカー」アイコンを右クリック
⇒「サウンドの設定を開く」を左クリック
⇒マイクの「デバイスのプロパティ」を左クリック
⇒「追加のデバイスのプロパティ」を左クリック
⇒「レベル」タブを左クリックして切り替え、
[マイクブースト]を0.0dbに設定して[OK]ボタンを押す
※注意2 マイクとスピーカーを近づけて使用するとハウリングが発生します
マイクとスピーカーの距離が近すぎるとハウリング(キーンと高い音)が発生する場合があります。
外付けマイク又は外付けスピーカーなどで離して使用するとうまくいくかもしれません。
少なくともスピーカーの音が出る方向側にマイクを置かないようにしてください。

内臓又は外付けマイクとスピーカー、又はヘッドフォンなどをパソコンに接続してください。
実行してマイクに向けて話すと高い声でスピーカーから出力されます。
コンボボックスを「低い」に設定してマイクに向けて話すと低い声でスピーカーから出力されます。
ノイズを減らしたい(音質を良くしたい)場合はクロスフェード(前の音声データをフェードアウトしながら新しい音声データをフェードインした合成音声の出力)を行うソースコードを追加する必要があるようです。