从两个独立的音频数据流生成一个 2 通道波形文件
Generating a 2-channel wave file from two independent streams of audio data
我正在将来自网络上两个客户端的音频数据流式传输到一个通用服务器软件中,该软件需要获取所述音频数据并将其组合成一个双通道波形文件。客户端和服务器都是我写的软件。
我正在努力研究如何在服务器端将其组合起来,输出 wave 文件中的一个关键指标是能够重新创建用户交谈的时间戳。我想做的是将每个客户端(每个波形文件只有 2 个)输出到一个 2 通道立体声波形文件中。
我该如何正确处理这种情况?客户端是否需要更改以不同方式流式传输音频数据?此外,您推荐什么方法来处理音频流中的暂停,即在没有消息到达服务器时捕获用户按下一键通按钮之间的延迟?
目前,客户端软件正在使用 pyaudio 从默认输入设备进行录制,并使用 TCP/IP 通过网络发送单独的帧。每帧一条消息。客户端以一键通方式工作,只有在按住一键通按钮时才发送音频数据,否则不发送任何消息。
我对 WAVE 文件格式做了相当多的研究,我知道要做到这一点,我需要为写入的每一帧交织来自每个通道的样本,这是我的主要困惑来源从。由于这种环境的动态特性以及在服务器端处理音频数据的同步方法,大多数时候我不会同时拥有来自两个客户端的数据,但如果我这样做了,我将不会有一个告诉服务器同时写入两个帧的良好逻辑机制。
这是我目前处理来自客户端的音频的方法。此 class 的一个实例是为每个客户端创建的,因此为每个客户端创建了一个单独的 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"[=10=][=10=]" * 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()
我输入这个是因为代码是在一台没有互联网访问权限的机器上,如果格式有误,我们深表歉意and/or 打字错误。
这也展示了我目前正在做什么来处理传输之间的时间,效果还不错。速率限制是一个 hack,确实会引起问题,但我认为我有一个真正的解决方案。当用户按下并释放按键通话按钮时,客户端发送消息,所以我可以使用这些作为标志来暂停空白帧的输出,只要用户向我发送真实的音频数据(这是真正的问题,当用户在发送音频数据时,我插入了一堆微小的停顿,这使音频断断续续。
预期的解决方案是让上面的代码不再与单个玩家 ID 绑定,而是使用来自服务器的两个客户端的记录调用 write(但仍然是每个玩家单独的一个,不在一起)并将每个播放器的音频数据交织成一个 2 通道波形文件,每个播放器都在一个单独的通道上。我只是在寻找有关如何处理此细节的建议。我最初的想法是需要涉及一个线程和来自每个客户端的两个音频帧队列,但我仍然不确定如何将它们全部组合到 wave 文件中并使其听起来正确并正确计时。
我设法使用 pydub 解决了这个问题,post在这里使用我的解决方案以防其他人偶然发现这个问题。通过跟踪客户端软件已经发送的传输开始和结束事件,我克服了原始 post 中提到的使用静默保持准确时间戳的问题。
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 还使跟踪无声片段变得更容易,因为我认为我仍然不真正了解音频 "frames" 的工作原理以及如何为特定的无声持续时间生成适量的帧。尽管如此,pydub 确实会帮我处理好它!
我正在将来自网络上两个客户端的音频数据流式传输到一个通用服务器软件中,该软件需要获取所述音频数据并将其组合成一个双通道波形文件。客户端和服务器都是我写的软件。
我正在努力研究如何在服务器端将其组合起来,输出 wave 文件中的一个关键指标是能够重新创建用户交谈的时间戳。我想做的是将每个客户端(每个波形文件只有 2 个)输出到一个 2 通道立体声波形文件中。
我该如何正确处理这种情况?客户端是否需要更改以不同方式流式传输音频数据?此外,您推荐什么方法来处理音频流中的暂停,即在没有消息到达服务器时捕获用户按下一键通按钮之间的延迟?
目前,客户端软件正在使用 pyaudio 从默认输入设备进行录制,并使用 TCP/IP 通过网络发送单独的帧。每帧一条消息。客户端以一键通方式工作,只有在按住一键通按钮时才发送音频数据,否则不发送任何消息。
我对 WAVE 文件格式做了相当多的研究,我知道要做到这一点,我需要为写入的每一帧交织来自每个通道的样本,这是我的主要困惑来源从。由于这种环境的动态特性以及在服务器端处理音频数据的同步方法,大多数时候我不会同时拥有来自两个客户端的数据,但如果我这样做了,我将不会有一个告诉服务器同时写入两个帧的良好逻辑机制。
这是我目前处理来自客户端的音频的方法。此 class 的一个实例是为每个客户端创建的,因此为每个客户端创建了一个单独的 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"[=10=][=10=]" * 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()
我输入这个是因为代码是在一台没有互联网访问权限的机器上,如果格式有误,我们深表歉意and/or 打字错误。
这也展示了我目前正在做什么来处理传输之间的时间,效果还不错。速率限制是一个 hack,确实会引起问题,但我认为我有一个真正的解决方案。当用户按下并释放按键通话按钮时,客户端发送消息,所以我可以使用这些作为标志来暂停空白帧的输出,只要用户向我发送真实的音频数据(这是真正的问题,当用户在发送音频数据时,我插入了一堆微小的停顿,这使音频断断续续。
预期的解决方案是让上面的代码不再与单个玩家 ID 绑定,而是使用来自服务器的两个客户端的记录调用 write(但仍然是每个玩家单独的一个,不在一起)并将每个播放器的音频数据交织成一个 2 通道波形文件,每个播放器都在一个单独的通道上。我只是在寻找有关如何处理此细节的建议。我最初的想法是需要涉及一个线程和来自每个客户端的两个音频帧队列,但我仍然不确定如何将它们全部组合到 wave 文件中并使其听起来正确并正确计时。
我设法使用 pydub 解决了这个问题,post在这里使用我的解决方案以防其他人偶然发现这个问题。通过跟踪客户端软件已经发送的传输开始和结束事件,我克服了原始 post 中提到的使用静默保持准确时间戳的问题。
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 还使跟踪无声片段变得更容易,因为我认为我仍然不真正了解音频 "frames" 的工作原理以及如何为特定的无声持续时间生成适量的帧。尽管如此,pydub 确实会帮我处理好它!