Интерполировать тишину в потоке Discord.js

Я делаю бота для разногласий с 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 */
}

В качестве ненужного бонуса было бы полезно, если бы можно было проверить, говорил ли пользователь когда-либо или нет, чтобы исключить создание пустых записей. Заранее спасибо.

Связанный:

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
92
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

После того, как я много прочитал о потоках Node, решение, которое я нашел, оказалось неожиданно простым.

  1. Создайте логическую переменную recording, которая принимает значение true, когда запись должна продолжаться, и значение false, когда она должна останавливаться.
  2. Создайте буфер для обработки обратного давления (т. Е. Когда данные вводятся с большей скоростью, чем их вывод)
let buffer = [];
  1. Создайте читаемый поток, для которого пользовательский аудиопоток передается в
// 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));
  1. В методе чтения этого потока обработайте запись потока с помощью таймера 48 кГц, чтобы он соответствовал частоте дискретизации пользовательского аудиопотока.
read() {
   if (recording) {
        let delay = new NanoTimer();
        delay.setTimeout(() => {
            if (buffer.length > 0) {
                this.push(buffer.shift());
            }
            else {
                this.push(SILENCE);
            }
        }, '', '20m');
    }
    // ...
}
  1. В том же методе также обработайте завершение потока
        // ...
        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);
}

Удачного кодирования!

Другие вопросы по теме