I'm making a discord bot with Discord.js v14 that records users' audio as individual files and one collective file. As Discord.js streams do not interpolate silence, my question is how to interpolate silence into streams.
My code is based off the Discord.js recording example.
In essence, a privileged user enters a voice channel (or stage), runs /record and all the users in that channel are recorded up until the point that they run /leave.
I've tried using Node packages like combined-stream, audio-mixer, multistream and multipipe, but I'm not familiar enough with Node streams to use the pros of each to fill in the gaps the cons add to the problem. I'm not entirely sure how to go about interpolating silence, either, whether it be through a Transform (likely requires the stream to be continuous, or for the receiver stream to be applied onto silence) or through a sort of "multi-stream" that swaps between piping the stream and a silence buffer. I also have yet to overlay the audio files (e.g, with ffmpeg).
Would it even be possible for a Readable to await an audio chunk and, if none is given within a certain timeframe, push a chunk of silence instead? My attempt at doing so is below (again, based off the Discord.js recorder example):
// CREDIT TO: https://stackoverflow.com/a/69328242/8387760
const SILENCE = Buffer.from([0xf8, 0xff, 0xfe]);
async function createListeningStream(connection, userId) {
// Creating manually terminated stream
let receiverStream = connection.receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.Manual
},
});
// Interpolating silence
// TODO Increases file length over tenfold by stretching audio?
let userStream = new Readable({
read() {
receiverStream.on('data', chunk => {
if (chunk) {
this.push(chunk);
}
else {
// Never occurs
this.push(SILENCE);
}
});
}
});
/* Piping userStream to file at 48kHz sample rate */
}
As an unnecessary bonus, it would help if it were possible to check whether a user ever spoke or not to eliminate creating empty recordings.
Thanks in advance.
Related:
Record all users in a voice channel in discord js v12
Adding silent frames to a node js stream when no data is received
After a lot of reading about Node streams, the solution I procured was unexpectedly simple.
Create a boolean variable recording that is true when the recording should continue and false when it should stop
Create a buffer to handle backpressuring (i.e, when data is input at a higher rate than its output)
let buffer = [];
Create a readable stream for which the receiving user audio stream is piped into
// New audio stream (with silence)
let userStream = new Readable({
// ...
});
// User audio stream (without silence)
let receiverStream = connection.receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.Manual,
},
});
receiverStream.on('data', chunk => buffer.push(chunk));
In that stream's read method, handle stream recording with a 48kHz timer to match the sample rate of the user audio stream
read() {
if (recording) {
let delay = new NanoTimer();
delay.setTimeout(() => {
if (buffer.length > 0) {
this.push(buffer.shift());
}
else {
this.push(SILENCE);
}
}, '', '20m');
}
// ...
}
In the same method, also handle ending the stream
// ...
else if (buffer.length > 0) {
// Stream is ending: sending buffered audio ASAP
this.push(buffer.shift());
}
else {
// Ending stream
this.push(null);
}
If we put it all together:
const NanoTimer = require('nanotimer'); // node
/* import NanoTimer from 'nanotimer'; */ // es6
const SILENCE = Buffer.from([0xf8, 0xff, 0xfe]);
async function createListeningStream(connection, userId) {
// Accumulates very, very slowly, but only when user is speaking: reduces buffer size otherwise
let buffer = [];
// Interpolating silence into user audio stream
let userStream = new Readable({
read() {
if (recording) {
// Pushing audio at the same rate of the receiver
// (Could probably be replaced with standard, less precise timer)
let delay = new NanoTimer();
delay.setTimeout(() => {
if (buffer.length > 0) {
this.push(buffer.shift());
}
else {
this.push(SILENCE);
}
// delay.clearTimeout();
}, '', '20m'); // A 20.833ms period makes for a 48kHz frequency
}
else if (buffer.length > 0) {
// Sending buffered audio ASAP
this.push(buffer.shift());
}
else {
// Ending stream
this.push(null);
}
}
});
// Redirecting user audio to userStream to have silence interpolated
let receiverStream = connection.receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.Manual, // Manually closed elsewhere
},
// mode: 'pcm',
});
receiverStream.on('data', chunk => buffer.push(chunk));
// pipeline(userStream, ...), etc.
}
From here, you can pipe that stream into a fileWriteStream, etc. for individual purposes. Note that it's a good idea to also close the receiverStream whenever recording = false with something like:
connection.receiver.subscriptions.delete(userId);
As well, the userStream should, too be closed if it's not, e.g, the first argument of the pipeline method.
As a side note, although outside the scope of my original question, there are many other modifications you can make to this. For instance, you can prepend silence to the audio before piping the receiverStream's data to the userStream, e.g, to make multiple audio streams of the same length:
// let startTime = ...
let creationTime;
for (let i = startTime; i < (creationTime = Date.now()); i++) {
buffer.push(SILENCE);
}
Happy coding!
Related
General setup
I developed an application using AWS Lambda node.js 14.
I use a custom Readable implementation FrameCreationStream that uses node-canvas to draw images, svgs and more on a canvas. This result is then extracted as a raw image buffer in BGRA. A single image buffer contains 1920 * 1080 * 4 Bytes = 8294400 Bytes ~8 MB.
This is then piped to stdin of a child_process running ffmpeg.
The highWaterMark of my Readable in objectMode:true is set to 25 so that the internal buffer can use up to 8 MB * 25 = 200 MB.
All this works fine and also doesn't contain too much RAM. But I noticed after some time, that the performance is not ideally.
Performance not optimal
I have an example input that generates a video of 315 frames. If I set highWaterMark to a value above 25 the performance increases to the point, when I set to a value of 315 or above.
For some reason ffmpeg doesn't start to pull any data until highWaterMark is reached. Obviously thats not what I want. ffmpeg should always consume data if minimum 1 frame is cached in the Readable and if it has finished processing the frame before. And the Readable should produce more frames as long highWaterMark isn't reached or the last frame has been reached. So ideally the Readable and the Writeable are busy all the time.
I found another way to improve the speed. If I add a timeout in the _read() method of the Readable after let's say every tenth frame for 100 ms. Then the ffmpeg-Writable will use this timeout to write some frames to ffmpeg.
It seems like frames aren't passed to ffmpeg during frame creation because some node.js main thread is busy?
The fastest result I have if I increase highWaterMark above the amount of frames - which doesn't work for longer videos as this would make the AWS Lambda RAM explode. And this makes the whole streaming idea useless. Using timeouts always gives me stomach pain. Also depending on the execution on different environments a good fitting timeout might differ. Any ideas?
FrameCreationStream
import canvas from 'canvas';
import {Readable} from 'stream';
import {IMAGE_STREAM_BUFFER_SIZE, PerformanceUtil, RenderingLibraryError, VideoRendererInput} from 'vm-rendering-backend-commons';
import {AnimationAssets, BufferType, DrawingService, FullAnimationData} from 'vm-rendering-library';
/**
* This is a proper back pressure compatible implementation of readable for a having a stream to read single frames from.
* Whenever read() is called a new frame is created and added to the stream.
* read() will be called internally until options.highWaterMark has been reached.
* then calling read will be paused until one frame is read from the stream.
*/
export class FrameCreationStream extends Readable {
drawingService: DrawingService;
endFrameIndex: number;
currentFrameIndex: number = 0;
startFrameIndex: number;
frameTimer: [number, number];
readTimer: [number, number];
fullAnimationData: FullAnimationData;
constructor(animationAssets: AnimationAssets, fullAnimationData: FullAnimationData, videoRenderingInput: VideoRendererInput, frameTimer: [number, number]) {
super({highWaterMark: IMAGE_STREAM_BUFFER_SIZE, objectMode: true});
this.frameTimer = frameTimer;
this.readTimer = PerformanceUtil.startTimer();
this.fullAnimationData = fullAnimationData;
this.startFrameIndex = Math.floor(videoRenderingInput.startFrameId);
this.currentFrameIndex = this.startFrameIndex;
this.endFrameIndex = Math.floor(videoRenderingInput.endFrameId);
this.drawingService = new DrawingService(animationAssets, fullAnimationData, videoRenderingInput, canvas);
console.time("read");
}
/**
* this method is only overwritten for debugging
* #param size
*/
read(size?: number): string | Buffer {
console.log("read("+size+")");
const buffer = super.read(size);
console.log(buffer);
console.log(buffer?.length);
if(buffer) {
console.timeLog("read");
}
return buffer;
}
// _read() will be called when the stream wants to pull more data in.
// _read() will be called again after each call to this.push(dataChunk) once the stream is ready to accept more data. https://nodejs.org/api/stream.html#readable_readsize
// this way it is ensured, that even though this.createImageBuffer() is async, only one frame is created at a time and the order is kept
_read(): void {
// as frame numbers are consecutive and unique, we have to draw each frame number (also the first and the last one)
if (this.currentFrameIndex <= this.endFrameIndex) {
PerformanceUtil.logTimer(this.readTimer, 'WAIT -> READ\t');
this.createImageBuffer()
.then(buffer => this.optionalTimeout(buffer))
// push means adding a buffered raw frame to the stream
.then((buffer: Buffer) => {
this.readTimer = PerformanceUtil.startTimer();
// the following two frame numbers start with 1 as first value
const processedFrameNumberOfScene = 1 + this.currentFrameIndex - this.startFrameIndex;
const totalFrameNumberOfScene = 1 + this.endFrameIndex - this.startFrameIndex;
// the overall frameId or frameIndex starts with frameId 0
const processedFrameIndex = this.currentFrameIndex;
this.currentFrameIndex++;
this.push(buffer); // nothing besides logging should happen after calling this.push(buffer)
console.log(processedFrameNumberOfScene + ' of ' + totalFrameNumberOfScene + ' processed - full video frameId: ' + processedFrameIndex + ' - buffered frames: ' + this.readableLength);
})
.catch(err => {
// errors will be finally handled, when subscribing to frameCreation stream in ffmpeg service
// this log is just generated for tracing errors and if for some reason the handling in ffmpeg service doesn't work
console.log("createImageBuffer: ", err);
this.emit("error", err);
});
} else {
// push(null) makes clear that this stream has ended
this.push(null);
PerformanceUtil.logTimer(this.frameTimer, 'FRAME_STREAM');
}
}
private optionalTimeout(buffer: Buffer): Promise<Buffer> {
if(this.currentFrameIndex % 10 === 0) {
return new Promise(resolve => setTimeout(() => resolve(buffer), 140));
}
return Promise.resolve(buffer);
}
// prevent memory leaks - without this lambda memory will increase with every call
_destroy(): void {
this.drawingService.destroyStage();
}
/**
* This creates a raw pixel buffer that contains a single frame of the video drawn by the rendering library
*
*/
public async createImageBuffer(): Promise<Buffer> {
const drawTimer = PerformanceUtil.startTimer();
try {
await this.drawingService.drawForFrame(this.currentFrameIndex);
} catch (err: any) {
throw new RenderingLibraryError(err);
}
PerformanceUtil.logTimer(drawTimer, 'DRAW -> FRAME\t');
const bufferTimer = PerformanceUtil.startTimer();
// Creates a raw pixel buffer, containing simple binary data
// the exact same information (BGRA/screen ratio) has to be provided to ffmpeg, because ffmpeg cannot detect format for raw input
const buffer = await this.drawingService.toBuffer(BufferType.RAW);
PerformanceUtil.logTimer(bufferTimer, 'CANVAS -> BUFFER');
return buffer;
}
}
FfmpegService
import {ChildProcess, execFile} from 'child_process';
import {Readable} from 'stream';
import {FPS, StageSize} from 'vm-rendering-library';
import {
FfmpegError,
LOCAL_MERGE_VIDEOS_TEXT_FILE, LOCAL_SOUND_FILE_PATH,
LOCAL_VIDEO_FILE_PATH,
LOCAL_VIDEO_SOUNDLESS_MERGE_FILE_PATH
} from "vm-rendering-backend-commons";
/**
* This class bundles all ffmpeg usages for rendering one scene.
* FFmpeg is a console program which can transcode nearly all types of sounds, images and videos from one to another.
*/
export class FfmpegService {
ffmpegPath: string = null;
constructor(ffmpegPath: string) {
this.ffmpegPath = ffmpegPath;
}
/**
* Convert a stream of raw images into an .mp4 video using the command line program ffmpeg.
*
* #param inputStream an input stream containing images in raw format BGRA
* #param stageSize the size of a single frame in pixels (minimum is 2*2)
* #param outputPath the filepath to write the resulting video to
*/
public imageToVideo(inputStream: Readable, stageSize: StageSize, outputPath: string): Promise<void> {
const args: string[] = [
'-f',
'rawvideo',
'-r',
`${FPS}`,
'-pix_fmt',
'bgra',
'-s',
`${stageSize.width}x${stageSize.height}`,
'-i',
// input "-" means input will be passed via pipe (streamed)
'-',
// codec that also QuickTime player can understand
'-vcodec',
'libx264',
'-pix_fmt',
'yuv420p',
/*
* "-movflags faststart":
* metadata at beginning of file
* needs more RAM
* file will be broken, if not finished properly
* higher application compatibility
* better for browser streaming
*/
'-movflags',
'faststart',
// "-preset ultrafast", //use this to speed up compression, but quality/compression ratio gets worse
// don't overwrite an existing file here,
// but delete file in the beginning of execution index.ts
// (this is better for local testing believe me)
outputPath
];
return this.execFfmpegPromise(args, inputStream);
}
private execFfmpegPromise(args: string[], inputStream?: Readable): Promise<void> {
const ffmpegServiceSelf = this;
return new Promise(function (resolve, reject) {
const executionProcess: ChildProcess = execFile(ffmpegServiceSelf.ffmpegPath, args, (err) => {
if (err) {
reject(new FfmpegError(err));
} else {
console.log('ffmpeg finished');
resolve();
}
});
if (inputStream) {
// it's important to listen on errors of input stream before piping it into the write stream
// if we don't do this here, we get an unhandled promise exception for every issue in the input stream
inputStream.on("error", err => {
reject(err);
});
// don't reject promise here as the error will also be thrown inside execFile and will contain more debugging info
// this log is just generated for tracing errors and if for some reason the handling in execFile doesn't work
inputStream.pipe(executionProcess.stdin).on("error", err => console.log("pipe stream: " , err));
}
});
}
}
I have two PCM-streams (decoder1 + decoder2):
var readable1 = fs.createReadStream("track1.mp3");
var decoder1 = new lame.Decoder({
channels: 2,
mode: lame.STEREO
});
readable1.pipe(decoder1);
and
var readable2 = fs.createReadStream("track2.mp3");
var decoder2 = new lame.Decoder({
channels: 2,
mode: lame.STEREO
});
readable2.pipe(decoder2);
Now I want to pipe the streams into one mix-function, where I can use the buffer-function like:
function mixStream(buf1, buf2, callback) {
// The mixStream-Function is not implemented yet (dummy)
var out = new Buffer(buf1.length);
for (i = 0; i < buf1.length; i+=2) {
var uint = Math.floor(.5 * buf1.readInt16LE(i));
out.writeInt16LE(uint, i);
}
this.push(out);
callback();
}
I need something like
mixStream(decoder1.pipe(), decoder2.pipe(), function() { }).pipe(new Speaker());
for output to speaker. Is this possible?
Well, pipe() function actually means a stream is linked to another, a readable to a writable, for instance. This 'linking' process is to write() to the writable stream once any data chunk is ready on the readable stream, along with a little more complex logic like pause() and resume(), to deal with the backpressure.
So all you have to do is to create a pipe-like function, to process two readable streams at the same time, which drains data from stream1 and stream2, and once the data is ready, write them to the destination writable stream.
I'd strongly recommend you to go through Node.js docs for Stream.
Hope this is what you are looking for :)
While attempting to experiment with Node.JS streams I ran into an interesting conundrum. When the input (Readable) stream pushes more data then the destination (Writable) cares about I was unable to apply back-pressure correctly.
The two methods I attempted was to return false from the Writable.prototype._write and to retain a reference to the Readable so I can call Readable.pause() from the Writable. Neither solution helped much which I'll explain.
In my exercise (which you can view the full source as a Gist) I have three streams:
Readable - PasscodeGenerator
util.inherits(PasscodeGenerator, stream.Readable);
function PasscodeGenerator(prefix) {
stream.Readable.call(this, {objectMode: true});
this.count = 0;
this.prefix = prefix || '';
}
PasscodeGenerator.prototype._read = function() {
var passcode = '' + this.prefix + this.count;
if (!this.push({passcode: passcode})) {
this.pause();
this.once('drain', this.resume.bind(this));
}
this.count++;
};
I thought that the return code from this.push() was enough to self pause and wait for the drain event to resume.
Transform - Hasher
util.inherits(Hasher, stream.Transform);
function Hasher(hashType) {
stream.Transform.call(this, {objectMode: true});
this.hashType = hashType;
}
Hasher.prototype._transform = function(sample, encoding, next) {
var hash = crypto.createHash(this.hashType);
hash.setEncoding('hex');
hash.write(sample.passcode);
hash.end();
sample.hash = hash.read();
this.push(sample);
next();
};
Simply add the hash of the passcode to the object.
Writable - SampleConsumer
util.inherits(SampleConsumer, stream.Writable);
function SampleConsumer(max) {
stream.Writable.call(this, {objectMode: true});
this.max = (max != null) ? max : 10;
this.count = 0;
}
SampleConsumer.prototype._write = function(sample, encoding, next) {
this.count++;
console.log('Hash %d (%s): %s', this.count, sample.passcode, sample.hash);
if (this.count < this.max) {
next();
} else {
return false;
}
};
Here I want to consume the data as fast as possible until I reach my max number of samples and then end the stream. I tried using this.end() instead of return false but that caused the dreaded write called after end problem. Returning false does stop everything if the sample size is small but when it is large I get an out of memory error:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory
Aborted (core dumped)
According to this SO answer in theory the Write stream would return false causing the streams to buffer until the buffers were full (16 by default for objectMode) and eventually the Readable would call it's this.pause() method. But 16 + 16 + 16 = 48; that's 48 objects in buffer till things fill up and the system is clogged. Actually less because there is no cloning involved so the objects passed between them is the same reference. Would that not mean only 16 objects in memory till the high water mark halts everything?
Lastly I realize I could have the Writable reference the Readable to call it's pause method using closures. However, this solution means the Writable stream knows to much about another object. I'd have to pass in a reference:
var foo = new PasscodeGenerator('foobar');
foo
.pipe(new Hasher('md5'))
.pipe(new SampleConsumer(samples, foo));
And this feels out of norm for how streams would work. I thought back-pressure was enough to cause a Writable to stop a Readable from pushing data and prevent out of memory errors.
An analogous example would be the Unix head command. Implementing that in Node I would assume that the destination could end and not just ignore causing the source to keep pushing data even if the destination has enough data to satisfy the beginning portion of the file.
How do I idiomatically construct custom streams such that when the destination is ready to end the source stream doesn't attempt to push more data?
This is a known issue with how _read() is called internally. Since your _read() is always pushing synchronously/immediately, the internal stream implementation can get into a loop in the right conditions. _read() implementations are generally expected to do some sort of async I/O (e.g. reading from disk or network).
The workaround for this (as noted in the link above) is to make your _read() asynchronous at least some of the time. You could also just make it async every time it's called with:
PasscodeGenerator.prototype._read = function(n) {
var passcode = '' + this.prefix + this.count;
var self = this;
// `setImmediate()` delays the push until the beginning
// of the next tick of the event loop
setImmediate(function() {
self.push({passcode: passcode});
});
this.count++;
};
I have a stream process like this:
Incomming file via HTTP (original stream)
-> Check if zipfile
- Yes -> push through an unzip2-stream
- No -> push to S3
When the unzip2-stream finds zip-entries, these are pushed through the same chain of streams, i.e.
Incomming file entry from zip file ("child" stream)
-> Check if zipfile
- Yes -> push through an unzip2-stream
- No -> push to S3
Thanks to https://stackoverflow.com/users/3580261/eljefedelrodeodeljefe I managed to solve the main problem after this conversation:
How to redirect a stream to other stream depending on data in first chunk?
The problem with creating new "child" streams for every zip entry is that these will have no connection to the original stream, so I cannot get a unified onFinish for all the streams.
I don't want to send a 202 of to the sender before I have processed (unzipped and sent to S3) every file. How can I accomplish this?
I'm thinking that I might need some kind of control object which awaits onFinish for all child streams and forces the process to dwell in the original onFinish event until all files are processed. Would this be overkill? Is there a simpler solution?
I ended up making a separate counter for the streams. There is probably a better solution, but this works.
I send the counter object as an argument to the first call to my saveFile() function. The counter is passed along to the unzip stream so it can be passed to saveFile for every file entry.
Just before a stream is started (i.e. piped) I call streamCounter.streamStarted().
In the last onFinish in the pipe chain I call streamCounter.streamFinished()
In the event of a stream going bad I call streamCounter.streamFailed()
Just before I send the 202 in the form post route I wait for streamCounter.streamPromise to resolve.
I'm not very proud of the setInterval solution. It'd probably be better with some kind of event emitting.
module.exports.streamCounter = function() {
let streamCount = 0;
let isStarted = false;
let errors = [];
this.streamStarted = function(options) {
isStarted = true;
streamCount += 1;
log.debug(`Stream started for ${options.filename}. New streamCount: ${streamCount}`);
};
this.streamFinished = function(options) {
streamCount -= 1;
log.debug(`Finished stream for ${options.filename}. New streamCount: ${streamCount}`);
};
this.streamFailed = function(err) {
streamCount -= 1;
errors.push(err);
log.debug(`Failed stream because (${err.message}). New streamCount: ${streamCount}`);
};
this.streamPromise = new Promise(function(resolve, reject) {
let interval = setInterval(function() {
if(isStarted && streamCount === 0) {
clearInterval(interval);
if(errors.length === 0) {
log.debug('StreamCounter back on 0. Resolving streamPromise');
resolve();
} else {
log.debug('StreamCounter back on 0. Errors encountered.. Rejecting streamPromise');
reject(errors[errors.length-1]);
}
}
}, 100);
});
};
At first I tried this concept with a promise array and waited for Promise.all() before sending status 202. But Promise.all() only works with static arrays as far as I can tell. My "streamCount" is changing during the streaming so I needed a more dynamic "Promise.all".
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.
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*/)
});
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);