ZOOM, Teamsなどのためのサブタイトル(字幕)表示ツール


ZOOM, Teamsなどのためのサブタイトル(字幕)表示ツール

ZOOM, Teamsなどのリモート会議システムで手軽にサブタイトル(字幕)を表示するアプリケーションを書いてみる。
 送信パートと受信(表示)パートを分けて作成する。(ひとつにするとスレッドとかめんどくさいので。)
 送信パートは以下のようにVC++だけで書いた短いプログラム。
エディットボックスの中に漢字を書いて送信ボタンをプレスするとudpで漢字が送出される。

(buildが苦手という方は、このhttps://github.com/ultrahamlet/SjFX
gitにあるbinaryファイルで実験できる。
SjFX.exeが受信・表示プログラム。WindowsProject.exeが文字送信プログラム。
実験時にWebCamの接続を忘れずに。)

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#pragma comment(lib, "wsock32.lib")
#include <winsock2.h>
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <iostream>
#include <comdef.h>

#define MSG(m) {\
    MessageBoxA(NULL,m,NULL,MB_OK);}

//ウィンドウハンドル
HWND hwnd;
//インスタンスハンドル
HINSTANCE hinst;

//ウィンドウ横幅
#define WIDTH 500
#define HEIGHT 300

//ボタンウィンドウハンドル
HWND hwnd_child;
HWND hwnd_child2;

#define CHILD_ID 1
#define BUTTON_ID1 2

// socket
SOCKET sock;
// アドレス等格納
struct sockaddr_in addr;

LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_COMMAND:
        if (LOWORD(wp) == CHILD_ID) {
            switch (HIWORD(wp)) {

            case EN_SETFOCUS:
                //  set focus
                return 0;
            case EN_KILLFOCUS:
                //  kill focus
                return 0;
            case EN_UPDATE:
                //
                return 0;
            case EN_CHANGE:
                return 0;
            }

        }
        if (LOWORD(wp) == BUTTON_ID1) {


            LPWSTR str = new WCHAR[256 + 1];
            GetWindowText(hwnd_child, str, 256);
            SetWindowText(hwnd, str);
            //char buf[512];
            // バッファ char配列
            _bstr_t b(str);
            const char *buf = b;
            char sbuf[512];
            for (int i = 0; i < b.length() * 2; i++) {
                sbuf[i] = buf[i];
            }
            //sbuf[b.length() * 2] = 0;

            // 送信
            //sendto(sock, sbuf, sizeof(buf)* b.length()/2, 0, (struct sockaddr *)&addr, sizeof(addr));
            sendto(sock, sbuf, sizeof(sbuf), 0, (struct sockaddr *)&addr, sizeof(addr));
            free(str);
            return 0;
        }
        break;
    }
    return DefWindowProc(hwnd, msg, wp, lp);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
    MSG msg;
    WNDCLASS wc;

    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WinProc;
    wc.cbClsExtra = wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hCursor = wc.hIcon = NULL;
    wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wc.lpszClassName = _T("test");
    wc.lpszMenuName = NULL;

    if (!RegisterClass(&wc)) {
        MSG("クラスの登録失敗");
        return -1;
    }
    // メインウィンドウ
    hwnd = CreateWindowA("test", "test", WS_VISIBLE | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,
        0, 0, WIDTH, HEIGHT, NULL, NULL, hinst, NULL);


    //EDITコントロール作成
    hwnd_child = CreateWindowA("edit", NULL, WS_VISIBLE | WS_CHILD | ES_MULTILINE | ES_LEFT,
        20, 20, 400, 100, hwnd, (HMENU)CHILD_ID, hInstance, NULL);

    //ボタン作成
    hwnd_child2 = CreateWindow(
        TEXT("BUTTON"), TEXT("SEND"),
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
        240, 150, 100, 50,
        hwnd, (HMENU)BUTTON_ID1, hInstance, NULL
    );

    if (hwnd == NULL || hwnd_child == NULL || hwnd_child2 == NULL) {
        MSG("ウィンドウ作成失敗");
        return -1;
    }
    //
    hinst = hInstance;
    //
    WSAData wsaData;
    WSAStartup(MAKEWORD(2, 0), &wsaData);   //
    // socket作成
    sock = socket(AF_INET, SOCK_DGRAM, 0);  //
    addr.sin_family = AF_INET;  //IPv4
    addr.sin_port = htons(12345);   //通信ポート
    addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 送信アドレス
    int check;

    while (check = GetMessage(&msg, NULL, 0, 0)) {
        if (check == -1) {
            break;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    //クラス解放
    UnregisterClass(_T("test"), hinst);
    // socketの破棄
    closesocket(sock);
      // winsockの終了
    WSACleanup();

    return 0;
}

次は、これを受信するテストプログラム
openframeworksで書いた。

漢字を受信して表示したところ。「漢字のテストです。」の行が受信して表示されたところ。

先ずは、ofApp.h

ofApp.h

#pragma once

#include "ofMain.h"
#include <Windows.h>
#include "ofxNetwork.h"
//#include "ofxTrueTypeFontUC.h"
//#include "ofxGui.h"

class ofApp : public ofBaseApp{

    public:
        void setup();
        void update();
        void draw();

        void keyPressed(int key);
        void keyReleased(int key);
        void mouseMoved(int x, int y );
        void mouseDragged(int x, int y, int button);
        void mousePressed(int x, int y, int button);
        void mouseReleased(int x, int y, int button);
        void mouseEntered(int x, int y);
        void mouseExited(int x, int y);
        void windowResized(int w, int h);
        void dragEvent(ofDragInfo dragInfo);
        void gotMessage(ofMessage msg);
        void onTextChange(std::string & text);


private:
    ofTrueTypeFont font;
    string str;
    string str2;
    int keyIndex;
    //ofxPanel _gui;
    ofParameterGroup _parameters;
    ofParameter<std::string> _textParameter;
    ofEventListener _textParameterListener;
    // udp
    ofxUDPManager udpConnectionRx;
    ofxUDPManager udpConnectionTx;

    string rxMessage;
    string txMessage;
    string recvStr;
    string ostr;
    BOOL received;

};

続いて、ofApp.cpp
フォントはWindows/Fontsからコピペする。
漢字をきちんと表示するところによっと手間取った。

ofApp.cpp

#include "ofApp.h"
#include <codecvt>
vector<string> split(const string &s, char delim) {
    vector<string> elems;
    stringstream ss(s);
    string item;
    while (getline(ss, item, delim)) {
        if (!item.empty()) {
            elems.push_back(item);
        }
    }
    return elems;
}

std::wstring multi_to_wide_capi(std::string const& src)
{
    std::size_t converted{};
    std::vector<wchar_t> dest(src.size(), L'\0');
    if (::_mbstowcs_s_l(&converted, dest.data(), dest.size(), src.data(), _TRUNCATE, ::_create_locale(LC_ALL, "jpn")) != 0) {
        throw std::system_error{ errno, std::system_category() };
    }
    dest.resize(std::char_traits<wchar_t>::length(dest.data()));
    dest.shrink_to_fit();
    return std::wstring(dest.begin(), dest.end());
}
std::string wide_to_utf8_cppapi(std::wstring const& src)
{
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return converter.to_bytes(src);
}

std::string multi_to_utf8_cppapi(std::string const& src)
{
    auto const wide = multi_to_wide_capi(src);
    return wide_to_utf8_cppapi(wide);
}

std::string toUtf8(const std::wstring &str)
{
    std::string ret;
    int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), str.length(), NULL, 0, NULL, NULL);
    if (len > 0)
    {
        ret.resize(len);
        WideCharToMultiByte(CP_UTF8, 0, str.c_str(), str.length(), &ret[0], len, NULL, NULL);
    }
    return ret;
}
//--------------------------------------------------------------
void ofApp::setup(){
    ofTrueTypeFontSettings settings("jadhei01m.ttf", 24);
    settings.addRanges(ofAlphabet::Emoji);//絵文字
    settings.addRanges(ofAlphabet::Japanese);//日本語
    settings.addRange(ofUnicode::Space);//スペース
    settings.addRange(ofUnicode::IdeographicSpace);//全角スペース
    settings.addRange(ofUnicode::Latin);//アルファベット等
    settings.addRange(ofUnicode::Latin1Supplement);//記号、アクサン付き文字など
    settings.addRange(ofUnicode::NumberForms);//数字?
    settings.addRange(ofUnicode::Arrows);//矢印
    settings.addRange(ofUnicode::MathOperators);//数式記号
    settings.addRange(ofUnicode::Hiragana);//ひらがな
    settings.addRange(ofUnicode::Katakana);//カタカナ
    settings.addRange(ofUnicode::MiscSymbolsAndPictographs);//絵文字など
    settings.addRange(ofUnicode::Emoticons);//エモーティコン
    //
    if (!font.load(settings))
        cout << "couldn't load font" << endl;
    auto bufferLines = ofBufferFromFile("intro.txt").getText();
    for (int i = 0; i < bufferLines.size(); i++) {
        str += bufferLines[i];
    }
    font.setLetterSpacing(1.2);
    keyIndex = 0;
    //std::cin >> str2;
    _textParameter.addListener(this, &ofApp::onTextChange);
    _parameters.setName("params");
    _parameters.add(_textParameter.set("text", "default"));
    //_gui.setup(_parameters);
    udpConnectionRx.Create();

    udpConnectionRx.Bind(12345); //incomming data on my port # ...  
    udpConnectionRx.SetNonBlocking(true);

    received = false;
}

//--------------------------------------------------------------
void ofApp::update(){

    char data[512];
    udpConnectionRx.Receive(data, 512);

    uint16_t *val = (uint16_t*)data;
    //std::cout << val[0] << std::endl;
    BOOL strOK = true;
    for (int i = 0; i < sizeof(val); i++) {
        if (val[i] < 256) { strOK = false; break; }
    }
    if (strOK) {
        recvStr = data;
        received = true;
        std::cout << recvStr << std::endl;

        // shift-jis -> utf8
        ostr = multi_to_utf8_cppapi(recvStr);
    }
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofClear(0);
    ofSetColor(255, 255, 255);
    font.drawString(u8"漢字テスト", 10, 200);
    vector<string> text = split(str, '\n');
    font.drawString(text[keyIndex], 20, 370);
    if(received)font.drawString(ostr, 10, 250);
}

//--------------------------------------------------------------


 今回はこれまで、これをZOOMやTeamsの仮想カメラとして認識する完成版は次の投稿で...

 その2はこちら、https://qiita.com/quittardis/items/17cdb78892aa1c1013a1