w3c-speech: initial W3C speech API plugin and test client implementation.
authorKrisztian Litkey <kli@iki.fi>
Fri, 10 Oct 2014 21:28:02 +0000 (00:28 +0300)
committerKrisztian Litkey <krisztian.litkey@intel.com>
Fri, 7 Nov 2014 09:18:15 +0000 (11:18 +0200)
The primary purpose of the W3C speech plugin is to server as
the backend for implementing the W3C Speech API for Crosswalk.

See README.protocol for a description of the protocol implemented
by the plugin.

configure.ac
src/Makefile.am
src/plugins/client-api/w3c-speech/Makefile [new file with mode: 0644]
src/plugins/client-api/w3c-speech/README.protocol [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-client.c [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-message.c [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-message.h [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-protocol.h [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-server.c [new file with mode: 0644]
src/plugins/client-api/w3c-speech/w3c-server.h [new file with mode: 0644]

index c29c9b2..bf58e35 100644 (file)
@@ -297,6 +297,20 @@ AC_SUBST(ESPEAK_ENABLED)
 AC_SUBST(ESPEAK_CFLAGS)
 AC_SUBST(ESPEAK_LIBS)
 
+# Check if W3C Speech API plugin was enabled.
+AC_ARG_ENABLE(w3c-speech,
+              [  --enable-w3c-speech enable W3C Speech API plugin],
+             [enable_w3cspeech=$enableval], [enable_w3cspeech=no])
+
+if test "$enable_w3cspeech" = "yes"; then
+    AC_MSG_NOTICE([W3C Speech API plugin is enabled.])
+    AC_DEFINE([W3C_SPEECH_ENABLED], 1, [Enable W3C Speech API plugin ?])
+else
+    AC_MSG_NOTICE([W3C Speech API plugin is disabled.])
+fi
+
+AM_CONDITIONAL(W3C_SPEECH_ENABLED, [test "$enable_w3cspeech" = "yes"])
+
 # Check if systemd socket-based activation was enabled.
 AC_ARG_ENABLE(systemd,
               [  --enable-systemd       enable systemd socket-based activation],
@@ -343,6 +357,7 @@ echo "D-Bus support: $enable_dbus"
 echo "Sphinx support: $enable_sphinx"
 echo "Festival support: $enable_festival"
 echo "Espeak support: $enable_espeak"
+echo "W3C Speech API: $enable_w3cspeech"
 echo "systemd socket-based activation: $enable_systemd"
 echo "Bluetooth voice recording support: $enable_bluetooth"
 echo "Mediaplayer remote interface support: $enable_mpris"
index 2727800..28da423 100644 (file)
@@ -340,6 +340,37 @@ plugin_espeak_voice_la_LIBADD =                                    \
                $(ESPEAK_LIBS)
 endif
 
+# W3C Speech API plugin and test client
+if W3C_SPEECH_ENABLED
+plugin_LTLIBRARIES += plugin-w3c-speech.la
+
+plugin_w3c_speech_la_SOURCES =                                 \
+               plugins/client-api/w3c-speech/w3c-server.c      \
+               plugins/client-api/w3c-speech/w3c-message.c
+
+plugin_w3c_speech_la_CFLAGS  =                         \
+               $(AM_CFLAGS)
+
+plugin_w3c_speech_la_LDFLAGS =                         \
+               -module -avoid-version
+
+plugin_w3c_speech_la_LIBADD =
+
+bin_PROGRAMS += srs-w3c-client
+
+srs_w3c_client_SOURCES =                                       \
+               plugins/client-api/w3c-speech/w3c-client.c
+
+srs_w3c_client_CFLAGS =                                                \
+               $(AM_CFLAGS)                                    \
+               $(MURPHY_BREEDLINE_CFLAGS)                      \
+               $(MURPHY_COMMON_CFLAGS)
+
+srs_w3c_client_LDADD =                                         \
+               $(MURPHY_BREEDLINE_LIBS)                        \
+               $(MURPHY_COMMON_LIBS)
+endif
+
 # simple-voice synthesizer plugin
 #plugin_LTLIBRARIES += plugin-simple-voice.la
 #
diff --git a/src/plugins/client-api/w3c-speech/Makefile b/src/plugins/client-api/w3c-speech/Makefile
new file mode 100644 (file)
index 0000000..1c6f118
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C ../../.. $@
+
+%:
+       $(MAKE) -C ../../.. $(MAKECMDGOALS)
diff --git a/src/plugins/client-api/w3c-speech/README.protocol b/src/plugins/client-api/w3c-speech/README.protocol
new file mode 100644 (file)
index 0000000..6b52632
--- /dev/null
@@ -0,0 +1,580 @@
+
+0. Introduction
+---------------
+
+This document describes the simple JSON-based protocol used between
+the Winthorpe W3C Speech plugin and an external entity talking to it.
+In this document the term 'server' is generally used to refer to the
+Winthorpe W3C Speech plugin while the term 'client' is used to refer
+to any external entity using the protocol described here to interact
+with the plugin. A client is typically but not necessarily an entity
+within a Web Runtime or a Web Browser that wishes to use Winthorpe as
+the backend for rendering speech recognition and synthesis services.
+
+
+1. Message Encoding, Framing, And IPC
+-------------------------------------
+
+The Winthorpe W3C protocol uses JSON to encode messages. Connection-
+oriented stream sockets are used for the actual transmission of the
+bytes comprising an encoded messages. A simple framing scheme is used
+to help the receiving end to stay in sync with message boundaries for
+decoding. Every message transmitted over the wire is prefixed with a
+4-byte header containing the length of the following encoded message
+in network byte order, as depicted below.
+
+
+  +---------------------------------------------------------------------+
+  |       frame #0      |      frame #1       |   |      frame #N       |
+  +---------------------------------------------------------------------+
+  | size0 :  message#0  | size1 :  message#1  |...| sizeN :  message#N  |
+  +---------------------------------------------------------------------+
+  |       | size0 bytes |       | size1 bytes |   |       | sizeN bytes |
+  +---------------------------------------------------------------------+
+
+
+2. Message Types
+----------------
+
+Messages can be logically divided into three categories:
+
+  - requests
+  - replies
+  - events
+
+A request is a message that is sent by a client to the server. For
+example, to create a new speech recognizer instance a client sends a
+request of the appropriate type to Winthorpe. A reply is a message
+that Winthorpe sends to a client in response to a request. For example,
+when a speech recognizer instance has been succefully created, the
+server sends a reply with recognizer ID to the client. Events are
+unsolicited messages that Winthorpe sends to clients to notify them
+about events that the client might be interested in. For example, when
+a speech utterance has been succesfully recognized, Winthorpe sends a
+corresponding event to the client that has started speech recognition.
+
+
+3. Message Structure
+--------------------
+
+Every message consists of a fixed number of common, type-agnostic
+fields and a variable number of type-specific fields. The type-agnostic
+fields are the following.
+
+Request Number (reqno)
+
+The request number is a monotonically increasing integer assigned by
+the requestor, IOW the client. It is used to correlate between requests
+and replies. A reply to a request will always have the same request
+number as the request itself. Events will always have 0 as their request
+number.
+
+Message Type (type)
+
+The message type field indicates what type of information the message is
+carrying. It can be one of
+
+Object Creation Request (create)
+
+An object creation request creates an object of the specified type on
+the server side and optionally initializes it with a set of initial
+variables which could alternatively be also set later with an Attribute
+Set Request (set, see below). Upon success, the server replies with a
+successful status message that contains the object ID of the newly
+created object. Upon failure, the server responds with an error status
+reply. Here is a sample create request:
+  {
+      reqno: 1,
+      type: 'create',
+      object: 'recognizer',
+      set: {
+          grammars: [ { src: 'winthorpe://player', weight: 1.0 } ],
+          lang: 'en-GB',
+          continuous: true,
+      },
+  }
+
+Object Deletion Request (delete)
+
+An object deletion request deletes the object with specified ID from
+the server. Upon success, the server replies with a success status
+reply, upon error with an error status reply. Here is a sample delete
+request:
+  {
+      reqno: 1,
+      type: 'delete',
+      id: 1,
+  }
+
+Attribute Setting Request (set)
+
+And object configuration request changes the given parameters of the
+given object. Upon success, the server replies with a success status
+reply. Upon error the server responds with an error status reply.
+Here is a sample set request:
+  {
+      reqno: 1,
+      type: 'set',
+      id: 1,
+      set: {
+          grammars: [ { src: 'winthorpe://player', weight: 1.0 } ],
+          lang: 'en-GB',
+          continuous: true,
+      },
+  }
+
+
+Reply Message (status)
+
+The server responds to all requests with a status reply. Successful
+requests are responded to with a success status reply. Unsuccessful
+requests are respondes to with an error status reply. Here is a
+sample success reply:
+  {
+      reqno: 1,
+      type: 'status',
+      status: 0,
+      message: 'recognizer instance created'
+  }
+
+Here is a sample error status reply:
+  {
+      reqno: 1,
+      type: 'status',
+      status: 5,
+      error: 'bad-grammar',
+      message: 'Unknown grammar player requested.'
+  }
+
+
+Method Invocation Request (invoke)
+
+A method invocation request asks the server to invoke the specified
+method on the given object with the given argument list. Upon succesful
+completion the server responds with a success status reply. Upon error
+the server responds with an error status reply. Here is a sample method
+invocation call request:
+  {
+      reqno: 5,
+      type: 'invoke',
+      id: 1,
+      method: 'start',
+      args: []
+  }
+
+Event Message (event)
+
+Event messages are sent by the server to notify clients about events of
+potential interest. Clients then decide whether and how to react to the
+events they received. Events are never replied to. From the servers point
+of view they are fire-and-forget type of messages. Here is a sample
+event:
+  {
+      reqno: 0,
+      type: 'event',
+      id: 0,
+      event: 'match',
+      timestamp: { sec: 1412792206, usec: 742317 },
+      match: {
+          final: true,
+          length: 1,
+          results: [ { confidence: 0.89, transcript: "play next" } ]
+      }
+  }
+
+
+Timestamp Request (timestamp)
+
+A timestamp request asks the server for its notion of time used in
+timestamps. The server responds to a timestamp request with a success
+status reply containing the timestamp obtained (right before) at the
+moment of sending the reply. Here is a sample timestamp request:
+  {
+      reqno: 5,
+      type: 'typestamp'
+  }
+
+And here is a sample reply:
+  {
+      reqno: 5,
+      type: 'status',
+      status: 0,
+      timestamp: { sec: 1412792206, usec: 742317 },
+  }
+
+The timestamp request can be used to establish a correlation between
+the clock on the server side and the clock on the client side, for
+instance in the events to be emitted in a WRT JS engine).
+
+The timestamp request can also be used to estimate/compensate for the
+communication delay associated with message delivery, although in the
+case of using unix domain sockets or TCP sockets over the loopback
+device this delay should probably be neglible from the JS engine point
+of view for all practical purposes.
+
+
+4. Speech Recognition Protocol
+------------------------------
+
+4.1 Creating An Instance
+
+A speech recognition instance can be created by sending a 'create'
+request with object type 'recognizer'.
+
+  {
+      reqno: 1,
+      type: 'create',
+      object: 'recognizer',
+      set: {
+          lang: 'en-GB',
+          grammars: [ { src: 'winthorpe://player', weight: 1.0 } ],
+          continuous: true,
+          interimResults: false,
+          maxAlternatives: 1,
+          events: [ 'result', 'nomatch', 'error', 'start', 'end' ]
+          shared: true,
+      }
+  }
+
+Although the server allows all standard attributes to be set, currently
+the interimResults, maxAlternatives and serviceURI attributes are silently
+ignored.
+
+In addition to the standard attributes, Winthorpe allows the following non-
+standard ones to be set:
+
+  - events: The set of events the client wants to be notified about. This
+    should reflect which event handlers have been set up for the related
+    JS object in the WRT.
+
+  - shared: Go for shared Winthorpe voice focus, instead of exclusive one.
+
+Upon successful creation, the server responds with a sucess status reply
+containing the object ID of the newly created recognizer.
+
+
+  {
+      reqno: 1,
+      type: 'status',
+      status: 0,
+      id: 1,
+      message: 'recognizer 1 created'
+  }
+
+
+This ID needs to be used in all subsequent requests releated to this
+instance.
+
+Upon error the server responds with an error status reply, containing an
+error status code, an error string, and optionally a verbose error message
+further clarifying to reason for failure.
+
+4.2 Explicit Deletion
+
+An existing recognizer instance can be explicitly deleted by sending a
+delete request to the server with the object ID of the recognizer.
+
+  {
+      reqno: 2,
+      type: 'delete',
+      id: 1,
+  }
+
+The server responds to the deletion request with a status reply.
+
+  {
+      reqno: 2,
+      type: 'status',
+      status: 0,
+  }
+
+4.3 Setting Recognizer Attributes
+
+Recognizer attributes can either be set during creation of the instance,
+or later using an attribute setting request. For a list of settable
+attributes, see the recognizer creation request above. The server might
+not be able to change certain attributes, depending on the state of the
+recognizer. If setting an attribute fails, the server will respond with
+an error status reply to the request.
+
+4.4 Starting, Stopping, And Aborting The Recognizer
+
+The recognizer start, stop, and abort methods can be invoked using a
+method invocation request.
+
+  {
+      reqno: 15,
+      type: 'invoke',
+      id: 1,
+      method: 'start', (or 'stop', or 'abort')
+      args: []
+  }
+
+The server responds with a status reply.
+
+4.5 Recognition Events
+
+Winthorpe delivers recognition events to the clients using a 'match'
+event message.
+
+  {
+      reqno: 0,
+      type: 'event',
+      id: 1,
+      event: 'match',
+      match: {
+          final: true,
+          length: 1,
+          results: [
+              {
+                  transcript: 'hal open the pod bay doors',
+                  confidence: 0.89,
+              }
+          ]
+      }
+  }
+
+The server will not provide any 'EMMA' or 'interpretation' attributes for
+the recognition event.
+
+4.6 Status Codes and Errors
+
+TBD.
+
+
+5. Synthesizer Protocol
+-----------------------
+
+5.1 Creating a Synthesizer Instance
+
+The speech synthesizer object is an implicitly create per-connection
+singleton object. It does not need to and cannot be explicitly created.
+The implicit singleton synthesizer object always exists with object ID
+0.
+
+
+5.2 Deleting the Synthesizer Instance
+
+The implicitly created singleton speech synthesizer object cannot be
+explicitly deleted.
+
+5.3 Creating an Utterance
+
+An utterance can be created by sending an utterance creation request
+to the server.
+
+  {
+      reqno: 16,
+      type: 'create',
+      object: 'utterance',
+      set: {
+          text: 'this is a test message',
+          lang: 'en-GB',
+          events: [ 'start', 'end', 'error' ],
+          volume: 1.0,
+          rate: 1.0,
+      }
+  }
+
+Although the server allows all standard attributes to be set, currently
+volume is always ignored and depending on the backend, rate and pitch
+might be ignore.
+
+In addition to the standard attributes, Winthirpe allows the following
+non-standard ones to be set:
+
+  - events: the set of events the client wants to be notified about. This
+    should reflect which event handlers have been set up for the related
+    JS object in the WRT. Currently only the start, stop and error events
+    are generated by the server, but rendering progress events will be
+    added in the soon future.
+
+  - timeout: timeout after which the utterance gets automatically cancelled
+    if its rendering has not been started. The timeout period is specified
+    in milliseconds.
+
+Upon sucessful creation the server responds with a success status reply
+containing the object ID of the newly created utterance.
+
+  {
+      reqno: 16,
+      type: 'status',
+      status: '0',
+      id: 3
+  }
+
+The supplied ID needs to be used in all subsequent requests related to this
+utterance instance.
+
+5.4 Setting Utterance Attributes
+
+Utterance attributes can either be set during creation of the instance,
+or later using an attribute setting request. For the list of settable
+attributes, see the utterance creation request above. The server might
+not be able to change certain attributes, depending on the state of
+the utterance. If setting an attribute fails, the server will respond with
+an error status reply to the request, otherwise with a sucessful status
+reply with status 0.
+
+5.5 Rendering an Utterance
+
+An utterance can be rendered by invoking the speak method of the singleton
+synthesizer object (with ID 0) and passing it the ID of the utterance in
+the utterance field.
+
+  {
+      reqno: 17,
+      type: 'invoke',
+      id: 0,
+      method: 'speak'
+      utterance: 3
+  }
+
+Upon success, the server will respond with a success status reply.
+
+  {
+      reqno: 17,
+      type: 'status',
+      status: 0
+  }
+
+5.6 Cancelling All Utterances
+
+All utterance can be cancelled by invoking the cancel method of the singleton
+synthesizer object (ID 0).
+
+  {
+      reqno: 18,
+      type: 'invoke',
+      id: 0,
+      method: 'cancel'
+  }
+
+This will cancel all queued utterances. Upon success the server responds with
+a sucess status reply.
+
+  {
+      reqno: 18,
+      type: 'status',
+      status: 0
+  }
+
+5.7 Pausing and Resuming Utterance Rendering
+
+Rendering of pending utterances can be paused and resumed by invoking the
+pause and resume methods of the singleton synthesizer object (ID 0).
+
+  {
+      reqno: 19,
+      type: 'invoke',
+      id: 0,
+      method: 'pause' (or resume)
+  }
+
+Once paused, rendering will not resume until the resume method is invoked.
+Upon success the server responds with a sucess status reply.
+
+  {
+      reqno: 19,
+      type: 'status',
+      status: 0
+  }
+
+5.8 Listing Available Voices
+
+The set of available voices can be queried by invoking the get-voices method
+of the singleton synthesizer objec (ID 0).
+
+  {
+      reqno: 20,
+      type: 'invoke',
+      id: 0,
+      method: 'get-voices'
+  }
+
+Upon success the server replies with the set of available voices.
+  {
+     reqno: 20,
+     type: 'status',
+     status: 0,
+     voices: [
+         {
+              voiceURI: 'english-male',
+              lang: 'English',
+              name: 'english-male',
+              localService: true,
+              default: false
+         },
+         {
+              voiceURI: 'english-british-male-1',
+              lang: 'English',
+              name: 'english-british-male-1',
+              localService: true,
+              default: false
+         },
+         {
+           voiceURI: 'english-scottish-male',
+           lang: 'English',
+           name: 'english-scottish-male',
+           localService: true,
+           default: false
+         }
+    ]
+  }
+
+5.9 Emitted Events
+
+The server emits event whenever the speaking, pending, or paused
+state of the server changes, ie. whenever
+  - rendering starts,
+  - rendering stops,
+  - the first utterance is inserted into an empty utterance queue,
+  - the last utterance is removed from the utterance queue,
+  - the utterance queue is paused, or
+  - the utterance queue is resumed
+
+These events are emitted with the ID (0) of the global singleton synthesizer
+object.
+
+Utterance specific events (start, end, error) are emitted with the utterance
+ID as the object id of the event. Rendering progress events will be similarly
+emitted in the future.
+
+
+6. Object Lifecycle
+-------------------
+
+The lifecycle of the objects created on the server side is tied to the
+lifecycle of the socket connection via which the objects have been
+created. If the client closes the connection, all objects associated with
+the connection/client will be automatically destroyed on the server side.
+
+
+7. Other Considerations
+-----------------------
+
+Attributes can optionally be initialized during object creation, or they
+can be set later using an attribute set request. In principle it does not
+matter whether any attributes are set during construction. You can always
+create an empty object and configure it with subsequent attribute setting
+requests.
+
+However, most objects are not possible/meaningful to use or sometimes even
+to construct on the backend side before certain attributes have been set.
+For instance, a recognition object is practically inactive from the clients
+point of view before an omatch handler has been set on it. Similarly, the
+recognition object cannot be constructed on the backend side before the
+grammars attribute has been set.
+
+For the sake of completeness and to allow implementation to attempt
+opportunistic optimizations of object creation and message exchange, the
+protocol supports attribute initialization during object creation. If it
+wishes to do so, an implementation can cache locally attribute settings
+until it sees fit to construct the object (once enough attributes has been
+set for the object to become fully operational) and only then create and
+initialize the object with a single message exchange in one go.
+
+However, it is also perfectly fine to not do so. The gains are mostly in
+doing less communication for the cost of more complex client-side object-
+state handling logic. Whether this makes sense and whether it is a real
+gain depends on the implementation details case by case.
diff --git a/src/plugins/client-api/w3c-speech/w3c-client.c b/src/plugins/client-api/w3c-speech/w3c-client.c
new file mode 100644 (file)
index 0000000..e8506b4
--- /dev/null
@@ -0,0 +1,1336 @@
+/*
+ * Copyright (c) 2012-2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <getopt.h>
+#include <stdarg.h>
+#include <ctype.h>
+
+#include <murphy/common/macros.h>
+#include <murphy/common/debug.h>
+#include <murphy/common/log.h>
+#include <murphy/common/mm.h>
+#include <murphy/common/mainloop.h>
+#include <murphy/common/transport.h>
+#include <murphy/common/json.h>
+
+#include <breedline/breedline-murphy.h>
+
+#define DEFAULT_SERVER "unxs:@winthorpe.w3c-speech"
+
+/*
+ * a W3C test client
+ */
+
+typedef struct {
+    mrp_mainloop_t  *ml;                 /* our mainloop */
+    brl_t           *brl;                /* breedline for terminal I/O */
+    int              log_mask;           /* log verbosity mask */
+    char            *server;             /* server (transport) address */
+    const char      *atype;              /* resolved type */
+    mrp_sockaddr_t   addr;               /* resolved address */
+    socklen_t        alen;               /* resolved length */
+    mrp_transport_t *t;                  /* transport to server */
+    mrp_timer_t     *conntmr;            /* connection timer */
+    int              connected : 1;      /* whether we have a connection */
+    uint32_t         reqno;              /* request number */
+} client_t;
+
+
+typedef struct {
+    const char  *command;
+    int        (*handler)(client_t *c, int narg, char **args);
+} command_t;
+
+static int transport_connect(client_t *c);
+static void transport_destroy(client_t *c);
+static void connection_timer_start(client_t *c);
+static void connection_timer_stop(client_t *c);
+static void mainloop_quit(client_t *c, int exit_code);
+
+static void execute_command(client_t *c, int narg, char **args);
+
+static void set_prompt(client_t *c, const char *prompt)
+{
+    brl_set_prompt(c->brl, prompt);
+}
+
+
+static void show_prompt(client_t *c)
+{
+    if (c->brl != NULL)
+        brl_show_prompt(c->brl);
+}
+
+
+static void hide_prompt(client_t *c)
+{
+    if (c->brl != NULL)
+        brl_hide_prompt(c->brl);
+}
+
+
+static void print(client_t *c, const char *format, ...)
+{
+    va_list ap;
+
+    hide_prompt(c);
+
+    va_start(ap, format);
+    vfprintf(stdout, format, ap);
+    fputc('\n', stdout);
+    fflush(stdout);
+    va_end(ap);
+
+    show_prompt(c);
+}
+
+
+static void client_set_defaults(client_t *c, const char *argv0)
+{
+    MRP_UNUSED(c);
+    MRP_UNUSED(argv0);
+
+    c->server   = mrp_strdup(DEFAULT_SERVER);
+    c->log_mask = MRP_LOG_UPTO(MRP_LOG_INFO);
+}
+
+
+static void client_connected(client_t *c)
+{
+    c->connected = true;
+    hide_prompt(c);
+    set_prompt(c, "w3c-client");
+    show_prompt(c);
+}
+
+
+static void client_disconnected(client_t *c)
+{
+    c->connected = false;
+    hide_prompt(c);
+    set_prompt(c, "disconnected");
+    show_prompt(c);
+}
+
+
+static client_t *client_create(const char *argv0)
+{
+    client_t *c;
+
+    c = mrp_allocz(sizeof(*c));
+
+    if (c != NULL)
+        client_set_defaults(c, argv0);
+
+    return c;
+}
+
+
+static void client_destroy(client_t *c)
+{
+    mrp_free(c);
+}
+
+
+static int mainloop_create(client_t *c)
+{
+    c->ml = mrp_mainloop_create();
+
+    if (c->ml != NULL)
+        return 0;
+    else {
+        fprintf(stderr, "Failed to create mainloop.");
+        exit(1);
+    }
+}
+
+
+static int mainloop_run(client_t *c)
+{
+    if (c != NULL && c->ml != NULL)
+        return mrp_mainloop_run(c->ml);
+    else {
+        errno = EINVAL;
+        return -1;
+    }
+}
+
+
+static void mainloop_quit(client_t *c, int exit_code)
+{
+    if (c != NULL && c->ml != NULL)
+        mrp_mainloop_quit(c->ml, exit_code);
+    else
+        exit(exit_code);
+}
+
+
+static void mainloop_destroy(client_t *c)
+{
+    mrp_mainloop_destroy(c->ml);
+    c->ml = NULL;
+}
+
+
+static void transport_recv_evt(mrp_transport_t *t, mrp_json_t *msg,
+                               void *user_data)
+{
+    client_t   *c = (client_t *)user_data;
+    const char *s;
+
+    MRP_UNUSED(t);
+    MRP_UNUSED(c);
+
+    s = mrp_json_object_to_string(msg);
+
+    print(c, "received message:");
+    print(c, "  %s", s);
+}
+
+
+static void transport_recvfrom_evt(mrp_transport_t *t, mrp_json_t *msg,
+                                   mrp_sockaddr_t *addr, socklen_t addrlen,
+                                   void *user_data)
+{
+    MRP_UNUSED(t);
+    MRP_UNUSED(msg);
+    MRP_UNUSED(addr);
+    MRP_UNUSED(addrlen);
+    MRP_UNUSED(user_data);
+}
+
+
+static void transport_closed_evt(mrp_transport_t *t, int error, void *user_data)
+{
+    client_t *c = (client_t *)user_data;
+
+    MRP_UNUSED(t);
+
+    if (error != 0)
+        print(c, "Connection to server closed with error %d (%s).", error,
+              strerror(error));
+    else
+        print(c, "Connection to server closed.");
+
+    transport_destroy(c);
+    connection_timer_start(c);
+}
+
+
+static int transport_connect(client_t *c)
+{
+    static mrp_transport_evt_t evt = {
+        { .recvjson     = transport_recv_evt,    },
+        { .recvjsonfrom = transport_recvfrom_evt },
+        .closed         = transport_closed_evt,
+        .connection     = NULL
+    };
+
+    mrp_sockaddr_t  addr;
+    socklen_t       alen;
+    const char     *atype;
+    int             flags;
+
+    alen = mrp_transport_resolve(NULL, c->server, &addr, sizeof(addr), &atype);
+
+    if (alen <= 0) {
+        print(c, "Failed to resolve transport address '%s'.", c->server);
+        return -1;
+    }
+
+    flags = MRP_TRANSPORT_MODE_JSON | MRP_TRANSPORT_REUSEADDR;
+
+    if (c->t == NULL)
+        c->t = mrp_transport_create(c->ml, atype, &evt, c, flags);
+
+    if (c->t == NULL) {
+        print(c, "Failed to create transport (for '%s').", c->server);
+        return -1;
+    }
+
+    if (!mrp_transport_connect(c->t, &addr, alen))
+        return -1;
+    else {
+        client_connected(c);
+        return 0;
+    }
+}
+
+
+static void transport_destroy(client_t *c)
+{
+    mrp_transport_destroy(c->t);
+    c->t = NULL;
+    client_disconnected(c);
+}
+
+
+static void try_connect(mrp_timer_t *t, void *user_data)
+{
+    client_t *c = (client_t *)user_data;
+
+    if (transport_connect(c) == 0) {
+        mrp_del_timer(t);
+        c->conntmr = NULL;
+    }
+}
+
+
+static int transport_send(client_t *c, mrp_json_t *msg)
+{
+    if (mrp_transport_sendjson(c->t, msg))
+        return 0;
+    else {
+        errno = EIO;
+        return -1;
+    }
+}
+
+
+static void connection_timer_start(client_t *c)
+{
+    if (transport_connect(c) < 0)
+        c->conntmr = mrp_add_timer(c->ml, 1500, try_connect, c);
+}
+
+
+static void connection_timer_stop(client_t *c)
+{
+    mrp_del_timer(c->conntmr);
+    c->conntmr = NULL;
+}
+
+
+static void sighandler(mrp_sighandler_t *h, int signum, void *user_data)
+{
+    client_t *c = (client_t *)user_data;
+
+    MRP_UNUSED(h);
+
+    switch (signum) {
+    case SIGINT:
+        printf("Received SIGINT, exiting...");
+        mainloop_quit(c, 0);
+        break;
+
+    case SIGTERM:
+        printf("Received SIGTERM, exiting...");
+        mainloop_quit(c, 0);
+        break;
+    }
+}
+
+
+static void setup_signals(client_t *c)
+{
+    mrp_add_sighandler(c->ml, SIGINT , sighandler, c);
+    mrp_add_sighandler(c->ml, SIGTERM, sighandler, c);
+}
+
+
+static int split_input(char *input, int narg, char **args)
+{
+    int   n, l;
+    char *p, *e;
+
+    n = 0;
+    p = input;
+    e = NULL;
+
+    while (*p) {
+        while (*p == ' ' || *p == '\t')
+            p++;
+
+        args[n++] = p;
+
+        if (n >= narg) {
+            errno = EOVERFLOW;
+            return -1;
+        }
+
+        while (*p && ((e && *p != *e) || (!e && (*p != ' ' && *p != '\t')))) {
+            if (*p == '\\') {
+                if ((l = strlen(p)) > 1) {
+                    memmove(p, p + 1, l - 1);
+                    goto next;
+                }
+                else {
+                    *p = '\0';
+                    break;
+                }
+            }
+
+            if (!e && (*p == '\'' || *p == '"'))
+                e = p;
+
+        next:
+            p++;
+        }
+
+        if (e && *p == *e)
+            p++;
+
+        if (*p)
+            *p++ = '\0';
+
+        e = NULL;
+
+        mrp_debug("arg #%d: '%s'\n", n, args[n-1]);
+    }
+
+    return n;
+}
+
+
+static void terminal_cb(brl_t *brl, const char *input, void *user_data)
+{
+    client_t *c   = (client_t *)user_data;
+    int       len = input ? strlen(input) + 1 : 0;
+    char      buf[len], *args[64];
+    int       narg;
+
+    if (len > 1) {
+        brl_add_history(brl, input);
+        hide_prompt(c);
+
+        strcpy(buf, input);
+        narg = split_input(buf, MRP_ARRAY_SIZE(args), args);
+        if (narg > 0)
+            execute_command(c, narg, &args[0]);
+        else
+            print(c, "failed to parse input '%s'", input);
+
+        show_prompt(c);
+    }
+}
+
+
+static void terminal_setup(client_t *c)
+{
+    if ((c->brl = brl_create_with_murphy(fileno(stdin), "disconnected", c->ml,
+                                         terminal_cb, c)) != NULL)
+        brl_show_prompt(c->brl);
+    else {
+        fprintf(stderr, "Failed to set up terminal input.");
+        exit(1);
+    }
+}
+
+
+static void terminal_cleanup(client_t *c)
+{
+    if (c != NULL && c->brl != NULL) {
+        brl_destroy(c->brl);
+        c->brl = NULL;
+    }
+}
+
+
+static void print_usage(const char *argv0, int exit_code, const char *fmt, ...)
+{
+    va_list     ap;
+    const char *exe;
+
+    if (fmt && *fmt) {
+        va_start(ap, fmt);
+        vprintf(fmt, ap);
+        va_end(ap);
+    }
+
+    exe = strrchr(argv0, '/');
+
+    printf("usage: %s [options]\n\n"
+           "The possible options are:\n"
+           "  -v, --verbose                  increase logging verbosity\n"
+           "  -d, --debug                    enable debug messages\n"
+           "  -h, --help                     show help on usage\n", exe);
+    printf("\n");
+
+    if (exit_code < 0)
+        return;
+    else
+        exit(exit_code);
+}
+
+
+static void parse_cmdline(client_t *c, int argc, char **argv)
+{
+#   define OPTIONS "s:vd:h"
+    struct option options[] = {
+        { "server"    , required_argument, NULL, 's' },
+        { "verbose"   , no_argument      , NULL, 'v' },
+        { "debug"     , required_argument, NULL, 'd' },
+        { "help"      , no_argument      , NULL, 'h' },
+        { NULL, 0, NULL, 0 }
+    };
+
+    int opt;
+
+    while ((opt = getopt_long(argc, argv, OPTIONS, options, NULL)) != -1) {
+        switch (opt) {
+        case 's':
+            mrp_free(c->server);
+            c->server = mrp_strdup(optarg);
+            break;
+
+        case 'v':
+            c->log_mask <<= 1;
+            c->log_mask  |= 1;
+            mrp_log_set_mask(c->log_mask);
+            break;
+
+        case 'd':
+            mrp_debug_set_config(optarg);
+            mrp_debug_enable(TRUE);
+            break;
+
+        case 'h':
+            print_usage(argv[0], -1, "");
+            exit(0);
+            break;
+
+        default:
+            print_usage(argv[0], EINVAL, "invalid option '%c'", opt);
+        }
+    }
+}
+
+
+static int check_connection(client_t *c, int notify)
+{
+    if (!c->connected) {
+        if (notify)
+            print(c, "Connection to server is down.");
+
+        errno = ENOTCONN;
+        return -1;
+    }
+
+    return 0;
+}
+
+
+static int cmd_get_timestamp(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         status;
+
+    MRP_UNUSED(args);
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg > 0)
+        print(c, "Ignoring unused arguments...");
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(req, "reqno", c->reqno++);
+        mrp_json_add_string (req, "type" , "timestamp");
+
+        if (transport_send(c, req) < 0)
+            status = -1;
+        else
+            status = 0;
+
+        mrp_json_unref(req);
+    }
+
+    return status;
+}
+
+
+static int parse_set(client_t *c, mrp_json_t *set, int narg, char **args)
+{
+    char var[64], *val;
+    int  i, l;
+
+    for (i = 0; i < narg; i++) {
+        val = strchr(args[i], '=');
+
+        if (val == NULL) {
+            print(c, "Invalid variable initializer/setting '%s'.", args[i]);
+
+            errno = EINVAL;
+            return -1;
+        }
+
+        snprintf(var, sizeof(var), "%*.*s",
+                 (int)(val - args[i]), (int)(val - args[i]), args[i]);
+        val++;
+
+        mrp_debug("* '%s' = '%s'\n", var, val);
+
+        if (!strcmp(var, "grammars")) {
+            mrp_json_t *arr = mrp_json_create(MRP_JSON_ARRAY);
+            mrp_json_t *grm;
+            char       *p, *n;
+
+            for (p = val; p && *p; p = n) {
+                n = strchr(p, ',');
+                l = n ? (int)(n - p) : (int)strlen(p);
+
+                mrp_json_array_append(arr,
+                                      grm = mrp_json_create(MRP_JSON_OBJECT));
+
+                mrp_json_add_string_slice(grm, "src"   , p, l);
+                mrp_json_add_double      (grm, "weight", 1.0);
+
+                n = n ? n + 1 : n;
+            }
+
+            mrp_json_add(set, var, arr);
+        }
+        else if (!strncmp(val, "int:", l=4))
+            mrp_json_add_integer(set, var, strtol(val+l, NULL, 10));
+        else if (!strncmp(val, "bln:", l=4))
+            mrp_json_add_boolean(set, var, var[l] == '1' || var[l] == 't');
+        else if (!strncmp(val, "str:", l=4))
+            mrp_json_add_string(set, var, val+l);
+        else if (!strncmp(val, "dbl:", l=4))
+            mrp_json_add_double(set, var, strtod(val+l, NULL));
+        else if (val[0] == '\'' || val[0] == '\"')
+            mrp_json_add_string_slice(set, var, val + 1, strlen(val) - 2);
+        else if (!strcasecmp(val, "true") || (!strcasecmp(val, "false")))
+            mrp_json_add_boolean(set, var, *val == 't');
+        else if (!strncmp(val, "strarr:", l=7)) {
+            mrp_json_t *arr = mrp_json_create(MRP_JSON_ARRAY);
+            char       *p, *n;
+
+            for (p = val + l; p && *p; p = n) {
+                n = strchr(p, ',');
+                l = n ? (int)(n - p) : (int)strlen(p);
+
+                mrp_json_array_append_string_slice(arr, p, l);
+
+                n = n ? n + 1 : n;
+            }
+
+            mrp_json_add(set, var, arr);
+        }
+        else if (!strncmp(val, "intarr:", l=7)) {
+            mrp_json_t *arr = mrp_json_create(MRP_JSON_ARRAY);
+            char       *p, *n;
+
+            for (p = val + l; p && *p; p = n) {
+                n = strchr(p, ',');
+                l = n ? (int)(n - p) : (int)strlen(p);
+
+                mrp_json_array_append_integer(arr, strtol(p, NULL, 10));
+
+                n = n ? n + 1 : n;
+            }
+
+            mrp_json_add(set, var, arr);
+        }
+        else if (!strncmp(val, "dblarr:", l=7)) {
+            mrp_json_t *arr = mrp_json_create(MRP_JSON_ARRAY);
+            char       *p, *n;
+
+            for (p = val + l; p && *p; p = n) {
+                n = strchr(p, ',');
+                l = n ? (int)(n - p) : (int)strlen(p);
+
+                mrp_json_array_append_double(arr, strtod(p, NULL));
+
+                n = n ? n + 1 : n;
+            }
+
+            mrp_json_add(set, var, arr);
+        }
+        else if (!strncmp(val, "blnarr:", l=7)) {
+            mrp_json_t *arr = mrp_json_create(MRP_JSON_ARRAY);
+            char       *p, *n;
+
+            for (p = val + l; p && *p; p = n) {
+                n = strchr(p, ',');
+                l = n ? (int)(n - p) : (int)strlen(p);
+
+                mrp_json_array_append_boolean(arr, *p == 't');
+
+                n = n ? n + 1 : n;
+            }
+
+            mrp_json_add(set, var, arr);
+        }
+        else if (isdigit(*val) || *val == '-' || *val == '+') {
+            int     iv;
+            double  dv;
+            char   *end;
+
+            iv = strtol(val, &end, 10);
+
+            if (end && !*end) {
+                mrp_json_add_integer(set, var, iv);
+                continue;
+            }
+
+            if (end && *end == '.') {
+                dv = strtod(val, &end);
+
+                if (end && !*end) {
+                    mrp_json_add_double(set, var, dv);
+                    continue;
+                }
+            }
+
+            mrp_json_add_string(set, var, val);
+        }
+        else
+            mrp_json_add_string(set, var, val);
+    }
+
+    return 0;
+}
+
+
+static int cmd_create_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req, *set;
+    int         status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(req, "reqno" , c->reqno++);
+        mrp_json_add_string (req, "type"  , "create");
+        mrp_json_add_string (req, "object", "recognizer");
+
+        if (narg > 0) {
+            mrp_json_add(req, "set", set = mrp_json_create(MRP_JSON_OBJECT));
+
+            if (parse_set(c, set, narg, args) < 0)
+                goto fail;
+        }
+
+        if (transport_send(c, req) < 0)
+            status = -1;
+        else
+            status = 0;
+
+        mrp_json_unref(req);
+
+        return status;
+    }
+
+ fail:
+    return -1;
+}
+
+
+static int cmd_delete_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, id, status;
+
+    if (narg <= 0) {
+        print(c, "Can't delete recognizer, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        id = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno", c->reqno++);
+            mrp_json_add_string (req, "type" , "delete");
+            mrp_json_add_integer(req, "id"   , id);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_set_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req, *set;
+    int         id, status;
+
+    if (narg < 2) {
+        print(c, "Can't set variable, need ID, and variable assignment.");
+        errno = EINVAL;
+        return -1;
+    }
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    id = strtoul(args[0], NULL, 10);
+    args++;
+    narg--;
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) == NULL)
+        return -1;
+
+    mrp_json_add_integer(req, "reqno", c->reqno++);
+    mrp_json_add_string (req, "type" , "set");
+    mrp_json_add_integer(req, "id"   , id);
+    mrp_json_add        (req, "set"  , set = mrp_json_create(MRP_JSON_OBJECT));
+
+    if (parse_set(c, set, narg, args) < 0) {
+        errno  = EINVAL;
+        status = -1;
+    }
+    else {
+        if (transport_send(c, req) < 0)
+            status = -1;
+        else
+            status = 0;
+    }
+
+    mrp_json_unref(req);
+
+    return status;
+}
+
+
+static int cmd_start_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, id, status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg <= 0) {
+        print(c, "Can't start recognizer, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        id = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno" , c->reqno++);
+            mrp_json_add_string (req, "type"  , "invoke");
+            mrp_json_add_string (req, "method", "start");
+            mrp_json_add_integer(req, "id"    , id);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_stop_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, id, status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg <= 0) {
+        print(c, "Can't start recognizer, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        id = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno" , c->reqno++);
+            mrp_json_add_string (req, "type"  , "invoke");
+            mrp_json_add_string (req, "method", "stop");
+            mrp_json_add_integer(req, "id"    , id);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_abort_recognizer(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, id, status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg <= 0) {
+        print(c, "Can't abort recognizer, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        id = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno", c->reqno++);
+            mrp_json_add_string (req, "type"  , "invoke");
+            mrp_json_add_string (req, "method", "abort");
+            mrp_json_add_integer(req, "id"   , id);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_get_voices(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    const char *lang;
+    int         status;
+
+    switch (narg) {
+    case 0: lang = NULL;    break;
+    case 1: lang = args[0]; break;
+    default:
+        print(c, "list-voices expects either a single or no arguments.");
+        errno = EINVAL;
+        return -1;
+    }
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) == NULL)
+        return -1;
+
+    mrp_json_add_integer(req, "reqno" , c->reqno++);
+    mrp_json_add_string (req, "type"  , "invoke");
+    mrp_json_add_integer(req, "id"    , 0);
+    mrp_json_add_string (req, "method", "get-voices");
+    if (lang != NULL)
+        mrp_json_add_string(req, "lang", lang);
+
+    if (transport_send(c, req) < 0)
+        status = -1;
+    else
+        status = 0;
+
+    mrp_json_unref(req);
+
+
+    return status;
+}
+
+
+static int cmd_create_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req, *set;
+    int         status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(req, "reqno" , c->reqno++);
+        mrp_json_add_string (req, "type"  , "create");
+        mrp_json_add_string (req, "object", "utterance");
+
+        if (narg > 0) {
+            mrp_json_add(req, "set", set = mrp_json_create(MRP_JSON_OBJECT));
+
+            if (parse_set(c, set, narg, args) < 0)
+                goto fail;
+        }
+
+        if (transport_send(c, req) < 0)
+            status = -1;
+        else
+            status = 0;
+
+        mrp_json_unref(req);
+
+        return status;
+    }
+
+ fail:
+    return -1;
+}
+
+
+static int cmd_delete_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, id, status;
+
+    if (narg <= 0) {
+        print(c, "Can't delete utterance, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        id = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno", c->reqno++);
+            mrp_json_add_string (req, "type" , "delete");
+            mrp_json_add_integer(req, "id"   , id);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_set_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req, *set;
+    int         id, status;
+
+    if (narg < 2) {
+        print(c, "Can't set variable, need ID, and variable assignment.");
+        errno = EINVAL;
+        return -1;
+    }
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    id = strtoul(args[0], NULL, 10);
+    args++;
+    narg--;
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) == NULL)
+        return -1;
+
+    mrp_json_add_integer(req, "reqno", c->reqno++);
+    mrp_json_add_string (req, "type" , "set");
+    mrp_json_add_integer(req, "id"   , id);
+    mrp_json_add        (req, "set"  , set = mrp_json_create(MRP_JSON_OBJECT));
+
+    if (parse_set(c, set, narg, args) < 0) {
+        errno  = EINVAL;
+        status = -1;
+    }
+    else {
+        if (transport_send(c, req) < 0)
+            status = -1;
+        else
+            status = 0;
+    }
+
+    mrp_json_unref(req);
+
+    return status;
+}
+
+
+static int cmd_speak_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, uid, status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg <= 0) {
+        print(c, "Can't speak utterance, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+
+    status = 0;
+    for (i = 0; i < narg; i++) {
+        uid = strtoul(args[i], NULL, 10);
+
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno"    , c->reqno++);
+            mrp_json_add_string (req, "type"     , "invoke");
+            mrp_json_add_string (req, "method"   , "speak");
+            mrp_json_add_integer(req, "id"       , 0);
+            mrp_json_add_integer(req, "utterance", uid);
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_cancel_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         i, uid, status;
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+#if 0
+    if (narg <= 0) {
+        print(c, "Can't cancel utterance, no ID given.");
+        errno = EINVAL;
+
+        return -1;
+    }
+#endif
+
+    status = 0;
+
+    if (narg == 0) {
+        if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+            mrp_json_add_integer(req, "reqno"    , c->reqno++);
+            mrp_json_add_string (req, "type"     , "invoke");
+            mrp_json_add_integer(req, "id"       , 0);
+            mrp_json_add_string (req, "method"   , "cancel");
+
+            if (transport_send(c, req) < 0)
+                status = -1;
+
+            mrp_json_unref(req);
+        }
+    }
+    else {
+        for (i = 0; i < narg; i++) {
+            uid = strtoul(args[i], NULL, 10);
+
+            if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+                mrp_json_add_integer(req, "reqno"    , c->reqno++);
+                mrp_json_add_string (req, "type"     , "invoke");
+                mrp_json_add_integer(req, "id"       , 0);
+                mrp_json_add_string (req, "method"   , "cancel");
+                mrp_json_add_integer(req, "utterance", uid);
+
+                if (transport_send(c, req) < 0)
+                    status = -1;
+
+                mrp_json_unref(req);
+            }
+        }
+    }
+
+    return status;
+}
+
+
+static int cmd_pause_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         status;
+
+    MRP_UNUSED(args);
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg != 0)
+        print(c, "Warning: ignoring extra pause arguments.");
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(req, "reqno" , c->reqno++);
+        mrp_json_add_string (req, "type"  , "invoke");
+        mrp_json_add_string (req, "method", "pause");
+        mrp_json_add_integer(req, "id"    , 0);
+
+        status = transport_send(c, req);
+
+        mrp_json_unref(req);
+    }
+
+    return status;
+}
+
+
+static int cmd_resume_utterance(client_t *c, int narg, char **args)
+{
+    mrp_json_t *req;
+    int         status;
+
+    MRP_UNUSED(args);
+
+    if (check_connection(c, TRUE) < 0)
+        return -1;
+
+    if (narg != 0)
+        print(c, "Warning: ignoring extra resume arguments.");
+
+    if ((req = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(req, "reqno" , c->reqno++);
+        mrp_json_add_string (req, "type"  , "invoke");
+        mrp_json_add_string (req, "method", "resume");
+        mrp_json_add_integer(req, "id"    , 0);
+
+        status = transport_send(c, req);
+
+        mrp_json_unref(req);
+    }
+
+    return status;
+}
+
+
+static int cmd_connect(client_t *c, int narg, char **args)
+{
+    MRP_UNUSED(narg);
+    MRP_UNUSED(args);
+
+    if (!c->connected)
+        if (c->conntmr == NULL)
+            connection_timer_start(c);
+
+    return 0;
+}
+
+
+static int cmd_disconnect(client_t *c, int narg, char **args)
+{
+    MRP_UNUSED(narg);
+    MRP_UNUSED(args);
+
+    if (c->connected)
+        transport_destroy(c);
+
+    return 0;
+}
+
+
+static int cmd_quit(client_t *c, int narg, char **args)
+{
+    MRP_UNUSED(narg);
+    MRP_UNUSED(args);
+
+    print(c, "Exiting...");
+    mrp_mainloop_quit(c->ml, 0);
+
+    return 0;
+}
+
+
+static void execute_command(client_t *c, int narg, char **args)
+{
+    static command_t commands[] = {
+        { "get-timestamp"       , cmd_get_timestamp        },
+        { "create-recognizer"   , cmd_create_recognizer    },
+        { "delete-recognizer"   , cmd_delete_recognizer    },
+        { "set-recognizer"      , cmd_set_recognizer       },
+        { "start-recognizer"    , cmd_start_recognizer     },
+        { "stop-recognizer"     , cmd_stop_recognizer      },
+        { "abort-recognizer"    , cmd_abort_recognizer     },
+        { "get-voices"          , cmd_get_voices           },
+        { "create-utterance"    , cmd_create_utterance     },
+        { "delete-utterance"    , cmd_delete_utterance     },
+        { "set-utterance"       , cmd_set_utterance        },
+        { "speak-utterance"     , cmd_speak_utterance      },
+        { "cancel-utterance"    , cmd_cancel_utterance     },
+        { "pause-utterance"     , cmd_pause_utterance      },
+        { "resume-utterance"    , cmd_resume_utterance     },
+        { "connect"             , cmd_connect              },
+        { "disconnect"          , cmd_disconnect           },
+        { "quit"                , cmd_quit                 },
+        { "exit"                , cmd_quit                 },
+        { NULL, NULL }
+    };
+
+    command_t *cmd;
+    size_t     len;
+
+    len = strlen(args[0]);
+    for (cmd = commands; cmd->command != NULL; cmd++) {
+        if (!strncmp(args[0], cmd->command, len)) {
+            cmd->handler(c, narg - 1, args + 1);
+            return;
+        }
+    }
+
+    print(c, "Unknown command '%s'...", args[0]);
+}
+
+
+int main(int argc, char *argv[])
+{
+    client_t *c;
+
+    if ((c = client_create(argv[0])) != NULL) {
+        parse_cmdline(c, argc, &argv[0]);
+
+        mainloop_create(c);
+        setup_signals(c);
+        terminal_setup(c);
+        connection_timer_start(c);
+
+        mainloop_run(c);
+
+        connection_timer_stop(c);
+        terminal_cleanup(c);
+        mainloop_destroy(c);
+
+        client_destroy(c);
+    }
+
+    return 0;
+}
diff --git a/src/plugins/client-api/w3c-speech/w3c-message.c b/src/plugins/client-api/w3c-speech/w3c-message.c
new file mode 100644 (file)
index 0000000..51be601
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <errno.h>
+#include <stdarg.h>
+
+#include <murphy/common/macros.h>
+#include <murphy/common/debug.h>
+#include <murphy/common/log.h>
+#include <murphy/common/json.h>
+
+
+typedef struct {
+    mrp_json_t *objects[64];
+    int         depth;
+    mrp_json_t *top;
+} json_stack_t;
+
+
+static inline mrp_json_t *push_container(json_stack_t *s, const char *name,
+                                         mrp_json_t *c)
+{
+    if (s->depth >= (int)MRP_ARRAY_SIZE(s->objects)) {
+        errno = ENOSPC;
+        return NULL;
+    }
+
+    s->objects[s->depth++] = c;
+
+    if (s->depth != 1) {
+        if (name != NULL)
+            mrp_json_add(s->top, name, c);
+        else
+            mrp_json_array_append(s->top, c);
+    }
+
+    s->top = c;
+
+    mrp_debug("push(#%d): %p", s->depth, s->top);
+
+    return c;
+}
+
+
+static inline mrp_json_t *pop_container(json_stack_t *s, const char *type)
+{
+    if (s->depth < 1) {
+        errno = ENOENT;
+        return NULL;
+    }
+
+    switch (mrp_json_get_type(s->top)) {
+    case MRP_JSON_OBJECT:
+        if (*type != '}') {
+            errno = EINVAL;
+            return NULL;
+        }
+        break;
+    case MRP_JSON_ARRAY:
+        if (*type != ']') {
+            errno = EINVAL;
+            return NULL;
+        }
+        break;
+    default:
+        break;
+    }
+
+    s->depth--;
+
+    if (s->depth < 1)
+        s->top = NULL;
+    else
+        s->top = s->objects[s->depth - 1];
+
+    mrp_debug("pop(#%d): %p", s->depth, s->top);
+
+    return s->top;
+}
+
+
+mrp_json_t *mrp_json_build(const char *type, ...)
+{
+    json_stack_t   stack = { .depth = 0, .top = NULL };
+    mrp_json_t    *o, *c;
+    const char    *name;
+    unsigned int   t;
+    void         **ptr, *obj;
+    va_list        ap;
+
+    /* push outermost container object */
+    switch (*type) {
+    case '{':
+        o = c = push_container(&stack, NULL, mrp_json_create(MRP_JSON_OBJECT));
+        break;
+    case '[':
+        o = c = push_container(&stack, NULL, mrp_json_create(MRP_JSON_ARRAY));
+        break;
+    default:
+        errno = EINVAL;
+        return NULL;
+    }
+
+    /* fill the object */
+    va_start(ap, type);
+    while ((name = va_arg(ap, const char *)) != NULL) {
+        t = (unsigned int)(ptrdiff_t)name;
+
+        /* take care of popping containers if necessary */
+        if ((t > 0x1000 && *name == ']') || t == ']') {
+            if (mrp_json_get_type(c) == MRP_JSON_ARRAY) {
+                c = pop_container(&stack, "]");
+                continue;
+            }
+            else {
+                errno = EILSEQ;
+                goto fail;
+            }
+        }
+        else if ((t > 0x1000 && *name == '}') || t == '}') {
+            if (mrp_json_get_type(c) == MRP_JSON_OBJECT) {
+                c = pop_container(&stack, "}");
+                continue;
+            }
+            else {
+                errno = EILSEQ;
+                goto fail;
+            }
+        }
+
+        if (c == NULL)
+            goto fail;
+
+        ptr = NULL;
+        obj = NULL;
+
+        switch (mrp_json_get_type(c)) {
+        case MRP_JSON_ARRAY:
+            if (t > 0x1000) {
+                if (name[0] == '&') {
+                    if ((name[1] == '{' || name[1] == '[') && name[2] == '\0') {
+                        ptr = va_arg(ap, void **);
+                        t   = name[1];
+                        goto add_item;
+                    }
+                }
+
+                if ((name[0] == '{' || name[0] == '[') && name[1] == '\0') {
+                    t = name[0];
+                    goto add_item;
+                }
+
+                errno = EINVAL;
+                goto fail;
+            }
+
+        add_item:
+            switch (t) {
+            case '{':
+            case MRP_JSON_OBJECT:
+                c = push_container(&stack, NULL,
+                                   mrp_json_create(MRP_JSON_OBJECT));
+                if (ptr != NULL)
+                    *ptr = c;
+                break;
+            case '[':
+            case MRP_JSON_ARRAY:
+                c = push_container(&stack, NULL,
+                                   mrp_json_create(MRP_JSON_ARRAY));
+                if (ptr != NULL)
+                    *ptr = c;
+                break;
+            case MRP_JSON_STRING:
+                mrp_json_array_append_string(c, va_arg(ap, const char *));
+                break;
+            case MRP_JSON_BOOLEAN:
+                mrp_json_array_append_boolean(c, va_arg(ap, int));
+                break;
+            case MRP_JSON_INTEGER:
+                mrp_json_array_append_integer(c, va_arg(ap, int));
+                break;
+            case MRP_JSON_DOUBLE:
+                mrp_json_array_append_double(c, va_arg(ap, double));
+                break;
+            default:
+                errno = EINVAL;
+                goto fail;
+            }
+            break;
+
+        case MRP_JSON_OBJECT:
+            type = va_arg(ap, const char *);
+            t    = (unsigned int)(ptrdiff_t)type;
+
+            if (t > 0x1000) {
+                if (type[0] == '&') {
+                    if ((type[1] == '{' || type[1] == '[') && type[2] == '\0') {
+                        ptr = va_arg(ap, void **);
+                        t   = type[1];
+                        goto add_member;
+                    }
+                }
+
+                if ((type[0] == '{' || type[0] == '[') && type[1] == '\0') {
+                    t = type[0];
+                    goto add_member;
+                }
+
+                errno = EINVAL;
+                goto fail;
+            }
+
+        add_member:
+            switch (t) {
+            case '{':
+            case MRP_JSON_OBJECT:
+                c = push_container(&stack, name,
+                                   mrp_json_create(MRP_JSON_OBJECT));
+                if (ptr != NULL)
+                    *ptr = c;
+                break;
+            case '[':
+            case MRP_JSON_ARRAY:
+                c = push_container(&stack, name,
+                                   mrp_json_create(MRP_JSON_ARRAY));
+                if (ptr != NULL)
+                    *ptr = c;
+                break;
+            case MRP_JSON_STRING:
+                mrp_json_add_string(c, name, va_arg(ap, const char *));
+                break;
+            case MRP_JSON_BOOLEAN:
+                mrp_json_add_boolean(c, name, va_arg(ap, int));
+                break;
+            case MRP_JSON_INTEGER:
+                mrp_json_add_integer(c, name, va_arg(ap, int));
+                break;
+            case MRP_JSON_DOUBLE:
+                mrp_json_add_double(c, name, va_arg(ap, double));
+                break;
+            default:
+                errno = EINVAL;
+                goto fail;
+            }
+
+            break;
+
+        default:
+            goto fail;
+        }
+    }
+
+    return o;
+
+ fail:
+    mrp_json_unref(o);
+    return NULL;
+}
diff --git a/src/plugins/client-api/w3c-speech/w3c-message.h b/src/plugins/client-api/w3c-speech/w3c-message.h
new file mode 100644 (file)
index 0000000..9b081e3
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __SRS_W3C_MESSAGE_H__
+#define __SRS_W3C_MESSAGE_H__
+
+#include <murphy/common/json.h>
+
+#define MRP_JSON_INT(v)   (const char *)MRP_JSON_INTEGER, (v)
+#define MRP_JSON_STR(v)   (const char *)MRP_JSON_STRING , (v)
+#define MRP_JSON_BLN(v)   (const char *)MRP_JSON_BOOLEAN, (v)
+#define MRP_JSON_DBL(v)   (const char *)MRP_JSON_DOUBLE , (v)
+#define MRP_JSON_ARR(...) "[", __VA_ARGS__, "]"
+#define MRP_JSON_OBJ(...) "{", __VA_ARGS__, "}"
+
+mrp_json_t *mrp_json_build(const char *type, ...);
+
+#endif /* __SRS_W3C_MESSAGE_H__ */
diff --git a/src/plugins/client-api/w3c-speech/w3c-protocol.h b/src/plugins/client-api/w3c-speech/w3c-protocol.h
new file mode 100644 (file)
index 0000000..81b2543
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2012-2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __SRS_W3C_PROTOCOL_H__
+#define __SRS_W3C_PROTOCOL_H__
+
+#define W3C_TYPE       "type"
+#define W3C_STATUS     "status"
+#define W3C_ERROR      "error"
+#define W3C_OBJECT     "object"
+#define W3C_EVENT      "event"
+#define W3C_RECOGNIZER "recognizer"
+#define W3C_UTTERANCE  "utterace"
+
+#define W3C_CREATE     "create"
+#define W3C_DELETE     "delete"
+#define W3C_SET        "set"
+#define W3C_START      "start"
+#define W3C_STOP       "stop"
+#define W3C_ABORT      "abort"
+
+#define W3C_NOTFOUND   "object not found"
+#define W3C_MALFORMED  "malformed request"
+#define W3C_FAILED     "request failed"
+#define W3C_BADGRAMMAR "bad-grammar"
+#define W3C_SERVERERR  "internal server error"
+#define W3C_NOMEM      "server out of memory"
+#define W3C_BADEVENTS  "invalid event array"
+#define W3C_BADEVENT   "invalid event"
+#define W3C_BUSY       "busy, request not possible now"
+#define W3C_BADVOICE   "unset, or unknown voice or language"
+
+#endif /* __SRS_W3C_PROTOCOL_H__ */
diff --git a/src/plugins/client-api/w3c-speech/w3c-server.c b/src/plugins/client-api/w3c-speech/w3c-server.c
new file mode 100644 (file)
index 0000000..2ac05f6
--- /dev/null
@@ -0,0 +1,2444 @@
+/*
+ * Copyright (c) 2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <errno.h>
+#include <stdarg.h>
+
+#include <murphy/common/macros.h>
+#include <murphy/common/debug.h>
+#include <murphy/common/log.h>
+#include <murphy/common/mm.h>
+#include <murphy/common/mainloop.h>
+#include <murphy/common/transport.h>
+#include <murphy/common/json.h>
+
+#include "srs/daemon/plugin.h"
+#include "srs/daemon/client.h"
+
+#include "w3c-server.h"
+#include "w3c-message.h"
+#include "w3c-protocol.h"
+
+#define W3C_PLUGIN  "w3c-speech"
+#define W3C_DESCR   "W3C speech API plugin for Winthorpe."
+#define W3C_AUTHORS "Krisztian Litkey <kli@iki.fi>"
+#define W3C_VERSION "0.0.1"
+
+#define W3C_TYPE_SYNTHESIZER     0x0
+#define W3C_TYPE_RECOGNIZER      0x1
+#define W3C_TYPE_UTTERANCE       0x2
+#define W3C_OBJECT_ID(type, cnt) ((cnt << 2) | (type))
+#define W3C_OBJECT_TYPE(id)      ((id) & 0x3)
+
+
+typedef struct w3c_client_s w3c_client_t;
+typedef struct w3c_synthesizer_s w3c_synthesizer_t;
+typedef struct w3c_attrdef_s w3c_attrdef_t;
+
+
+/*
+ * W3C server (plugin runtime context)
+ */
+
+typedef struct {
+    srs_plugin_t    *self;               /* our plugin instance */
+    const char      *address;            /* transport address to listen on */
+    int              sock;               /* or existing socket for transport */
+    const char      *grammar_dir;        /* grammar directory */
+    mrp_transport_t *lt;                 /* transport we listen on */
+    mrp_list_hook_t  clients;            /* connected clients */
+    int              next_id;            /* next client id */
+} w3c_server_t;
+
+
+/*
+ * W3C client events
+ */
+
+typedef enum {
+    W3C_EVENT_NONE        = 0x0000,
+    W3C_EVENT_START       = 0x0001,
+    W3C_EVENT_END         = 0x0002,
+    W3C_EVENT_RESULT      = 0x0004,
+    W3C_EVENT_NOMATCH     = 0x0008,
+    W3C_EVENT_ERROR       = 0x0010,
+    W3C_EVENT_AUDIOSTART  = 0x0020,
+    W3C_EVENT_AUDIOEND    = 0x0040,
+    W3C_EVENT_SOUNDSTART  = 0x0080,
+    W3C_EVENT_SOUNDEND    = 0x0100,
+    W3C_EVENT_SPEECHSTART = 0x0200,
+    W3C_EVENT_SPEECHEND   = 0x0400,
+    W3C_EVENT_PAUSE       = 0x0800,
+    W3C_EVENT_RESUME      = 0x1000,
+    W3C_EVENT_MARK        = 0x2000,
+    W3C_EVENT_BOUNDARY    = 0x4000,
+} w3c_event_t;
+
+
+/*
+ * W3C client requests
+ */
+
+typedef enum {
+    W3C_REQUEST_NONE  = 0,
+    W3C_REQUEST_START,
+    W3C_REQUEST_STOP,
+    W3C_REQUEST_ABORT,
+    W3C_REQUEST_PAUSE,
+    W3C_REQUEST_CANCEL,
+    W3C_REQUEST_RESUME,
+} w3c_request_t;
+
+
+/*
+ * W3C backend state
+ */
+
+typedef enum {
+    W3C_BACKEND_STOPPED = 0,
+    W3C_BACKEND_STARTED,
+    W3C_BACKEND_RENDERING
+} w3c_backend_t;
+
+
+/*
+ * a W3C client
+ */
+
+struct w3c_client_s {
+    mrp_list_hook_t    hook;             /* to list of clients */
+    int                id;               /* client id */
+    w3c_server_t      *s;                /* back pointer to W3C server/plugin */
+    mrp_transport_t   *t;                /* transport to this client */
+    w3c_synthesizer_t *syn;              /* singleton per-client synthesizer */
+    mrp_list_hook_t    recognizers;      /* recognizer instances */
+    int                next_id;          /* next recognizer/utterance id */
+};
+
+
+/*
+ * a W3C recognizer instance
+ */
+
+typedef struct {
+    char      *name;                     /* backend client name */
+    char      *appclass;                 /* backend client application class */
+    uint32_t   events;                   /* mask of events of interest */
+    char     **grammars;                 /* recognizer grammars */
+    int        ngrammar;                 /* number of grammars */
+    char      *lang;                     /* recognizer language */
+    bool       continuous;               /* whether in continuous mode */
+    bool       interim;                  /* whether to deliver interim results */
+    int        max_alt;                  /* max. alternatives to deliver */
+    char      *service;                  /* recognizer service URI */
+    bool       shared;                   /* whether use shared focus */
+    char     **commands;                 /* commands from grammars */
+    int        ncommand;                 /* number of commands */
+} w3c_rec_attr_t;
+
+
+typedef struct {
+    mrp_list_hook_t  hook;               /* to list of recognizers */
+    w3c_client_t    *c;                  /* client we belong to  */
+    int              id;                 /* recognizer object id */
+    w3c_rec_attr_t   attr;               /* recognizer attributes */
+    uint32_t         mask;               /* attribute mask */
+    srs_client_t    *srsc;               /* associated backend client */
+    int              request;            /* W3C client request */
+    int              backend;            /* W3C backend state */
+} w3c_recognizer_t;
+
+
+/*
+ * a W3C synthesizer instance (per-client singleton)
+ */
+
+typedef struct {
+    char *name;                          /* backend client name */
+    char *appclass;                      /* backend client application class */
+} w3c_syn_attr_t;
+
+
+struct w3c_synthesizer_s {
+    w3c_client_t    *c;                  /* client we belong to */
+    w3c_syn_attr_t   attr;               /* synthesizer attributes */
+    srs_client_t    *srsc;               /* associated backend client */
+    mrp_list_hook_t  utterances;         /* existing utterances */
+    mrp_list_hook_t  pending;            /* pending utterances */
+    bool             paused;             /* whether paused */
+};
+
+
+/*
+ * a W3C utterance instance
+ */
+
+typedef struct {
+    char     *text;                      /* text to synthesize */
+    char     *lang;                      /* language to use */
+    char     *voice;                     /* voice to use for synthesis */
+    double    volume;                    /* volume to use for synthesis */
+    double    rate;                      /* rate to use for synthesis */
+    double    pitch;                     /* pitch to use for synthesis */
+    uint32_t  events;                    /* events to deliver to client */
+    int       timeout;                   /* rendering timeout */
+} w3c_utt_attr_t;
+
+
+typedef struct {
+    mrp_list_hook_t    hook;             /* to utterance list */
+    mrp_list_hook_t    pending;          /* to pending list */
+    w3c_synthesizer_t *syn;              /* associated synthesizer */
+    int                id;               /* (W3C) utterance object id */
+    w3c_utt_attr_t     attr;             /* utterance attributes */
+    uint32_t           vid;              /* backend id */
+} w3c_utterance_t;
+
+
+/*
+ * an attribute definition
+ */
+
+typedef int (*w3c_attr_parser_t)(mrp_json_t *attr, void *obj, size_t base,
+                                 w3c_attrdef_t *def, int *errc,
+                                 const char **errs);
+typedef int (*w3c_attr_check_t)(void *obj, w3c_attrdef_t *def, void *value,
+                                int *errc, const char **errs);
+
+struct w3c_attrdef_s {
+    const char        *name;             /* attribute name   */
+    int                type;             /* attribute type   */
+    size_t             offs;             /* attribute offset */
+    int                mask;             /* change mask bit  */
+    w3c_attr_parser_t  parser;           /* attribute parser */
+    w3c_attr_check_t   check;            /* attribute checker */
+};
+
+typedef enum {
+    W3C_ATTR_NAME       = (0x1 <<  0),
+    W3C_ATTR_APPCLASS   = (0x1 <<  1),
+    W3C_ATTR_EVENTS     = (0x1 <<  2),
+    W3C_ATTR_GRAMMARS   = (0x1 <<  3),
+    W3C_ATTR_LANG       = (0x1 <<  4),
+    W3C_ATTR_CONTINUOUS = (0x1 <<  5),
+    W3C_ATTR_INTERIM    = (0x1 <<  6),
+    W3C_ATTR_MAXALT     = (0x1 <<  7),
+    W3C_ATTR_SERVICE    = (0x1 <<  8),
+    W3C_ATTR_TEXT       = (0x1 <<  9),
+    W3C_ATTR_VOICE      = (0x1 << 10),
+    W3C_ATTR_VOLUME     = (0x1 << 11),
+    W3C_ATTR_RATE       = (0x1 << 12),
+    W3C_ATTR_PITCH      = (0x1 << 13),
+    W3C_ATTR_SHARED     = (0x1 << 14),
+    W3C_ATTR_TIMEOUT    = (0x1 << 15),
+} w3c_attrmask_t;
+
+
+/*
+ * a client request handler
+ */
+
+typedef int (*request_handler_t)(w3c_client_t *c, int reqno, mrp_json_t *req);
+
+
+static int create_synthesizer(w3c_client_t *c);
+static void destroy_synthesizer(w3c_synthesizer_t *syn);
+static void destroy_recognizer(w3c_recognizer_t *rec);
+static w3c_utterance_t *lookup_utterance(w3c_client_t *c, int id, uint32_t vid);
+static void destroy_utterance(w3c_utterance_t *utt);
+
+
+static w3c_client_t *w3c_client_create(w3c_server_t *s)
+{
+    w3c_client_t *c;
+    int           flags;
+
+    if ((c = mrp_allocz(sizeof(*c))) == NULL) {
+        mrp_transport_destroy(mrp_transport_accept(s->lt, NULL, 0));
+        return NULL;
+    }
+
+    mrp_list_init(&c->hook);
+    mrp_list_init(&c->recognizers);
+
+    if (create_synthesizer(c) < 0) {
+        mrp_transport_destroy(mrp_transport_accept(s->lt, NULL, 0));
+        mrp_free(c);
+        return NULL;
+    }
+
+    flags = MRP_TRANSPORT_REUSEADDR;
+
+    if ((c->t = mrp_transport_accept(s->lt, c, flags)) == NULL) {
+        mrp_free(c);
+        return NULL;
+    }
+
+    c->id      = s->next_id++;
+    c->s       = s;
+    c->next_id = 1;
+
+    mrp_list_append(&s->clients, &c->hook);
+
+    mrp_log_info("Created W3C client #%d.", c->id);
+
+    return c;
+}
+
+
+static void w3c_client_destroy(w3c_client_t *c)
+{
+    mrp_list_hook_t  *p, *n;
+    w3c_recognizer_t *rec;
+
+    mrp_log_info("Destroying W3C client #%d...", c->id);
+
+    mrp_list_foreach(&c->recognizers, p, n) {
+        rec = mrp_list_entry(p, typeof(*rec), hook);
+        destroy_recognizer(rec);
+    }
+
+    destroy_synthesizer(c->syn);
+
+    mrp_list_delete(&c->hook);
+    mrp_transport_destroy(c->t);
+    mrp_free(c);
+}
+
+
+static void connection_evt(mrp_transport_t *lt, void *user_data)
+{
+    w3c_server_t *s = (w3c_server_t *)user_data;
+    w3c_client_t *c;
+
+    MRP_UNUSED(lt);
+
+    if ((c = w3c_client_create(s)) == NULL)
+        mrp_log_error("Failed to create new W3C client.");
+    else
+        mrp_log_info("Accepted connection from W3C client #%d.", c->id);
+}
+
+
+static void closed_evt(mrp_transport_t *t, int error, void *user_data)
+{
+    w3c_client_t *c = (w3c_client_t *)user_data;
+
+    MRP_UNUSED(t);
+
+    if (error != 0)
+        mrp_log_error("W3C speech connection closed with error %d (%s).",
+                      error, strerror(error));
+    else
+        mrp_log_info("W3C speech connection closed.");
+
+    w3c_client_destroy(c);
+}
+
+
+static inline void dump_request(mrp_json_t *req)
+{
+    const char *str;
+
+#if 1
+    str = mrp_json_object_to_string(req);
+#else
+    str = json_object_to_json_string_ext(req, JSON_C_TO_STRING_PRETTY);
+#endif
+
+    mrp_log_info("received W3C speech request:");
+    mrp_log_info("  %s", str);
+}
+
+
+static mrp_json_t *add_json_timestamp(mrp_json_t *msg)
+{
+    mrp_json_t     *ts;
+    struct timeval  tv;
+
+    if (gettimeofday(&tv, NULL) < 0)
+        return NULL;
+
+    if (msg != NULL)
+        ts = mrp_json_add_member(msg, "timestamp", MRP_JSON_OBJECT);
+    else
+        msg = ts = mrp_json_create(MRP_JSON_OBJECT);
+
+    if (ts == NULL)
+        return NULL;
+
+    mrp_json_add_integer(ts, "sec" , tv.tv_sec);
+    mrp_json_add_integer(ts, "usec", tv.tv_usec);
+
+    return msg;
+}
+
+
+static int _reply_status(mrp_transport_t *t, int reqno, int status, ...)
+{
+    mrp_json_t *rpl;
+    va_list     ap;
+    const char *key;
+    int         type;
+
+    if ((rpl = mrp_json_create(MRP_JSON_OBJECT)) == NULL)
+        return -1;
+
+    mrp_json_add_integer(rpl, "reqno" , reqno);
+    mrp_json_add_string (rpl, "type"  , "status");
+    mrp_json_add_integer(rpl, "status", status);
+
+    va_start(ap, status);
+
+    while ((key = va_arg(ap, const char *)) != NULL) {
+        switch ((type = va_arg(ap, int))) {
+        case MRP_JSON_BOOLEAN:
+            mrp_json_add_boolean(rpl, key, va_arg(ap, int));
+            break;
+        case MRP_JSON_STRING:
+            mrp_json_add_string(rpl, key, va_arg(ap, const char *));
+            break;
+        case MRP_JSON_INTEGER:
+            mrp_json_add_integer(rpl, key, va_arg(ap, int));
+            break;
+        case MRP_JSON_DOUBLE:
+            mrp_json_add_double(rpl, key, va_arg(ap, double));
+            break;
+        case MRP_JSON_OBJECT:
+            mrp_json_add(rpl, key, va_arg(ap, void *));
+            break;
+        case MRP_JSON_ARRAY:
+            mrp_json_add(rpl, key, va_arg(ap, void *));
+            break;
+        default:
+            mrp_json_unref(rpl);
+            errno  = EINVAL;
+
+            va_end(ap);
+
+            return -1;
+        }
+    }
+
+    va_end(ap);
+
+    if (mrp_transport_sendjson(t, rpl))
+        status = 0;
+    else {
+        errno  = EIO;
+        status = -1;
+    }
+
+    mrp_json_unref(rpl);
+
+    return status;
+}
+
+#define reply_status(...) _reply_status(__VA_ARGS__, NULL)
+
+
+static int reply_error(mrp_transport_t *t, int reqno, int status,
+                       const char *error, mrp_json_t *req, const char *fmt, ...)
+{
+    const char *request;
+    char        msg[4096];
+    va_list     ap;
+
+    va_start(ap, fmt);
+    vsnprintf(msg, sizeof(msg), fmt ? fmt : "", ap);
+    va_end(ap);
+
+    if (req != NULL)
+        request = "request";
+    else
+        request = NULL;
+
+    if (reqno < 0)
+        mrp_json_get_integer(req, "reqno", &reqno);
+
+    return reply_status(t, reqno, status,
+                        "error"  , MRP_JSON_STRING, error,
+                        "message", MRP_JSON_STRING, msg  ,
+                        request  , MRP_JSON_OBJECT, req   );
+}
+
+
+static int _send_event(mrp_transport_t *t, int id, const char *event, ...)
+{
+    mrp_json_t *evt;
+    va_list     ap;
+    const char *key;
+    int         type, status;
+
+    if ((evt = mrp_json_create(MRP_JSON_OBJECT)) != NULL) {
+        mrp_json_add_integer(evt, "reqno", 0);
+        mrp_json_add_string (evt, "type" , "event");
+        mrp_json_add_integer(evt, "id"   , id);
+        add_json_timestamp  (evt);
+        mrp_json_add_string (evt, "event", event);
+
+        va_start(ap, event);
+
+        while ((key = va_arg(ap, const char *)) != NULL) {
+            switch ((type = va_arg(ap, int))) {
+            case MRP_JSON_BOOLEAN:
+                mrp_json_add_boolean(evt, key, va_arg(ap, int));
+                break;
+            case MRP_JSON_STRING:
+                mrp_json_add_string(evt, key, va_arg(ap, const char *));
+                break;
+            case MRP_JSON_INTEGER:
+                mrp_json_add_integer(evt, key, va_arg(ap, int));
+                break;
+            case MRP_JSON_DOUBLE:
+                mrp_json_add_double(evt, key, va_arg(ap, double));
+                break;
+            case MRP_JSON_OBJECT:
+                mrp_json_add(evt, key, va_arg(ap, void *));
+                break;
+            case MRP_JSON_ARRAY:
+                mrp_json_add(evt, key, va_arg(ap, void *));
+                break;
+            default:
+                errno  = EINVAL;
+                mrp_json_unref(evt);
+                return -1;
+            }
+        }
+
+        va_end(ap);
+
+        if (mrp_transport_sendjson(t, evt))
+            status = 0;
+        else {
+            errno  = EIO;
+            status = -1;
+        }
+
+        mrp_json_unref(evt);
+    }
+    else
+        status = -1;
+
+    return status;
+}
+
+#define send_event(...) _send_event(__VA_ARGS__, NULL)
+
+
+static int malformed_request(mrp_transport_t *t, mrp_json_t *req,
+                             const char *fmt, ...)
+{
+    int     reqno;
+    char    msg[4096];
+    va_list ap;
+
+    if (!mrp_json_get_integer(req, "reqno", &reqno))
+        reqno = 0;
+
+    va_start(ap, fmt);
+    vsnprintf(msg, sizeof(msg), fmt, ap);
+    va_end(ap);
+
+    if (reqno > 0)
+        return reply_error(t, reqno, EINVAL, W3C_MALFORMED, req, "%s", msg);
+    else
+        return send_event(t, 0, "error",
+                          "errorCode", MRP_JSON_STRING, W3C_MALFORMED,
+                          "message"  , MRP_JSON_STRING, msg);
+}
+
+
+static inline int update_speaking(w3c_synthesizer_t *syn, bool state)
+{
+    w3c_client_t *c = syn->c;
+
+    return send_event(c->t, 0, "speaking", "state", MRP_JSON_BOOLEAN, state);
+}
+
+
+static inline int update_pending(w3c_synthesizer_t *syn, bool prev)
+{
+    w3c_client_t *c    = syn->c;
+    bool          curr = !mrp_list_empty(&c->syn->pending);
+
+    if (curr != prev)
+        return send_event(c->t, 0, "pending", "state", MRP_JSON_BOOLEAN, curr);
+    else
+        return 0;
+}
+
+
+static inline int update_paused(w3c_synthesizer_t *syn, bool state)
+{
+    w3c_client_t *c = syn->c;
+
+    return send_event(c->t, 0, "paused", "state", MRP_JSON_BOOLEAN, state);
+}
+
+
+static int w3c_focus_notify(srs_client_t *c, srs_voice_focus_t focus)
+{
+    w3c_recognizer_t *rec = (w3c_recognizer_t *)c->user_data;
+
+    mrp_log_info("W3C-recognizer#%d has now %s focus", rec->id,
+                 (focus == SRS_VOICE_FOCUS_NONE ? "no" :
+                  (focus == SRS_VOICE_FOCUS_SHARED ? "shared" : "exclusive")));
+
+    if (focus == SRS_VOICE_FOCUS_NONE) {
+        switch (rec->request) {
+        case W3C_REQUEST_START:
+            send_event(rec->c->t, rec->id, "error",
+                       "error"  , MRP_JSON_STRING, "aborted",
+                       "message", MRP_JSON_STRING, "voice focus lost");
+            break;
+        case W3C_REQUEST_STOP:
+        case W3C_REQUEST_ABORT:
+            send_event(rec->c->t, rec->id, "stopped");
+            break;
+        default:
+            break;
+        }
+        rec->backend = W3C_BACKEND_STOPPED;
+    }
+    else {
+        switch (rec->request) {
+        case W3C_REQUEST_START:
+            send_event(rec->c->t, rec->id, "started");
+            break;
+        default:
+            break;
+        }
+        rec->backend = W3C_BACKEND_STARTED;
+    }
+
+    return 0;
+}
+
+
+static char *concat_tokens(char *buf, size_t size, int ntoken, char **tokens)
+{
+    char *p;
+    int   l, n, i;
+
+    p = buf;
+    l = size;
+
+    for (i = 0; i < ntoken; i++) {
+        n  = snprintf(p, l, "%s%s", i ? " " : "", tokens[i]);
+        p += n;
+        l -= n;
+
+        if (l <= 0) {
+            buf[size - 1] = '\0';
+            break;
+        }
+    }
+
+    return buf;
+}
+
+
+static int w3c_command_notify(srs_client_t *c, int idx, int ntoken,
+                              char **tokens, uint32_t *start, uint32_t *end,
+                              srs_audiobuf_t *audio)
+{
+    w3c_recognizer_t *rec = (w3c_recognizer_t *)c->user_data;
+    mrp_json_t       *results, *r;
+    char              text[16*1024];
+
+    MRP_UNUSED(idx);
+    MRP_UNUSED(start);
+    MRP_UNUSED(end);
+    MRP_UNUSED(audio);
+
+    r = results = NULL;
+
+    if ((results = mrp_json_create(MRP_JSON_ARRAY))  == NULL ||
+        (r       = mrp_json_create(MRP_JSON_OBJECT)) == NULL) {
+        mrp_json_unref(results);
+        mrp_json_unref(r);
+
+        return -1;
+    }
+
+    concat_tokens(text, sizeof(text), ntoken, tokens);
+
+    mrp_json_add_double(r, "confidence", 0.89);
+    mrp_json_add_string(r, "transcript", text);
+
+    if (mrp_json_array_append(results, r)) {
+        send_event(rec->c->t, rec->id, "match",
+                   "final"  , MRP_JSON_BOOLEAN, true,
+                   "length" , MRP_JSON_INTEGER, 1,
+                   "results", MRP_JSON_OBJECT , results);
+
+        return 0;
+    }
+    else {
+        mrp_json_unref(results);
+        mrp_json_unref(r);
+
+        return -1;
+    }
+}
+
+
+static int no_voice_notify(srs_client_t *c, srs_voice_event_t *event)
+{
+    MRP_UNUSED(c);
+    MRP_UNUSED(event);
+
+    return 0;
+}
+
+
+static int create_recognizer_client(w3c_recognizer_t *rec, int *errc,
+                                    const char **errs)
+{
+    static srs_client_ops_t ops = {
+        .notify_focus   = w3c_focus_notify,
+        .notify_command = w3c_command_notify,
+        .notify_render  = no_voice_notify
+    };
+
+    srs_context_t  *srs      = rec->c->s->self->srs;
+    char          **commands = rec->attr.commands;
+    int             ncommand = rec->attr.ncommand;
+    const char     *name     = rec->attr.name;
+    const char     *appclass = rec->attr.appclass;
+    char            cid[256];
+
+    if (rec->srsc != NULL)
+        return 0;
+
+    if (!commands) {
+        *errc = EINVAL;
+        *errs = W3C_BADGRAMMAR;
+        return -1;
+    }
+
+    snprintf(cid, sizeof(cid), "W3C-client #%d.%d", rec->c->id, rec->id);
+
+    if (name == NULL)
+        name = cid;
+
+    if (appclass == NULL)
+        appclass = "player";
+
+    rec->srsc = client_create(srs, SRS_CLIENT_TYPE_EXTERNAL, name, appclass,
+                              commands, ncommand, cid, &ops, rec);
+
+    if (rec->srsc == NULL) {
+        *errc = EINVAL;
+        *errs = W3C_FAILED;
+
+        return -1;
+    }
+
+    rec->request = W3C_REQUEST_NONE;
+    rec->backend = W3C_BACKEND_STOPPED;
+
+    return 0;
+}
+
+
+static void destroy_recognizer_client(w3c_recognizer_t *rec)
+{
+    client_destroy(rec->srsc);
+
+    mrp_log_info("Destroying recognizer #%d.%d...", rec->c->id, rec->id);
+
+    rec->srsc    = NULL;
+    rec->backend = W3C_BACKEND_STOPPED;
+}
+
+
+static int start_recognizer_client(w3c_recognizer_t *rec, int *errc,
+                                   const char **errs)
+{
+    int focus;
+
+    if (rec->attr.shared)
+        focus = SRS_VOICE_FOCUS_SHARED;
+    else
+        focus = SRS_VOICE_FOCUS_EXCLUSIVE;
+
+    if (client_request_focus(rec->srsc, focus))
+        return 0;
+    else {
+        *errc = EINVAL;
+        *errs = W3C_FAILED;
+
+        return -1;
+    }
+}
+
+
+static int stop_recognizer_client(w3c_recognizer_t *rec, int *errc,
+                                  const char **errs)
+{
+    if (client_request_focus(rec->srsc, SRS_VOICE_FOCUS_NONE))
+        return 0;
+    else {
+        *errc = EINVAL;
+        *errs = W3C_FAILED;
+
+        return -1;
+    }
+}
+
+
+static int w3c_voice_notify(srs_client_t *c, srs_voice_event_t *e)
+{
+    w3c_synthesizer_t *syn  = (w3c_synthesizer_t *)c->user_data;
+    w3c_utterance_t   *utt  = lookup_utterance(syn->c, -1, e->id);
+    int                mask = 1 << e->type;
+
+    switch (e->type) {
+    case SRS_VOICE_EVENT_STARTED:
+        if (utt->attr.events & W3C_EVENT_START)
+            send_event(syn->c->t, utt->id, "start");
+        break;
+    case SRS_VOICE_EVENT_COMPLETED:
+        if (utt->attr.events & W3C_EVENT_END)
+            send_event(syn->c->t, utt->id, "end");
+        break;
+    case SRS_VOICE_EVENT_TIMEOUT:
+        if (utt->attr.events & W3C_EVENT_ERROR)
+            send_event(syn->c->t, utt->id, "error",
+                       "error", MRP_JSON_STRING, "timeout while queued");
+        break;
+    case SRS_VOICE_EVENT_ABORTED:
+        if (utt->attr.events & W3C_EVENT_ERROR)
+            send_event(syn->c->t, utt->id, "error",
+                       "error", MRP_JSON_STRING, "aborted");
+        break;
+    case SRS_VOICE_EVENT_PROGRESS:
+        break;
+    default:
+        break;
+    }
+
+    if (mask & SRS_VOICE_MASK_STARTED) {
+        update_speaking(utt->syn, true);
+    }
+    else if (mask & SRS_VOICE_MASK_DONE) {
+        utt->vid = SRS_VOICE_INVALID;
+        mrp_list_delete(&utt->pending);
+
+        update_speaking(utt->syn, false);
+        update_pending(utt->syn, true);
+    }
+
+    return 0;
+}
+
+
+static int create_synthesizer_client(w3c_synthesizer_t *syn, int *errc,
+                                     const char **errs)
+{
+    static srs_client_ops_t ops = {
+        .notify_focus   = NULL,
+        .notify_command = NULL,
+        .notify_render  = w3c_voice_notify
+    };
+
+    srs_context_t  *srs      = syn->c->s->self->srs;
+    const char     *name     = syn->attr.name;
+    const char     *appclass = syn->attr.appclass;
+    char            cid[256];
+
+    if (syn->srsc != NULL)
+        return 0;
+
+    snprintf(cid, sizeof(cid), "W3C-renderer #%d", syn->c->id);
+
+    if (name == NULL)
+        name = cid;
+
+    if (appclass == NULL)
+        appclass = "player";
+
+    syn->srsc = client_create(srs, SRS_CLIENT_TYPE_EXTERNAL, name, appclass,
+                              NULL, 0, cid, &ops, syn);
+
+    if (syn->srsc == NULL) {
+        *errc = EINVAL;
+        *errs = W3C_FAILED;
+
+        return -1;
+    }
+
+    return 0;
+}
+
+
+static void destroy_synthesizer_client(w3c_synthesizer_t *syn)
+{
+    client_destroy(syn->srsc);
+    syn->srsc = NULL;
+}
+
+
+static int create_synthesizer(w3c_client_t *c)
+{
+    w3c_synthesizer_t *syn;
+
+    if (c->syn != NULL)
+        return 0;
+
+    if ((syn = mrp_allocz(sizeof(*syn))) == 0)
+        return -1;
+
+    mrp_list_init(&syn->utterances);
+    mrp_list_init(&syn->pending);
+
+    syn->c = c;
+    c->syn = syn;
+
+    return 0;
+}
+
+
+static void destroy_synthesizer(w3c_synthesizer_t *syn)
+{
+    mrp_list_hook_t *p, *n;
+    w3c_utterance_t *utt;
+
+    if (syn == NULL)
+        return;
+
+    destroy_synthesizer_client(syn);
+
+    syn->c->syn = NULL;
+
+    mrp_list_foreach(&syn->utterances, p, n) {
+        utt = mrp_list_entry(p, typeof(*utt), hook);
+        destroy_utterance(utt);
+
+    }
+
+    mrp_free(syn);
+}
+
+
+static w3c_utterance_t *create_utterance(w3c_synthesizer_t *syn)
+{
+    w3c_utterance_t *utt;
+
+    if ((utt = mrp_allocz(sizeof(*utt))) != NULL) {
+        mrp_list_init(&utt->hook);
+        mrp_list_init(&utt->pending);
+
+        utt->syn = syn;
+        utt->id  = W3C_OBJECT_ID(W3C_TYPE_UTTERANCE, syn->c->next_id++);
+        utt->vid = SRS_VOICE_INVALID;
+
+        utt->attr.volume  = 1.0;
+        utt->attr.rate    = 1.0;
+        utt->attr.pitch   = 1.0;
+        utt->attr.timeout = -1;
+
+        mrp_list_append(&syn->utterances, &utt->hook);
+    }
+
+    return utt;
+}
+
+
+static void destroy_utterance(w3c_utterance_t *utt)
+{
+    mrp_list_delete(&utt->hook);
+    mrp_list_delete(&utt->pending);
+
+    mrp_free(utt->attr.text);
+    mrp_free(utt->attr.lang);
+    mrp_free(utt->attr.voice);
+
+    mrp_free(utt);
+}
+
+
+static w3c_utterance_t *lookup_utterance(w3c_client_t *c, int id, uint32_t vid)
+{
+    mrp_list_hook_t *p, *n;
+    w3c_utterance_t *utt;
+
+    mrp_list_foreach(&c->syn->utterances, p, n) {
+        utt = mrp_list_entry(p, typeof(*utt), hook);
+
+        if (id != -1 && utt->id == id)
+            return utt;
+
+        if (id == -1 && utt->vid == vid)
+            return utt;
+    }
+
+    return NULL;
+}
+
+
+static int activate_utterance(w3c_utterance_t *utt)
+{
+    const char *msg, *voice;
+    double      rate, pitch;
+    int         timeout, events;
+
+    if (utt->vid != SRS_VOICE_INVALID)
+        return 0;
+
+    msg     = utt->attr.text;
+    voice   = utt->attr.voice ? utt->attr.voice : utt->attr.lang;
+    rate    = utt->attr.rate;
+    pitch   = utt->attr.pitch;
+    timeout = utt->attr.timeout;
+
+    events  = SRS_VOICE_MASK_ALL;
+
+    if (!utt->syn->paused) {
+        utt->vid = client_render_voice(utt->syn->srsc, msg, voice, rate, pitch,
+                                       timeout, events);
+
+        if (utt->vid == SRS_VOICE_INVALID) {
+            errno = EINVAL;
+            return -1;
+        }
+    }
+
+    mrp_list_delete(&utt->pending);
+    mrp_list_append(&utt->syn->pending, &utt->pending);
+
+    return 0;
+}
+
+
+static int cancel_utterance(w3c_utterance_t *utt)
+{
+    client_cancel_voice(utt->syn->srsc, utt->vid);
+    utt->vid = SRS_VOICE_INVALID;
+    mrp_list_delete(&utt->pending);
+
+    return 0;
+}
+
+
+static int pause_utterance(w3c_utterance_t *utt)
+{
+    /* XXX TODO: not right pause since will restart from the start. */
+    client_cancel_voice(utt->syn->srsc, utt->vid);
+    utt->vid = SRS_VOICE_INVALID;
+
+    return 0;
+}
+
+
+static int resume_utterance(w3c_utterance_t *utt)
+{
+    /* XXX TODO: since we can't resume ATM, for now we restart from scratch */
+    return activate_utterance(utt);
+}
+
+
+static int parse_string_attr(mrp_json_t *attr, void *obj, size_t base,
+                             w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    char       **valuep = (char **)(((char *)obj) + base + def->offs);
+    const char  *value;
+    int          status;
+
+    value  = mrp_json_string_value(attr);
+    status = def->check(obj, def, (void *)&value, errc, errs);
+
+    if (status < 0)
+        return -(errno = -status);
+
+    mrp_free(*valuep);
+    *valuep = mrp_strdup(value);
+    mrp_debug("string attribute '%s' set to '%s'", def->name, value);
+
+    return 0;
+}
+
+
+static int parse_boolean_attr(mrp_json_t *attr, void *obj, size_t base,
+                              w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    bool *valuep = (bool *)(((char *)obj) + base + def->offs);
+    bool  value;
+    int   status;
+
+    value  = !!mrp_json_boolean_value(attr);
+    status = def->check(obj, def, (void *)&value, errc, errs);
+
+    if (status < 0)
+        return -(errno = -status);
+
+    *valuep = value;
+    mrp_debug("boolean attribute '%s' set to %s", def->name,
+              value ? "true" : "false");
+
+    return 0;
+}
+
+
+static int parse_integer_attr(mrp_json_t *attr, void *obj, size_t base,
+                              w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    int *valuep = (int *)(((char *)obj) + base + def->offs);
+    int  value, status;
+
+    value  = (int)mrp_json_integer_value(attr);
+    status = def->check(obj, def, (void *)&value, errc, errs);
+
+    if (status < 0)
+        return -(errno = -status);
+
+    *valuep = value;
+    mrp_debug("integer attribute '%s' set to %d", def->name, value);
+
+    return 0;
+}
+
+
+static int parse_double_attr(mrp_json_t *attr, void *obj, size_t base,
+                             w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    double *valuep = (double *)(((char *)obj) + base + def->offs);
+    double  value;
+    int     status;
+
+    value  = (double)mrp_json_double_value(attr);
+    status = def->check(obj, def, (void *)&value, errc, errs);
+
+    if (status < 0)
+        return -(errno = -status);
+
+    *valuep = value;
+    mrp_debug("double attribute '%s' set to %f", def->name, value);
+
+    return 0;
+}
+
+
+static int parse_events(mrp_json_t *attr, void *obj, size_t base,
+                        w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    static struct {
+        const char *name;
+        int         mask;
+    } events[] = {
+#define E(_n, _m) { _n, W3C_EVENT_##_m }
+        E("start"       , START        ),
+        E("end"         , END          ),
+        E("result"      , RESULT       ),
+        E("nomatch"     , NOMATCH      ),
+        E("error"       , ERROR        ),
+        E("audiostart"  , AUDIOSTART   ),
+        E("audioend"    , AUDIOEND     ),
+        E("soundstart"  , SOUNDSTART   ),
+        E("soundend"    , SOUNDEND     ),
+        E("speechstart" , SPEECHSTART  ),
+        E("speechend"   , SPEECHEND    ),
+        E("pause"       , PAUSE        ),
+        E("resume"      , RESUME       ),
+        E("mark"        , MARK         ),
+        E("boundary"    , BOUNDARY     ),
+#undef E
+        { NULL, 0 }
+    }, *e;
+
+    const char *name;
+    int        *valuep = (int *)(((char *)obj) + base + def->offs);
+    int         mask, len, i, status;
+
+    len  = mrp_json_array_length(attr);
+    mask = W3C_EVENT_NONE;
+
+    for (i = 0; i < len; i++) {
+        if (!mrp_json_array_get_string(attr, i, &name)) {
+            errno = EINVAL;
+            *errc = EINVAL;
+            *errs = W3C_BADEVENTS;
+            return -1;
+        }
+
+        for (e = events; e->name; e++) {
+            if (!strcmp(e->name, name)) {
+                mask |= e->mask;
+                break;
+            }
+        }
+
+        if (e->name == NULL) {
+            mrp_log_error("Unknown W3C event '%s' requested", name);
+            errno = EINVAL;
+            *errc = EINVAL;
+            *errs = W3C_BADEVENT;
+            return -1;
+        }
+    }
+
+    status = def->check(obj, def, (void *)&mask, errc, errs);
+
+    if (status < 0)
+        return -(errno = -status);
+
+    *valuep = mask;
+    mrp_debug("events attribute '%s' set to 0x%x", def->name, *valuep);
+
+    return 0;
+}
+
+
+static int parse_grammars(mrp_json_t *attr, void *obj, size_t base,
+                          w3c_attrdef_t *def, int *errc, const char **errs)
+{
+    char       ***grammarsp = (char ***)(((char *)obj) + base + def->offs);
+    int          *ngrammarp;
+    char        **grammars;
+    int           ngrammar, len, i, status;
+    mrp_json_t   *grm;
+
+    if (grammarsp != (char ***)(((char *)obj) +
+                                MRP_OFFSET(w3c_recognizer_t, attr) +
+                                MRP_OFFSET(w3c_rec_attr_t, grammars))) {
+        *errc = EINVAL;
+        *errs = W3C_SERVERERR;
+        return -1;
+    }
+
+    ngrammarp = (int *)(((char *)obj) +
+                        MRP_OFFSET(w3c_recognizer_t, attr) +
+                        MRP_OFFSET(w3c_rec_attr_t, ngrammar));
+
+    grammars = NULL;
+    ngrammar = 0;
+    len      = mrp_json_array_length(attr);
+
+    for (i = 0; i < len; i++) {
+        if (!mrp_reallocz(grammars, ngrammar, ngrammar + 1)) {
+            *errc = ENOMEM;
+            *errs = W3C_NOMEM;
+            return -1;
+        }
+
+        if (!mrp_json_array_get_item(attr, i, MRP_JSON_OBJECT, &grm))
+            goto invalid_grammars;
+
+        if (!mrp_json_get_string(grm, "src", grammars + i))
+            goto invalid_grammars;
+
+        grammars[i] = mrp_strdup(grammars[i]);
+        ngrammar++;
+    }
+
+    if (!mrp_reallocz(grammars, ngrammar, ngrammar + 1)) {
+        *errc = ENOMEM;
+        *errs = W3C_NOMEM;
+        return -1;
+    }
+
+    status = def->check(obj, def, (void *)grammars, errc, errs);
+
+    if (status < 0) {
+        errno = -status;
+        goto invalid_grammars;
+    }
+
+    for (i = 0; i < *ngrammarp; i++)
+        mrp_free((*grammarsp)[i]);
+    mrp_free(*grammarsp);
+
+    *grammarsp = grammars;
+    *ngrammarp = ngrammar;
+
+    mrp_debug("grammar attribute '%s' set to:", def->name);
+    for (i = 0; i < ngrammar; i++)
+        mrp_debug("    #%d: '%s'", i, grammars[i]);
+
+    return 0;
+
+ invalid_grammars:
+    for (i = 0; i < ngrammar; i++)
+        mrp_free(grammars[i]);
+    mrp_free(grammars);
+
+    *errc = EINVAL;
+    *errs = W3C_BADGRAMMAR;
+
+    return -1;
+}
+
+
+static int w3c_set_attributes(mrp_json_t *set, void *obj, size_t base,
+                              w3c_attrdef_t *defs, int *errc, const char **errs)
+{
+    w3c_attrdef_t *d;
+    mrp_json_t    *v;
+    int            i, mask, vtype;
+
+    if (set == NULL)
+        return 0;
+
+    /*
+     * Notes:
+     *
+     *   Currently unsupported attributes in set are silently ignored.
+     *   Detecting those would require to replace the loop below with
+     *   two nested loops, the outer of which loopinh through all key-
+     *   value pairs in the set and the inner looking for the descriptor
+     *   for key.
+     */
+
+    mask = 0;
+    for (i = 0, d = defs; d->name != NULL; d++, i++) {
+        v = mrp_json_get(set, d->name);
+
+        if (v == NULL)
+            continue;
+
+        if ((vtype = (int)mrp_json_get_type(v)) != d->type) {
+            if (!(vtype = MRP_JSON_INTEGER && d->type == MRP_JSON_DOUBLE)) {
+                *errc = EINVAL;
+                *errs = W3C_MALFORMED;
+                return -(i + 1);
+            }
+        }
+
+        if (d->parser(v, obj, base, d, errc, errs) < 0)
+            return -(i + 1);
+
+        mask |= d->mask;
+    }
+
+    return mask;
+}
+
+
+static w3c_recognizer_t *create_recognizer(w3c_client_t *c)
+{
+    w3c_recognizer_t *rec;
+
+    if ((rec = mrp_allocz(sizeof(*rec))) != NULL) {
+        mrp_list_init(&rec->hook);
+        mrp_list_append(&c->recognizers, &rec->hook);
+
+        rec->c  = c;
+        rec->id = W3C_OBJECT_ID(W3C_TYPE_RECOGNIZER, c->next_id++);
+    }
+
+    mrp_log_info("Created W3C recognizer #%d.%d.", rec->c->id, rec->id);
+
+    return rec;
+}
+
+
+static void destroy_recognizer(w3c_recognizer_t *rec)
+{
+    int i;
+
+    if (rec == NULL)
+        return;
+
+    mrp_log_info("Destroying W3C recognizer #%d.%d.", rec->c->id, rec->id);
+
+    mrp_list_delete(&rec->hook);
+
+    destroy_recognizer_client(rec);
+
+    mrp_free(rec->attr.name);
+    mrp_free(rec->attr.appclass);
+    mrp_free(rec->attr.lang);
+    mrp_free(rec->attr.service);
+
+    for (i = 0; i < rec->attr.ngrammar; i++)
+        mrp_free(rec->attr.grammars[i]);
+    mrp_free(rec->attr.grammars);
+
+
+    mrp_free(rec);
+}
+
+
+static w3c_recognizer_t *lookup_recognizer(w3c_client_t *c, int id)
+{
+    mrp_list_hook_t  *p, *n;
+    w3c_recognizer_t *rec;
+
+    mrp_list_foreach(&c->recognizers, p, n) {
+        rec = mrp_list_entry(p, typeof(*rec), hook);
+
+        if (rec->id == id)
+            return rec;
+    }
+
+    return NULL;
+}
+
+
+
+static int check_recognizer_attr(void *obj, w3c_attrdef_t *def, void *val,
+                                 int *errc, const char **errs)
+{
+    w3c_recognizer_t *rec = (w3c_recognizer_t *)obj;
+
+    MRP_UNUSED(val);
+
+    switch (def->mask) {
+    case W3C_ATTR_NAME:
+    case W3C_ATTR_APPCLASS:
+    case W3C_ATTR_GRAMMARS:
+    case W3C_ATTR_LANG:
+        if (rec->srsc == NULL)
+            return 0;
+
+        *errc = EBUSY;
+        *errs = W3C_BUSY;
+        return -EBUSY;
+
+    case W3C_ATTR_EVENTS:     return 0;
+    case W3C_ATTR_CONTINUOUS: return 0;
+    case W3C_ATTR_INTERIM:    return 0;
+    case W3C_ATTR_MAXALT:     return 0;
+    case W3C_ATTR_SERVICE:    return 0;
+    case W3C_ATTR_SHARED:     return 0;
+    default:
+        *errc = EINVAL;
+        *errs = W3C_MALFORMED;
+        return -EINVAL;
+    }
+}
+
+
+static int check_utterance_attr(void *obj, w3c_attrdef_t *def, void *val,
+                                int *errc, const char **errs)
+{
+    w3c_utterance_t *utt = (w3c_utterance_t *)obj;
+
+    MRP_UNUSED(val);
+
+    switch (def->mask) {
+    case W3C_ATTR_TEXT:
+    case W3C_ATTR_LANG:
+    case W3C_ATTR_VOICE:
+        if (utt->vid == SRS_VOICE_INVALID)
+            return 0;
+
+        *errc = EBUSY;
+        *errs = W3C_BUSY;
+        return -EBUSY;
+
+    case W3C_ATTR_VOLUME:
+    case W3C_ATTR_RATE:
+    case W3C_ATTR_PITCH:
+    case W3C_ATTR_TIMEOUT:
+        if (utt->vid == SRS_VOICE_INVALID)
+            return 0;
+
+        *errc = EBUSY;
+        *errs = W3C_BUSY;
+        return -EBUSY;
+
+    case W3C_ATTR_EVENTS:
+        return 0;
+
+    default:
+        *errc = EINVAL;
+        *errs = W3C_MALFORMED;
+        return -EINVAL;
+    }
+}
+
+
+static inline w3c_attrdef_t *recognizer_attributes(void)
+{
+#define DEFAULT NULL
+#define BOOLEAN(_n, _an, _m, _p) \
+    { _n, MRP_JSON_BOOLEAN, MRP_OFFSET(w3c_rec_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_boolean_attr, check_recognizer_attr }
+#define STRING(_n, _an, _m, _p)  \
+    { _n, MRP_JSON_STRING , MRP_OFFSET(w3c_rec_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_string_attr , check_recognizer_attr }
+#define INTEGER(_n, _an, _m, _p) \
+    { _n, MRP_JSON_INTEGER, MRP_OFFSET(w3c_rec_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_integer_attr, check_recognizer_attr }
+#define ARRAY(_n, _an, _m, _p)   \
+    { _n, MRP_JSON_ARRAY  , MRP_OFFSET(w3c_rec_attr_t, _an), W3C_ATTR_##_m, \
+            _p, check_recognizer_attr }
+
+    static w3c_attrdef_t def[] = {
+        STRING ("name"           , name      , NAME      , DEFAULT       ),
+        STRING ("appclass"       , appclass  , APPCLASS  , DEFAULT       ),
+        ARRAY  ("events"         , events    , EVENTS    , parse_events  ),
+        ARRAY  ("grammars"       , grammars  , GRAMMARS  , parse_grammars),
+        STRING ("lang"           , lang      , LANG      , DEFAULT       ),
+        BOOLEAN("continuous"     , continuous, CONTINUOUS, DEFAULT       ),
+        BOOLEAN("interimResults" , interim   , INTERIM   , DEFAULT       ),
+        INTEGER("maxAlternatives", max_alt   , MAXALT    , DEFAULT       ),
+        STRING ("serviceURI"     , service   , SERVICE   , DEFAULT       ),
+        BOOLEAN("shared"         , shared    , SHARED    , DEFAULT       ),
+        { NULL, 0, 0, 0, NULL, NULL }
+    };
+
+#undef DEFAULT
+#undef BOOLEAN
+#undef STRING
+#undef INTEGER
+#undef ARRAY
+
+    return def;
+}
+
+
+static inline w3c_attrdef_t *utterance_attributes(void)
+{
+#define DEFAULT NULL
+#define BOOLEAN(_n, _an, _m, _p) \
+    { _n, MRP_JSON_BOOLEAN, MRP_OFFSET(w3c_utt_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_boolean_attr, check_utterance_attr }
+#define STRING(_n, _an, _m, _p)  \
+    { _n, MRP_JSON_STRING , MRP_OFFSET(w3c_utt_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_string_attr , check_utterance_attr }
+#define INTEGER(_n, _an, _m, _p) \
+    { _n, MRP_JSON_INTEGER, MRP_OFFSET(w3c_utt_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_integer_attr, check_utterance_attr }
+#define DOUBLE(_n, _an, _m, _p) \
+    { _n, MRP_JSON_DOUBLE, MRP_OFFSET(w3c_utt_attr_t, _an), W3C_ATTR_##_m, \
+            _p ? _p : parse_double_attr, check_utterance_attr }
+#define ARRAY(_n, _an, _m, _p)   \
+    { _n, MRP_JSON_ARRAY  , MRP_OFFSET(w3c_utt_attr_t, _an), W3C_ATTR_##_m, \
+            _p, check_utterance_attr }
+
+    static w3c_attrdef_t def[] = {
+        STRING ("text"           , text      , TEXT      , DEFAULT       ),
+        STRING ("lang"           , lang      , LANG      , DEFAULT       ),
+        STRING ("voiceURI"       , voice     , VOICE     , DEFAULT       ),
+        DOUBLE ("volume"         , volume    , VOLUME    , DEFAULT       ),
+        DOUBLE ("rate"           , rate      , RATE      , DEFAULT       ),
+        DOUBLE ("pitch"          , pitch     , PITCH     , DEFAULT       ),
+        INTEGER("timeout"        , timeout   , TIMEOUT   , DEFAULT       ),
+        ARRAY  ("events"         , events    , EVENTS    , parse_events  ),
+        { NULL, 0, 0, 0, NULL, NULL }
+    };
+
+#undef DEFAULT
+#undef BOOLEAN
+#undef STRING
+#undef INTEGER
+#undef DOUBLE
+#undef ARRAY
+
+    return def;
+}
+
+
+
+static char *strip_whitespace(char *buf)
+{
+    char *e;
+
+    if (buf == NULL)
+        return NULL;
+
+    while (*buf == ' ' || *buf == '\t')
+        buf++;
+
+    e = buf + strlen(buf) - 1;
+    while (e >= buf && (*e == '\n' || *e == ' ' || *e == '\t'))
+        *e-- = '\0';
+
+    return buf;
+}
+
+
+static FILE *open_grammar(w3c_server_t *s, const char *URI)
+{
+    char  path[PATH_MAX];
+    FILE *fp;
+
+    if (strncmp(URI, W3C_URI, sizeof(W3C_URI) - 1)) {
+        mrp_log_error("W3C: invalid grammar '%s'.", URI);
+        errno = EINVAL;
+        return NULL;
+    }
+
+    URI += sizeof(W3C_URI) - 1;
+    snprintf(path, sizeof(path), "%s/%s", s->grammar_dir, URI);
+
+    fp = fopen(path, "r");
+
+    if (fp != NULL)
+        mrp_debug("W3C: grammar '%s' -> '%s'", URI, path);
+
+    return fp;
+}
+
+
+int read_grammars(w3c_recognizer_t *rec, const char **errs)
+{
+    char **cmds, *cmd, buf[4096];
+    int    n, i;
+    FILE  *fp;
+
+    cmds = NULL;
+    n    = 0;
+
+    for (i = 0; i < rec->attr.ngrammar; i++) {
+        fp = open_grammar(rec->c->s, rec->attr.grammars[i]);
+
+        if (fp == NULL) {
+            *errs = W3C_BADGRAMMAR;
+            goto fail;
+        }
+
+        while (fgets(buf, sizeof(buf), fp) != NULL) {
+            cmd = strip_whitespace(buf);
+
+            if (!*cmd)
+                continue;
+
+            if (mrp_reallocz(cmds, n, n + 1) == NULL ||
+                (cmds[n] = mrp_strdup(cmd))  == NULL) {
+                *errs = W3C_NOMEM;
+                goto fail;
+            }
+
+            mrp_debug("command #%d: '%s'", n, cmd);
+
+            n++;
+        }
+
+        fclose(fp);
+    }
+
+    for (i = 0; i < rec->attr.ncommand; i++)
+        mrp_free(rec->attr.commands[i]);
+    mrp_free(rec->attr.commands);
+
+    rec->attr.commands = cmds;
+    rec->attr.ncommand = n;
+
+    return 0;
+
+ fail:
+    for (i = 0; i < n; i++)
+        mrp_free(cmds[i]);
+    mrp_free(cmds);
+
+    return -1;
+}
+
+
+static int check_id(w3c_client_t *c, mrp_json_t *req, int *idp)
+{
+    if (mrp_json_get_integer(req, "id", idp))
+        return 0;
+    else {
+        malformed_request(c->t, req, "missing object ID");
+        return -1;
+    }
+}
+
+
+static w3c_recognizer_t *check_recognizer(w3c_client_t *c, mrp_json_t *req,
+                                          int id)
+{
+    w3c_recognizer_t *rec;
+
+    if (id < 0)
+        if (check_id(c, req, &id) < 0)
+            return NULL;
+
+    rec = lookup_recognizer(c, id);
+
+    if (rec != NULL)
+        return rec;
+
+    reply_error(c->t, -1, ENOENT, W3C_NOTFOUND, req,
+                "recognizer object not found");
+    return NULL;
+}
+
+
+static w3c_utterance_t *check_utterance(w3c_client_t *c, mrp_json_t *req, int id)
+{
+    w3c_utterance_t *utt;
+
+    if (id < 0)
+        if (check_id(c, req, &id) < 0)
+            return NULL;
+
+    utt = lookup_utterance(c, id, -1);
+
+    if (utt != NULL)
+        return utt;
+
+    reply_error(c->t, -1, ENOENT, W3C_NOTFOUND, req,
+                "utterance object not found");
+    return NULL;
+}
+
+
+
+
+static int w3c_create_recognizer(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_attrdef_t    *def = recognizer_attributes();
+    w3c_recognizer_t *rec;
+    mrp_json_t       *set;
+    size_t            base;
+    int               mask;
+    int               errc;
+    const char       *errs;
+
+    if ((rec = create_recognizer(c)) == NULL) {
+        return reply_error(c->t, reqno, ENOMEM, W3C_FAILED, req,
+                           "failed to create new recognizer instance");
+    }
+
+    set = mrp_json_get(req, "set");
+
+    if (set != NULL) {
+        base = MRP_OFFSET(w3c_recognizer_t, attr);
+        errc = 0;
+        errs = NULL;
+        mask = w3c_set_attributes(set, rec, base, def, &errc, &errs);
+    }
+    else
+        mask = 0;
+
+    if (mask < 0) {
+        reply_error(c->t, reqno, errc, errs, req,
+                    "failed to set recognizer attributes");
+        destroy_recognizer(rec);
+        return -1;
+    }
+
+    if (mask & W3C_ATTR_GRAMMARS) {
+        if (read_grammars(rec, &errs) < 0) {
+            errc = errno;
+            reply_error(c->t, reqno, errc, errs, req,
+                        "failed to locate/parse given grammars");
+            destroy_recognizer(rec);
+            return -1;
+        }
+    }
+
+    return reply_status(c->t, reqno, 0, "id", MRP_JSON_INTEGER, rec->id);
+}
+
+
+static int w3c_delete_object(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    int               id;
+    w3c_recognizer_t *rec;
+    w3c_utterance_t  *utt;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    switch (W3C_OBJECT_TYPE(id)) {
+    case W3C_TYPE_RECOGNIZER:
+        rec = check_recognizer(c, req, id);
+
+        if (rec != NULL)
+            destroy_recognizer(rec);
+
+        return reply_status(c->t, reqno, 0);
+
+    case W3C_TYPE_UTTERANCE:
+        utt = check_utterance(c, req, id);
+
+        if (utt != NULL)
+            destroy_utterance(utt);
+
+        return reply_status(c->t, reqno, 0);
+
+    default:
+        break;
+    }
+
+    return -1;
+}
+
+
+static int w3c_set_attribute(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    int               id;
+    w3c_recognizer_t *rec;
+    w3c_utterance_t  *utt;
+    mrp_json_t       *set;
+    w3c_attrdef_t    *defs;
+    size_t            base;
+    int               mask;
+    int               errc;
+    const char       *errs;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if ((set = mrp_json_get(req, "set")) == NULL)
+        return malformed_request(c->t, req, "missing attributes");
+
+    if (mrp_json_get_type(set) != MRP_JSON_OBJECT)
+        return malformed_request(c->t, req, "invalid attributes");
+
+    switch (W3C_OBJECT_TYPE(id)) {
+    case W3C_TYPE_RECOGNIZER:
+        if ((rec = check_recognizer(c, req, id)) == NULL)
+            return 0;
+
+        defs = recognizer_attributes();
+        base = MRP_OFFSET(w3c_recognizer_t, attr);
+        mask = w3c_set_attributes(set, rec, base, defs, &errc, &errs);
+
+        if (mask < 0)
+            return reply_error(c->t, reqno, errc, errs, req,
+                               "failed to set attribute #%d", -(mask - 1));
+
+        if (mask & W3C_ATTR_GRAMMARS) {
+            if (read_grammars(rec, &errs) < 0) {
+                errc = errno;
+                return reply_error(c->t, reqno, errc, errs, req,
+                                   "failed to locate/parse some given grammar");
+            }
+        }
+
+        return reply_status(c->t, reqno, 0);
+
+    case W3C_TYPE_UTTERANCE:
+        if ((utt = check_utterance(c, req, id)) == NULL)
+            return 0;
+
+        defs = utterance_attributes();
+        base = MRP_OFFSET(w3c_utterance_t, attr);
+        mask = w3c_set_attributes(set, utt, base, defs, &errc, &errs);
+
+        if (mask < 0)
+            return reply_error(c->t, reqno, errc, errs, req,
+                               "failed to set attribute #%d", -(mask - 1));
+        else
+            return reply_status(c->t, reqno, 0);
+
+    default:
+        break;
+    }
+
+    return -1;
+}
+
+
+static int w3c_get_timestamp(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    mrp_log_info("%s(#%d: %s)", __FUNCTION__, reqno,
+                 mrp_json_object_to_string(req));
+
+    return reply_status(c->t, reqno, 0,
+                        "timestamp", MRP_JSON_OBJECT, add_json_timestamp(NULL));
+}
+
+
+static int w3c_start_recognizer(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_recognizer_t *rec;
+    int               id, errc;
+    const char       *errs;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if ((rec = check_recognizer(c, req, id)) == NULL)
+        return -1;
+
+    if (rec->srsc == NULL) {
+        if (create_recognizer_client(rec, &errc, &errs) < 0) {
+            reply_error(c->t, reqno, errc, errs, req,
+                        "failed to create backend client");
+            return -1;
+        }
+    }
+
+    if (start_recognizer_client(rec, &errc, &errs) < 0) {
+        reply_error(c->t, reqno, errc, errs, req,
+                    "failed to start backend client");
+        return -1;
+    }
+
+    rec->request = W3C_REQUEST_START;
+
+    return reply_status(c->t, reqno, 0);
+}
+
+
+static int w3c_stop_recognizer(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_recognizer_t *rec;
+    int               id, errc;
+    const char       *errs;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if ((rec = check_recognizer(c, req, id)) == NULL)
+        return -1;
+
+    if (stop_recognizer_client(rec, &errc, &errs) < 0) {
+        reply_error(c->t, reqno, errc, errs, req,
+                    "failed to stop backend client");
+        return -1;
+    }
+
+    rec->request = W3C_REQUEST_STOP;
+
+    return reply_status(c->t, reqno, 0);
+}
+
+
+static int w3c_abort_recognizer(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_recognizer_t *rec;
+    int               id, errc;
+    const char       *errs;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if ((rec = check_recognizer(c, req, id)) == NULL)
+        return -1;
+
+    if (stop_recognizer_client(rec, &errc, &errs) < 0) {
+        reply_error(c->t, reqno, errc, errs, req,
+                    "failed to stop backend client");
+        return -1;
+    }
+
+    rec->request = W3C_REQUEST_ABORT;
+
+    return reply_status(c->t, reqno, 0);
+}
+
+
+static int w3c_create_utterance(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_synthesizer_t *syn = c->syn;
+    w3c_attrdef_t     *def = utterance_attributes();
+    w3c_utterance_t   *utt;
+    mrp_json_t        *set;
+    size_t             base;
+    int                mask;
+    int                errc;
+    const char        *errs;
+
+    if ((utt = create_utterance(syn)) == NULL)
+        return reply_error(c->t, reqno, ENOMEM, W3C_FAILED, req,
+                           "failed to create new utterance instance");
+
+    if ((set = mrp_json_get(req, "set")) != NULL) {
+        base = MRP_OFFSET(w3c_utterance_t, attr);
+        errc = 0;
+        errs = NULL;
+        mask = w3c_set_attributes(set, utt, base, def, &errc, &errs);
+    }
+    else
+        mask = 0;
+
+    if (mask < 0) {
+        reply_error(c->t, reqno, errc, errs, req,
+                    "failed to set utterance attribute (#%d)", -mask);
+        destroy_utterance(utt);
+        return -1;
+    }
+
+    return reply_status(c->t, reqno, 0, "id", MRP_JSON_INTEGER, utt->id);
+}
+
+
+static int w3c_speak_utterance(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_utterance_t   *utt;
+    w3c_synthesizer_t *syn;
+    int                id, uid;
+    int                errc;
+    const char        *errs;
+    bool               had_pending;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if (id != 0)
+        return reply_error(c->t, reqno, EINVAL, W3C_MALFORMED, req,
+                           "speak must use implicit ID 0");
+
+    if (!mrp_json_get_integer(req, "utterance", &uid))
+        return malformed_request(c->t, req, "missing utterace ID");
+
+    if ((utt = check_utterance(c, req, uid)) == NULL)
+        return -1;
+
+    if (utt->vid != SRS_VOICE_INVALID) {
+        reply_error(c->t, reqno, EBUSY, W3C_BUSY, req,
+                    "utterance is already being played/queued");
+        return -1;
+    }
+
+    if (utt->attr.text == NULL) {
+        reply_error(c->t, reqno, EINVAL, W3C_FAILED, req,
+                    "utterance text not set");
+        return -1;
+    }
+
+    if (utt->attr.lang == NULL && utt->attr.voice == NULL) {
+        reply_error(c->t, reqno, EINVAL, W3C_FAILED, req,
+                    "neither voice nor language is set");
+        return -1;
+    }
+
+    syn = utt->syn;
+
+    if (syn->srsc == NULL) {
+        if (create_synthesizer_client(syn, &errc, &errs) < 0) {
+            reply_error(c->t, reqno, errc, errs, req,
+                        "failed to create backend client");
+            return -1;
+        }
+    }
+
+    had_pending = !mrp_list_empty(&syn->pending);
+
+    if (activate_utterance(utt) < 0) {
+        reply_error(c->t, reqno, EINVAL, W3C_FAILED, req,
+                    "synthesizer backend failed");
+        return -1;
+    }
+
+    reply_status(c->t, reqno, 0);
+    update_pending(syn, had_pending);
+
+    return 0;
+}
+
+
+static int w3c_cancel_utterance(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_synthesizer_t *syn = c->syn;
+    w3c_utterance_t   *utt;
+    mrp_list_hook_t   *p, *n;
+    int                id, uid;
+    bool               cancelled;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    cancelled = false;
+
+    if (id != 0)
+        return reply_error(c->t, reqno, EINVAL, W3C_MALFORMED, req,
+                           "cancel must use implicit ID 0");
+
+    if (mrp_json_get_integer(req, "utterance", &uid)) {
+        if ((utt = check_utterance(c, req, uid)) == NULL)
+            return -1;
+
+        cancel_utterance(utt);
+    }
+    else {
+        mrp_list_foreach(&syn->pending, p, n) {
+            utt = mrp_list_entry(p, typeof(*utt), pending);
+
+            cancel_utterance(utt);
+            cancelled = true;
+        }
+    }
+
+    reply_status(c->t, reqno, 0);
+    update_pending(syn, cancelled);
+
+    return 0;
+}
+
+
+static int w3c_pause_utterance(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_synthesizer_t *syn = c->syn;
+    w3c_utterance_t   *utt;
+    mrp_list_hook_t   *p, *n;
+    int                id;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if (id != 0)
+        return reply_error(c->t, reqno, EINVAL, W3C_MALFORMED, req,
+                           "pause must use implicit ID 0");
+
+    mrp_list_foreach(&syn->pending, p, n) {
+        utt = mrp_list_entry(p, typeof(*utt), pending);
+
+        pause_utterance(utt);
+    }
+
+    reply_status(c->t, reqno, 0);
+
+    syn->paused = true;
+    update_paused(syn, true);
+
+    return 0;
+}
+
+
+static int w3c_resume_utterance(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    w3c_synthesizer_t *syn = c->syn;
+    w3c_utterance_t   *utt;
+    mrp_list_hook_t   *p, *n;
+    int                id;
+    bool               first;
+
+    if (check_id(c, req, &id) < 0)
+        return -1;
+
+    if (id != 0)
+        return reply_error(c->t, reqno, EINVAL, W3C_MALFORMED, req,
+                           "pause must use implicit ID 0");
+
+    syn->paused = false;
+
+    first = true;
+    mrp_list_foreach(&syn->pending, p, n) {
+        utt = mrp_list_entry(p, typeof(*utt), pending);
+
+        if (first)
+            resume_utterance(utt);
+        else
+            activate_utterance(utt);
+
+        first = false;
+    }
+
+    reply_status(c->t, reqno, 0);
+    update_paused(syn, false);
+
+    return 0;
+}
+
+
+static int w3c_get_voices(w3c_client_t *c, int reqno, mrp_json_t *req)
+{
+    srs_context_t     *srs    = c->s->self->srs;
+    mrp_json_t        *voices = NULL;
+    const char        *lang   = NULL;
+    srs_voice_actor_t *actors, *a;
+    int                nactor, i;
+    mrp_json_t        *actor;
+
+    mrp_json_get_string(req, "lang", &lang);
+
+    nactor = srs_query_voices(srs, lang, &actors);
+
+    if (nactor < 0) {
+        reply_error(c->t, reqno, EINVAL, W3C_FAILED, req,
+                    "failed to query backend voices");
+        return -1;
+    }
+
+    voices = mrp_json_create(MRP_JSON_ARRAY);
+
+    if (voices == NULL)
+        goto oom;
+
+    for (i = 0, a = actors; i < nactor; i++, a++) {
+        actor = mrp_json_create(MRP_JSON_OBJECT);
+
+        if (actor == NULL)
+            goto oom;
+
+        if (!mrp_json_array_append(voices, actor)) {
+            mrp_json_unref(actor);
+            goto oom;
+        }
+
+        mrp_json_add_string (actor, "voiceURI"    , a->name);
+        mrp_json_add_string (actor, "lang"        , a->lang);
+        mrp_json_add_string (actor, "name"        , a->name);
+        mrp_json_add_boolean(actor, "localService", true);
+        mrp_json_add_boolean(actor, "default"     , false);
+    }
+
+    return reply_status(c->t, reqno, 0,
+                        "voices", MRP_JSON_ARRAY, voices);
+
+ oom: /* Rrright... surely this is going to succeed then. */
+    reply_error(c->t, reqno, ENOMEM, W3C_NOMEM, req, "out of memory");
+    srs_free_queried_voices(actors);
+    mrp_json_unref(voices);
+    return -1;
+}
+
+
+static request_handler_t get_handler(w3c_client_t *c, mrp_json_t *req,
+                                     int *reqnop)
+{
+    static struct {
+        const char        *type;
+        const char        *key;
+        const char        *value;
+        request_handler_t  handler;
+    } *h, handlers[] = {
+        { "create"   , "object" , "recognizer", w3c_create_recognizer },
+        { "create"   , "object" , "utterance" , w3c_create_utterance  },
+        { "delete"   , NULL     , NULL        , w3c_delete_object     },
+        { "set"      , NULL     , NULL        , w3c_set_attribute     },
+        { "timestamp", NULL     , NULL        , w3c_get_timestamp     },
+        { "invoke"   , "method" , "start"     , w3c_start_recognizer  },
+        { "invoke"   , "method" , "stop"      , w3c_stop_recognizer   },
+        { "invoke"   , "method" , "abort"     , w3c_abort_recognizer  },
+        { "invoke"   , "method" , "speak"     , w3c_speak_utterance   },
+        { "invoke"   , "method" , "cancel"    , w3c_cancel_utterance  },
+        { "invoke"   , "method" , "pause"     , w3c_pause_utterance   },
+        { "invoke"   , "method" , "resume"    , w3c_resume_utterance  },
+        { "invoke"   , "method" , "get-voices", w3c_get_voices        },
+        { NULL, NULL, NULL, NULL }
+    };
+
+    const char *type, *key, *value;
+    int         reqno;
+
+    if (!mrp_json_get_string (req, "type" , &type)) {
+        malformed_request(c->t, req, "missing request type");
+        goto fail;
+    }
+
+    if (!mrp_json_get_integer(req, "reqno", &reqno)) {
+        malformed_request(c->t, req, "missing request number");
+        goto fail;
+    }
+
+    if (reqnop != NULL)
+        *reqnop = reqno;
+
+    key   = NULL;
+    value = NULL;
+
+    for (h = handlers; h->type != NULL; h++) {
+        if (strcmp(type, h->type) != 0)
+            continue;
+
+        if (h->key == NULL)
+            break;
+
+        if (key == NULL || strcmp(key, h->key) != 0) {
+            if (!mrp_json_get_string(req, (key = h->key), &value)) {
+                malformed_request(c->t, req, "missing request %s", key);
+                goto fail;
+            }
+        }
+
+        if (!strcmp(value, h->value))
+            break;
+    }
+
+    if (h->type != NULL)
+        return h->handler;
+    else
+        malformed_request(c->t, req, "unknown request type");
+
+ fail:
+    w3c_client_destroy(c);
+
+    return NULL;
+}
+
+
+static void recv_evt(mrp_transport_t *t, mrp_json_t *req, void *user_data)
+{
+    w3c_client_t      *c = (w3c_client_t *)user_data;
+    request_handler_t  h;
+    int                reqno;
+
+    MRP_UNUSED(t);
+
+    dump_request(req);
+
+    if ((h = get_handler(c, req, &reqno)) != NULL)
+        h(c, reqno, req);
+    else
+        mrp_log_error("Failed to find request handler for request %s.",
+                      mrp_json_object_to_string(req));
+}
+
+
+static int transport_create(w3c_server_t *s)
+{
+    static mrp_transport_evt_t evt = {
+        { .recvjson     = recv_evt },
+        { .recvjsonfrom = NULL     },
+        .connection     = connection_evt,
+        .closed         = closed_evt,
+    };
+
+    srs_context_t  *srs = s->self->srs;
+    mrp_sockaddr_t  addr;
+    socklen_t       alen;
+    const char     *type;
+    int             flags, state;
+
+    flags = MRP_TRANSPORT_NONBLOCK | MRP_TRANSPORT_MODE_JSON | \
+        MRP_TRANSPORT_REUSEADDR;
+
+    alen = mrp_transport_resolve(NULL, s->address, &addr, sizeof(addr), &type);
+
+    if (alen <= 0)
+        goto resolve_failed;
+
+    if (s->sock >= 0) {
+        state = MRP_TRANSPORT_LISTENED;
+        s->lt = mrp_transport_create_from(srs->ml, type, &s->sock, &evt,
+                                          s, flags, state);
+
+        if (s->lt == NULL)
+            goto create_failed;
+
+        mrp_log_info("Using socket %d for W3C speech transport.", s->sock);
+    }
+    else {
+        s->lt = mrp_transport_create(srs->ml, type, &evt, s, flags);
+
+        if (s->lt == NULL)
+            goto create_failed;
+
+        if (!mrp_transport_bind(s->lt, &addr, alen))
+            goto bind_failed;
+
+        if (!mrp_transport_listen(s->lt, 0))
+            goto bind_failed;
+
+        mrp_log_info("Listening on W3C speech transport '%s'.", s->address);
+    }
+
+    return 0;
+
+ resolve_failed:
+    mrp_log_error("Can't resolve W3C speech transport '%s'.", s->address);
+    goto cleanup_fail;
+
+ create_failed:
+    mrp_log_error("Can't create W3C speech transport.");
+    goto cleanup_fail;
+
+ bind_failed:
+    mrp_log_error("Can't bind/listen W3C speech transport '%s'.", s->address);
+    /* fallthru */
+
+ cleanup_fail:
+    mrp_transport_destroy(s->lt);
+    s->lt = NULL;
+
+    return -1;
+}
+
+
+static void transport_destroy(w3c_server_t *s)
+{
+    mrp_transport_destroy(s->lt);
+    s->lt = NULL;
+}
+
+
+static int w3c_create(srs_plugin_t *plugin)
+{
+    w3c_server_t *s;
+
+    mrp_debug("creating W3C Speech API plugin");
+
+    s = mrp_allocz(sizeof(*s));
+
+    if (s == NULL)
+        return FALSE;
+
+    mrp_list_init(&s->clients);
+    s->self = plugin;
+
+    plugin->plugin_data = s;
+
+    return TRUE;
+}
+
+
+static int w3c_config(srs_plugin_t *plugin, srs_cfg_t *cfg)
+{
+    w3c_server_t *s = (w3c_server_t *)plugin->plugin_data;
+
+    mrp_debug("configuring W3C speech plugin");
+
+    s->address = srs_config_get_string(cfg, CONFIG_ADDRESS, DEFAULT_ADDRESS);
+    s->sock    = srs_config_get_int32(cfg, CONFIG_SOCKET, DEFAULT_SOCKET);
+
+    if (s->sock < 0)
+        mrp_log_info("Using W3C speech transport '%s'.", s->address);
+    else
+        mrp_log_info("Using W3C speech socket %d.", s->sock);
+
+    s->grammar_dir = srs_config_get_string(cfg, CONFIG_GRAMMARDIR,
+                                           DEFAULT_GRAMMARDIR);
+
+    mrp_log_info("Looking for W3C grammar files in '%s'.", s->grammar_dir);
+
+    return TRUE;
+}
+
+
+static int w3c_start(srs_plugin_t *plugin)
+{
+    w3c_server_t *s = (w3c_server_t *)plugin->plugin_data;
+
+    if (transport_create(s) < 0)
+        return FALSE;
+    else
+        return TRUE;
+}
+
+
+static void w3c_stop(srs_plugin_t *plugin)
+{
+    MRP_UNUSED(plugin);
+}
+
+
+static void w3c_destroy(srs_plugin_t *plugin)
+{
+    w3c_server_t *s = (w3c_server_t *)plugin->plugin_data;
+
+    transport_destroy(s);
+    mrp_free(s);
+}
+
+
+SRS_DECLARE_PLUGIN(W3C_PLUGIN, W3C_DESCR, W3C_AUTHORS, W3C_VERSION,
+                   w3c_create, w3c_config, w3c_start, w3c_stop, w3c_destroy);
diff --git a/src/plugins/client-api/w3c-speech/w3c-server.h b/src/plugins/client-api/w3c-speech/w3c-server.h
new file mode 100644 (file)
index 0000000..804c2a2
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2014, Intel Corporation
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *   * Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *   * Neither the name of Intel Corporation nor the names of its contributors
+ *     may be used to endorse or promote products derived from this software
+ *     without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __SRS_W3C_SERVER_H__
+#define __SRS_W3C_SERVER_H__
+
+/** Transport address configuration key. */
+#define CONFIG_ADDRESS  "w3c-speech.address"
+
+/** Default transport address. */
+#define DEFAULT_ADDRESS "unxs:@winthorpe.w3c-speech"
+
+/** Transport socket configuration key, set only by socket-based activation. */
+#define CONFIG_SOCKET   "w3c-speech.socket"
+
+/** Default transport socket. */
+#define DEFAULT_SOCKET  -1
+
+/** Grammar directory configuration key. */
+#define CONFIG_GRAMMARDIR "w3c-speech.grammars"
+
+/** Default grammar directory. */
+#define DEFAULT_GRAMMARDIR "/etc/speech-recongition/w3c-grammars"
+
+/** Winthorpe W3C grammar URI prefix. */
+#define W3C_URI "winthorpe://"
+
+#endif /* __SRS_W3C_SERVER_H__ */