从两个独立的音频数据流中生成2声道波形文件

时间:2019-08-14 14:25:31

标签: python-3.x pyaudio wave

我正在将来自网络上两个客户端的音频数据流式传输到通用服务器软件中,该服务器软件需要获取所述音频数据并将其组合成两个通道的wave文件。客户端和服务器都是我编写的软件。

我在如何在服务器端进行合并方面很挣扎,输出wave文件中的一个关键指标是能够重新创建用户交谈的时间戳。我要做的是将每个客户端(每个wave文件只有2个)输出到2声道立体声wave文件中。

如何正确处理这种情况?客户是否需要更改以不同的方式传输音频数据?另外,作为处理音频流中的停顿的一种方法,即当没有消息发送到服务器时,捕获用户按下按键通话按钮之间的延迟,您建议什么?

当前,客户端软件正在使用pyaudio从默认输入设备进行录制,并使用TCP / IP通过网络发送单个帧。每帧一条消息。客户端以一键通方式工作,并且仅在按住一键通按钮时发送音频数据,否则不发送任何消息。

我已经对WAVE文件格式进行了相当多的研究,并且我知道要做到这一点,我需要为每个写入的帧对每个通道的样本进行交织,这就是我产生混乱的主要根源。由于这种环境的动态性质以及在服务器端处理音频数据的同步方法,大多数情况下,我不会同时获得两个客户端的数据,但是如果我这样做,我将不会告诉服务器一次将两个框架同时写入的良好逻辑机制。

到目前为止,这里是我处理来自客户端的音频的工具。为每个客户端创建一个此类的实例,因此为每个客户端创建一个单独的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()

之所以输入是因为代码位于无法访问互联网的计算机上,所以很抱歉,如果格式混乱或错别字。

这也说明了我当前正在做的工作以处理两次传输之间的时间,该工作效果很好。限速的东西是hack,确实会引起问题,但是我认为我对此有一个真正的解决方案。当用户按下并释放一键通按钮时,客户端会发送消息,因此只要用户向我发送真实的音频数据,我就可以将其用作标记来暂停空白帧的输出(这是真正的问题,当用户在发送音频数据时,我会放一堆小小的停顿,使音频变得断断续续。)

预期的解决方案是使上面的代码不再绑定到单个玩家ID,而是使用服务器的两个客户端的记录调用写入(但仍将分别来自每个玩家,而不是一起来自每个玩家) ),并将每个音频数据插入2通道的Wave文件中,而每个播放器都在单独的通道上。我只是在寻找有关如何处理此细节的建议。我最初的想法是一个线程,每个客户端都需要涉及两个队列的音频帧,但是我仍然对如何将它们全部组合到wave文件中并使其听起来正确并适时地工作感到困惑。

1 个答案:

答案 0 :(得分:0)

我设法使用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确实会帮我照顾它!