├── .cursorrules
├── .gitignore
├── Plugin_Socket
├── PluginSettings.cs
├── Plugin_WebSocket.cs
├── Plugin_WebSocket.csproj
├── Properties
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── SettingFormData.cs
└── SettingPropertyGrid.cs
├── Plugin_WebSocket.sln
├── README.md
└── bouyomi_api.md
/.cursorrules:
--------------------------------------------------------------------------------
1 | # 棒読みちゃんプラグイン開発ルール
2 |
3 | ## .NET Framework バージョン
4 | - .NET Framework 2.0 を厳守すること
5 | - 新しい言語機能は使用不可
6 |
7 |
8 | ## コーディング規約
9 | - C# 2.0 の文法のみ使用可能
10 | - auto-implemented プロパティ (get; set;) は使用不可
11 | - var キーワードは使用不可
12 | - LINQ は使用不可
13 | - ラムダ式は使用不可
14 | - 文字列補間($"...")は使用不可
15 | - null 条件演算子(?.)は使用不可
16 | - null 合体演算子(??)は使用不可
17 |
18 | ## 設定ファイル
19 | - FNF.XmlSerializerSetting.SettingsBase を継承して実装
20 | - 設定ファイルは Plugin_*.config の形式で保存
21 | - 設定ファイルはプラグインと同じディレクトリに配置
22 |
23 | ## エラー処理
24 | - 例外は適切にキャッチして処理すること
25 | - ユーザーに分かりやすいエラーメッセージを表示
26 |
27 | ## スレッド
28 | - Thread クラスを使用したマルチスレッド処理は可能
29 | - Task クラスは使用不可
30 |
31 | ## その他
32 | - プラグインのファイル名は「Plugin_*.dll」の形式にすること
33 | - リソースファイルは Properties/Resources.resx に配置
34 |
35 | ## プラグイン実装の参考記事
36 | - https://c-loft.com/blog/?p=1673
37 | - https://c-loft.com/blog/?p=1751
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | syntax:glob
2 | *.obj
3 | *.exe
4 | *.exp
5 | *.pdb
6 | *.dll
7 | *.user
8 | *.aps
9 | *.pch
10 | *.vspscc
11 | *_i.c
12 | *_p.c
13 | *.ncb
14 | *.suo
15 | *.tlb
16 | *.tlh
17 | *.bak
18 | *.cache
19 | *.ilk
20 | *.log
21 | *.zip
22 | [Dd]ebug*/
23 | *.lib
24 | *.sbr
25 | Thumbs.db
26 | [Ll]ib/
27 | [Rr]elease*/
28 | [Tt]est[Rr]esults/
29 | _UpgradeReport_Files/
30 | _ReSharper.*/
31 | /Plugin_Socket/.vs
32 | /.vs
33 |
--------------------------------------------------------------------------------
/Plugin_Socket/PluginSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using FNF.XmlSerializerSetting;
4 |
5 | namespace Plugin_WebSocket {
6 | ///
7 | /// WebSocketプラグインの設定を管理するクラス
8 | ///
9 | public class PluginSettings : SettingsBase {
10 | private int _port;
11 |
12 | // ポート番号が変更されたときに発生するイベント
13 | public event EventHandler PortChanged;
14 |
15 | // ポート番号
16 | public int Port {
17 | get { return _port; }
18 | set {
19 | if (_port != value) {
20 | int oldPort = _port;
21 | _port = value;
22 | // ポート番号が変更されたらイベントを発生
23 | if (PortChanged != null) {
24 | PortChanged(this, EventArgs.Empty);
25 | }
26 | }
27 | }
28 | }
29 |
30 | // コンストラクタ
31 | public PluginSettings() {
32 | _port = 55000; // デフォルトポート
33 | }
34 |
35 | ///
36 | /// 設定フォームが閉じられたときに呼ばれる
37 | ///
38 | public override void OnSettingFormClosed(FNF.XmlSerializerSetting.SettingForm.CloseKind closeKind) {
39 | base.OnSettingFormClosed(closeKind);
40 | // OKボタンで閉じられた場合のみ処理
41 | if (closeKind == FNF.XmlSerializerSetting.SettingForm.CloseKind.OK) {
42 | // ポート番号が変更されていたらイベントを発生
43 | if (PortChanged != null) {
44 | PortChanged(this, EventArgs.Empty);
45 | }
46 | }
47 | }
48 |
49 | ///
50 | /// 設定ファイルから設定を読み込む際に呼ばれる
51 | ///
52 | public override void ReadSettings() {
53 | return;
54 | }
55 |
56 | ///
57 | /// 設定ファイルに設定を保存する際に呼ばれる
58 | ///
59 | public override void WriteSettings() {
60 | return;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/Plugin_Socket/Plugin_WebSocket.cs:
--------------------------------------------------------------------------------
1 | //プラグインのファイル名は、「Plugin_*.dll」という形式にして下さい。
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using System.IO;
7 | using System.Drawing;
8 | using System.Threading;
9 | using System.ComponentModel;
10 | using System.Windows.Forms;
11 | using FNF.Utility;
12 | using FNF.Controls;
13 | using FNF.XmlSerializerSetting;
14 | using FNF.BouyomiChanApp;
15 | using System.Net;
16 | using System.Net.Sockets;
17 | using System.Security.Cryptography;
18 |
19 | namespace Plugin_WebSocket {
20 | public class Plugin_WebSocket : IPlugin {
21 |
22 | #region ■フィールド
23 | private string _path = Base.CallAsmPath + Base.CallAsmName + ".setting";
24 | private PluginSettings _settings;
25 | private SettingFormData _settingFormData;
26 | private int _currentPort;
27 | private Accept _wsAccept;
28 | #endregion
29 |
30 |
31 | #region ■IPluginメンバの実装
32 |
33 | public string Name { get { return "WebSocketサーバー"; } }
34 | public string Version { get { return "2025/03/02版"; } }
35 | public string Caption { get { return "WebSocketからの読み上げリクエストを受け付けます。"; } }
36 | public ISettingFormData SettingFormData { get { return _settingFormData; } }
37 |
38 | //プラグイン開始時処理
39 | public void Begin() {
40 | // 設定ファイルを読み込む
41 | _settings = new PluginSettings();
42 | if (File.Exists(_path)) {
43 | _settings.Load(_path);
44 | }
45 |
46 | // 現在のポート番号を記憶
47 | _currentPort = _settings.Port;
48 |
49 | // ポート番号変更イベントを登録
50 | _settings.PortChanged += new EventHandler(Settings_PortChanged);
51 |
52 | // 設定画面の初期化
53 | _settingFormData = new SettingFormData(_settings);
54 |
55 | // サーバー起動
56 | StartServer();
57 | }
58 |
59 | //プラグイン終了時処理
60 | public void End() {
61 | if (_wsAccept != null) {
62 | _wsAccept.Stop();
63 | _wsAccept = null;
64 | }
65 | _settings.Save(this._path);
66 | }
67 |
68 | // ポート番号変更イベントハンドラ
69 | private void Settings_PortChanged(object sender, EventArgs e) {
70 | // ポート番号が変更されていたらサーバーを再起動
71 | if (_settings.Port != _currentPort) {
72 | Pub.AddTalkTask("ポート番号が" + _currentPort + "から" + _settings.Port + "に変更されました。サーバーを再起動します。", -1, -1, VoiceType.Default);
73 | _currentPort = _settings.Port;
74 | RestartServer();
75 | }
76 | }
77 |
78 | // サーバーを起動
79 | private void StartServer() {
80 | if (_wsAccept != null) {
81 | _wsAccept.Stop();
82 | _wsAccept = null;
83 | }
84 |
85 | _wsAccept = new Accept(_settings.Port);
86 | _wsAccept.Start();
87 | }
88 |
89 | // サーバーを再起動
90 | private void RestartServer() {
91 | StartServer();
92 | }
93 |
94 | #endregion
95 |
96 | // 受付クラス
97 | class Accept {
98 | private int mPort;
99 | public bool active = true;
100 | Thread thread;
101 | Socket server;
102 |
103 | // コンストラクタ
104 | public Accept(int port) {
105 | mPort = port;
106 | }
107 |
108 | public void Start() {
109 | thread = new Thread(new ThreadStart(Run));
110 | Pub.AddTalkTask("ポート" + mPort + "でソケット受付を開始しました。", -1, -1, VoiceType.Default);
111 | thread.Start();
112 | }
113 |
114 | public void Stop() {
115 | active = false;
116 | if (server != null) {
117 | try {
118 | server.Close();
119 | }
120 | catch {
121 | // エラーは無視
122 | }
123 | }
124 | if (thread != null && thread.IsAlive) {
125 | try {
126 | thread.Abort();
127 | }
128 | catch {
129 | // エラーは無視
130 | }
131 | }
132 | Pub.AddTalkTask("ソケット受付を終了しました。", -1, -1, VoiceType.Default);
133 | }
134 |
135 | private void Run() {
136 | try {
137 | // ポートが使用可能かチェック
138 | using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {
139 | socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
140 | socket.Bind(new IPEndPoint(IPAddress.Any, mPort));
141 | socket.Close();
142 | }
143 |
144 | // 元のサーバー起動コード
145 | server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
146 | server.Bind(new IPEndPoint(IPAddress.Any, mPort));
147 | server.Listen(10);
148 | }
149 | catch (SocketException ex) {
150 | MessageBox.Show("サーバーの起動に失敗しました。\nポート" + mPort + "が既に使用されているか、アクセス権限がありません。\n\nエラー詳細: " + ex.Message,
151 | "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
152 | throw;
153 | }
154 |
155 | // 要求待ち(無限ループ)
156 | while (active) {
157 | try {
158 | Socket client = server.Accept();
159 | Response response = new Response(client);
160 | response.Start();
161 | }
162 | catch (Exception) {
163 | if (active) {
164 | // アクティブな状態でエラーが発生した場合は少し待機
165 | Thread.Sleep(1000);
166 | }
167 | else {
168 | // 非アクティブならループを抜ける
169 | break;
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
176 |
177 | // 応答クラス
178 | class Response {
179 | enum STATUS {
180 | CHECKING, // 調査中
181 | OK, // OK
182 | ERROR, // ERROR
183 | };
184 |
185 | private Socket mClient;
186 | private STATUS mStatus;
187 | private Thread thread;
188 |
189 | // コンストラクタ
190 | public Response(Socket client) {
191 | mClient = client;
192 | mStatus = STATUS.CHECKING;
193 | }
194 |
195 | // 開始
196 | public void Start() {
197 | thread = new Thread(new ThreadStart(Run));
198 | thread.Start();
199 | }
200 |
201 | // 応答実行
202 | private void Run()
203 | {
204 | try
205 | {
206 | // 要求受信
207 | int bsize = mClient.ReceiveBufferSize;
208 | byte[] buffer = new byte[bsize];
209 | int recvLen = mClient.Receive(buffer);
210 |
211 | if (recvLen <= 0)
212 | return;
213 |
214 | String header = Encoding.ASCII.GetString(buffer, 0, recvLen);
215 | Console.WriteLine("【" + System.DateTime.Now + "】\n" + header);
216 |
217 | // 要求URL確認 & 応答内容生成
218 | int pos = header.IndexOf("GET / HTTP/");
219 |
220 | if (mStatus == STATUS.CHECKING && 0 == pos)
221 | {
222 | doWebSocketMain(header);
223 | }
224 |
225 | }
226 | catch (System.Net.Sockets.SocketException e)
227 | {
228 | Console.Write(e.Message);
229 | }
230 | finally
231 | {
232 | mClient.Close();
233 | }
234 | }
235 |
236 | // WebSocketメイン
237 | private void doWebSocketMain(String header) {
238 | String key = "Sec-WebSocket-Key: ";
239 | int pos = header.IndexOf(key);
240 | if (pos < 0) return;
241 |
242 | // "Sec-WebSocket-Accept"に設定する文字列を生成
243 | String value = header.Substring(pos + key.Length, (header.IndexOf("\r\n", pos) - (pos + key.Length)));
244 | byte[] byteValue = Encoding.UTF8.GetBytes(value + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
245 | SHA1 crypto = new SHA1CryptoServiceProvider();
246 | byte[] hash = crypto.ComputeHash(byteValue);
247 | String resValue = Convert.ToBase64String(hash);
248 |
249 | // 応答内容送信
250 | byte[] buffer = Encoding.UTF8.GetBytes(
251 | "HTTP/1.1 101 OK\r\n" +
252 | "Upgrade: websocket\r\n" +
253 | "Connection: Upgrade\r\n" +
254 | "Sec-WebSocket-Accept: " + resValue + "\r\n" +
255 | "\r\n");
256 |
257 | mClient.Send(buffer);
258 |
259 | // クライアントからテキストを受信
260 | int bsize = mClient.ReceiveBufferSize;
261 | byte[] request = new byte[bsize];
262 | mClient.Receive(request);
263 |
264 | // WebSocketデータをデコード
265 | string jsonText = DecodeWebSocketData(request, bsize);
266 | if (string.IsNullOrEmpty(jsonText)) {
267 | return;
268 | }
269 |
270 | try {
271 | // JSONデータをパース
272 | Dictionary jsonData = ParseJson(jsonText);
273 |
274 | // コマンドタイプを取得
275 | string command = "talk"; // デフォルトは読み上げ
276 | if (jsonData.ContainsKey("command")) {
277 | command = jsonData["command"].ToString().ToLower();
278 | }
279 |
280 | // コマンドに応じた処理
281 | switch (command) {
282 | case "talk":
283 | // 読み上げコマンド
284 | ProcessTalkCommand(jsonData);
285 | break;
286 | case "stop":
287 | // 停止コマンド
288 | Pub.ClearTalkTasks();
289 | break;
290 | case "pause":
291 | // 一時停止コマンド
292 | Pub.Pause = true;
293 | break;
294 | case "resume":
295 | // 再開コマンド
296 | Pub.Pause = false;
297 | break;
298 | case "skip":
299 | // スキップコマンド
300 | Pub.SkipTalkTask();
301 | break;
302 | default:
303 | // 不明なコマンドの場合は読み上げコマンドとして処理
304 | Pub.AddTalkTask("不明なコマンド: " + command + " を読み上げコマンドとして処理します", -1, -1, VoiceType.Default);
305 | ProcessTalkCommand(jsonData);
306 | break;
307 | }
308 | }
309 | catch (Exception ex) {
310 | // エラーログ
311 | Console.WriteLine("JSONパースエラー: " + ex.Message);
312 | }
313 | }
314 |
315 | // 読み上げコマンドの処理
316 | private void ProcessTalkCommand(Dictionary jsonData) {
317 | // 各パラメータを取得
318 | int speed = -1;
319 | int pitch = -1;
320 | int volume = -1;
321 | int voiceType = 0;
322 | string text = string.Empty;
323 |
324 | if (jsonData.ContainsKey("speed")) {
325 | speed = Convert.ToInt32(jsonData["speed"]);
326 | }
327 | if (jsonData.ContainsKey("pitch")) {
328 | pitch = Convert.ToInt32(jsonData["pitch"]);
329 | }
330 | if (jsonData.ContainsKey("volume")) {
331 | volume = Convert.ToInt32(jsonData["volume"]);
332 | }
333 | if (jsonData.ContainsKey("voiceType")) {
334 | voiceType = Convert.ToInt32(jsonData["voiceType"]);
335 | }
336 | if (jsonData.ContainsKey("text")) {
337 | text = jsonData["text"].ToString();
338 | }
339 |
340 | // VoiceTypeを設定
341 | VoiceType vt = VoiceType.Default;
342 | switch (voiceType) {
343 | case 0: vt = VoiceType.Default; break;
344 | case 1: vt = VoiceType.Female1; break;
345 | case 2: vt = VoiceType.Female2; break;
346 | case 3: vt = VoiceType.Male1; break;
347 | case 4: vt = VoiceType.Male2; break;
348 | case 5: vt = VoiceType.Imd1; break;
349 | case 6: vt = VoiceType.Robot1; break;
350 | case 7: vt = VoiceType.Machine1; break;
351 | case 8: vt = VoiceType.Machine2; break;
352 | default: vt = (VoiceType)voiceType; break;
353 | }
354 |
355 | // 読み上げ
356 | Pub.AddTalkTask(text, pitch, volume, speed, vt);
357 | }
358 |
359 | // WebSocketのデータをデコード
360 | private string DecodeWebSocketData(byte[] buffer, int size) {
361 | try {
362 | if (size < 2) {
363 | return "";
364 | }
365 |
366 | // 基本情報取得
367 | bool fin = (buffer[0] & 0x80) != 0; // 終了フレームかどうか
368 | int opcode = buffer[0] & 0x0F; // opcode
369 | bool mask = (buffer[1] & 0x80) != 0; // マスクされているかどうか
370 | int len = buffer[1] & 0x7F; // ペイロード長
371 | int pos = 2; // ヘッダサイズ
372 |
373 | // 長さが126以上の場合
374 | if (len == 126) {
375 | len = (buffer[2] << 8) + buffer[3];
376 | pos = 4;
377 | }
378 | else if (len == 127) {
379 | // 64ビット長の処理
380 | long longLen = 0;
381 | for (int i = 0; i < 8; i++) {
382 | longLen = (longLen << 8) | buffer[2 + i];
383 | }
384 |
385 | // int.MaxValueを超える場合は処理できない
386 | if (longLen > Int32.MaxValue) {
387 | Console.WriteLine("メッセージが大きすぎます: " + longLen + " バイト");
388 | return "";
389 | }
390 |
391 | len = (int)longLen;
392 | pos = 10; // 2 + 8バイト
393 | }
394 |
395 | // マスクキー取得
396 | byte[] maskKey = null;
397 | if (mask) {
398 | maskKey = new byte[4];
399 | for (int i = 0; i < 4; i++) {
400 | maskKey[i] = buffer[pos + i];
401 | }
402 | pos += 4;
403 | }
404 |
405 | // ペイロード取得
406 | byte[] payload = new byte[len];
407 | for (int i = 0; i < len; i++) {
408 | if (pos + i < size) {
409 | payload[i] = buffer[pos + i];
410 | }
411 | else {
412 | // サイズが足りない
413 | Console.WriteLine("不完全なメッセージ: 必要なサイズ " + len + " バイト, 実際のサイズ " + (size - pos) + " バイト");
414 | return "";
415 | }
416 | }
417 |
418 | // マスク解除
419 | if (mask) {
420 | for (int i = 0; i < len; i++) {
421 | payload[i] = (byte)(payload[i] ^ maskKey[i % 4]);
422 | }
423 | }
424 |
425 | // テキストの場合のみ処理
426 | if (opcode == 1) {
427 | return Encoding.UTF8.GetString(payload);
428 | }
429 | }
430 | catch (Exception ex) {
431 | // エラーログ
432 | Console.WriteLine("WebSocketデータのデコードエラー: " + ex.Message);
433 | }
434 | return "";
435 | }
436 |
437 | // JSONデータをパースする簡易メソッド(.NET 2.0互換)
438 | private Dictionary ParseJson(string jsonText) {
439 | Dictionary result = new Dictionary();
440 |
441 | try {
442 | // 中括弧を削除
443 | jsonText = jsonText.Trim();
444 | if (jsonText.StartsWith("{") && jsonText.EndsWith("}")) {
445 | jsonText = jsonText.Substring(1, jsonText.Length - 2);
446 | } else {
447 | return result;
448 | }
449 |
450 | // キーと値のペアを解析
451 | bool inQuotes = false;
452 | bool escaped = false;
453 | StringBuilder keyBuilder = new StringBuilder();
454 | StringBuilder valueBuilder = new StringBuilder();
455 | bool buildingKey = true;
456 |
457 | for (int i = 0; i < jsonText.Length; i++) {
458 | char c = jsonText[i];
459 |
460 | // エスケープシーケンスの処理
461 | if (escaped) {
462 | if (buildingKey) {
463 | keyBuilder.Append(c);
464 | } else {
465 | valueBuilder.Append(c);
466 | }
467 | escaped = false;
468 | continue;
469 | }
470 |
471 | // バックスラッシュはエスケープ文字
472 | if (c == '\\') {
473 | escaped = true;
474 | continue;
475 | }
476 |
477 | // 引用符の処理
478 | if (c == '"') {
479 | inQuotes = !inQuotes;
480 | continue;
481 | }
482 |
483 | // キーと値の区切り
484 | if (!inQuotes && c == ':' && buildingKey) {
485 | buildingKey = false;
486 | continue;
487 | }
488 |
489 | // 次のキーと値のペアへ
490 | if (!inQuotes && c == ',') {
491 | string key = keyBuilder.ToString().Trim().Trim('"');
492 | string value = valueBuilder.ToString().Trim().Trim('"');
493 |
494 | // 値を適切な型に変換
495 | if (value == "true") {
496 | result[key] = true;
497 | } else if (value == "false") {
498 | result[key] = false;
499 | } else if (value == "null") {
500 | result[key] = null;
501 | } else {
502 | // 数値かどうか確認
503 | int intValue;
504 | if (int.TryParse(value, out intValue)) {
505 | result[key] = intValue;
506 | } else {
507 | result[key] = value;
508 | }
509 | }
510 |
511 | keyBuilder.Length = 0;
512 | valueBuilder.Length = 0;
513 | buildingKey = true;
514 | continue;
515 | }
516 |
517 | // 文字を追加
518 | if (buildingKey) {
519 | keyBuilder.Append(c);
520 | } else {
521 | valueBuilder.Append(c);
522 | }
523 | }
524 |
525 | // 最後のキーと値のペアを処理
526 | if (keyBuilder.Length > 0) {
527 | string key = keyBuilder.ToString().Trim().Trim('"');
528 | string value = valueBuilder.ToString().Trim().Trim('"');
529 |
530 | // 値を適切な型に変換
531 | if (value == "true") {
532 | result[key] = true;
533 | } else if (value == "false") {
534 | result[key] = false;
535 | } else if (value == "null") {
536 | result[key] = null;
537 | } else {
538 | // 数値かどうか確認
539 | int intValue;
540 | if (int.TryParse(value, out intValue)) {
541 | result[key] = intValue;
542 | } else {
543 | result[key] = value;
544 | }
545 | }
546 | }
547 | } catch (Exception ex) {
548 | Console.WriteLine("JSONパースエラー: " + ex.Message);
549 | }
550 |
551 | return result;
552 | }
553 | }
554 |
555 |
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/Plugin_Socket/Plugin_WebSocket.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.50727
7 | 2.0
8 | {169CA507-E901-438D-A486-059DA01F9287}
9 | Library
10 | Properties
11 | Plugin_WebSocket
12 | Plugin_WebSocket
13 |
14 |
15 | v2.0
16 |
17 |
18 | 2.0
19 |
20 |
21 | publish\
22 | true
23 | Disk
24 | false
25 | Foreground
26 | 7
27 | Days
28 | false
29 | false
30 | true
31 | 0
32 | 1.0.0.%2a
33 | false
34 | false
35 | true
36 |
37 |
38 | true
39 | full
40 | false
41 | bin\Debug\
42 | TRACE;DEBUG
43 | prompt
44 | 4
45 |
46 |
47 | none
48 | true
49 | bin\Release\
50 |
51 |
52 | prompt
53 | 4
54 | false
55 |
56 |
57 |
58 | C:\tools\Accesary\棒読みちゃん\BouyomiChan.exe
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | True
72 | True
73 | Resources.resx
74 |
75 |
76 |
77 |
78 | Designer
79 | ResXFileCodeGenerator
80 | Resources.Designer.cs
81 |
82 |
83 |
84 |
85 | False
86 | Microsoft .NET Framework 4 %28x86 および x64%29
87 | true
88 |
89 |
90 | False
91 | .NET Framework 3.5 SP1 Client Profile
92 | false
93 |
94 |
95 | False
96 | .NET Framework 3.5 SP1
97 | false
98 |
99 |
100 | False
101 | Windows インストーラー 3.1
102 | true
103 |
104 |
105 |
106 |
107 |
108 |
109 |
116 |
--------------------------------------------------------------------------------
/Plugin_Socket/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // このコードはツールによって生成されました。
4 | // ランタイム バージョン:4.0.30319.34209
5 | //
6 | // このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
7 | // コードが再生成されるときに損失したりします。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Plugin_WebSocket.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// ローカライズされた文字列などを検索するための、厳密に型指定されたリソース クラスです。
17 | ///
18 | // このクラスは StronglyTypedResourceBuilder クラスが ResGen
19 | // または Visual Studio のようなツールを使用して自動生成されました。
20 | // メンバーを追加または削除するには、.ResX ファイルを編集して、/str オプションと共に
21 | // ResGen を実行し直すか、または VS プロジェクトをビルドし直します。
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// このクラスで使用されているキャッシュされた ResourceManager インスタンスを返します。
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Plugin_WebSocket.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// 厳密に型指定されたこのリソース クラスを使用して、すべての検索リソースに対し、
51 | /// 現在のスレッドの CurrentUICulture プロパティをオーバーライドします。
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Plugin_Socket/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/Plugin_Socket/SettingFormData.cs:
--------------------------------------------------------------------------------
1 | using FNF.XmlSerializerSetting;
2 |
3 | namespace Plugin_WebSocket {
4 | ///
5 | /// 設定画面を管理するクラス
6 | ///
7 | public class SettingFormData : ISettingFormData {
8 | private PluginSettings _settings = null;
9 | public SettingPropertyGrid _propertyGrid = null;
10 |
11 | // ISettingFormDataの実装
12 | public string Title { get { return "WebSocketサーバー設定"; } }
13 | public bool ExpandAll { get { return false; } }
14 | public SettingsBase Setting { get { return _settings; } }
15 |
16 | ///
17 | /// コンストラクタ
18 | ///
19 | /// 設定オブジェクト
20 | public SettingFormData(PluginSettings settings) {
21 | _settings = settings;
22 | _propertyGrid = new SettingPropertyGrid(_settings);
23 |
24 | }
25 |
26 | }
27 | }
--------------------------------------------------------------------------------
/Plugin_Socket/SettingPropertyGrid.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Forms;
3 | using System.ComponentModel;
4 | using FNF.XmlSerializerSetting;
5 |
6 | namespace Plugin_WebSocket {
7 | ///
8 | /// 設定用プロパティグリッド
9 | ///
10 | public class SettingPropertyGrid : ISettingPropertyGrid {
11 | private PluginSettings _settings;
12 |
13 | // コンストラクタ
14 | public SettingPropertyGrid(PluginSettings setting) {
15 | _settings = setting;
16 | }
17 |
18 | //シート名
19 | public string GetName() { return "WebSocketサーバー設定"; }
20 |
21 | // ポート番号
22 | [Category("サーバー設定")]
23 | [DisplayName("ポート番号")]
24 | [Description("WebSocketサーバーが使用するポート番号を指定します。(1024~65535)")]
25 | [DefaultValue(55000)]
26 | public int Port {
27 | get { return _settings.Port; }
28 | set { _settings.Port = value; }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Plugin_WebSocket.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.10.34928.147
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin_WebSocket", "Plugin_Socket\Plugin_WebSocket.csproj", "{169CA507-E901-438D-A486-059DA01F9287}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {169CA507-E901-438D-A486-059DA01F9287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {169CA507-E901-438D-A486-059DA01F9287}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {169CA507-E901-438D-A486-059DA01F9287}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {169CA507-E901-438D-A486-059DA01F9287}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {6F6EC3B6-FF87-4683-BCC3-0989E8EAC61C}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 棒読みちゃん用のWebSocket受付プラグイン
2 | ---------------------------------------
3 |
4 | ビルド
5 | ------
6 |
7 | コンパイル前に参照設定ですでに設定されているBouyomichanを削除し、
8 | 手持ちのBouyomiChan.exeを指定して追加してください。
9 |
10 |
11 | インストール
12 | ------------
13 | コンパイルして作成したPlugin_WebSocket.dllをBouyomichan.exeと同じフォルダに入れるだけです。
14 |
15 | 棒読みちゃんを起動し、その他タブのプラグインでWebSocketサーバーにチェックを入れて有効化してください。
16 |
17 |
18 | JavaScriptからプラグインへ喋らせる方法
19 | ---------------------------------------
20 |
21 | ### JSON形式での送信方法
22 | ```javascript
23 | // WebSocketを使用して棒読みちゃんに接続
24 | var socket = new WebSocket('ws://localhost:55000/');
25 |
26 | // 読み上げリクエスト
27 | socket.onopen = function() {
28 | // 読み上げコマンド
29 | var talkData = {
30 | command: "talk",
31 | speed: 100, // 速度50-200。-1を指定すると本体設定
32 | pitch: 100, // ピッチ50-200。-1を指定すると本体設定
33 | volume: 100, // ボリューム0-100。-1を指定すると本体設定
34 | voiceType: 0, // 声質(0.本体設定/1.女性1/2.女性2/3.男性1/4.男性2/5.中性/6.ロボット/7.機械1/8.機械2)
35 | text: "こんにちは、棒読みちゃんです"
36 | };
37 |
38 | socket.send(JSON.stringify(talkData));
39 | };
40 | ```
41 |
42 | ### 制御コマンド
43 | JSON形式では、読み上げ以外にも以下の制御コマンドが使用できます:
44 |
45 | ```javascript
46 | // 読み上げ停止
47 | socket.send(JSON.stringify({ command: "stop" }));
48 |
49 | // 読み上げ一時停止
50 | socket.send(JSON.stringify({ command: "pause" }));
51 |
52 | // 読み上げ再開
53 | socket.send(JSON.stringify({ command: "resume" }));
54 |
55 | // 現在の読み上げをスキップ
56 | socket.send(JSON.stringify({ command: "skip" }));
57 | ```
58 |
--------------------------------------------------------------------------------
/bouyomi_api.md:
--------------------------------------------------------------------------------
1 | # 棒読みちゃん プラグイン開発 API リファレンス
2 | このドキュメントは棒読みちゃんプラグイン開発で使用される主要なクラスとインターフェースの定義をまとめたものです。.NET Framework 2.0 に準拠しています。
3 |
4 | ## 目次
5 |
6 | - プラグイン開発の基本
7 | - FNF.BouyomiChanApp 名前空間
8 | - FNF.XmlSerializerSetting 名前空間
9 | - FNF.Utility 名前空間
10 | - FNF.Controls 名前空間
11 |
12 | ## プラグイン開発の基本
13 |
14 | ### プラグインの命名規則
15 | - プラグインのファイル名は「Plugin_.dll」という形式にする必要があります(例:「Plugin_WebSocket.dll」)
16 | - 設定ファイルは「Plugin_.config」または「.setting」という形式で保存されます
17 |
18 | ### プラグイン開発の基本手順
19 |
20 | 1. `IPlugin` インターフェースを実装したクラスを作成する
21 | 2. 設定が必要な場合は `SettingsBase` を継承した設定クラスを作成する
22 | 3. 設定画面が必要な場合は `ISettingFormData` と `ISettingPropertyGrid` を実装したクラスを作成する
23 | 4. `Begin()` メソッドで初期化処理を行い、`End()` メソッドで終了処理を行う
24 | 5. 読み上げ機能を使用する場合は `Pub.AddTalkTask()` メソッドを呼び出す
25 |
26 | ### 開発上の制約
27 |
28 | - .NET Framework 2.0 の機能のみ使用可能
29 | - C# 2.0 の文法のみ使用可能
30 | - 以下の機能は使用不可:
31 | - auto-implemented プロパティ (get; set;)
32 | - var キーワード
33 | - LINQ
34 | - ラムダ式
35 | - 文字列補間($"...")
36 | - null 条件演算子(?.)
37 | - null 合体演算子(??)
38 |
39 | ## FNF.BouyomiChanApp 名前空間
40 |
41 | ### IPlugin インターフェース
42 |
43 | プラグインの基本インターフェース。すべてのプラグインはこのインターフェースを実装する必要があります。
44 |
45 | ```csharp
46 | public interface IPlugin {
47 | string Name { get; } // プラグインの名前
48 | string Version { get; } // プラグインのバージョン
49 | string Caption { get; } // プラグインの説明
50 | ISettingFormData SettingFormData { get; } // プラグインの設定画面データ
51 | void Begin(); // プラグイン開始時処理
52 | void End(); // プラグイン終了時処理
53 | }
54 | ```
55 |
56 | ### Pub クラス
57 | 棒読みちゃんの主要な機能を提供する静的クラス。
58 |
59 | ```csharp
60 | public static class Pub {
61 | // 読み上げタスクを追加
62 | public static void AddTalkTask(string text, int speaker, int speed, VoiceType voiceType);
63 | public static void AddTalkTask(string text, int speaker, int volume, int speed, VoiceType voiceType);
64 |
65 | // 読み上げを停止
66 | public static void Stop();
67 |
68 | // 設定データ
69 | public static Settings Data { get; }
70 |
71 | // アプリケーション終了
72 | public static void ExitApplication(bool bReboot, bool bCloseLog);
73 |
74 | // メインフォーム
75 | public static FormMain FormMain { get; }
76 | }
77 | ```
78 |
79 | ### Base クラス
80 | 棒読みちゃんの基本情報を提供する静的クラス。
81 |
82 | ```csharp
83 | public static class Base {
84 | public static string CallAsmPath { get; } // 呼び出し元アセンブリのパス
85 | public static string CallAsmName { get; } // 呼び出し元アセンブリの名前
86 | }
87 | ```
88 |
89 | ### VoiceType 列挙型
90 | 音声の種類を定義する列挙型。
91 |
92 | ```csharp
93 | public enum VoiceType {
94 | Default, // デフォルト
95 | Female1, // 女性1
96 | Female2, // 女性2
97 | Male1, // 男性1
98 | Male2, // 男性2
99 | Imd1, // 中性
100 | Robot1, // ロボット
101 | Machine1, // 機械1
102 | Machine2 // 機械2
103 | }
104 | ```
105 |
106 | ### FormMain クラス
107 | 棒読みちゃんのメインフォームクラス。
108 |
109 | ```csharp
110 | public class FormMain : Form {
111 | // プラグインの読み込み
112 | public void LoadPlugins();
113 |
114 | // プラグインの解放
115 | public void UnloadPlugins();
116 |
117 | // 読み上げタスクを追加
118 | public void AddTalkTask(string text, int speaker, int volume, int speed, VoiceType voiceType);
119 |
120 | // 読み上げを停止
121 | public void Stop();
122 | }
123 | ```
124 |
125 | ### BouyomiChanHttpServer クラス
126 | 棒読みちゃんのHTTPサーバークラス。
127 |
128 | ```csharp
129 | public class BouyomiChanHttpServer : IDisposable {
130 | // コンストラクタ
131 | public BouyomiChanHttpServer(int port, FormMain owner);
132 |
133 | // ポート番号
134 | public int Port { get; }
135 |
136 | // 実行中かどうか
137 | public bool IsRunning { get; }
138 |
139 | // リソースの解放
140 | public void Dispose();
141 | }
142 | ```
143 |
144 | ### BouyomiChanTcpServer クラス
145 | 棒読みちゃんのTCPサーバークラス。
146 |
147 | ```csharp
148 | public class BouyomiChanTcpServer : IDisposable {
149 | // コンストラクタ
150 | public BouyomiChanTcpServer(int port, FormMain owner);
151 |
152 | // ポート番号
153 | public int Port { get; }
154 |
155 | // 実行中かどうか
156 | public bool IsRunning { get; }
157 |
158 | // リソースの解放
159 | public void Dispose();
160 | }
161 | ```
162 |
163 | ## FNF.XmlSerializerSetting 名前空間
164 |
165 | ### SettingsBase クラス
166 | 設定ファイルの基本クラス。XMLシリアライズを使用して設定を保存・読み込みます。
167 |
168 | ```csharp
169 | public class SettingsBase {
170 | // 設定の読み込み
171 | public void Load(string path);
172 |
173 | // 設定の保存
174 | public void Save(string path);
175 |
176 | // 設定読み込み時に呼ばれる(オーバーライド用)
177 | public virtual void ReadSettings();
178 |
179 | // 設定保存時に呼ばれる(オーバーライド用)
180 | public virtual void WriteSettings();
181 | }
182 | ```
183 |
184 | ### ISettingFormData インターフェース
185 | 設定画面のデータを管理するインターフェース。
186 |
187 | ```csharp
188 | public interface ISettingFormData {
189 | string Title { get; } // 設定画面のタイトル
190 | bool ExpandAll { get; } // すべての項目を展開するかどうか
191 | SettingsBase Setting { get; } // 設定オブジェクト
192 |
193 | // 設定画面のコントロールを作成
194 | void CreateControls(Form form);
195 |
196 | // リソースの解放
197 | void Dispose();
198 | }
199 | ```
200 |
201 | ### ISettingPropertyGrid インターフェース
202 | 設定プロパティグリッドのインターフェース。
203 |
204 | ```csharp
205 | public interface ISettingPropertyGrid {
206 | // シート名を取得
207 | string GetName();
208 | }
209 | ```
210 |
211 | ### XSSBase クラス
212 | 設定の基本クラス。ジェネリック型を使用して様々な設定を管理します。
213 |
214 | ```csharp
215 | public class XSSBase {
216 | // 設定の読み込み
217 | public void ReadSetting(T obj);
218 |
219 | // 設定の保存
220 | public void WriteSetting(T obj);
221 | }
222 | ```
223 |
224 | ### XSSForm クラス
225 | 設定の基本クラス。ジェネリック型を使用して様々な設定を管理します。
226 |
227 | ```csharp
228 | public class XSSForm : XSSBase