Skip to content

Working with Node Clients

In this tutorial, we will explore how to use soundworks to create clients that run within a Node.js process, opening new possibilities for creating applications outside Web Browsers.

Indeed, this functionality enables to work with screen less embedded hardware, such as the Raspberry Pi, which allows us to access some functionalities (e.g. control a motor or a LED) which are hard or impossible to achieve in the sandboxed environment that are Web browsers.

To illustrate this possibility and discover some of the tools you have at hand to help you, we will build a very simple application where a browser controller client can trigger a sound synthesized by another client running in a Node.js process.

The final source code of this tutorial can be found here

Scaffolding Application

Let's start as usual with scaffolding our application:

sh
cd path/to/tutorials
npx @soundworks/create@latest node-clients

When the wizard asks you for plugins and libraries, just select the @ircam/sc-components and node-web-audio-api libraries:

# Install plugins
 Select the plugins you would like to install/uninstall  
+ nothing to do, aborting...

# Install libraries
? Select the libraries you would like to install/uninstall  - Space to select. Return to submit 
   @ircam/sc-components
◯   @ircam/sc-scheduling
◯   @ircam/sc-utils
   node-web-audio-api

INFO

The node-web-audio-api package provides an implementation of the Web Audio API specification written in the Rust language to be used in Node.js programs.

For those who might be interested in using the library directly in a Rust project, see web-audio-api-rs

Then when the wizard asks you to create the default client, let's just create a browser client with the "controller" template (we will create our Node.js client later in the tutorial):

# Create client
 Name of your new client (lowercase, no-space):  controller
 Which runtime for your client?  browser
 Which template would you like to use?  controller

- Creating client "controller" in file "src/clients/controller.js"
- name: controller
- runtime: browser
- template: controller
- default: true

 Confirm?  no / yes

Then let's jump into the directory and start our application:

sh
cd node-clients
npm run dev

Implementing the controller

Before implementing our Node.js client, let's use the features we have learned so far so that we can trigger a sound on any of our future Node.js clients through the controller interface.

Let's thus start with defining a shared state class, we will call thing, that will be created by each of our Node.js clients when they connect to the application.

js
// src/server.js
const server = new Server(config);
configureHttpRouter(server);

server.stateManager.defineClass('thing', { 
  id: { 
    type: 'integer', 
    default: null, 
    nullable: true, 
  }, 
  triggerSound: { 
    type: 'boolean', 
    event: true, 
  }, 
}); 

And that's everything we have to do on the server side!

Let's then create our control interface. First, we need to grab a collection of our "thing" shared states:

js
// src/clients/controller.js
await client.start();

const thingCollection = await client.stateManager.getCollection('thing'); 
// update GUI when any changes in the collection occurs 
thingCollection.onChange(() => renderApp()); 

Then. let's modify the renderApp function to show a simple graphical control interface for each thing state in the collection:

js
function renderApp() {
  render(html`
    <div class="controller-layout">
      <header>
        <h1>${client.config.app.name} | ${client.role}</h1>
        <sw-audit .client="${client}"></sw-audit>
      </header>
      <section>
        <p>Hello ${client.config.app.name}!</p> 
        ${thingCollection.map(thing => { 
          return html` 
            <div> 
              <sc-text>${thing.get('id')}</sc-text> 
              <sc-button 
                @input=${e => thing.set({ triggerSound: true })} 
              >trigger sound</sc-button> 
            </div> 
          `; 
        })} 
      </section>
    </div>
  `, $container);
}

Of course if you launch the controller (http://127.0.0.1:8000/) right now, the screen will be empty as we don't have any thing state in our colllection, but everything is now ready to create and control our Node.js clients.

Creating and running a Node.js client

Let's shutdown our server (Ctrl + C) for a while and create our Node.js client using the soundworks wizard with the following command line:

sh
npx soundworks --create-client

Enter "thing" as name and "node" as target:

# Create client
 Name of your new client (lowercase, no-space):  thing
 Which runtime for your client?  node

- Creating client "thing" in file "src/clients/thing.js"
- name: thing
- runtime: node

? Confirm?  no / yes

Once done, you can just restart the development server:

sh
npm run dev

To launch our node client, let's open a second "Terminal", and run the following commands:

sh
# go to the node-clients tutorial directory
cd path/to/tutorials/node-clients
# run you newly created node client in watch mode
npm run watch thing

And tada! Your node client should now be connected to the server:

> watching process	 .build/clients/thing.js
[launcher][client thing] connecting to http://127.0.0.1:8000
[launcher][client thing(0)] connected
Hello node-clients!

Implementing the Node.js client

Now that everything is setup and ready, let's write the code needed so our newly created client plays some sound when the button is clicked on the controller interface.

First, let's thus create our "thing" shared state and initialize its id field with the id of the soundworks client. Open the src/clients/thing.js file and add the following snippet:

js
await client.start();
// create the thing state and initialize it's id field 
const thing = await client.stateManager.create('thing', { 
  id: client.id, 
}); 

INFO

If you go in the "Terminal" where you launched the Node.js client, you should see that the client automatically restarted each time you save a file, just as with the server. This is the behavior and goal of the npm run watch [process_name] command.

If reload your controller now, you should also see the interface updated with your connected client:

controller

Let's finally write our Web Audio code so that a sound is triggered from the Node.js process when the "trigger sound" button is pressed on the controller interface:

js
// src/clients/thing.js
import { Client } from '@soundworks/core/client.js';
import launcher from '@soundworks/helpers/launcher.js';
// import some classes from the node-web-audio-api package 
import { AudioContext, GainNode, OscillatorNode } from 'node-web-audio-api'; 

// ...

await client.start();
// create an audio context (note that it is resumed by default)
const audioContext = new AudioContext(); 
// create the thing state and initialize it's id field
const thing = await client.stateManager.create('thing', {
  id: client.id,
});
// react to updates triggered from controller
thing.onUpdate(updates => { 
  if ('triggerSound' in updates) { 
    const now = audioContext.currentTime; 
 
    const env = new GainNode(audioContext, { gain: 0 }); 
    env.connect(audioContext.destination); 
    env.gain.setValueAtTime(0, now); 
    env.gain.linearRampToValueAtTime(1, now + 0.01); 
    env.gain.exponentialRampToValueAtTime(0.0001, now + 1); 
    // randomly pick one of the harmonics of a sound at 50Hz 
    const frequency = Math.floor(Math.random() * 10) * 50 + 100; 
    const osc = new OscillatorNode(audioContext, { frequency }); 
    osc.connect(env); 
    osc.start(now); 
    osc.stop(now + 1); 
  } 
}); 

And that's all! You have now a simple soundworks client that runs into Node.js process and can synthesize some sound.

TIP

As you can see, the code you wrote to make this Node.js client work is the same as the one you would have written in a browser client.

Abstracting the platform in such a way is an important goal of soundworks and of the related libraries, such as the node-web-audio-api package.

Notes

Locally emulate multiple clients

In previous tutorials, we often used the ?emulate=6 query parameter in our URL to emulate several clients in only one browser window, which is handy when developing distributed applications.

A similar behavior can be achieved with Node.js clients to run many of them in the same terminal. For example, to launch four clients in parallel, you can just write:

sh
EMULATE=4 npm run watch thing

Running Node.js clients on a network

Another important point to consider is that Node.js clients do need some configuration to be able to connect to the server. Indeed, when we launch a client in a browser, we just tell the browser where to reach the server when we write the URL in the address bar of the Web browser.

However, as you may have notices, Terminals and Node.js processes don't have any address bar... Hence if your node client doesn't run into your computer but in a remote device, it will need a bit of configuration to know the IP address of the server.

The soundworks wizard has an dedicated command to help you to create additional environment config files:

sh
npx soundworks --create-env

The command will prompt you with a series of questions to configure a environment, and in particular the question about the "Address of the server":

# Create environment configuration file:
 Name of the config  remote
 Type:  development
 Port (default is 80 for http and 443 for https):  80
 Address of the server (domain or ip), leave empty for local development:  192.168.1.34
 Use https?  no
 baseUrl (if the application live behind a proxy server, leave empty for most cases):  
 Do you want to protect some clients with a password?  no

- creating config file "env-remote.yaml":
```
type: development
port: 80
baseUrl: ""
serverAddress: 192.168.1.34
useHttps: false
httpsInfos:
  cert: null
  key: null
auth:
  clients: []
  login: ""
  password: ""

```

 Confirm?  yes

+ config file "env-remote.yaml" successfully created

To use this configuration file, you will just have to start your clients by giving them the name of the config, e.g.:

sh
ENV=remote npm run watch thing

Conclusion

In this tutorial, we have explored an important feature of soundworks, i.e. the possibility to create clients that are not running in a Web browser, but rather in a Node.js process.

In the next tutorials, we will continue our journey into soundworks, by tackling the important question of synchronization between different processes and machines.