Delphiでボイスチェンジャー作成|FFT+ピッチ変換のサンプルコード
Delphiでボイスチェンジャーを作成する方法を紹介します。
マイクから取得した音声をFFT(高速フーリエ変換)で周波数解析し、ピッチシフト後にIFFTで再構成してリアルタイム再生します。
waveIn/waveOutを使った音声処理の基本から、マイクブーストの注意点まで、実用的なサンプルコード付きで解説しています。
プロジェクトを作成する
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 マイクとスピーカーを近づけて使用するとハウリングが発生します
-
マイクとスピーカーの距離が近すぎるとハウリング(キーンと高い音)が発生する場合があります。
外付けマイク又は外付けスピーカーなどで離して使用するとうまくいくかもしれません。
少なくともスピーカーの音が出る方向側にマイクを置かないようにしてください。
内臓又は外付けマイクとスピーカー、又はヘッドフォンなどをパソコンに接続してください。
実行してマイクに向けて話すと高い声でスピーカーから出力されます。
コンボボックスを「低い」に設定してマイクに向けて話すと低い声でスピーカーから出力されます。
ノイズを減らしたい(音質を良くしたい)場合はクロスフェード(前の音声データをフェードアウトしながら新しい音声データをフェードインした合成音声の出力)を行うソースコードを追加する必要があるようです。
