努力したWiki

推敲の足りないメモ書き多数

ユーザ用ツール

サイト用ツール


documents:voiceroid:voiceroid-011

VOICEROID2/+EX/CeVIOをコマンドラインからしゃべらせる

2018/05/21

  • SeikaCenterへアクセスするためのDLLを同梱しました。

2018/05/19

  • 音声保存が可能になりました。

2018/05/03

  • Codeer.Friendlyを使ってどうにかウインドウフォーカスを乗っ取りされず制御できるようになりました。

概要

起動している VOICEROID2/VOICEROID+EX/CeVIO を外部から制御するアプリケーション SeikaCenter です。 制御を司る SeikaCenter と、SeikaCenter に指示を送るクライアントプログラム seikasay.exe で構成されています。

製品のGUIを使わずコマンドラインから読み上げを行わせることができます。
他の開発者様が公開している製品のような、ツイッターのタイムラインを読み上げたり、他のアプリケーションにデータを受け渡しするような機能はありません。音声再生・保存のみ可能です。

株式会社Codeer(コーディア)の提供する無償のライブラリFriendlyを利用しています。
CeVIOの提供する外部連携インターフェースを利用しています。※製品を持っていないと使えません
.NETで音声を扱うライブラリNAudioを利用しています。
プロセス間通信でSeikaSayコマンドからSeikaCenterへパラメタや発声テキストを渡しています。

Codeer.Friendly IPC API DLL NAudio CeVIO external interface ipc://echoSeika/seikaCenter + VOICEROID2 SeikaCenter SeikaSay VOICERID+ EX use CoreAudio interface(audio capture) CeVIO 6 SeikaCenter : Server Service. SeiklaSay CLI Client.

また、以下のプロダクト製作者様が公開されているコードは非常に参考になりました。誠にありがとうございます。

  • TTSController 各種 Text-to-Speech エンジンを統一的に操作するライブラリです
  • VoiceRoid2.vb VOICEROID2を外部から読み上げさせる奴
  • VoiCeUtil The support tool for VOICEROID, CeVIO, etc…

ダウンロード

seikacenter20180522a.zip 2018/05/22公開。VOICEROID2の感情パラメタが転送されない事例への対応。
seikacenter20180521c.zip 2018/05/21公開。SeikaCenterへアクセスするためのDLLを用意しました。

ダウンロードしたアーカイブには以下が含まれています。

  • SeikaCenter.msi
  • setup.exe
  • seikasay.exe
  • SeikaCenterAPI.dll
  • SeikaCenterAPI.xml

以下はインストールで必要になるファイルです。setup.exe を実行してインストーラーを実行してください。

  • SeikaCenter.msi
  • setup.exe

seikasay.exe と SeikaCenterAPI.dll は同じパスに格納してください。seikasay.exeがSeikaCenterAPI.dllを必要とします。

  • seikasay.exe
  • SeikaCenterAPI.dll

注意

  • アンチウイルス製品が邪魔をして起動しないかもしれません。こちらでは ハミングヘッズの DeP HE 利用環境下で、FriendlyTest.exeを監視対象から除外する必要がありました。

対応製品

利用できる製品は以下になります。

製品 備考
VOICEROID+ 京町セイカ EX 動作確認済み
VOICEROID+ 東北ずん子 動作確認済み
VOICEROID+ 東北ずん子 EX 動作確認済み
VOICEROID+ 民安ともえ EX 動作確認済み
VOICEROID+ 結月ゆかり EX 動作確認済み
VOICEROID+ 琴葉 茜・葵 コメントで動作確認報告を頂いた(2018/05/20)
VOICEROID+ 東北きりたん EX コメントで動作確認報告を頂いた(2018/05/20)
VOICEROID+ 鷹の爪 吉田くん EX 製品を持っていないので確認できていない
VOICEROID+ 月読アイ EX 製品を持っていないので確認できていない
VOICEROID+ 月読ショウタ EX 製品を持っていないので確認できていない
VOICEROID+ 水奈瀬コウ EX コメントで動作確認報告を頂いた(2018/05/20)
音街ウナTalk Ex 動いたというツイートをTwitterで見つける(2018/05/17)
ギャラ子Talk 製品を持っていないので確認できていない
VOICEROID2 琴葉 茜・葵、+EXからインポートした話者で確認済み
CeVIO 6 CeVIO Creative Studio S にインストールしたさとうささら、すずきつづみ、タカハシ、で確認済み
SAPI Haruka Desktop、David Desktop で確認

作者は Windows 10 Pro(1709) 64bit で開発・確認を行っています。

使用例

使用方法

SeikaCenterの起動

デスクトップにできたショートカットもしくはメニューの「SeikaCenter」をクリックしてSeikaCenterを起動します。以下のようなウインドウが出てきます。

次にVOICEROID、CeVIOを起動します。
起動出来たら、SeikaCenterの「使用製品選択」にある必要なチェックボックスにチェックを入れ、「サービス開始/再始動」ボタンを押します。
一覧に話者・製品が表示されたら、制御可能な状態です。

一覧にある “cid” がseikasayコマンドで「話者」を指定するためのコードになります。
この環境だと、VOICEROID+民安ともえEX は1700、VOICEROID2の琴葉茜は2000、CeVIOさとうささらは3000、となります。

SeikaSay.exe で操作

seikasay.exe コマンドは SeikaCenter を利用するためのインタフェースになります。SeikaCenterAPI.dllを利用するように変更されました。

VOICEROID2琴葉茜を発声させる最少の記述は以下となります。

seikasay -cid 2000 -t "これは音声発声のテストです"

その他オプションは以下のようになります。

E:\seikacenter>seikasay
seikasay [-list | -cid ID -params]
print informations.
options:
  -list       : print speaker list.
  -params     : print default effect parameters.

seikasay -cid ID [-save filename] [-volume VOL] [-speed SPD] -t TalkTexts
Use SAPI5 speaker.
options:
  -cid    ID  : select SAPI speaker.
  -volume VOL : sound volume.        VOL =    0 ~  100 default 100
  -speed  SPD : play rate.           SPD =  -10 ~   10 default 0
  -save   FILE: save voice.          FILE = wave file name

seikasay -cid ID [-save filename] [ [option [option [option [.... [option ] ] ] ] ] ] -t TalkTexts
Use VOICEROID2/VOICEROID+EX speaker.
options:
  -cid        ID  : select VOICEROID2 speaker.
  -save       FILE: save voice.           FILE = wave file name
  -volume     VOL : sound volume.          VOL =  0.00 ~  2.00 default 1.00
  -speed      SPD : play speed.            SPD =  0.50 ~  4.00 default 1.00
  -pitch      PCH : play pitch.            PCH =  0.50 ~  2.00 default 1.00
  -intonation ITN : play intonation.       ITN =  0.00 ~  2.00 default 1.00
  -happiness  HPN : add emotion happiness. HPN =  0.00 ~  1.00 default 0.00
  -hatred     HAT : add emotion hatred.    HAT =  0.00 ~  1.00 default 0.00
  -sadness    SDN : add emotion sadness.   SDN =  0.00 ~  1.00 default 0.00

seikasay -cid ID [-save filename] [ [option [option [option [.... [option ] ] ] ] ] ] -t TalkTexts
Use CeVIO speaker.
options:
  -cid        ID  : select CeVIO speaker.
  -save       FILE: save voice.           FILE = wave file name
  -volume     VOL : sound volume.          VOL =  0 ~  100 default 50
  -speed      SPD : play speed.            SPD =  0 ~  100 default 50
  -pitch      PCH : play pitch.            PCH =  0 ~  100 default 50
  -alpha      ALP : play alpha.            ALP =  0 ~  100 default 50
  -intonation ITN : play intonation.       ITN =  0 ~  100 default 50
  -emotion KEY VAL: add emotion. Example: -emotion "喜び" 100  VAL =  0 ~  100 default 0


E:\seikacenter>

オプション説明

seikasayコマンドに指定するオプション -cid は必ず最初に指定します。
オプション -t 以降はすべて発声させるテキストとみなされます。発声させたい文字列にオプションと同じ記述が含まれる場合を考えると常時 -t オプションを使うほうが良いでしょう。

コマンド 発声内容
seikasay -cid 2000 -volume 1.3 -t "発声しましたか?" "これは音声発声のテストです。"  
“発声しましたか?これは音声発声のテストです。”
seikasay -cid 2000 -volume 1.3 "これは音声発声のテストです。" -volume 1.3 "で発声しましたか?"
“これは音声発声のテストです。で発声しましたか?”
seikasay -cid 2000 -volume 1.3 -t "これは音声発声のテストです。" -volume 1.3 "で発声しましたか?"
“これは音声発声のテストです。-volume1.3で発声しましたか?”

他のオプションは以下の通りです。

オプション 説明
-cid ID VOICEROID2/+EX/CeVIO/SAPIの話者cid ID を指定します。ID はSeikaCenterの「利用可能話者」タブに一覧されています。
-save WaveFile WaveFile で示すファイルに音声データ(Wav形式)を書き出します。絶対パスでの指定をしてください。
-volume P 音量 Pを指定します。
-speed P 話速(速さ) Pを指定します。
-pitch P 高さ Pを指定します。
-alpha P 声質 Pを指定します。
-intonation P 抑揚 Pを指定します。
-emotion eP P 感情パラメタ eP に P を設定します。
-t このオプション以降、すべて発声テキストとみなします。

VOICEROID2,CeVIO,SAPIではそれぞれ指定可能なオプションと指定する値が異なるので注意してください。
簡単な範囲チェックはしていますが、例えば VOICEROID2 のオプション -speed で 0.559 なんて指定した場合はたぶんエラーになるので、あまりイレギュラーな指定はしないでください。順次修正していきます。

オプションの数値範囲。

オプション VOICEROID2 VOICEROID+EX CeVIO SAPI
-volume 0.00 ~ 2.00 0.00 ~ 2.00 0 ~ 100 0 ~ 100
-speed 0.50 ~ 4.00 0.50 ~ 4.00 0 ~ 100 -10 ~ 10
-pitch 0.50 ~ 2.00 0.50 ~ 2.00 0 ~ 100 -
-alpha - - 0 ~ 100 -
-intonation 0.00 ~ 2.00 0.00 ~ 2.00 0 ~ 100 -
-emotion 0.00 ~ 1.00 - 0 ~ 100 -

VOICEROID2ではスタイル、CeVIOでは感情・コンディションと説明されている、感情のパラメタを指定するオプションが -emotion になります。

感情パラメタは日本語の名前がついています。この名前をそのまま指定する事になります。

記述 説明
seikasay -cid 3000 -emotion “元気” 50 “さとうささらは元気です” CeVIO さとうささら の元気パラメタを指定
seikasay -cid 3000 -emotion “哀しみ” 50 “さとうささらは哀しいです” CeVIO さとうささら の哀しみパラメタを指定
seikasay -cid 3000 -emotion “クール” 100 “さとうささらはクールなのです” CeVIO すずきつずみ のクールパラメタは指定しても無効
seikasay -cid 3000 -emotion “元気” 0 -emotion “哀しみ” 100 “さとうささらはとても哀しいのです” CeVIO さとうささら の元気・哀しみパラメタを指定

CeVIOの話者はそれぞれ異なる感情パラメタになるので、たとえば すずきつづみ の感情パラメタ“クール”をさとうささらに指定しても有効になりません。

VOICEROID2は3つの感情パラメタがあり全話者共通かと思います。喜び、怒り、悲しみ、がそれぞれ -happiness、-hatred、-sadness の各オプションになります。
これらはオプション -emotion でも指定可能です。

記述 同等記述
seikasay -cid 2000 -emotion “喜び” 1.00 “茜ちゃん元気ー!” seikasay -cid 2000 -happiness 1.00 “茜ちゃん元気ー!”  
seikasay -cid 2000 -emotion “怒り” 1.00 “茜ちゃん激オコー!” seikasay -cid 2000 -hatred 1.00 “茜ちゃん激オコー!”
seikasay -cid 2000 -emotion “悲しみ” 1.00 “茜ちゃんショボーン” seikasay -cid 2000 -sadness 1.00 “茜ちゃんショボーン”
seikasay -cid 2000 -emotion “悲しみ” 0.7 -emotion “怒り” 0.3 “茜ちゃんしんみり” seikasay -cid 2000 -sadness 0.7 -hatred 0.3 “茜ちゃんしんみり”

音声保存時の注意

オプション -save を使う際の注意。

指定するファイル名は絶対パスで指定してください。音声保存処理は SeikaCenterが実行するので、相対パスの場合、SeikaCenter 基準で保存先が決められてしまいます。

VOICEROID2/VOICEROID2+EXの音声保存時は、音声保存時ダイアログを表示させたくないため製品の機能を使わず、発声と同時にWindowsの音声出力デバイスで再生されている音声を録音しています。

  • VOICEROID2/VOICEROID2+EXの機能で出力する音声ファイルとチャンネル数やデータサイズ等が異なります。
  • 音声保存処理中に他のアプリケーションで音が出るとその音も一緒に録音してしまします。
    ※なので「BGMを流しながら音声保存」するとBGMもそのまま録音されてしまいます。

ソースコード

DLLの使用例と、VOICEROID2のUI操作関係クラスだけ表示します。

DLL利用のサンプル

sample.cs
using System.Collections.Generic;
using SeikaCenter;
 
namespace sampleSpeak
{
    class Program
    {
        static void Main(string[] args)
        {
            SeikaCenterControl scc = new SeikaCenterControl();
 
            decimal volume = 1.0m;
            decimal speed = 0.9m;
            decimal pitch = 1.3m;
            decimal alpha = 0.0m;
            decimal intonation = 1.0m;
            Dictionary<string, decimal> emotions = new Dictionary<string, decimal>()
            {
                {"喜び", 0.60m }
            };
 
            // 音声発声時
            scc.Talk(2000, "あー嬉しいなー", "", volume, speed, pitch, alpha, intonation, emotions);
 
            // 音声保存時
            // scc.Talk(2000, "あー嬉しいなー", @"E:\seikacenter\ureshi.wav", volume, speed, pitch, alpha, intonation, emotionns);
        }
    }
}

SeikaSayソース

SeikaCenterAPI.dll 経由でSeikaCenterにデータを渡します。コードの大半はコマンドラインのオプション解析処理です。
SeikaCenterAPI.dllの使用例でもあります。

プロパティ SeikaCenterControl.AvatorList と メソッド SeikaCenterControl.GetAvatorParams() は動的にUIを作るときなんかに使えるかもしれませんねぇ。

Program.cs
using System;
using System.Collections.Generic;
using System.Text;
using SeikaCenter;
 
namespace SeikaSay
{
    class Program
    {
        static void Main(string[] args)
        {
            opts opt = new opts(args); // オプション解析クラスです
 
            if (opt.isActive)
            {
                try
                {
                    SeikaCenterControl scc = new SeikaCenterControl(); // SeikaCenterへ接続するSeikaCenterControlクラスのインスタンスを作成
 
                    if (opt.useAvatorList)
                    {
                        Console.WriteLine("cid   speaker");
                        Console.WriteLine("----- ---------------------------");
 
                        // SeikaCenterControl.AvatorList はSeikaCenterが認識している話者の一覧を返す
                        foreach (var item in scc.AvatorList)
                        {
                            Console.WriteLine("{0,5:d} {1}", item.Key, item.Value);
                        }
                        Console.WriteLine("----- ---------------------------");
                        return;
                    }
 
                    if (opt.useParamList)
                    {
                        Console.WriteLine("cid:{0}",opt.cid);
                        Console.WriteLine("----- ---------------------------");
 
                        // SeikaCenterControl.GetAvatorParams() は指定話者の持つパラメタの情報を返す
                        foreach (var item in scc.GetAvatorParams(opt.cid))
                        {
                            Console.WriteLine("{0} : {1}", item.Key, item.Value);
                        }
                        Console.WriteLine("----- ---------------------------");
                        return;
                    }
 
                    // SeikaCenterControl.Talk()は指定話者に指定の音声効果パラメタを与えて発声させる
                    scc.Talk(opt.cid, opt.talkText, opt.saveFilename, opt.volume, opt.speed, opt.pitch, opt.alpha, opt.intonation, opt.emotions);
                }
                catch (Exception e)
                {
                    Console.WriteLine("ccc:{0}", e.Message + e.StackTrace);
                }
            }
            else
            {
                help();
            }
        }
 
        static void help()
        {
            Console.WriteLine("seikasay [-list | -cid ID -params]");
            Console.WriteLine("print informations.");
            Console.WriteLine("options:");
            Console.WriteLine("  -list       : print speaker list.");
            Console.WriteLine("  -params     : print default effect parameters.");
            Console.WriteLine("");
            Console.WriteLine("seikasay -cid ID [-save filename] [-volume VOL] [-speed SPD] -t TalkTexts");
            Console.WriteLine("Use SAPI5 speaker.");
            Console.WriteLine("options:");
            Console.WriteLine("  -cid    ID  : select SAPI speaker.");
            Console.WriteLine("  -volume VOL : sound volume.        VOL =    0 ~  100 default 100");
            Console.WriteLine("  -speed  SPD : play rate.           SPD =  -10 ~   10 default 0");
            Console.WriteLine("  -save   FILE: save voice.          FILE = wave file name");
            Console.WriteLine("");
            Console.WriteLine("seikasay -cid ID [-save filename] [ [option [option [option [.... [option ] ] ] ] ] ] -t TalkTexts");
            Console.WriteLine("Use VOICEROID2/VOICEROID+EX speaker.");
            Console.WriteLine("options:");
            Console.WriteLine("  -cid        ID  : select VOICEROID2 speaker.");
            Console.WriteLine("  -save       FILE: save voice.           FILE = wave file name");
            Console.WriteLine("  -volume     VOL : sound volume.          VOL =  0.00 ~  2.00 default 1.00");
            Console.WriteLine("  -speed      SPD : play speed.            SPD =  0.50 ~  4.00 default 1.00");
            Console.WriteLine("  -pitch      PCH : play pitch.            PCH =  0.50 ~  2.00 default 1.00");
            Console.WriteLine("  -intonation ITN : play intonation.       ITN =  0.00 ~  2.00 default 1.00");
            Console.WriteLine("  -happiness  HPN : add emotion happiness. HPN =  0.00 ~  1.00 default 0.00");
            Console.WriteLine("  -hatred     HAT : add emotion hatred.    HAT =  0.00 ~  1.00 default 0.00");
            Console.WriteLine("  -sadness    SDN : add emotion sadness.   SDN =  0.00 ~  1.00 default 0.00");
            Console.WriteLine("");
            Console.WriteLine("seikasay -cid ID [-save filename] [ [option [option [option [.... [option ] ] ] ] ] ] -t TalkTexts");
            Console.WriteLine("Use CeVIO speaker.");
            Console.WriteLine("options:");
            Console.WriteLine("  -cid        ID  : select CeVIO speaker.");
            Console.WriteLine("  -save       FILE: save voice.           FILE = wave file name");
            Console.WriteLine("  -volume     VOL : sound volume.          VOL =  0 ~  100 default 50");
            Console.WriteLine("  -speed      SPD : play speed.            SPD =  0 ~  100 default 50");
            Console.WriteLine("  -pitch      PCH : play pitch.            PCH =  0 ~  100 default 50");
            Console.WriteLine("  -alpha      ALP : play alpha.            ALP =  0 ~  100 default 50");
            Console.WriteLine("  -intonation ITN : play intonation.       ITN =  0 ~  100 default 50");
            Console.WriteLine("  -emotion KEY VAL: add emotion. Example: -emotion \"喜び\" 100  VAL =  0 ~  100 default 0");
            Console.WriteLine("");
        }
    }
}

ProductControlBase.cs

クラス Voiceroid2 や VoiceroidEx のベースクラスになります。

ProductControlBase.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using System.Text.RegularExpressions;
using System.Threading;
 
namespace FriendlyTest
{
    class ProductControlBase
    {
        protected StringBuilder TalkTextSb = new StringBuilder();
        protected StringBuilder WavFilePathSb = new StringBuilder();
        protected bool aliveInstance = false;
        protected int selectedAvator = 0;
        protected Dictionary<int, avatorParam> avatorParams;
 
        public enum VoiceEffect
        {
            volume,
            speed,
            pitch,
            alpha,
            intonation
        }
 
        protected class avatorParam
        {
            public int avatorIndex = 0;
            public string avatorName = "";
            public Dictionary<VoiceEffect, EffectValueInfo> voiceEffects = null;
            public Dictionary<VoiceEffect, EffectValueInfo> voiceEffects_default = null;
            public Dictionary<string, EffectValueInfo> voiceEmotions = null;
            public Dictionary<string, EffectValueInfo> voiceEmotions_default = null;
        }
 
        protected class EffectValueInfo
        {
            public decimal value;
            public decimal value_min;
            public decimal value_max;
            public decimal value_step;
 
            public EffectValueInfo(decimal val, decimal min, decimal max, decimal step)
            {
                value = val;
                value_min = min;
                value_max = max;
                value_step = step;
            }
        }
 
        private decimal truncs(decimal val, int digits)
        {
            decimal scale = (decimal)Math.Pow(10, digits);
            decimal ans = val > 0 ? Math.Floor(val * scale) / scale : Math.Ceiling(val * scale) / scale;
 
            return ans;
        }
 
        public string TalkText
        {
            set
            {
                TalkTextSb.Clear();
                TalkTextSb.Append(value);
            }
            get
            {
                return TalkTextSb.ToString();
            }
        }
 
        public string WavFilePath
        {
            set
            {
                WavFilePathSb.Clear();
                if ((""!=value)&&(!Regex.IsMatch(value, @"\.[Ww][Aa][Vv]$")))
                {
                    WavFilePathSb.Append(value + ".wav");
                }
                else
                {
                    WavFilePathSb.Append(value);
                }
            }
            get
            {
                return WavFilePathSb.ToString();
            }
        }
 
        public bool isAlive {
            get
            {
                return aliveInstance;
            }
        }
 
        public string[] AvatorList
        {
            get
            {
                return avatorParams.OrderBy(x => x.Key).Select(x => x.Value.avatorName).ToArray<string>();
            }
        }
 
        public int AvatorsCount
        {
            get
            {
                return avatorParams.Count;
            }
        }
 
        public Dictionary<string,decimal> emotionParams
        {
            get
            {
                return avatorParams[selectedAvator].voiceEmotions.ToDictionary(x => x.Key, y => y.Value.value);
            }
        }
 
        public string GetAvatorName(int avatorIndex)
        {
            if ((avatorIndex < 0) || (avatorIndex > avatorParams.Count)) return "";
 
            return avatorParams[avatorIndex].avatorName;
        }
 
        public virtual bool SelectAvator(int avatorIndex)
        {
            return false;
        }
        public virtual bool Play(bool async = false)
        {
            return false;
        }
        public virtual bool Save()
        {
            bool ans = false;
 
            var cap = new WasapiLoopbackCapture();
            var capWriter = new WaveFileWriter(WavFilePath, cap.WaveFormat);
 
            try
            {
                cap.ShareMode = AudioClientShareMode.Shared;
                cap.DataAvailable += ((s, buf) =>
                {
                    capWriter.Write(buf.Buffer, 0, buf.BytesRecorded);
                });
                cap.RecordingStopped += ((s, buf) =>
                {
                    capWriter.Dispose();
                    capWriter = null;
                    cap.Dispose();
                    cap = null;
                });
                cap.StartRecording();
                ans = Play();
                Thread.Sleep(100);
                cap.StopRecording();
            }
            catch (Exception e)
            {
                if (null != capWriter) capWriter.Dispose();
                if (null != cap) cap.Dispose();
                Console.WriteLine("fs:{0}", e.Message + e.StackTrace);
            }
 
            return ans;
        }
        public virtual bool SetVoiceEffect(VoiceEffect ef, decimal value)
        {
            bool ans = false;
 
            if (avatorParams[selectedAvator].voiceEffects.ContainsKey(ef))
            {
                if (value < avatorParams[selectedAvator].voiceEffects[ef].value_min)
                {
                    avatorParams[selectedAvator].voiceEffects[ef].value = avatorParams[selectedAvator].voiceEffects[ef].value_min;
                }
                else if (value > avatorParams[selectedAvator].voiceEffects[ef].value_max)
                {
                    avatorParams[selectedAvator].voiceEffects[ef].value = avatorParams[selectedAvator].voiceEffects[ef].value_max;
                }
                else
                {
                    avatorParams[selectedAvator].voiceEffects[ef].value = value;
                }
 
                ans = true;
            }
 
            return ans;
        }
        public virtual bool SetVoiceEmotion(string em, decimal value)
        {
            bool ans = false;
 
            if (avatorParams[selectedAvator].voiceEmotions.ContainsKey(em))
            {
                if (value < avatorParams[selectedAvator].voiceEmotions[em].value_min)
                {
                    avatorParams[selectedAvator].voiceEmotions[em].value = avatorParams[selectedAvator].voiceEmotions[em].value_min;
                }
                else if (value > avatorParams[selectedAvator].voiceEmotions[em].value_max)
                {
                    avatorParams[selectedAvator].voiceEmotions[em].value = avatorParams[selectedAvator].voiceEmotions[em].value_max;
                }
                else
                {
                    avatorParams[selectedAvator].voiceEmotions[em].value = value;
                }
 
                ans = true;
            }
 
            return ans;
        }
        public virtual decimal GetVoiceEffect(VoiceEffect ef)
        {
            if (avatorParams[selectedAvator].voiceEffects.ContainsKey(ef))
            {
                throw new Exception("No Effect Data");
            }
 
            return avatorParams[selectedAvator].voiceEffects[ef].value;
        }
        public virtual decimal GetVoiceEmotion(string emo)
        {
            if (avatorParams[selectedAvator].voiceEmotions.ContainsKey(emo))
            {
                throw new Exception("No Emotion Data");
            }
 
            return avatorParams[selectedAvator].voiceEmotions[emo].value;
        }
        public virtual bool ResetVoiceEffect()
        {
            foreach (KeyValuePair<VoiceEffect, EffectValueInfo> item in avatorParams[selectedAvator].voiceEffects_default)
            {
                avatorParams[selectedAvator].voiceEffects[item.Key].value = item.Value.value;
            }
 
            return true;
        }
        public virtual bool ResetVoiceEmotion()
        {
            foreach (KeyValuePair<string, EffectValueInfo> item in avatorParams[selectedAvator].voiceEmotions_default)
            {
                avatorParams[selectedAvator].voiceEmotions[item.Key].value = item.Value.value;
            }
 
            return true;
        }
    }
}

Voiceroid2.cs

UIコンポーネントの特定方法はかなりいい加減です。将来のアップデートで対応できなくなってしまう可能性があります。

ボイスタブのスタイルで表示されている喜び、怒り、悲しみのパラメタですが、これらは1度レンダリングされたら維持されるものではなくて毎回作られる模様です。よくわかりません。

Voiceroid2.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
 
using Codeer.Friendly;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using RM.Friendly.WPFStandardControls;
 
namespace FriendlyTest
{
    class Voiceroid2 : ProductControlBase
    {
        WindowsAppFriend _app = null;
        WindowControl uiTreeTop = null;
 
        WPFListView avatorListView = null;
 
        WPFTextBox talkTextBox = null;
        WPFButtonBase playButton = null;
        WPFButtonBase saveButton = null;
 
        Dictionary<int, avatorUIParam> avatorUIParams;
        class avatorUIParam
        {
            public bool withEmotionParams = false;
            public WPFSlider volumeSlider = null;
            public WPFSlider speedSlider = null;
            public WPFSlider pitchSlider = null;
            public WPFSlider intonationSlider = null;
            public WPFSlider happinessSlider = null;
            public WPFSlider hatredSlider = null;
            public WPFSlider sadnessSlider = null;
        }
 
        public Voiceroid2()
        {
            avatorParams = new Dictionary<int, avatorParam>();
            avatorUIParams = new Dictionary<int, avatorUIParam>();
 
            Process p = GetVoiceroidEditorProcess();
 
            aliveInstance = false;
 
            if (p != null)
            {
                try
                {
                    _app = new WindowsAppFriend(p);
                    uiTreeTop = WindowControl.FromZTop(_app);
 
                    //判明しているGUI要素特定
                    var editUis = uiTreeTop.GetFromTypeFullName("AI.Talk.Editor.TextEditView")[0].LogicalTree();
 
                    talkTextBox = new WPFTextBox(editUis[4]);
                    playButton = new WPFButtonBase(editUis[6]);
                    saveButton = new WPFButtonBase(editUis[24]);
 
                    //標準タブにいる各話者毎のGUI要素データを取得
                    avatorListView = new WPFListView( uiTreeTop.GetFromTypeFullName("System.Windows.Controls.ListView")[0] );
 
                    for (int i = 0; i < avatorListView.ItemCount; i++)
                    {
                        avatorParam item = new avatorParam();
                        avatorUIParam uiItem = new avatorUIParam();
 
                        avatorListView.EmulateChangeSelectedIndex(i);
 
                        //ListViewから話者名を取れなかったのでダサい方法で対処
                        var params1 = uiTreeTop.GetFromTypeFullName("AI.Framework.Wpf.Controls.TextBoxEx");
                        var nameTextBox = new WPFTextBox(params1[8]);
 
                        //スライダーの配列を取得
                        var params2 = uiTreeTop.GetFromTypeFullName("System.Windows.Controls.Slider");
                        uiItem.volumeSlider = new WPFSlider(params2[7]);
                        uiItem.speedSlider = new WPFSlider(params2[8]);
                        uiItem.pitchSlider = new WPFSlider(params2[9]);
                        uiItem.intonationSlider = new WPFSlider(params2[10]);
 
                        //アイテムがあるリストボックスが見つかるならスタイル(喜び・怒り・悲しみ)がある
                        var params3 = uiTreeTop.GetFromTypeFullName("System.Windows.Controls.ListBox");
                        if (params3.Length > 0)
                        {
                            if (params2.Length > 13)
                            {
                                uiItem.happinessSlider = new WPFSlider(params2[13]);
                                uiItem.hatredSlider = new WPFSlider(params2[14]);
                                uiItem.sadnessSlider = new WPFSlider(params2[15]);
                                uiItem.withEmotionParams = true;
                            }
                        }
 
                        item.avatorIndex = i;
                        item.avatorName = nameTextBox.Text;
 
                        item.voiceEffects = new Dictionary<VoiceEffect, EffectValueInfo>
                        {
                            {VoiceEffect.volume,     new EffectValueInfo(1.0m, 0.0m, 2.0m, 0.01m)},
                            {VoiceEffect.speed,      new EffectValueInfo(1.0m, 0.5m, 4.0m, 0.01m)},
                            {VoiceEffect.pitch,      new EffectValueInfo(1.0m, 0.5m, 2.0m, 0.01m)},
                            {VoiceEffect.intonation, new EffectValueInfo(1.0m, 0.0m, 2.0m, 0.01m)}
                        };
                        item.voiceEffects_default = new Dictionary<VoiceEffect, EffectValueInfo>
                        {
                            {VoiceEffect.volume,     new EffectValueInfo(1.0m, 0.0m, 2.0m, 0.01m)},
                            {VoiceEffect.speed,      new EffectValueInfo(1.0m, 0.5m, 4.0m, 0.01m)},
                            {VoiceEffect.pitch,      new EffectValueInfo(1.0m, 0.5m, 2.0m, 0.01m)},
                            {VoiceEffect.intonation, new EffectValueInfo(1.0m, 0.0m, 2.0m, 0.01m)}
                        };
 
                        item.voiceEmotions = new Dictionary<string, EffectValueInfo>();
                        item.voiceEmotions_default = new Dictionary<string, EffectValueInfo>();
                        if (uiItem.withEmotionParams)
                        {
                            item.voiceEmotions.Add("喜び", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                            item.voiceEmotions.Add("怒り", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                            item.voiceEmotions.Add("悲しみ", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                            item.voiceEmotions_default.Add("喜び", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                            item.voiceEmotions_default.Add("怒り", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                            item.voiceEmotions_default.Add("悲しみ", new EffectValueInfo(0.00m, 0.00m, 1.00m, 0.01m));
                        }
 
                        avatorParams.Add(i, item);
                        avatorUIParams.Add(i, uiItem);
                    }
 
                    aliveInstance = true;
                }
                catch(Exception ev2)
                {
                    Console.WriteLine("ev2:{0}", ev2.Message + ev2.StackTrace);
                    aliveInstance = false;
                }
            }
        }
        public override bool SelectAvator(int avatorIndex)
        {
            if ((avatorIndex < 0) || (avatorIndex > avatorParams.Count)) return false;
            if (avatorListView == null) return false;
 
            selectedAvator = avatorIndex;
            avatorListView.EmulateChangeSelectedIndex(avatorIndex);
 
            return true;
        }
        public override bool Play(bool async = false)
        {
            if (playButton == null) return false;
            if (saveButton == null) return false;
            if (talkTextBox == null) return false;
 
            talkTextBox.EmulateChangeText(TalkText);
 
            ApplyEffectParameters();
            ApplyEmotionParameters();
 
            if (!saveButton.IsEnabled)
            {
                Console.WriteLine("a:wait..");
                while (!saveButton.IsEnabled)
                {
                    Thread.Sleep(100);
                }
                Console.WriteLine("a:finish");
            }
 
            playButton.EmulateClick();
            Thread.Sleep(1000);
 
            if (!async)
            {
                if (!saveButton.IsEnabled)
                {
                    Console.WriteLine("b:wait..");
                    while (!saveButton.IsEnabled)
                    {
                        Thread.Sleep(100);
                    }
                    Console.WriteLine("b:finish");
                }
            }
 
            return true;
        }
 
        private void ApplyEmotionParameters()
        {
            if (avatorParams.Count == 0) return;
 
            if (avatorUIParams[selectedAvator].withEmotionParams)
            {
                WPFSlider ui1 = null;
                AppVar ui2 = null;
                AppVar[] faders = uiTreeTop.GetFromTypeFullName("System.Windows.Controls.Slider");
 
                foreach (KeyValuePair<string, EffectValueInfo> item in avatorParams[selectedAvator].voiceEmotions)
                {
                    switch (item.Key)
                    {
                        case "喜び":
                            ui1 = avatorUIParams[selectedAvator].happinessSlider;
                            ui2 = faders.Length > 14 ? faders[13] : null;
                            break;
 
                        case "怒り":
                            ui1 = avatorUIParams[avatorListView.SelectedIndex].hatredSlider;
                            ui2 = faders.Length > 15 ? faders[14] : null;
                            break;
 
                        case "悲しみ":
                            ui1 = avatorUIParams[avatorListView.SelectedIndex].sadnessSlider;
                            ui2 = faders.Length == 16 ? faders[15] : null;
                            break;
                    }
 
                    double p = Convert.ToDouble(avatorParams[selectedAvator].voiceEmotions[item.Key].value);
 
                    if (ui1 != null) ui1.EmulateChangeValue(p);
                    if (ui2 != null) ui2["Value"](p);
                }
            }
        }
        private void ApplyEffectParameters()
        {
            if (avatorParams.Count == 0) return;
 
            WPFSlider ui1 = null;
 
            foreach (KeyValuePair<VoiceEffect, EffectValueInfo> item in avatorParams[selectedAvator].voiceEffects)
            {
                switch (item.Key)
                {
                    case VoiceEffect.volume:
                        ui1 = avatorUIParams[selectedAvator].volumeSlider;
                        break;
 
                    case VoiceEffect.speed:
                        ui1 = avatorUIParams[selectedAvator].speedSlider;
                        break;
 
                    case VoiceEffect.pitch:
                        ui1 = avatorUIParams[selectedAvator].pitchSlider;
                        break;
 
                    case VoiceEffect.intonation:
                        ui1 = avatorUIParams[selectedAvator].intonationSlider;
                        break;
                }
 
                double p = Convert.ToDouble(avatorParams[selectedAvator].voiceEffects[item.Key].value);
 
                if (ui1 != null) ui1.EmulateChangeValue(p);
            }
        }
        private Process GetVoiceroidEditorProcess()
        {
            string winTitle1 = "VOICEROID2";
            string winTitle2 = winTitle1 + "*";
 
            int RetryCount = 3;
            int RetryWaitms = 500;
            Process p = null;
 
            for (int i = 0; i < 3; i++)
            {
                Process[] ps = Process.GetProcesses();
 
                foreach (Process pitem in ps)
                {
                    if ((pitem.MainWindowHandle != IntPtr.Zero) &&
                         ((pitem.MainWindowTitle.Equals(winTitle1)) || (pitem.MainWindowTitle.Equals(winTitle2))))
                    {
                        p = pitem;
                        if (i < (RetryCount - 1)) Thread.Sleep(RetryWaitms);
                    }
                }
            }
 
            return p;
        }
 
        public bool withEmotionParams(int avatorIndex)
        {
            if ((avatorIndex < 0) || (avatorIndex > avatorParams.Count)) return false;
 
            return avatorUIParams[avatorIndex].withEmotionParams;
        }
 
        public decimal GetSliderValue(VoiceEffect ef)
        {
            decimal ans = 0.00m;
            WPFSlider ui1 = null;
 
            if (!avatorParams[selectedAvator].voiceEffects.ContainsKey(ef))
            {
                throw new Exception("No Effect Slider");
            }
 
            switch (ef)
            {
                case VoiceEffect.volume:
                    ui1 = avatorUIParams[selectedAvator].volumeSlider;
                    break;
 
                case VoiceEffect.speed:
                    ui1 = avatorUIParams[selectedAvator].speedSlider;
                    break;
 
                case VoiceEffect.pitch:
                    ui1 = avatorUIParams[selectedAvator].pitchSlider;
                    break;
 
                case VoiceEffect.intonation:
                    ui1 = avatorUIParams[selectedAvator].intonationSlider;
                    break;
            }
 
            ans = Convert.ToDecimal( ui1.Value );
 
            return ans;
        }
        public decimal GetSliderValue(string emo)
        {
            AppVar ui2 = null;
            AppVar[] faders = uiTreeTop.GetFromTypeFullName("System.Windows.Controls.Slider");
 
            if (!avatorParams[selectedAvator].voiceEmotions.ContainsKey(emo))
            {
                throw new Exception("No Effect Slider");
            }
 
            switch (emo)
            {
                case "喜び":
                    ui2 = faders.Length > 14 ? faders[13] : null;
                    break;
 
                case "怒り":
                    ui2 = faders.Length > 15 ? faders[14] : null;
                    break;
 
                case "悲しみ":
                    ui2 = faders.Length == 16 ? faders[15] : null;
                    break;
            }
 
            if (ui2 == null)
            {
                throw new Exception("Effect Slider not found");
            }
 
            return Convert.ToDecimal(ui2["value"]);
        }
    }
}

コメント

kaiza, 2018/05/22 19:48

お疲れ様です。 うちの環境ですと、seikacenter20180521c においてVOICEROID2の感情値の制御が効きませんでした。 読み上げ自体は正常に行われております。

試したコマンドは以下のような形です。 seikasay -cid 2001 -emotion “怒り” 1.00 “茜ちゃん激オコー!”

SeikaCenter上では次のように認識されておりました。 (2001 琴葉 茜 voiceroid2 2001:琴葉 茜)

k896951, 2018/05/22 20:09

御手数をお掛けし申し訳ございません。 先のコマンド実行後、発声情報のタブに表示されている内容をご確認頂けますでしょうか。 感情パラメタの情報が表示されているか、表示されていれば該当パラメタの値が1.00になっているか、をみて頂けますと幸いです。 なんとなく、DLL利用のコマンドに差し替えたときに入り込んだバグな気が……

kaiza, 2018/05/22 20:18

ありがとうございます!

こちらのコマンドを実行したところ

seikasay -cid 2001 -emotion “怒り” 1.00 “茜ちゃん激オコー!”

発生情報タブでは以下のように表示されておりました。 (これを見る限り感情の項目が無いように見えます)

cid : 2001 prod : voiceroid2 name : 琴葉 茜 text : 茜ちゃん激オコー! active : True volume : 1.0 speed : 1.0 pitch : 1.0 intonation : 1.0

k896951, 2018/05/22 20:21

感情パラメタの転送がされていない事がわかりました。ごめんなさい。 いま帰宅中なので修正は少しお待ちくださいませ。

kaiza, 2018/05/22 20:24

ありがとうございます。お手数をおかけいたします。

k896951, 2018/05/22 22:19

再パッケージングしバージョン表示が出るようにしたものをアップロードしますので、大変お手数かとは思いますが再度インストールし直して事象が改善/再現するかお確かめいただけますでしょうか。 ※他にも感情パラメタの扱いをいくつか直しています。

この版ではSeikaCenterのウインドウにバージョン文字列“20180522/a”、seikasay.exeでもバージョン文字列“20180522/a”の表示がされますのでこちらが表示されている事もご確認ください。 ※過去に、新版をインストールしたはずが旧版のままになっていた、と言う事例がありましたため、間違いを防ぐため表示させることにしました。

kaiza, 2018/05/22 22:28

SeikaCenter/saikaseyともに20180522/aになっていることを確認したうえで試したところ怒り、喜び、悲しみともに問題なく感情が反映されることを確認いたしました。

大変素早い対応を大変感謝いたします!

堕天使マヨネーズ, 2018/05/20 21:31

水奈瀬コウEX、東北きりたんEX、琴葉茜・葵(2じゃない方)で動作確認しました。

k896951, 2018/05/20 22:01

動作報告のご連絡を頂き誠にありがとうございます。 さっそく製品の動確リストに追加させていただきます。

コメントを入力. Wiki文法が有効です:
画像の文字が読めなければ、文字を読んだ.wavファイルをダウンロードして下さい。
 
documents/voiceroid/voiceroid-011.txt · 最終更新: 2018/05/23 09:28 by k896951

ページ用ツール