Управление буфером необработанных данных PCM в NodeJS - PullRequest
0 голосов
/ 14 ноября 2018

Я работаю над личным проектом, который включает в себя получение аудио с YouTube, манипулирование аудио и потоковую передачу результата в браузер. Пока у меня есть первый и последний шаг вниз, но середина - это вызов.

Благодаря пакету youtube-audio-stream получить аудио стало легко. Я хотел манипулировать необработанными аудиосэмплами, поэтому я последовал их примеру README и передал поток в декодер из пакета lame.

Я собрал пару потоковых преобразований ... одно, чтобы объединить входящие чанки вместе, пока не будет достигнут порог размера, а другое - что-то действительно сделать с этими чанками. В конце конвейера я добавил WAV Writer (который добавляет WAV-заголовок, чтобы браузер не запутался в поступлении необработанных данных).

Это фактически приводит к нормальному выводу звука, если мое преобразование звука просто проходит по частям без каких-либо изменений. Так что я знаю, что сам трубопровод не сломан. Но по какой-то причине выполнение следующей операции приводит к искаженному шуму:

chunk.reverse();

(Это не конечная цель - это связано с БПФ - но я подумал, что реверсирование аудио-блоков было хорошей операцией для начала.)

Я ожидал, что это превратит поток в обращенные фрагменты звука, но вместо этого он исказил его до неузнаваемости. Я знаю, что Node.js-буферы - это Uint8Arrays, поэтому мне интересно, хранится ли каждый семпл как 4 отдельных 8-битных целых числа. Но я попытался сделать что-то вроде этого:

const arr = Float32Array.from(chunk);
this.push(new Buffer(arr.reverse()));

и он все еще искажен. Я также попытался написать цикл, который использовал бы Buffer.readFloatLE и Buffer.writeFloatLE, но он также не вел себя как ожидалось. Что мне здесь не хватает? Как я могу получить и установить данные аудиосэмпла в буфере Node.js.?

Редактировать: Добавление примера кода (я запускаю это локально как микросервис с использованием 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 };

Редактировать 2: Решено! Для дальнейшего использования, вот функции утилит, которые я написал для декодирования / кодирования аудиоданных:

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;
}

1 Ответ

0 голосов
/ 14 ноября 2018

Вы не можете просто перевернуть байтовый массив. Как вы и подозревали, сэмплы будут занимать более одного байта.

Кажется вероятным, что у вас неправильный формат семпла. Вероятно, это не 32-разрядное число с плавающей запятой, но, вероятно, это 16-разрядные целые числа со знаком. Это плохо документировано, но если вы покопаетесь в исходном коде для node-lame, , вы найдете это :

if (ret == MPG123_NEW_FORMAT) {
  var format = binding.mpg123_getformat(mh);
  debug('new format: %j', format);
  self.emit('format', format);
  return read();
}

Похоже, что базовый MPG123 может возвращать PCM в нескольких форматах :

  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;

Я бы попробовал вашу методику зацикливания еще раз, чтобы инвертировать сэмплы, сохраняя байты в каждом такте, но попробуйте это с разными размерами сэмплов. Начните с 16-разрядного знака с прямым порядком байтов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...