4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Delphi から VOICEVOX を操作するのだ!

Last updated at Posted at 2025-08-17

はじめに

ずんだもんに喋らせてみたくなったのだ。

VOICEVOX

VOICEVOX』は無料で使えるテキスト読み上げ・歌声合成ソフトウェアなのだ。

Delphi から喋らせるのだ

VOICEVOX を起動しておくと、REST API を使って音声ファイルをゲットできるのだ。使える API は http://localhost:50021/docs で確認できるのだ。

image.png

ソースコードなのだ

ソースコードは Delphi 12 Athens で書かれているのだ。10.3 Rio 以降ならそのまま動作すると思うのだ。もちろん、無償の Community Edition でも大丈夫なのだ。

サポートユニット

まずは VOICEVOX 用のサポートユニットなのだ。

uVoicevox.pas
{ ---------------------------------------------------------------------------- }
{ VOICEVOX Support unit                                                        }
{ ---------------------------------------------------------------------------- }
unit uVoicevox;

interface

uses
  System.Classes, System.SysUtils, System.IOUtils, System.JSON.Readers,
  System.JSON.Types, System.JSON.Builders, System.Rtti, REST.Types, REST.Client;

type
  TRESTRequestEx = class(TRESTRequest)
  public
    constructor Create(AOwner: TComponent); override;
  end;

  TVVSpeaker = record
    ID: Integer;
    Name: string;
    Voice: string;
  end;
  TVVSpeakers = array of TVVSpeaker;

  function GetSynthesisSpeakers: TVVSpeakers;
  function GetSynthesisWavFile(FileName: string; Speaker: Integer; Text: string): Boolean;

implementation

const
  BASE_URL = 'http://localhost:50021';
  CONTENT_TYPE = 'application/json';

function GetSynthesisSpeakers: TVVSpeakers;
begin
  var Cnt := 0;
  with TRESTRequestEx.Create(nil) do
    try
      Client.BaseURL := BASE_URL;
      Client.ContentType := CONTENT_TYPE;
      Method := rmGET;
      Resource := 'speakers';
      Execute;
      if (Response.StatusCode = 200) then
      begin
        var Iterator := TJSONIterator.Create(Response.JSONReader);
        try
          Iterator.Recurse;
          while Iterator.Next do
          begin
            Iterator.Recurse;
            Iterator.Next('name');
            var CharName := Iterator.AsString;
            Iterator.Next('styles');
            Iterator.Recurse;
            while Iterator.Next do
            begin
              if (Cnt mod 128) = 0 then
               SetLength(Result, Length(Result) + 128);
              Iterator.Recurse;
              Result[Cnt].Name := CharName;
              Iterator.Next('name');
              Result[Cnt].Voice := Iterator.AsString;
              Iterator.Next('id');
              Result[Cnt].ID := Iterator.AsInteger;
              Inc(Cnt);
              Iterator.Return;
            end;
            Iterator.Return;
          end;
        finally
          Iterator.Free;
        end;
      end;
    finally
      Free;
    end;
  SetLength(Result, Cnt);
end;

function GetSynthesisWavFile(FileName: string; Speaker: Integer; Text: string): Boolean;
begin
  Result := False;
  with TRESTRequestEx.Create(nil) do
    try
      Client.BaseURL := BASE_URL;
      Client.ContentType := CONTENT_TYPE;
      Method := rmPOST;
      Resource := 'audio_query';
      Params.AddItem('speaker', Speaker.ToString, TRESTRequestParameterKind.pkQUERY);
      Params.AddItem('text'   , Text            , TRESTRequestParameterKind.pkQUERY);
      Execute;
      if (Response.StatusCode = 200) then
      begin
        Resource := 'synthesis';
        Params.Delete('text');
        Params.AddBody(Response.JSONText, CONTENT_TYPE);
        Execute;
        if (Response.StatusCode = 200) then
        begin
          TFile.WriteAllBytes(FileName, Response.RawBytes);
          Result := True;
        end;
      end;
    finally
      Free;
    end;
end;

{ TRESTRequestEx }

constructor TRESTRequestEx.Create(AOwner: TComponent);
begin
  inherited;
  Self.Client := TRESTClient.Create(Self);
  Self.Response := TRESTResponse.Create(Self);
end;

end.

フォームファイル

次はフォームファイルなのだ。

Unit1.dfm
object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'VOICEVOX TEST'
  ClientHeight = 92
  ClientWidth = 710
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -16
  Font.Name = #12513#12452#12522#12458
  Font.Style = []
  Position = poScreenCenter
  OnCreate = FormCreate
  TextHeight = 24
  object btnPlay: TButton
    Left = 614
    Top = 32
    Width = 75
    Height = 32
    Caption = 'Play'
    Enabled = False
    TabOrder = 2
    OnClick = btnPlayClick
  end
  object edText: TEdit
    Left = 279
    Top = 32
    Width = 329
    Height = 32
    TabOrder = 1
    Text = #12371#12435#12395#12385#12399#12290
  end
  object cbSpeaker: TComboBox
    Left = 16
    Top = 32
    Width = 257
    Height = 32
    Style = csDropDownList
    TabOrder = 0
  end
end

image.png

cbSpeaker の Style プロパティを csDropDownList に、btnPlay の Enabled プロパティが False に設定しておいてほしいのだ。

ユニットファイル

ユニットファイルなのだ。

Unit1.pas
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, REST.Types, System.IOUtils,
  Vcl.MPlayer, Winapi.MMSystem, Vcl.ComCtrls, uVoicevox;

type
  TForm1 = class(TForm)
    btnPlay: TButton;
    edText: TEdit;
    cbSpeaker: TComboBox;
    procedure btnPlayClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    Speakers: TVVSpeakers;
    procedure GetSpeakersThreadTerminate(Sender: TObject);
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

// フォーム作成時
procedure TForm1.FormCreate(Sender: TObject);
begin
  // スピーカーのリストを生成
  var GetSpeakersThread := TThread.CreateAnonymousThread(
    procedure
    begin
      Speakers := GetSynthesisSpeakers;
    end
  );
  GetSpeakersThread.OnTerminate := GetSpeakersThreadTerminate;
  GetSpeakersThread.Start;
end;

// スレッド終了時
procedure TForm1.GetSpeakersThreadTerminate(Sender: TObject);
begin
  // スピーカーのリストをコンボボックスに入れる
  var Speaker: TVVSpeaker;
  for Speaker in Speakers do
  begin
    // "ID: 名前 (種類)" 形式のリストを cbSpeaker にセット
    var s := Format('%d: %s (%s)', [Speaker.ID, Speaker.Name, Speaker.Voice]);
    cbSpeaker.Items.Add(s);
  end;
  if cbSpeaker.Items.Count > 0 then
  begin
    cbSpeaker.ItemIndex := 0;
    btnPlay.Enabled := True;
  end;
end;

// ボタン押下時
procedure TForm1.btnPlayClick(Sender: TObject);
begin
  // 音声合成
  var FileName := 'WORK.WAV';
  var SpeakerID := cbSpeaker.Items[cbSpeaker.ItemIndex].Split([':'])[0].ToInteger; // ID を取得
  if GetSynthesisWavFile(FileName, SpeakerID , edText.Text) then
    PlaySound(PChar(FileName), 0, SND_ASYNC);
end;
end.

実行してみるのだ

実行するとこんな感じになるのだ。

image.png

コンボボックスにはスピーカー (キャラクター) の一覧がセットされてるので、お好きなスピーカーを選択してくださいなのだ。ボクは 1, 3, 5, 7, 22, 38, 75, 76 なのだ。

そうしたらエディットボックスに喋らせたい文字列を入力して [Play] ボタンを押すだけなのだ。とってもお手軽なのだ。

説明なのだ

uVoicevox.pas にはたった 2 つの関数があるだけなのだ。使い勝手の悪い実装になっているかもしれないけれど、解り易いとは思うのだ。

GetSynthesisSpeakers()

GetSynthesisSpeakers() はスピーカーの情報を動的配列 (TVVSpeakers) で返す関数なのだ。スピーカーの ID が決め打ちでいいのなら呼び出す必要はないのだ。

GetSynthesisWavFile()

GetSynthesisWavFile() は文字列から音声合成を行って WAV ファイルを出力する関数なのだ。

パラメータ 説明
FileName string 出力する WAV ファイルの名前
Speaker Integer スピーカー ID
Text string 喋らせたい文字列

正常終了した場合には結果が True になるのだ。

サンプルプログラムは出力された WAV ファイルを、そのまま PlaySound() API で鳴らしているのだ。別に TMediaPlayer で鳴らしても構わないのだ。

おわりに

喉の調子が悪い時にはずんだの妖精を REST API で呼ぶといいのだ。
「VOICEVOX をそのまま使えばいい」?そんな事言う奴ははんごろしにするのだ。

See also:

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?