| @@ -731,7 +731,7 @@ CARLA_EXPORT float carla_get_internal_parameter_value(uint pluginId, int32_t par | |||
| * Get a plugin's peak values. | |||
| * @param pluginId Plugin | |||
| */ | |||
| float* carla_get_peak_values(uint pluginId); | |||
| CARLA_EXPORT float* carla_get_peak_values(uint pluginId); | |||
| /*! | |||
| * Get a plugin's input peak value. | |||
| @@ -20,6 +20,7 @@ | |||
| # Imports (Global) | |||
| import requests | |||
| from websocket import WebSocket, WebSocketConnectionClosedException | |||
| # --------------------------------------------------------------------------------------------------------------------- | |||
| # Imports (Custom) | |||
| @@ -29,49 +30,6 @@ from carla_backend_qt import * | |||
| import os | |||
| from time import sleep | |||
| # --------------------------------------------------------------------------------------------------------------------- | |||
| # Iterates over the content of a file-like object line-by-line. | |||
| # Based on code by Lars Kellogg-Stedman, see https://github.com/requests/requests/issues/2433 | |||
| def iterate_stream_nonblock(stream, chunk_size=1024): | |||
| pending = None | |||
| while True: | |||
| try: | |||
| chunk = os.read(stream.raw.fileno(), chunk_size) | |||
| except BlockingIOError: | |||
| break | |||
| if not chunk: | |||
| break | |||
| if pending is not None: | |||
| chunk = pending + chunk | |||
| pending = None | |||
| lines = chunk.splitlines() | |||
| if lines and lines[-1]: | |||
| pending = lines.pop() | |||
| for line in lines: | |||
| yield line | |||
| if not pending: | |||
| break | |||
| if pending: | |||
| yield pending | |||
| # --------------------------------------------------------------------------------------------------------------------- | |||
| def create_stream(baseurl): | |||
| stream = requests.get("{}/stream".format(baseurl), stream=True, timeout=0.1) | |||
| if stream.encoding is None: | |||
| stream.encoding = 'utf-8' | |||
| return stream | |||
| # --------------------------------------------------------------------------------------------------------------------- | |||
| # Carla Host object for connecting to the REST API backend | |||
| @@ -79,14 +37,23 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| def __init__(self): | |||
| CarlaHostQtNull.__init__(self) | |||
| self.baseurl = "http://localhost:2228" | |||
| self.stream = create_stream(self.baseurl) | |||
| self.host = "localhost" | |||
| self.port = 2228 | |||
| self.baseurl = "http://{}:{}".format(self.host, self.port) | |||
| self.socket = WebSocket() | |||
| self.socket.connect("ws://{}:{}/ws".format(self.host, self.port), timeout=1) | |||
| self.isRemote = True | |||
| self.isRunning = True | |||
| self.peaks = [] | |||
| for i in range(99): | |||
| self.peaks.append((0.0, 0.0, 0.0, 0.0)) | |||
| def get_engine_driver_count(self): | |||
| # FIXME | |||
| return int(requests.get("{}/get_engine_driver_count".format(self.baseurl)).text) - 1 | |||
| return int(requests.get("{}/get_engine_driver_count".format(self.baseurl)).text) | |||
| def get_engine_driver_name(self, index): | |||
| return requests.get("{}/get_engine_driver_name".format(self.baseurl), params={ | |||
| @@ -114,13 +81,22 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| return bool(int(requests.get("{}/engine_close".format(self.baseurl)).text)) | |||
| def engine_idle(self): | |||
| closed = False | |||
| stream = self.stream | |||
| if not self.isRunning: | |||
| return | |||
| while True: | |||
| try: | |||
| line = self.socket.recv().strip() | |||
| except WebSocketConnectionClosedException: | |||
| self.isRunning = False | |||
| if self.fEngineCallback is None: | |||
| self.fEngineCallback(None, ENGINE_CALLBACK_QUIT, 0, 0, 0, 0.0, "") | |||
| return | |||
| for line in iterate_stream_nonblock(stream): | |||
| line = line.decode('utf-8', errors='ignore') | |||
| if line == "Keep-Alive": | |||
| return | |||
| if line.startswith("Carla: "): | |||
| elif line.startswith("Carla: "): | |||
| if self.fEngineCallback is None: | |||
| continue | |||
| @@ -137,15 +113,24 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| # pass to callback | |||
| self.fEngineCallback(None, action, pluginId, value1, value2, value3, valueStr) | |||
| elif line == "Connection: close": | |||
| if not closed: | |||
| self.stream = create_stream(self.baseurl) | |||
| closed = True | |||
| elif line.startswith("Peaks: "): | |||
| # split values from line | |||
| pluginId, value1, value2, value3, value4 = line[7:].split(" ",5) | |||
| if closed: | |||
| stream.close() | |||
| # convert to proper types | |||
| pluginId = int(pluginId) | |||
| value1 = float(value1) | |||
| value2 = float(value2) | |||
| value3 = float(value3) | |||
| value4 = float(value4) | |||
| # store peaks | |||
| self.peaks[pluginId] = (value1, value2, value3, value4) | |||
| def is_engine_running(self): | |||
| if not self.isRunning: | |||
| return False | |||
| try: | |||
| return bool(int(requests.get("{}/is_engine_running".format(self.baseurl)).text)) | |||
| except requests.exceptions.ConnectionError: | |||
| @@ -192,7 +177,7 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| def patchbay_refresh(self, external): | |||
| return bool(int(requests.get("{}/patchbay_refresh".format(self.baseurl), params={ | |||
| 'external': external, | |||
| 'external': int(external), | |||
| }).text)) | |||
| def transport_play(self): | |||
| @@ -215,7 +200,13 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| return int(requests.get("{}/get_current_transport_frame".format(self.baseurl)).text) | |||
| def get_transport_info(self): | |||
| return requests.get("{}/get_transport_info".format(self.baseurl)).json() | |||
| if self.isRunning: | |||
| try: | |||
| return requests.get("{}/get_transport_info".format(self.baseurl)).json() | |||
| except requests.exceptions.ConnectionError: | |||
| if self.fEngineCallback is None: | |||
| self.fEngineCallback(None, ENGINE_CALLBACK_QUIT, 0, 0, 0, 0.0, "") | |||
| return PyCarlaTransportInfo() | |||
| def get_current_plugin_count(self): | |||
| return int(requests.get("{}/get_current_plugin_count".format(self.baseurl)).text) | |||
| @@ -411,10 +402,16 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| }).text) | |||
| def get_current_parameter_value(self, pluginId, parameterId): | |||
| return float(requests.get("{}/get_current_parameter_value".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'parameterId': parameterId, | |||
| }).text) | |||
| if self.isRunning: | |||
| try: | |||
| return float(requests.get("{}/get_current_parameter_value".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'parameterId': parameterId, | |||
| }).text) | |||
| except requests.exceptions.ConnectionError: | |||
| if self.fEngineCallback is None: | |||
| self.fEngineCallback(None, ENGINE_CALLBACK_QUIT, 0, 0, 0, 0.0, "") | |||
| return 0.0 | |||
| def get_internal_parameter_value(self, pluginId, parameterId): | |||
| return float(requests.get("{}/get_internal_parameter_value".format(self.baseurl), params={ | |||
| @@ -423,28 +420,22 @@ class CarlaHostQtWeb(CarlaHostQtNull): | |||
| }).text) | |||
| def get_input_peak_value(self, pluginId, isLeft): | |||
| return float(requests.get("{}/get_input_peak_value".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'isLeft': isLeft, | |||
| }).text) | |||
| return self.peaks[pluginId][0 if isLeft else 1] | |||
| def get_output_peak_value(self, pluginId, isLeft): | |||
| return float(requests.get("{}/get_output_peak_value".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'isLeft': isLeft, | |||
| }).text) | |||
| return self.peaks[pluginId][2 if isLeft else 3] | |||
| def set_option(self, pluginId, option, yesNo): | |||
| requests.get("{}/set_option".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'option': option, | |||
| 'yesNo': yesNo, | |||
| 'yesNo': int(yesNo), | |||
| }) | |||
| def set_active(self, pluginId, onOff): | |||
| requests.get("{}/set_active".format(self.baseurl), params={ | |||
| 'pluginId': pluginId, | |||
| 'onOff': onOff, | |||
| 'onOff': int(onOff), | |||
| }) | |||
| def set_drywet(self, pluginId, value): | |||
| @@ -23,7 +23,10 @@ endif | |||
| BUILD_CXX_FLAGS += -I$(CWD) -I$(CWD)/backend -I$(CWD)/includes -I$(CWD)/modules -I$(CWD)/utils | |||
| LINK_FLAGS += -L$(BINDIR) -lcarla_standalone2 -lcarla_utils -lrestbed -lpthread -Wl,-rpath=$(shell realpath $(CWD)/../bin) | |||
| LINK_FLAGS += -Wl,-rpath=$(shell realpath $(CWD)/../bin) | |||
| LINK_FLAGS += -L$(BINDIR) -lcarla_standalone2 -lcarla_utils | |||
| LINK_FLAGS += -lrestbed -lssl -lcrypto | |||
| LINK_FLAGS += -lpthread | |||
| # ---------------------------------------------------------------------------------------------------------------------- | |||
| @@ -24,23 +24,15 @@ | |||
| static bool gEngineRunning = false; | |||
| void engine_idle_handler() | |||
| { | |||
| if (gEngineRunning) | |||
| carla_engine_idle(); | |||
| } | |||
| // ------------------------------------------------------------------------------------------------------------------- | |||
| static void EngineCallback(void* ptr, EngineCallbackOpcode action, uint pluginId, int value1, int value2, float value3, const char* valueStr) | |||
| { | |||
| #if 0 | |||
| carla_stdout("EngineCallback(%p, %u:%s, %u, %i, %i, %f, %s)", | |||
| ptr, (uint)action, EngineCallbackOpcode2Str(action), pluginId, value1, value2, value3, valueStr); | |||
| #endif | |||
| carla_debug("EngineCallback(%p, %u:%s, %u, %i, %i, %f, %s)", | |||
| ptr, (uint)action, EngineCallbackOpcode2Str(action), pluginId, value1, value2, value3, valueStr); | |||
| char msgBuf[1024]; | |||
| std::snprintf(msgBuf, 1023, "Carla: %u %u %i %i %f %s\n", action, pluginId, value1, value2, value3, valueStr); | |||
| std::snprintf(msgBuf, 1023, "Carla: %u %u %i %i %f %s", action, pluginId, value1, value2, value3, valueStr); | |||
| msgBuf[1023] = '\0'; | |||
| switch (action) | |||
| @@ -56,7 +48,10 @@ static void EngineCallback(void* ptr, EngineCallbackOpcode action, uint pluginId | |||
| break; | |||
| } | |||
| send_server_side_message(msgBuf); | |||
| return send_server_side_message(msgBuf); | |||
| // maybe unused | |||
| (void)ptr; | |||
| } | |||
| static const char* FileCallback(void* ptr, FileCallbackOpcode action, bool isDir, const char* title, const char* filter) | |||
| @@ -274,7 +269,7 @@ void handle_carla_transport_bpm(const std::shared_ptr<Session> session) | |||
| const std::shared_ptr<const Request> request = session->get_request(); | |||
| const double bpm = std::atof(request->get_query_parameter("bpm").c_str()); | |||
| CARLA_SAFE_ASSERT_RETURN(bpm > 0.0,) // FIXME | |||
| CARLA_SAFE_ASSERT_RETURN(bpm > 0.0, session->close(OK)) // FIXME | |||
| carla_transport_bpm(bpm); | |||
| session->close(OK); | |||
| @@ -31,11 +31,26 @@ | |||
| // ------------------------------------------------------------------------------------------------------------------- | |||
| std::vector<std::shared_ptr<Session>> gSessions; | |||
| #include <map> | |||
| #include <restbed> | |||
| #include <system_error> | |||
| #include <openssl/sha.h> | |||
| #include <openssl/hmac.h> | |||
| #include <openssl/evp.h> | |||
| #include <openssl/bio.h> | |||
| #include <openssl/buffer.h> | |||
| using namespace std; | |||
| using namespace restbed; | |||
| using namespace std::chrono; | |||
| // std::vector<std::shared_ptr<Session>> gSessions; | |||
| CarlaStringList gSessionMessages; | |||
| CarlaMutex gSessionMessagesMutex; | |||
| std::map< string, shared_ptr< WebSocket > > sockets = { }; | |||
| // ------------------------------------------------------------------------------------------------------------------- | |||
| void send_server_side_message(const char* const message) | |||
| @@ -47,20 +62,6 @@ void send_server_side_message(const char* const message) | |||
| // ------------------------------------------------------------------------------------------------------------------- | |||
| static void register_server_side_handler(const std::shared_ptr<Session> session) | |||
| { | |||
| const auto headers = std::multimap<std::string, std::string> { | |||
| { "Connection", "keep-alive" }, | |||
| { "Cache-Control", "no-cache" }, | |||
| { "Content-Type", "text/event-stream" }, | |||
| { "Access-Control-Allow-Origin", "*" } //Only required for demo purposes. | |||
| }; | |||
| session->yield(OK, headers, [](const std::shared_ptr<Session> rsession) { | |||
| gSessions.push_back(rsession); | |||
| }); | |||
| } | |||
| static void event_stream_handler(void) | |||
| { | |||
| static bool firstInit = true; | |||
| @@ -71,12 +72,10 @@ static void event_stream_handler(void) | |||
| carla_stdout("Carla REST-API Server started"); | |||
| } | |||
| gSessions.erase( | |||
| std::remove_if(gSessions.begin(), gSessions.end(), | |||
| [](const std::shared_ptr<Session> &a) { | |||
| return a->is_closed(); | |||
| }), | |||
| gSessions.end()); | |||
| const bool running = carla_is_engine_running(); | |||
| if (running) | |||
| carla_engine_idle(); | |||
| CarlaStringList messages; | |||
| @@ -89,27 +88,214 @@ static void event_stream_handler(void) | |||
| for (auto message : messages) | |||
| { | |||
| // std::puts(message); | |||
| for (auto entry : sockets) | |||
| { | |||
| auto socket = entry.second; | |||
| if (socket->is_open()) | |||
| socket->send(message); | |||
| } | |||
| } | |||
| if (running) | |||
| { | |||
| if (const uint count = carla_get_current_plugin_count()) | |||
| { | |||
| char msgBuf[1024]; | |||
| float* peaks; | |||
| for (uint i=0; i<count; ++i) | |||
| { | |||
| peaks = carla_get_peak_values(i); | |||
| CARLA_SAFE_ASSERT_BREAK(peaks != nullptr); | |||
| std::snprintf(msgBuf, 1023, "Peaks: %u %f %f %f %f", i, peaks[0], peaks[1], peaks[2], peaks[3]); | |||
| msgBuf[1023] = '\0'; | |||
| for (auto entry : sockets) | |||
| { | |||
| auto socket = entry.second; | |||
| if (socket->is_open()) | |||
| socket->send(msgBuf); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| for (auto entry : sockets) | |||
| { | |||
| auto socket = entry.second; | |||
| if (socket->is_open()) | |||
| socket->send("Keep-Alive"); | |||
| } | |||
| } | |||
| // ------------------------------------------------------------------------------------------------------------------- | |||
| string base64_encode( const unsigned char* input, int length ) | |||
| { | |||
| BIO* bmem, *b64; | |||
| BUF_MEM* bptr; | |||
| b64 = BIO_new( BIO_f_base64( ) ); | |||
| bmem = BIO_new( BIO_s_mem( ) ); | |||
| b64 = BIO_push( b64, bmem ); | |||
| BIO_write( b64, input, length ); | |||
| ( void ) BIO_flush( b64 ); | |||
| BIO_get_mem_ptr( b64, &bptr ); | |||
| char* buff = ( char* )malloc( bptr->length ); | |||
| memcpy( buff, bptr->data, bptr->length - 1 ); | |||
| buff[ bptr->length - 1 ] = 0; | |||
| BIO_free_all( b64 ); | |||
| return buff; | |||
| } | |||
| multimap< string, string > build_websocket_handshake_response_headers( const shared_ptr< const Request >& request ) | |||
| { | |||
| auto key = request->get_header( "Sec-WebSocket-Key" ); | |||
| key.append( "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ); | |||
| Byte hash[ SHA_DIGEST_LENGTH ]; | |||
| SHA1( reinterpret_cast< const unsigned char* >( key.data( ) ), key.length( ), hash ); | |||
| multimap< string, string > headers; | |||
| headers.insert( make_pair( "Upgrade", "websocket" ) ); | |||
| headers.insert( make_pair( "Connection", "Upgrade" ) ); | |||
| headers.insert( make_pair( "Sec-WebSocket-Accept", base64_encode( hash, SHA_DIGEST_LENGTH ) ) ); | |||
| for (auto session : gSessions) | |||
| session->yield(OK, message); | |||
| return headers; | |||
| } | |||
| void close_handler( const shared_ptr< WebSocket > socket ) | |||
| { | |||
| carla_stdout("CLOSE %i", __LINE__); | |||
| if ( socket->is_open( ) ) | |||
| { | |||
| auto response = make_shared< WebSocketMessage >( WebSocketMessage::CONNECTION_CLOSE_FRAME, Bytes( { 10, 00 } ) ); | |||
| socket->send( response ); | |||
| } | |||
| carla_stdout("CLOSE %i", __LINE__); | |||
| const auto key = socket->get_key( ); | |||
| sockets.erase( key ); | |||
| fprintf( stderr, "Closed connection to %s.\n", key.data( ) ); | |||
| } | |||
| void error_handler( const shared_ptr< WebSocket > socket, const error_code error ) | |||
| { | |||
| const auto key = socket->get_key( ); | |||
| fprintf( stderr, "WebSocket Errored '%s' for %s.\n", error.message( ).data( ), key.data( ) ); | |||
| } | |||
| void message_handler( const shared_ptr< WebSocket > source, const shared_ptr< WebSocketMessage > message ) | |||
| { | |||
| const auto opcode = message->get_opcode( ); | |||
| if (const uint count = carla_get_current_plugin_count()) | |||
| if ( opcode == WebSocketMessage::PING_FRAME ) | |||
| { | |||
| auto response = make_shared< WebSocketMessage >( WebSocketMessage::PONG_FRAME, message->get_data( ) ); | |||
| source->send( response ); | |||
| } | |||
| else if ( opcode == WebSocketMessage::PONG_FRAME ) | |||
| { | |||
| char msgBuf[1024]; | |||
| float* peaks; | |||
| //Ignore PONG_FRAME. | |||
| // | |||
| //Every time the ping_handler is scheduled to run, it fires off a PING_FRAME to each | |||
| //WebSocket. The client, if behaving correctly, will respond with a PONG_FRAME. | |||
| // | |||
| //On each occasion the underlying TCP socket sees any packet data transfer, whether | |||
| //a PING, PONG, TEXT, or BINARY... frame. It will automatically reset the timeout counter | |||
| //leaving the connection active; see also Settings::set_connection_timeout. | |||
| return; | |||
| } | |||
| else if ( opcode == WebSocketMessage::CONNECTION_CLOSE_FRAME ) | |||
| { | |||
| source->close( ); | |||
| } | |||
| else if ( opcode == WebSocketMessage::BINARY_FRAME ) | |||
| { | |||
| //We don't support binary data. | |||
| auto response = make_shared< WebSocketMessage >( WebSocketMessage::CONNECTION_CLOSE_FRAME, Bytes( { 10, 03 } ) ); | |||
| source->send( response ); | |||
| } | |||
| else if ( opcode == WebSocketMessage::TEXT_FRAME ) | |||
| { | |||
| auto response = make_shared< WebSocketMessage >( *message ); | |||
| response->set_mask( 0 ); | |||
| for (uint i=0; i<count; ++i) | |||
| for ( auto socket : sockets ) | |||
| { | |||
| peaks = carla_get_peak_values(i); | |||
| CARLA_SAFE_ASSERT_BREAK(peaks != nullptr); | |||
| auto destination = socket.second; | |||
| destination->send( response ); | |||
| } | |||
| const auto key = source->get_key( ); | |||
| const auto data = String::format( "Received message '%.*s' from %s\n", message->get_data( ).size( ), message->get_data( ).data( ), key.data( ) ); | |||
| fprintf( stderr, "%s", data.data( ) ); | |||
| } | |||
| } | |||
| std::snprintf(msgBuf, 1023, "Peaks: %u %f %f %f %f\n", i, peaks[0], peaks[1], peaks[2], peaks[3]); | |||
| msgBuf[1023] = '\0'; | |||
| void get_method_handler(const shared_ptr<Session> session) | |||
| { | |||
| carla_stdout("HERE %i", __LINE__); | |||
| const auto request = session->get_request(); | |||
| const auto connection_header = request->get_header("connection", String::lowercase); | |||
| carla_stdout("HERE %i", __LINE__); | |||
| for (auto session : gSessions) | |||
| session->yield(OK, msgBuf); | |||
| if ( connection_header.find( "upgrade" ) not_eq string::npos ) | |||
| { | |||
| if ( request->get_header( "upgrade", String::lowercase ) == "websocket" ) | |||
| { | |||
| const auto headers = build_websocket_handshake_response_headers( request ); | |||
| session->upgrade( SWITCHING_PROTOCOLS, headers, [ ]( const shared_ptr< WebSocket > socket ) | |||
| { | |||
| if ( socket->is_open( ) ) | |||
| { | |||
| socket->set_close_handler( close_handler ); | |||
| socket->set_error_handler( error_handler ); | |||
| socket->set_message_handler( message_handler ); | |||
| socket->send("Welcome to Corvusoft Chat!"); | |||
| auto key = socket->get_key( ); | |||
| sockets[key] = socket; | |||
| } | |||
| else | |||
| { | |||
| fprintf( stderr, "WebSocket Negotiation Failed: Client closed connection.\n" ); | |||
| } | |||
| } ); | |||
| return; | |||
| } | |||
| } | |||
| session->close( BAD_REQUEST ); | |||
| } | |||
| void ping_handler( void ) | |||
| { | |||
| for ( auto entry : sockets ) | |||
| { | |||
| auto key = entry.first; | |||
| auto socket = entry.second; | |||
| if ( socket->is_open( ) ) | |||
| { | |||
| socket->send( WebSocketMessage::PING_FRAME ); | |||
| } | |||
| else | |||
| { | |||
| socket->close( ); | |||
| } | |||
| } | |||
| } | |||
| @@ -132,11 +318,11 @@ int main(int, const char**) | |||
| { | |||
| Service service; | |||
| // server-side messages | |||
| // websocket | |||
| { | |||
| std::shared_ptr<Resource> resource = std::make_shared<Resource>(); | |||
| resource->set_path("/stream"); | |||
| resource->set_method_handler("GET", register_server_side_handler); | |||
| resource->set_path("/ws"); | |||
| resource->set_method_handler("GET", get_method_handler); | |||
| service.publish(resource); | |||
| } | |||
| @@ -251,8 +437,8 @@ int main(int, const char**) | |||
| make_resource(service, "/get_cached_plugin_info", handle_carla_get_cached_plugin_info); | |||
| // schedule events | |||
| service.schedule(engine_idle_handler); // FIXME, crashes on fast times, but we need ~30Hz for OSC.. | |||
| service.schedule(event_stream_handler, std::chrono::milliseconds(500)); | |||
| service.schedule(event_stream_handler, std::chrono::milliseconds(33)); | |||
| service.schedule(ping_handler, milliseconds(5000)); | |||
| std::shared_ptr<Settings> settings = std::make_shared<Settings>(); | |||
| settings->set_port(2228); | |||