Я делаю бота для разногласий с Discord.js v14, который записывает аудио пользователей в виде отдельных файлов и одного коллективного файла. Поскольку потоки Discord.js не интерполируют тишину, мой вопрос заключается в том, как интерполировать тишину в потоки.
Мой код основан на примере записи Discord.js.
По сути, привилегированный пользователь входит в голосовой канал (или этап), запускает /record
, и все пользователи в этом канале записываются до момента, когда они запускаются /leave
.
Я пытался использовать пакеты Node, такие как комбинированный поток , аудио-микшер , многопоток и многоканальный, но я недостаточно знаком с потоками Node, чтобы использовать преимущества каждого из них для заполнения. в пробелах минусы добавляют проблем. Я не совсем уверен, как интерполировать тишину, будь то через преобразование (вероятно, требует, чтобы поток был непрерывным или чтобы поток приемника применялся к тишине) или через своего рода «многопотоковое ", который переключается между передачей потока и буфером тишины. Мне также еще предстоит наложить аудиофайлы (например, с помощью ffmpeg).
Возможно ли, чтобы Readable ожидал звуковой фрагмент и, если он не был предоставлен в течение определенного периода времени, вместо этого вставлял фрагмент тишины? Моя попытка сделать это ниже (опять же, на основе примера рекордера Discord.js):
// 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 */
}
В качестве ненужного бонуса было бы полезно, если бы можно было проверить, говорил ли пользователь когда-либо или нет, чтобы исключить создание пустых записей. Заранее спасибо.
Связанный:
После того, как я много прочитал о потоках Node, решение, которое я нашел, оказалось неожиданно простым.
recording
, которая принимает значение true, когда запись должна продолжаться, и значение false, когда она должна останавливаться.let buffer = [];
// 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));
read() {
if (recording) {
let delay = new NanoTimer();
delay.setTimeout(() => {
if (buffer.length > 0) {
this.push(buffer.shift());
}
else {
this.push(SILENCE);
}
}, '', '20m');
}
// ...
}
// ...
else if (buffer.length > 0) {
// Stream is ending: sending buffered audio ASAP
this.push(buffer.shift());
}
else {
// Ending stream
this.push(null);
}
Если сложить все вместе:
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.
}
Отсюда вы можете направить этот поток в fileWriteStream и т. д. для отдельных целей. Обратите внимание, что рекомендуется также закрывать ReceiveStream всякий раз, когда recording = false
с помощью чего-то вроде:
connection.receiver.subscriptions.delete(userId);
Кроме того, пользовательский поток тоже должен быть закрыт, если он не является, например, первым аргументом метода pipeline
.
В качестве примечания, хотя и выходящего за рамки моего первоначального вопроса, есть много других модификаций, которые вы можете внести в это. Например, вы можете добавить тишину к звуку перед передачей данных ReceiverStream в userStream, например, чтобы создать несколько аудиопотоков одинаковой длины:
// let startTime = ...
let creationTime;
for (let i = startTime; i < (creationTime = Date.now()); i++) {
buffer.push(SILENCE);
}
Удачного кодирования!