2018年10月9日 星期二

Using Chromium Embedded Framework as the editor GUI of Virtual Studio Technology on Visual Studio

This article will show you how to use cef to run a javascript to create a slider GUI to be a gain controller of vst.


Step 1

Prepare the sdk and framework that we need

1. vst sdk (I used version 2.4)
2. cef (check the official link)
3. Microsoft Visual Studio (I used Visual Studio 2013 community to do the whole work)


Step 2

Create VS 2013 project by CMake

Make sure you have got the VS installed before using CMake.
This step is quite straightforward so I will skip it.


Step 3

Let's write some code


Part 1: Create a vst editor class.

Here, I provide the main implementation of them.

(1) Create a derived class of AEffEditor

class AGEditor : public AEffEditor
{
public:
 AGEditor(AudioEffect *effect);
 virtual ~AGEditor();

 virtual bool open(void *ptr);
 virtual void close();
 virtual bool getRect(ERect **ppErect);

private:
 LPSTR getEditorProcArg(void *ptr);
 LPCSTR getEditorPath();
 bool createPipeThread(); /* Receive UI value changed from editor */

private:
 ERect m_rect;
 PROCESS_INFORMATION m_cefEditorProcInfo;
 HANDLE m_pipeThreadHandle;
};


(2) Open the CEF as another process (Crucial!!)

bool AGEditor::open(void *ptr)
{
 LPCSTR strEditorPath = getEditorPath();
 LPSTR strEditorArg = getEditorProcArg(ptr);
 STARTUPINFO si;

 ZeroMemory(&si, sizeof(si));
 si.cb = sizeof(si);

 if (!CreateProcess(strEditorPath, strEditorArg, NULL, NULL, FALSE, 0, NULL, NULL, &si, &m_cefEditorProcInfo)) {
  printf("Failed to CreateProcess. Error=%d.\n", GetLastError());
  return false;
 }

 if (!createPipeThread()) {
  printf("Failed to Create pipe thread.\n");
  return false;
 }

 AEffEditor::open(ptr);

 return true;
}


(3) Communicate with cef using any IPC technique.
Here I use pipe.

DWORD WINAPI PipeThreadWork(LPVOID lpvParam)
{ 
 BOOL blConnected = FALSE;
 DWORD dwThreadId = 0;
 HANDLE hPipe = INVALID_HANDLE_VALUE;
 HANDLE hThread = NULL;
 LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\vstvolumecontrolpipe");
 AGEditor *pEditor = static_cast<AGEditor*>(lpvParam);

 while (true) {
  //printf("\nPipe Server: Main thread awaiting client connection on %s\n", lpszPipename);
  const DWORD dwPipeMode = PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT;
  
  hPipe = CreateNamedPipe(lpszPipename, PIPE_ACCESS_DUPLEX, dwPipeMode, PIPE_UNLIMITED_INSTANCES, BUFSIZE, BUFSIZE, 0, NULL);

  if (hPipe == INVALID_HANDLE_VALUE) {
   printf("Failed to CreateNamedPipe. Error=%d.\n", GetLastError());
   return -1;
  } 

  /* Wait for pipe to connect */
  blConnected = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);

  if (blConnected) {
   //printf("Client connected, creating a processing thread.\n");
   ParamType *Param = new ParamType{ hPipe, pEditor };

   hThread = CreateThread(NULL, 0, InstanceThread, static_cast<LPVOID>(Param), 0, &dwThreadId);

   if (hThread == NULL) {
    printf("Failed to CreateThread, Error=%d.\n", GetLastError());
    return -1;
   } else {
    CloseHandle(hThread);
   }
  } else {
   CloseHandle(hPipe);
  }
 }

 return 0;
}


Part 2: Base on cef example to create a stand-alone application that can render javascript and communicate with C++.


(1) Write the browser-side C++ logic to handle request from GUI.

If there's a GUI action, then it will send a request to C++, and create a pipe to communicate with vst.

bool OnQuery(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int64 query_id,
             const CefString& request, bool persistent, CefRefPtr<Callback> callback) OVERRIDE
{
...//call valueChanged
}

void valueChanged(std::string strVol)
{
 const LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\vstvolumecontrolpipe");
 LPCSTR lpvMessage = strVol.c_str();
 HANDLE hPipe;
 BOOL blSuccess = FALSE;
 DWORD cbToWrite;
 DWORD cbWritten;
 DWORD dwMode = PIPE_READMODE_MESSAGE;
 
 while (true) {
  hPipe = CreateFile(lpszPipename, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
  
  if (hPipe != INVALID_HANDLE_VALUE) {
   break;
  }
  
  // Exit if an error other than ERROR_PIPE_BUSY occurs.
  if (GetLastError() != ERROR_PIPE_BUSY) {
   printf("Failed to open pipe. Error=%d\n", GetLastError());
   return;
  }

  // All pipe instances are busy, so wait for 20 seconds. 
  if (!WaitNamedPipe(lpszPipename, 20000)) {
   printf("Could not open pipe: 20 second wait timed out.");
   return;
  }
 }
 
 // The pipe connected; change to message-read mode. 
 blSuccess = SetNamedPipeHandleState(hPipe, &dwMode, NULL, NULL);

 if (!blSuccess) {
  printf("Failed to SetNamedPipeHandleState. Error=%d\n", GetLastError());
  return;
 }
 
 // Send a message to the pipe server.
 cbToWrite = (static_cast<unsigned long>(strlen(lpvMessage)) + 1) * sizeof(TCHAR);

 blSuccess = WriteFile(hPipe, lpvMessage, cbToWrite, &cbWritten, NULL);
 
 if (!blSuccess) {
  printf("Failed to WriteFile to pipe. Error=%d\n", GetLastError());
  return;
 }
  
  CloseHandle(hPipe);
  
  return;
}


(2) Write a GUI and its corresponding functionality by JavaScript

<script>
    $(function () {
        $("#slider-vertical").slider({
            orientation: "vertical",
            range: "min",
            min: 0,
            max: 100,
            value: 60,
            slide: function (event, ui) {
                $("#amount").val(ui.value);
                // Send a query to the browser process.
                window.cefQuery({
                    request: 'MessageRouterTest:' + ui.value,
                    onSuccess: function (response) { },
                    onFailure: function (error_code, error_message) { }
                });
            }
        });
        $("#amount").val($("#slider-vertical").slider("value"));
    });
</script>
<p>
    <label for="amount">Volume:</label>
    <input type="text" id="amount" readonly style="border:0; color:#f6931f; font-weight:bold;">
</p>

<div id="slider-vertical" style="height:200px;"></div>


(3) To let the window created by cef be put inside the window provided by vst, we need to take care of the "hwnd" as following:

const bool hasHwnd = command_line->HasSwitch("hwnd");
CefWindowInfo window_info;

if (hasHwnd) {
 const std::string& strHwnd = command_line->GetSwitchValue("hwnd");
 RECT wnd_rect = { 0, 0, 400, 400 };

 window_info.SetAsChild((HWND)(atoi(strHwnd.c_str())), wnd_rect);
} else {
 window_info.SetAsPopup(NULL, "Volume Controller - Gary Hsieh");
 window_info.height = 400;
 window_info.width = 400;
}

hwnd is a parameter I created for passing the hwnd to cef, and the vst part looks like:

LPSTR AGEditor::getEditorProcArg(void *ptr)
{
 std::ostringstream sStream;
 sStream << "Dummy --hwnd=\"" << (int)ptr << "\"";
 LPSTR editorArg = _strdup(sStream.str().c_str());

 return editorArg;
}

ptr is the hwnd provided by vst.
I simply cast it to int and cast it back to make it work.


Step 4

Verify by the mini host provided by the vst sdk. Then done!


Source code can be checked on here.

Reference

沒有留言:

張貼留言