1

disclaimer: newbie to nodeJS and audio parsing

I'm trying to proxy a digital radio stream through an expressJS app with the help of node-icecast which works great. I am getting the radio's mp3 stream, and via node-lame decoding the mp3 to PCM and then sending it to the speakers. All of this just works straight from the github project's readme example:

var lame = require('lame');
var icecast = require('icecast');
var Speaker = require('speaker');

// URL to a known Icecast stream
var url = 'http://firewall.pulsradio.com';

// connect to the remote stream
icecast.get(url, function (res) {

// log the HTTP response headers
console.error(res.headers);

// log any "metadata" events that happen
res.on('metadata', function (metadata) {
  var parsed = icecast.parse(metadata);
  console.error(parsed);
});

// Let's play the music (assuming MP3 data).
// lame decodes and Speaker sends to speakers!
res.pipe(new lame.Decoder())
   .pipe(new Speaker());
});

I'm now trying to setup a service to identify the music using the Doreso API. Problem is I'm working with a stream and don't have the file (and I don't know enough yet about readable and writable streams, and slow learning). I have been looking around for a while at trying to write the stream (ideally to memory) until I had about 10 seconds worth. Then I would pass that portion of audio to my API, however I don't know if that's possible or know where to start with slicing 10 seconds of a stream. I thought possibly trying passing the stream to ffmpeg as it has a -t option for duration, and perhaps that could limit it, however I haven't got that to work yet.

Any suggestions to cut a stream down to 10 seconds would be awesome. Thanks!

Updated: Changed my question as I originally thought I was getting PCM and converting to mp3 ;-) I had it backwards. Now I just want to slice off part of the stream while the stream still feeds the speaker.

mfink
  • 1,309
  • 23
  • 32
  • 1
    Basically instead of piping it into lame.Decoder() you should just dump out n seconds or bytes of it, either to memory or to a file. Once you have that you can probably just hand it right over to that API you mention. – TBR Mar 12 '15 at 20:00
  • Thanks for that. I like the sounds of this, although I'm not sure I know how I would dump this out in n seconds / bytes ( _read newbie_ )Perhaps you could provide an example? as an answer? I think this is what I'm looking for. – mfink Mar 12 '15 at 20:33

2 Answers2

2

It's not that easy.. but I've managed it this weekend. I would be happy if you guys could point out how to even improve this code. I don't really like the approach of simulating the "end" of a stream. Is there something like "detaching" or "rewiring" parts of a pipe-wiring of streams in node?

First, you should create your very own Writable Stream class which itself creates a lame encoding instance. This writable stream will receive the decoded PCM data.

It works like this:

var stream = require('stream');
var util = require('util');
var fs = require('fs');
var lame = require('lame');
var streamifier = require('streamifier');
var WritableStreamBuffer = require("stream-buffers").WritableStreamBuffer;

var SliceStream = function(lameConfig) {

    stream.Writable.call(this);

    this.encoder = new lame.Encoder(lameConfig);

    // we need a stream buffer to buffer the PCM data
    this.buffer = new WritableStreamBuffer({
        initialSize: (1000 * 1024),      // start as 1 MiB.
        incrementAmount: (150 * 1024)    // grow by 150 KiB each time buffer overflows.
    });
};

util.inherits(SliceStream, stream.Writable);

// some attributes, initialization
SliceStream.prototype.writable = true;
SliceStream.prototype.encoder = null;
SliceStream.prototype.buffer = null;

// will be called each time the decoded steam emits "data" 
// together with a bunch of binary data as Buffer
SliceStream.prototype.write = function(buf) {

    //console.log('bytes recv: ', buf.length);

    this.buffer.write(buf);

    //console.log('buffer size: ', this.buffer.size());
};

// this method will invoke when the setTimeout function
// emits the simulated "end" event. Lets encode to MP3 again...
SliceStream.prototype.end = function(buf) {

    if (arguments.length) {
        this.buffer.write(buf);
    }
    this.writable = false;

    //console.log('buffer size: ' + this.buffer.size());

    // fetch binary data from buffer
    var PCMBuffer = this.buffer.getContents();

    // create a stream out of the binary buffer data
    streamifier.createReadStream(PCMBuffer).pipe(

        // and pipe it right into the MP3 encoder...
        this.encoder
    );

    // but dont forget to pipe the encoders output 
    // into a writable file stream
    this.encoder.pipe(
        fs.createWriteStream('./fooBar.mp3')
    );
};

Now you can pipe the decoded stream into an instance of your SliceStream class, like this (additional to the other pipes):

icecast.get(streamUrl, function(res) {

    var lameEncoderConfig = {
        // input
        channels: 2,        // 2 channels (left and right)
        bitDepth: 16,       // 16-bit samples
        sampleRate: 44100,   // 44,100 Hz sample rate

        // output
        bitRate: 320,
        outSampleRate: 44100,
        mode: lame.STEREO // STEREO (default), JOINTSTEREO, DUALCHANNEL or MONO
    };
    var decodedStream = res.pipe(new lame.Decoder());

    // pipe decoded PCM stream into a SliceStream instance
    decodedStream.pipe(new SliceStream(lameEncoderConfig));

    // now play it...
    decodedStream.pipe(new Speaker());

    setTimeout(function() {

        // after 10 seconds, emulate an end of the stream.
        res.emit('end');

    }, 10 * 1000 /*milliseconds*/)
});
kyr0
  • 341
  • 4
  • 6
  • thank you! I'm gonna try this out tonight and report back. Also, huge thanks for your commenting :) – mfink Mar 24 '15 at 14:49
  • 1
    If you get "write after end" error from time to time, the root cause could ne the: res.emit('end'); After I questioned myself if there is a better solution, I stumbled upon: http://www.bennadel.com/blog/2679-how-error-events-affect-piped-streams-in-node-js.htm And finally spotted: https://nodejs.org/api/stream.html#stream_readable_unpipe_destination If you find a stable solution to detach the pipes after 10 secs based on unpipe() I would be thankful if you would submit it :) – kyr0 Mar 24 '15 at 15:01
  • This works which is awesome, but the 'end' event wasn't always firing (and therefore wasn't always writing to a fooBar.mp3 file). I changed this around to use the unpipe method like you mention and I then actually call the end event manually. Since this is our own class, it doesn't behave like other stream end events. Here is what I have: `var cut = new SliceStream(lameEncoderConfig); //here's the stream to cut decodedStream.pipe(cut); setTimeout(function() { cut.end(); decodedStream.unpipe(cut); }, 10 * 1000 /*milliseconds*/);` – mfink Mar 25 '15 at 02:29
  • messy to put code in comments, here's the portion of your code with the unpipe and manual call to end ( not necessarily a stream end event): http://pastebin.com/aJ5iLHSk – mfink Mar 25 '15 at 02:39
-1

Can I suggest using removeListener after 10 seconds? That will prevent future events from being sent through the listener.

var request = require('request'),
    fs = require('fs'),
    masterStream = request('-- mp3 stream --')

var writeStream = fs.createWriteStream('recording.mp3'),
handler = function(bit){
   writeStream.write(bit);
}

masterStream.on('data', handler);
setTimeout(function(){
    masterStream.removeListener('data', handler);
    writeStream.end();
}, 1000 * 10);
Adam Fowler
  • 1,750
  • 1
  • 17
  • 18