Генерация 2-канального волнового файла из двух независимых потоков аудиоданных

Я передаю аудиоданные от двух клиентов в сети в общее серверное программное обеспечение, которое должно принимать указанные аудиоданные и объединять их в двухканальный волновой файл. И клиент, и сервер написаны мной.

Я изо всех сил пытаюсь совместить это на стороне сервера, и ключевой показатель в выходном волновом файле — это возможность воссоздать временные метки, с которыми общались пользователи. Что я хочу сделать, так это вывести каждого клиента (всего 2 на файл волны) в 2-канальный файл стереофонической волны.

Как правильно поступить в подобной ситуации? Нужно ли менять клиентов для потоковой передачи аудиоданных по-другому? Кроме того, что вы рекомендуете в качестве подхода к работе с паузами в аудиопотоке, т. е. захвата задержек между пользователями, нажимающими кнопку «нажми и говори», когда на сервер не поступают сообщения?

В настоящее время клиентское программное обеспечение использует pyaudio для записи с устройства ввода по умолчанию и отправки отдельных кадров по сети с использованием TCP/IP. Одно сообщение на кадр. Клиенты работают по принципу «нажми и говори» и отправляют аудиоданные только тогда, когда удерживается кнопка «нажми и говори», в противном случае сообщения не отправляются.

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

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

class AudioRepository(object):
    def __init__(self, root_directory, test_id, player_id):
        self.test_id = test_id
        self.player_id = player_id

        self.audio_filepath = os.path.join(root_directory, "{0}_{1}_voice_chat.wav".format(test_id, player_id))
        self.audio_wave_writer = wave.open(self.audio_filepath, "wb")
        self.audio_wave_writer.setnchannels(1)
        self.audio_wave_writer.setframerate(44100)
        self.audio_wave_writer.setsampwidth(
            pyaudio.get_sample_size(pyaudio.paInt16))
        self.first_audio_record = True
        self.previous_audio_time = datetime.datetime.now()

    def write(self, record: Record):
        now = datetime.datetime.now()
        time_passed_since_last = now - self.previous_audio_time
        number_blank_frames = int(44100 * time_passed_since_last.total_seconds())
        blank_data = b"\0\0" * number_blank_frames
        if not self.first_audio_record and time_passed_since_last.total_seconds() >= 1:
            self.audio_wave_writer.writeframes(blank_data)
        else:
            self.first_audio_record = False

        self.audio_wave_writer.writeframes(
            record.additional_data["audio_data"])
        self.previous_audio_time = datetime.datetime.now()

    def close(self):
        self.audio_wave_writer.close()

Я набрал это, потому что код находится на машине без доступа к Интернету, так что извините, если форматирование и/или опечатки.

Это также демонстрирует, что я сейчас делаю, чтобы управлять временем между передачами, которое работает умеренно хорошо. Ограничение скорости — это хак и вызывает проблемы, но я думаю, что у меня есть реальное решение для этого. Клиенты отправляют сообщения, когда пользователи нажимают и отпускают кнопку разговора, поэтому я могу использовать их как флаги для приостановки вывода пустых кадров, пока пользователь отправляет мне настоящие аудиоданные (что было настоящей проблемой, когда пользователи отправляли аудиоданные, я добавлял кучу маленьких крошечных пауз, которые делали звук прерывистым).

Ожидаемое решение состоит в том, чтобы сделать приведенный выше код больше не привязанным к одному идентификатору игрока, а вместо этого запись будет вызываться с записями от обоих клиентов сервера (но все же будет по одному от каждого игрока индивидуально, а не вместе) и чередование аудиоданные от каждого в 2-канальный волновой файл с каждым проигрывателем на отдельном канале. Я просто ищу предложения о том, как справиться с деталями этого. Мои первоначальные мысли заключаются в том, что необходимо будет задействовать поток и две очереди аудиокадров от каждого клиента, но я все еще не уверен, как объединить все это в волновой файл и сделать так, чтобы он звучал правильно и был правильно рассчитан.


person TheKewlStore    schedule 14.08.2019    source источник


Ответы (1)


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

class AudioRepository(Repository):
    def __init__(self, test_id, board_sequence):
        Repository.__init__(self, test_id, board_sequence)

        self.audio_filepath = os.path.join(self.repository_directory, "{0}_voice_chat.wav".format(test_id))
        self.player1_audio_segment = AudioSegment.empty()
        self.player2_audio_segment = AudioSegment.empty()

        self.player1_id = None
        self.player2_id = None

        self.player1_last_record_time = datetime.datetime.now()
        self.player2_last_record_time = datetime.datetime.now()

    def write_record(self, record: Record):
        player_id = record.additional_data["player_id"]

        if record.event_type == Record.VOICE_TRANSMISSION_START:
            if self.is_player1(player_id):
                time_elapsed = datetime.datetime.now() - self.player1_last_record_time
                segment = AudioSegment.silent(time_elapsed.total_seconds() * 1000)
                self.player1_audio_segment += segment
            elif self.is_player2(player_id):
                time_elapsed = datetime.datetime.now() - self.player2_last_record_time
                segment = AudioSegment.silent(time_elapsed.total_seconds() * 1000)
                self.player2_audio_segment += segment
        elif record.event_type == Record.VOICE_TRANSMISSION_END:
            if self.is_player1(player_id):
                self.player1_last_record_time = datetime.datetime.now()
            elif self.is_player2(player_id):
                self.player2_last_record_time = datetime.datetime.now()

        if not record.event_type == Record.VOICE_MESSAGE_SENT:
            return

        frame_data = record.additional_data["audio_data"]
        segment = AudioSegment(data=frame_data, sample_width=2, frame_rate=44100, channels=1)

        if self.is_player1(player_id):
            self.player1_audio_segment += segment
        elif self.is_player2(player_id):
            self.player2_audio_segment += segment

    def close(self):
        Repository.close(self)

        # pydub's AudioSegment.from_mono_audiosegments expects all the segments given to be of the same frame count.
        # To ensure this, we check each segment's length and pad with silence as necessary.
        player1_frames = self.player1_audio_segment.frame_count()
        player2_frames = self.player2_audio_segment.frame_count()
        frames_needed = abs(player1_frames - player2_frames)
        duration = frames_needed / 44100
        padding = AudioSegment.silent(duration * 1000, frame_rate=44100)

        if player1_frames > player2_frames:
           self.player2_audio_segment += padding
        elif player1_frames < player2_frames:
            self.player1_audio_segment += padding

        stereo_segment = AudioSegment.from_mono_audiosegments(self.player1_audio_segment, self.player2_audio_segment)
        stereo_segment.export(self.audio_filepath, format="wav")

Таким образом, я сохраняю два аудиосегмента как независимые аудиосегменты на протяжении всего сеанса и объединяю их в один стереосегмент, который затем экспортируется в wav-файл репозитория. pydub также упростил отслеживание сегментов молчания, потому что я до сих пор не думаю, что действительно понимаю, как работают звуковые «кадры» и как генерировать нужное количество кадров для определенной продолжительности тишины. Тем не менее, pydub, безусловно, делает это и позаботится об этом за меня!

person TheKewlStore    schedule 14.08.2019