Playing audio from Node.js using Edge.js

Hero Image

The Edge.js project allows you to use .NET Framework inside of a Node.js application. Why would you ever do that? Scott Hanselman puts it this way:

image

One such problem is playing audio. Node.js core does not support this functionality, so one must resort to writing a native extension in C/C++. You can dust off that Stroustrup book, tool up for memory leak detection, prepare for segfaults\avs, get yourself a bucket of coffee, and plow on to write some serious C code.

Alternatively, you can do it with two lines of C# code…

Enter Edge.js

… and then call into these two lines of C# code from Node.js using Edge.js:

   var edge = require('edge');

var play = edge.func(function() {/*
    async (input) => {
        var player = new System.Media.SoundPlayer((string)input);
        player.PlaySync();
        return null;
    }
*/});

console.log('Starting playing');
play('dday.wav');
console.log('Done playing');

So what happens here? We are using the System.Media.SoundPlayer class from .NET Framework to play a PCM WAV file (lines 5 & 6). We wrap this logic in a C# async lambda expression (line 4). Then we use the edge.func function of Edge.js to create a JavaScript proxy around this async lambda expression (line 3). Lastly, we call that JavaScript proxy function and pass it the file name of the WAV file to play (line 12).

Edge.js allows you to call .NET functions from Node.js and Node.js functions from .NET. Edge.js takes care of marshalling data between CLR and V8. Edge.js also reconciles threading models of single threaded V8 and multi-threaded CLR, and ensures correct lifetime of objects on V8 and CLR heaps. And all that happens within a single process – Edge does not spawn separate CLR processes. Read more in the Edge.js documentation.

Coming back to playing audio. If you run the code above you will notice that the Done playing message is only printed to the console after the audio has finished playing. This is because the C# code executes on the singleton V8 thread of Node.js, and the Node.js event loop remains blocked. This is of course unacceptable…

Enter CLR threads

… so let’s fix it. We need to add two more C# lines to play our audio on a CLR thread pool thread and avoid blocking the V8 thread:

var edge = require('edge');

var play = edge.func(function() {/*
    async (input) => {
        return await Task.Run<object>(async () => {
            var player = new System.Media.SoundPlayer((string)input);
            player.PlaySync();
            return null;
        });
    }
*/});

console.log('Starting playing');
play('dday.wav', function (err) {
    if (err) throw err;
    console.log('Done playing');
});
console.log('Started playing');

Notice how we create a new CLR thread pool thread in line 5, and let that thread play our audio. This leaves the V8 thread free to process whatever other events need processing. Also notice that the play JavaScript proxy function can still detect when the audio has finished playing by supplying an async callback in line 14. Edge.js will invoke that async callback only after the C# async lambda expression completes, which happens when the audio playing on the CLR thread pool thread has finished playing and the thread terminates in line 8. The fact that the Node.js event loop remains unblocked is evidenced by the Started playing message from line 18 showing up before the Done playing message from line 16.

At this point we seem to be done. While we wait for the folks cranking C code to finish (ETA: one more week), we can indulge in a more fancy experiment.

Enter closures

Now that we can play a simple WAV file asynchronously, how about adding some more control over the experience. Let’s have a way to start and stop playing the audio asynchronously at any time.

This calls for one of the more interesting features of Edge.js: the ability to marshal function proxies between V8 and CLR boundary. Moreover, functions exposed from CLR to Node.js can be implemented as a closure over some other CLR state, which opens interesting possibilities. For example, allowing an instance of System.Media.SoundPlayer to be controlled from Node.js:

var edge = require('edge');

var createPlayer = edge.func(function() {/*
    async (input) => {
        var player = new System.Media.SoundPlayer((string)input);
        return new {
            start = (Func<object,Task<object>>)(async (i) => {
                player.Play();
                return null;
            }),
            stop = (Func<object,Task<object>>)(async (i) => {
                player.Stop();
                return null;
            })
        };
    }
*/});

We are using Edge.js to construct a createPlayer JavaScript function (line 3). This function wraps a logic in C# which acts as a factory method. It first creates an instance of System.Media.SoundPlayer (line 5). Then it returns an anonymous object with two functions on it: play and stop. Both functions are implemented as closures over the instance of SoundPlayer created in line 5, starting and stopping the playback, respectively.

This is how you can use the createPlayer function:

console.log('Creating player');
var player = createPlayer('dday.wav', true);

player.start(null, function (err) {
    if (err) throw err;
    console.log('Started playing');
});

setTimeout(function () {
    player.stop(null, function(err) {
        if (err) throw err;
        console.log('Stopped playing');
    });
}, 5000);

First we create a player in line 2. The player is a JavaScript object with two properties: play and stop. Both are JavaScript functions acting as proxies to the corresponding C# async lambda expressions created within createPlayer. You can invoke the play function to start playing the audio asynchronously on a CLR thread pool thread. Five seconds later, we can stop the playback by calling the stop function (line 10).

So what does it all mean?

It means that in many cases it is much easier to write a few lines of C# and use Edge.js rather than a truckload of C code to add “native” functionality to Node.js.

Dude, Edge.js surely only works on Windows, why are you wasting my time?

Dear Dude, I am pleased to inform you that Edge.js works on Mac and Linux as well as Windows. Yours truly.


ArrowPrevious
NextArrow

Related Content

2 February 2022
fetch() In Node.js Core: Why You Should Care

Node 17.5 introduces support for the fetch() HTTP client, a new way to send requests to HTTP APIs.

23 February 2022
Node.js Adds Support for Direct Registry-less HTTPS Imports

Node is planning to introduce support for HTTPS imports in Node 18 - a feature that enables you to use urls to directly import modules over HTTPS into your project.

15 February 2022
Run Every Node.js Version in AWS Lambda

Run any version of Node.js in AWS Lambda within hours after release using custom AWS Lambda runtimes from Fusebit