Manipulating a Buffer of raw PCM data in NodeJS
I'm working on a personal project that involves retrieving audio from YouTube, manipulating the audio, and streaming the result to the browser. So far I have the first and last steps down, but the middle is proving a challenge.
Thanks to the youtube-audio-stream
package, getting the audio was easy. I wanted to manipulate the raw audio samples, so I followed their README example and piped the stream into a Decoder from the lame
package.
I threw together a couple stream transforms... one to merge incoming chunks together until a size threshold was met, and the other one to actually do something with those chunks. At the end of the pipeline, I added a wav writer (which adds a WAV header so the browser isn't confused about the raw data coming in).
This actually results in normal audio output if my audio transform just passes along the chunks without any modification. So I know that the pipeline itself isn't broken. But for some reason, performing the following operation results in garbled noise:
chunk.reverse();
(This isn't the ultimate goal -- that involves FFT -- but I figured reversing audio chunks was a good operation to start with.)
I expected this to transform the stream into reversed fragments of sound, but instead it distorted it beyond recognition. I know that Node.js Buffers are Uint8Arrays, so I'm wondering if each sample is stored as 4 separate 8-bit integers. But I tried doing something like this:
const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));
and it's still garbled. I also tried writing a loop that used Buffer.readFloatLE
and Buffer.writeFloatLE
, but that didn't behave as expected either. What am I missing here? How can I retrieve and set audio sample data in a Node.js Buffer?
Edit: Adding example code (I'm running this locally as a microservice using micro
):
index.js
const stream = require('youtube-audio-stream');
const wav = require('wav');
const decoder = require('lame').Decoder;
const Chunker, AudioThing = require('./transforms');
module.exports = (req, res) =>
const url = 'https://www.youtube.com/watch?v=-L7IdUqaZxo';
res.setHeader('Content-Type', 'audio/wav');
return stream(url)
.pipe(decoder())
.pipe(new Chunker(2 ** 16))
.pipe(new AudioThing())
.pipe(new wav.Writer());
transforms.js
const Transform = require('stream');
class Chunker extends Transform
constructor(threshold)
super();
this.size = 0;
this.chunks = ;
this.threshold = threshold;
_transform(chunk, encoding, done)
this.size += chunk.length;
this.chunks.push(chunk);
if (this.size >= this.threshold)
this.push(Buffer.concat(this.chunks, this.size));
this.chunks = ;
this.size = 0;
done();
class AudioThing extends Transform
_transform(chunk, encoding, done)
this.push(chunk.reverse());
done();
module.exports = Chunker, AudioThing ;
Edit 2: Solved! For future reference, here are the utility functions I wrote to decode/encode the audio data:
function decodeBuffer (buffer)
return Array.from(
length: buffer.length / 2 ,
(v, i) => buffer.readInt16LE(i * 2) / (2 ** 15)
);
function encodeArray (array)
const buf = Buffer.alloc(array.length * 2);
for (let i = 0; i < array.length; i++)
buf.writeInt16LE(array[i] * (2 ** 15), i * 2);
return buf;
node.js audio
add a comment |
I'm working on a personal project that involves retrieving audio from YouTube, manipulating the audio, and streaming the result to the browser. So far I have the first and last steps down, but the middle is proving a challenge.
Thanks to the youtube-audio-stream
package, getting the audio was easy. I wanted to manipulate the raw audio samples, so I followed their README example and piped the stream into a Decoder from the lame
package.
I threw together a couple stream transforms... one to merge incoming chunks together until a size threshold was met, and the other one to actually do something with those chunks. At the end of the pipeline, I added a wav writer (which adds a WAV header so the browser isn't confused about the raw data coming in).
This actually results in normal audio output if my audio transform just passes along the chunks without any modification. So I know that the pipeline itself isn't broken. But for some reason, performing the following operation results in garbled noise:
chunk.reverse();
(This isn't the ultimate goal -- that involves FFT -- but I figured reversing audio chunks was a good operation to start with.)
I expected this to transform the stream into reversed fragments of sound, but instead it distorted it beyond recognition. I know that Node.js Buffers are Uint8Arrays, so I'm wondering if each sample is stored as 4 separate 8-bit integers. But I tried doing something like this:
const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));
and it's still garbled. I also tried writing a loop that used Buffer.readFloatLE
and Buffer.writeFloatLE
, but that didn't behave as expected either. What am I missing here? How can I retrieve and set audio sample data in a Node.js Buffer?
Edit: Adding example code (I'm running this locally as a microservice using micro
):
index.js
const stream = require('youtube-audio-stream');
const wav = require('wav');
const decoder = require('lame').Decoder;
const Chunker, AudioThing = require('./transforms');
module.exports = (req, res) =>
const url = 'https://www.youtube.com/watch?v=-L7IdUqaZxo';
res.setHeader('Content-Type', 'audio/wav');
return stream(url)
.pipe(decoder())
.pipe(new Chunker(2 ** 16))
.pipe(new AudioThing())
.pipe(new wav.Writer());
transforms.js
const Transform = require('stream');
class Chunker extends Transform
constructor(threshold)
super();
this.size = 0;
this.chunks = ;
this.threshold = threshold;
_transform(chunk, encoding, done)
this.size += chunk.length;
this.chunks.push(chunk);
if (this.size >= this.threshold)
this.push(Buffer.concat(this.chunks, this.size));
this.chunks = ;
this.size = 0;
done();
class AudioThing extends Transform
_transform(chunk, encoding, done)
this.push(chunk.reverse());
done();
module.exports = Chunker, AudioThing ;
Edit 2: Solved! For future reference, here are the utility functions I wrote to decode/encode the audio data:
function decodeBuffer (buffer)
return Array.from(
length: buffer.length / 2 ,
(v, i) => buffer.readInt16LE(i * 2) / (2 ** 15)
);
function encodeArray (array)
const buf = Buffer.alloc(array.length * 2);
for (let i = 0; i < array.length; i++)
buf.writeInt16LE(array[i] * (2 ** 15), i * 2);
return buf;
node.js audio
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40
add a comment |
I'm working on a personal project that involves retrieving audio from YouTube, manipulating the audio, and streaming the result to the browser. So far I have the first and last steps down, but the middle is proving a challenge.
Thanks to the youtube-audio-stream
package, getting the audio was easy. I wanted to manipulate the raw audio samples, so I followed their README example and piped the stream into a Decoder from the lame
package.
I threw together a couple stream transforms... one to merge incoming chunks together until a size threshold was met, and the other one to actually do something with those chunks. At the end of the pipeline, I added a wav writer (which adds a WAV header so the browser isn't confused about the raw data coming in).
This actually results in normal audio output if my audio transform just passes along the chunks without any modification. So I know that the pipeline itself isn't broken. But for some reason, performing the following operation results in garbled noise:
chunk.reverse();
(This isn't the ultimate goal -- that involves FFT -- but I figured reversing audio chunks was a good operation to start with.)
I expected this to transform the stream into reversed fragments of sound, but instead it distorted it beyond recognition. I know that Node.js Buffers are Uint8Arrays, so I'm wondering if each sample is stored as 4 separate 8-bit integers. But I tried doing something like this:
const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));
and it's still garbled. I also tried writing a loop that used Buffer.readFloatLE
and Buffer.writeFloatLE
, but that didn't behave as expected either. What am I missing here? How can I retrieve and set audio sample data in a Node.js Buffer?
Edit: Adding example code (I'm running this locally as a microservice using micro
):
index.js
const stream = require('youtube-audio-stream');
const wav = require('wav');
const decoder = require('lame').Decoder;
const Chunker, AudioThing = require('./transforms');
module.exports = (req, res) =>
const url = 'https://www.youtube.com/watch?v=-L7IdUqaZxo';
res.setHeader('Content-Type', 'audio/wav');
return stream(url)
.pipe(decoder())
.pipe(new Chunker(2 ** 16))
.pipe(new AudioThing())
.pipe(new wav.Writer());
transforms.js
const Transform = require('stream');
class Chunker extends Transform
constructor(threshold)
super();
this.size = 0;
this.chunks = ;
this.threshold = threshold;
_transform(chunk, encoding, done)
this.size += chunk.length;
this.chunks.push(chunk);
if (this.size >= this.threshold)
this.push(Buffer.concat(this.chunks, this.size));
this.chunks = ;
this.size = 0;
done();
class AudioThing extends Transform
_transform(chunk, encoding, done)
this.push(chunk.reverse());
done();
module.exports = Chunker, AudioThing ;
Edit 2: Solved! For future reference, here are the utility functions I wrote to decode/encode the audio data:
function decodeBuffer (buffer)
return Array.from(
length: buffer.length / 2 ,
(v, i) => buffer.readInt16LE(i * 2) / (2 ** 15)
);
function encodeArray (array)
const buf = Buffer.alloc(array.length * 2);
for (let i = 0; i < array.length; i++)
buf.writeInt16LE(array[i] * (2 ** 15), i * 2);
return buf;
node.js audio
I'm working on a personal project that involves retrieving audio from YouTube, manipulating the audio, and streaming the result to the browser. So far I have the first and last steps down, but the middle is proving a challenge.
Thanks to the youtube-audio-stream
package, getting the audio was easy. I wanted to manipulate the raw audio samples, so I followed their README example and piped the stream into a Decoder from the lame
package.
I threw together a couple stream transforms... one to merge incoming chunks together until a size threshold was met, and the other one to actually do something with those chunks. At the end of the pipeline, I added a wav writer (which adds a WAV header so the browser isn't confused about the raw data coming in).
This actually results in normal audio output if my audio transform just passes along the chunks without any modification. So I know that the pipeline itself isn't broken. But for some reason, performing the following operation results in garbled noise:
chunk.reverse();
(This isn't the ultimate goal -- that involves FFT -- but I figured reversing audio chunks was a good operation to start with.)
I expected this to transform the stream into reversed fragments of sound, but instead it distorted it beyond recognition. I know that Node.js Buffers are Uint8Arrays, so I'm wondering if each sample is stored as 4 separate 8-bit integers. But I tried doing something like this:
const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));
and it's still garbled. I also tried writing a loop that used Buffer.readFloatLE
and Buffer.writeFloatLE
, but that didn't behave as expected either. What am I missing here? How can I retrieve and set audio sample data in a Node.js Buffer?
Edit: Adding example code (I'm running this locally as a microservice using micro
):
index.js
const stream = require('youtube-audio-stream');
const wav = require('wav');
const decoder = require('lame').Decoder;
const Chunker, AudioThing = require('./transforms');
module.exports = (req, res) =>
const url = 'https://www.youtube.com/watch?v=-L7IdUqaZxo';
res.setHeader('Content-Type', 'audio/wav');
return stream(url)
.pipe(decoder())
.pipe(new Chunker(2 ** 16))
.pipe(new AudioThing())
.pipe(new wav.Writer());
transforms.js
const Transform = require('stream');
class Chunker extends Transform
constructor(threshold)
super();
this.size = 0;
this.chunks = ;
this.threshold = threshold;
_transform(chunk, encoding, done)
this.size += chunk.length;
this.chunks.push(chunk);
if (this.size >= this.threshold)
this.push(Buffer.concat(this.chunks, this.size));
this.chunks = ;
this.size = 0;
done();
class AudioThing extends Transform
_transform(chunk, encoding, done)
this.push(chunk.reverse());
done();
module.exports = Chunker, AudioThing ;
Edit 2: Solved! For future reference, here are the utility functions I wrote to decode/encode the audio data:
function decodeBuffer (buffer)
return Array.from(
length: buffer.length / 2 ,
(v, i) => buffer.readInt16LE(i * 2) / (2 ** 15)
);
function encodeArray (array)
const buf = Buffer.alloc(array.length * 2);
for (let i = 0; i < array.length; i++)
buf.writeInt16LE(array[i] * (2 ** 15), i * 2);
return buf;
node.js audio
node.js audio
edited Nov 14 '18 at 4:46
Matt Diamond
asked Nov 14 '18 at 3:20
Matt DiamondMatt Diamond
10.2k32233
10.2k32233
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40
add a comment |
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40
add a comment |
1 Answer
1
active
oldest
votes
You can't simply reverse the byte array. As you suspected, samples are going to span more than one byte.
It seems plausible that you have the sample format wrong. It's probably not 32-bit float, but is probably signed 16-bit integers. This isn't documented well, but if you dig into the source code for node-lame
, you find this:
if (ret == MPG123_NEW_FORMAT)
var format = binding.mpg123_getformat(mh);
debug('new format: %j', format);
self.emit('format', format);
return read();
It looks like the underlying MPG123 can return PCM in several formats:
if (ret == MPG123_OK) {
Local<Object> o = Nan::New<Object>();
Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
if (encoding & MPG123_ENC_8)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
else if (encoding & MPG123_ENC_16)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
else if (encoding & MPG123_ENC_24)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
else if (encoding & MPG123_ENC_FLOAT_64)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
rtn = o;
I would try your looping technique again to reverse the samples while keeping the bytes in each sample in-tact, but try this with different sample sizes. Start with 16-bit signed, little-endian.
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profilingbuffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use.copy()
in lengths of2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.
– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53292716%2fmanipulating-a-buffer-of-raw-pcm-data-in-nodejs%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
You can't simply reverse the byte array. As you suspected, samples are going to span more than one byte.
It seems plausible that you have the sample format wrong. It's probably not 32-bit float, but is probably signed 16-bit integers. This isn't documented well, but if you dig into the source code for node-lame
, you find this:
if (ret == MPG123_NEW_FORMAT)
var format = binding.mpg123_getformat(mh);
debug('new format: %j', format);
self.emit('format', format);
return read();
It looks like the underlying MPG123 can return PCM in several formats:
if (ret == MPG123_OK) {
Local<Object> o = Nan::New<Object>();
Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
if (encoding & MPG123_ENC_8)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
else if (encoding & MPG123_ENC_16)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
else if (encoding & MPG123_ENC_24)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
else if (encoding & MPG123_ENC_FLOAT_64)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
rtn = o;
I would try your looping technique again to reverse the samples while keeping the bytes in each sample in-tact, but try this with different sample sizes. Start with 16-bit signed, little-endian.
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profilingbuffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use.copy()
in lengths of2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.
– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
add a comment |
You can't simply reverse the byte array. As you suspected, samples are going to span more than one byte.
It seems plausible that you have the sample format wrong. It's probably not 32-bit float, but is probably signed 16-bit integers. This isn't documented well, but if you dig into the source code for node-lame
, you find this:
if (ret == MPG123_NEW_FORMAT)
var format = binding.mpg123_getformat(mh);
debug('new format: %j', format);
self.emit('format', format);
return read();
It looks like the underlying MPG123 can return PCM in several formats:
if (ret == MPG123_OK) {
Local<Object> o = Nan::New<Object>();
Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
if (encoding & MPG123_ENC_8)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
else if (encoding & MPG123_ENC_16)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
else if (encoding & MPG123_ENC_24)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
else if (encoding & MPG123_ENC_FLOAT_64)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
rtn = o;
I would try your looping technique again to reverse the samples while keeping the bytes in each sample in-tact, but try this with different sample sizes. Start with 16-bit signed, little-endian.
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profilingbuffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use.copy()
in lengths of2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.
– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
add a comment |
You can't simply reverse the byte array. As you suspected, samples are going to span more than one byte.
It seems plausible that you have the sample format wrong. It's probably not 32-bit float, but is probably signed 16-bit integers. This isn't documented well, but if you dig into the source code for node-lame
, you find this:
if (ret == MPG123_NEW_FORMAT)
var format = binding.mpg123_getformat(mh);
debug('new format: %j', format);
self.emit('format', format);
return read();
It looks like the underlying MPG123 can return PCM in several formats:
if (ret == MPG123_OK) {
Local<Object> o = Nan::New<Object>();
Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
if (encoding & MPG123_ENC_8)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
else if (encoding & MPG123_ENC_16)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
else if (encoding & MPG123_ENC_24)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
else if (encoding & MPG123_ENC_FLOAT_64)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
rtn = o;
I would try your looping technique again to reverse the samples while keeping the bytes in each sample in-tact, but try this with different sample sizes. Start with 16-bit signed, little-endian.
You can't simply reverse the byte array. As you suspected, samples are going to span more than one byte.
It seems plausible that you have the sample format wrong. It's probably not 32-bit float, but is probably signed 16-bit integers. This isn't documented well, but if you dig into the source code for node-lame
, you find this:
if (ret == MPG123_NEW_FORMAT)
var format = binding.mpg123_getformat(mh);
debug('new format: %j', format);
self.emit('format', format);
return read();
It looks like the underlying MPG123 can return PCM in several formats:
if (ret == MPG123_OK) {
Local<Object> o = Nan::New<Object>();
Nan::Set(o, Nan::New<String>("raw_encoding").ToLocalChecked(), Nan::New<Number>(encoding));
Nan::Set(o, Nan::New<String>("sampleRate").ToLocalChecked(), Nan::New<Number>(rate));
Nan::Set(o, Nan::New<String>("channels").ToLocalChecked(), Nan::New<Number>(channels));
Nan::Set(o, Nan::New<String>("signed").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_SIGNED));
Nan::Set(o, Nan::New<String>("float").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_FLOAT));
Nan::Set(o, Nan::New<String>("ulaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ULAW_8));
Nan::Set(o, Nan::New<String>("alaw").ToLocalChecked(), Nan::New<Boolean>(encoding & MPG123_ENC_ALAW_8));
if (encoding & MPG123_ENC_8)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(8));
else if (encoding & MPG123_ENC_16)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(16));
else if (encoding & MPG123_ENC_24)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(24));
else if (encoding & MPG123_ENC_32 || encoding & MPG123_ENC_FLOAT_32)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(32));
else if (encoding & MPG123_ENC_FLOAT_64)
Nan::Set(o, Nan::New<String>("bitDepth").ToLocalChecked(), Nan::New<Integer>(64));
rtn = o;
I would try your looping technique again to reverse the samples while keeping the bytes in each sample in-tact, but try this with different sample sizes. Start with 16-bit signed, little-endian.
answered Nov 14 '18 at 4:01
BradBrad
115k27230393
115k27230393
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profilingbuffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use.copy()
in lengths of2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.
– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
add a comment |
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profilingbuffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use.copy()
in lengths of2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.
– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
Holy crap, I think that actually worked. My secondary question is that of performance... is there a faster way to convert this besides calling buffer.readInt16LE in a loop? Can I use a TypedArray?
– Matt Diamond
Nov 14 '18 at 4:28
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profiling
buffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use .copy()
in lengths of 2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.– Brad
Nov 14 '18 at 4:32
@MattDiamond First, make sure you've allocated the target buffer ahead of time. Next, try profiling
buffer.copy()
. You don't actually need to read those byte values, you just need to copy them to a new buffer in a different order. So, use .copy()
in lengths of 2
. Just be sure to profile it, since this is a tight loop with a lot of iterations.– Brad
Nov 14 '18 at 4:32
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
Okay cool. I think this counts as solved, even if I have some more fine-tuning to do... thanks!
– Matt Diamond
Nov 14 '18 at 4:40
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53292716%2fmanipulating-a-buffer-of-raw-pcm-data-in-nodejs%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
An MCVE would go a long way here. Just a short listing with just the necessary code and maybe even a "suitable" example URL ( though presumably any YouTube URL should suffice ). If you provide the minimal code to reproduce, then it's a lot easier for others to debug the process.
– Neil Lunn
Nov 14 '18 at 3:25
@NeilLunn Thanks for the suggestion... not sure how easily I could get a live example up, but I've edited my question to include the bulk of my code thus far.
– Matt Diamond
Nov 14 '18 at 3:33
Does not need to be "live", but simply enough code to "reproduce the problem". Also "snippets" are meant for code that does in fact run live in the browser. The button next to that one on the editor does normal code block indentation
– Neil Lunn
Nov 14 '18 at 3:39
Ah sorry, thanks for fixing it
– Matt Diamond
Nov 14 '18 at 3:40