Controlling plug-ins in ELK, Part 2: Google Remote Procedure Call

As we have covered in previous articles in this series, SUSHI, ELK’s DAW and plug-in host, and the plug-ins running within it, are headless. This means they all run as command-line processes, and lack a Graphical User Interface (GUI). That also means that a plugin that is ported to work with ELK, might need some refactoring to compile without including desktop GUI dependencies.

Once your plug-in is running without its original GUI (assuming it had one to begin with), you now need to control it. In the previous entry in this series, we covered the first option supported – using Open Sound Control (OSC).

In this second installment, we will present how the Google Remote Procedure Call protocol (gRPC) can be used, and discuss what the differences are to using OSC.

If you have not read the OSC article, do please go ahead and do so before proceeding with this one.

What is gRPC

gRPC allows a client application to directly invoke methods on a server application – running locally or remotely – as if it were available locally. That is what the “Remote Procedure Call” acronym in the name stands for.

The methods that can be invoked on the remote server are specified using Protocol Buffers, “a mechanism for serializing structured data”. Protocol Buffers are specified in plain-text files which use the .proto extension.

Importantly, the server and client can each be written in any one out of a plethora of supported programming languages, with no need of one being aware of implementation details in the other.

Using gRPC is most easily demonstrated with an example – the simplest HelloWorld example serves as a great start!

The helloworld.proto file contains the following:

// The greeting service definition:
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
 
// The request message containing the user's name:
message HelloRequest {
  string name = 1;
}
 
// The response message containing the greetings:
message HelloReply {
  string message = 1;
}

The above text file, when compiled using the protocol buffer compiler protoc, generates classes for accessing the described data from the programming language at hand. These classes support accessing, and setting, the values described.

So, to invoke SayHello with the parameters expected as input to the method, the below is all that is needed on the client side (C++ code):

#include "helloworld.grpc.pb.h"

(...)

// Assembles the client's payload, sends it and presents the response back
// from the server.
std::string SayHello(const std::string& user) 
{
    // Data we are sending to the server.
    HelloRequest request;
    request.set_name(user);
 
    // Container for the data we expect from the server.
    HelloReply reply;
 
    // Context for the client. It could be used to convey extra information to
    // the server and/or tweak certain RPC behaviors.
    ClientContext context;
 
    // The actual RPC.
    Status status = stub_->SayHello(&context, request, &reply);
 
    // Act upon its status.
    if (status.ok()) 
   {
        return reply.message();
    }
    else 
    {
        std::cout << status.error_code() << ": " << status.error_message() << std::endl;
        return "RPC failed";
    }
}

And this is all that is needed on the server side:

#include "helloworld.grpc.pb.h"
(...)
Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override 
{
    std::string prefix("Hello ");
    reply->set_message(prefix + request->name());
    return Status::OK;
}

As you can see, HelloRequest, HelloReply, SayHello, are all defined, and accessible through including “helloworld.grpc.pb.h”.

Communication between client and server is over HTTP/2, and is in code transparently initiated simply by providing a network IP and Port.

The entire “HelloWorld” code is included in the gprc examples for C++.

Before deciding to implement gRPC support, we evaluated a spectrum of alternative RPC frameworks, such as ZeroMQ, msgpack-rpc, Thrift, to name a few. We found gRPC to be the most advantageous framework for the following reasons:

  • It has stronger connection handling: you can instantiate the client and server in any order without worry about initialization order side-effects.
  • Automatic heartbeat is sent through HTTP/2.
  • It supports both synchronous and asynchronous mechanisms.
  • It supports bidirectional streams from server to client and vice-versa, which allows for example the implementation of push notifications from server to client.
  • There is strong backward compatibility support when the protocol is extended.
  • It has strong resilience features for when Protocol Buffers are altered independently at server and client.
  • There is support for encryption, authentication, etc.
  • gRPC is finally also audited for security.

Relating gRPC to OSC

While OSC is a human-readable control message format also at runtime, gRPC and Protocol Buffers are human-readable only for the programmer. The Protocol Buffer files are plain-text, and the classes generated to access the described data, follow the conventions of each programming language at hand, which of course to programmers is also human-readable.

The human-readability of OSC messages comes at a performance cost: Each payload always includes the full string of the OSC Address Path, while with gRPC / Protobuf everything is instead encoded efficiently in binary form. This allows more efficient client / server implementations.

Also, in gRPC, a method invocation involves sending a message, and getting a related message in return – HelloRequest is a parameter in the SayHello invocation, which returns a HelloReply, in the above example. OSC on the other hand is one-directional. There is no built in mechanism for two-way communication. While that is of course implementable, it is up to the developer to define how, and enforce correct use – it is not part of the OSC specification.

OSC is independent of the transport layer, and is mainly used in a connectionless context with messages passed as UDP network packets. The reason being that reducing communication latency is prioritized in the contexts where OSC is used, over maintaining the order messages are received in, or ensuring that they are even received at all. That, despite the processing overhead in OSC compared to binay communication protocols, due to the string parsing computation required for each message.

gRPC on the other hand requires a connection to be established, and uses HTTP/2 for its transport layer. As message transport is binary the performance overhead is low, while also ensuring packed order and delivery..

You see from the above that OSC is a useful prototyping tool, as well as a  feature providing great flexibility for end-user customization and development. But for production code where the message passing parameter spaces  and mappings are stabilized, gRPC is a better choice.

Hence, for ELK, we support both.

Using gRPC with ELK

When Protocol Buffers are compiled gRPC exposes an API through the generated code. So to control SUSHI and the plugins it hosts using gRPC, a small program needs to be written which uses this generated library. We mainly use Python for this purpose, purely due to its widespread adoption. You are however free to use any other language supported by gRPC – and there are very many.

This Python program can interface with SUSHI gRPC interface, and map that to any other source of control signals accessible, for example data from the physical controls interfaces over the SENSEI software, introduced in our previous article on OSC. Any other programmatical source of control imaginable is also implementable in this manner however. You could for example implement a rich, interactive GUI for a touch screen connected to the ELK device. One may conceptualize this program as the “glue” program – that which implements the communication between SUSHI and its hosted plugins, and any sources and destinations  of control.

To wrap up, let’s look at a simple example. 

The SUSHI gRPC API exposes the method SetParameterValue, which as its argument takes the message ParameterSetRequest. Here is their Protocol Buffer Specification:

rpc GetParameterInfo (ParameterIdentifier) returns (ParameterInfo) {}
 
rpc SetParameterValue(ParameterSetRequest) returns () {}
 
message ParameterSetRequest {
    ParameterIdentifier parameter = 1;
    float value = 2;
}
 
message ParameterIdRequest {
    ProcessorIdentifier processor  = 1;
    string ParameterName = 2;
}
 
message ParameterIdentifier {
    int32 processor_id = 1;
    int32 parameter_id = 2;
}
 
message ParameterInfo {
    int32  id = 1;
    ParameterType  type = 2;
    string label = 3;
    string name = 4;
    string unit = 5;
    bool   automatable = 6;
    float  min_range = 7;
    float  max_range = 8;
}

The first step is to query all the information for all parameters. That is done by iterating over all processors, and for each, asking for all parameter id’s, using the GetParameterInfo rpc call.

That information can then be cached in the Python program if needed, and crucially, used for you to discover which parameters are exposed by each plugin (“processor”) – as they are described in the returned ParameterInfo message.

To then set the value to a parameter, a SetParameterValue invocation is used.

The gRPC API exposed by SUSHI is of course much more extensive, but it is no more complex in use than the above small example.

Closing Words

We hope you are now inspired to also start using gRPC – Thank you for reading! 

For any questions on gRPC, ELK, or anything else, please write to us at tech@mindmusiclabs.com

References

The official gRPC documentation:

https://grpc.io/docs/guides/

The Hello World C++ example used:

https://github.com/grpc/grpc/tree/v1.21.0/examples/cpp/helloworld

The official HTTP/2 repository and documentation:

https://http2.github.io/

© 2019 Modern Ancient Instruments Networked AB - Built by Ensoul

Headquartered at Jarlaplan 2, 113 57 Stockholm, Sweden

Email info@mindmusiclabs.com

Privacy Policy Cookie Policy