OpenVoice实时语音克隆功能实现

前言

        在【OpenVoice本地部署教程与踩坑记录】一文中介绍了OpenVoice的基本概念与,并且完成了项目的安装与运行。官方给的示例和用法中仅包含了文本转TTS再克隆音色的功能,仅能用于TTS场景下的文字朗读。

        本文基于官方示例改造,实现了实时采集麦克风音频进行语音克隆的功能。在阅读项目论文理论后,少量修改了官方源码,取得了不错的实测效果。

论文

        项目论文可以在这里查看【OpenVoice: Versatile Instant Voice Cloning】,我将原文机器翻译成了中文,也可以参考中文版的论文【百度网盘,提取码: d7ja】。

        阅读论文后可以知道,OpenVoice将 IVC 任务解耦为独立的子任务,与耦合任务相比,每个子任务都更容易实现,音调颜色的克隆与所有其余风格参数和语言的控制完全分离。我们也可以不依赖原有的TTS,可以使用本地录音进行音色转换。在阅读完论文和相关代码后发现大部分为音频数据的基础数学计算,未包含前后强逻辑关联,故我们可以引入Buff的概念,将音频数据一段一段的送入转换,所以实时音频转换从理论上是可行的。

*我不是专业研究员,上述内容如有错误还望不吝赐教

实现

        我们实现的基本思路是利用pyaudio库从麦克风实时采集np.float32的音频数据,然后传入到convert方法中进行转换,将转换后的结果同样通过pyaudio库播放即可。

pyaudio安装

        使用pip下载pyaudio库即可正常使用:

pip install pyaudio

pyaudio基础使用

        关于pyaudio的基础使用可以参考网上的其它文章,这个库用起来不复杂,仅需要简单配置即可,我让chatGPT帮我写了一个测试示例供大家参考:

import pyaudio
import numpy as npdef play_and_record():# 设置参数FORMAT = pyaudio.paInt16CHANNELS = 1  # 单声道RATE = 44100  # 采样率,您可以根据需要调整CHUNK = 1024  # 每次读取的样本数p = pyaudio.PyAudio()# 打开麦克风stream_in = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,input=True,frames_per_buffer=CHUNK)# 打开扬声器stream_out = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,output=True,frames_per_buffer=CHUNK)print("开始录音并播放...")try:while True:# 从麦克风读取数据data = stream_in.read(CHUNK)# 将数据写入扬声器以播放stream_out.write(data)except KeyboardInterrupt:print("停止录音并播放。")stream_in.stop_stream()stream_in.close()stream_out.stop_stream()stream_out.close()p.terminate()if __name__ == '__main__':play_and_record()

        运行上述代码后,您应该能够从麦克风采集声音并立即通过扬声器播放。请注意,此示例简单地读取和播放声音,没有进行任何处理或分析。

OpenVoice修改

        经过上文的入门后,相信你对OpenVoice的运行方式有了一个基础认识,OpenVoice核心的方法就是通过convert调用转换,要求我们传入源文件、目标输出、相关音色文件等参数,调用的代码看起来像这样:

tone_color_converter.convert(audio_src_path=src_path, src_se=source_se, tgt_se=target_se, output_path=save_path,message=encode_message)

        我们进入convert方法里面,可以看到是使用librosa库读取音频文件,采样率为22050(hps.data.sampling_rate中定义)。然后转换为torch数据传入后文进行处理,进行上文提到的音频数据的基础数学计算(OpenVoice框架图中的黄色部分),在处理完成后得到audio原始数据,最后进行添加水印的操作后存储到目标文件或直接返回。这里的代码十分清晰明了,完整的还原了框架图中的步骤,我们的改造不必关心底层的处理逻辑,到API这一层已经足够满足功能了。

    def convert(self, audio_src_path, src_se, tgt_se, output_path=None, tau=0.3, message="default"):hps = self.hps# load audioaudio, sample_rate = librosa.load(audio_src_path, sr=hps.data.sampling_rate)audio = torch.tensor(audio).float()with torch.no_grad():y = torch.FloatTensor(audio).to(self.device)y = y.unsqueeze(0)spec = spectrogram_torch(y, hps.data.filter_length,hps.data.sampling_rate, hps.data.hop_length, hps.data.win_length,center=False).to(self.device)spec_lengths = torch.LongTensor([spec.size(-1)]).to(self.device)audio = self.model.voice_conversion(spec, spec_lengths, sid_src=src_se, sid_tgt=tgt_se, tau=tau)[0][0, 0].data.cpu().float().numpy()audio = self.add_watermark(audio, message)if output_path is None:return audioelse:soundfile.write(output_path, audio, hps.data.sampling_rate)

        在理解了官方代码后,我们加入断点进行debug查看各个环节的数据流转。librosa库读取出来的是float32的np数组格式的音频数据,最后生成的audio也是float32的np数组格式的音频数据,我们需要保证实时音频满足相应的数据格式。我们仅需完成以下步骤的改造即可:

  1. 使用pyaudio采集22050采样率的float32格式的音频
  2. 将pyaudio读取到的byte数据转为np数组
  3. 修改入参,传入实时音频数据而不是音频文件地址
  4. 取消librosa文件读取相关代码
  5. 取消水印代码,因为加水印对音频时长有要求
  6. 取消文件保存相关代码,直接返回原始音频数组

        我们的核心思路是将一整段音频文件转换的方式变为一小段一小段的实时音频转换的方式,所以需要引入buff的概念,经过实测发现我们的buff长度最好不低于10000(0.9s),这个参数可以在pyaudio中进行配置,完整的代码如下

import pyaudio
import numpy as np
import torch
import se_extractor
from api import ToneColorConverterckpt_converter = 'checkpoints/converter'
# 使用CPU推理
device = 'cpu'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
# 欲克隆的声音,建议自行录制一段其他人的音频,官方例子是英语,对中文不太友好
reference_speaker = 'resources/example_reference.mp3'
target_se, _ = se_extractor.get_se(reference_speaker, tone_color_converter, target_dir='processed', vad=True)
# 当前说话人的音色文件,可以录音一段然后通过上面的方法生成,在processed文件夹中就可以找到
source_se = torch.load(f'processed/yl3/se.pth').to(device)def play_and_record():# 设置参数FORMAT = pyaudio.paFloat32CHANNELS = 1  # 单声道RATE = 22050  # 采样率,固定BUFF = 10000  # 每次读取的样本数,每段的音频长度BUFF_OUT = 9984  # 每次播放的样本数,convert后长度有一定变化,避免产生杂音p = pyaudio.PyAudio()# 打开麦克风stream_in = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,input=True,frames_per_buffer=BUFF)# 打开扬声器stream_out = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,output=True,frames_per_buffer=BUFF_OUT) print("开始录音并播放...")try:while True:# 从麦克风读取数据data = stream_in.read(BUFF)# 转换为np数组data = np.frombuffer(data, dtype=np.float32)# 实时变音audio = tone_color_converter.convertRealTime(audio_data= data, src_se=source_se, tgt_se=target_se)# 将数据写入扬声器以播放stream_out.write(audio.tobytes())except KeyboardInterrupt:print("停止录音并播放。")stream_in.stop_stream()stream_in.close()stream_out.stop_stream()stream_out.close()p.terminate()if __name__ == '__main__':play_and_record()

修改api.py文件,加入convertRealTime方法:

def convertRealTime(self, audio_data, src_se, tgt_se, tau=0.3):hps = self.hps# 重采样,可选# audio = librosa.resample(audio_data, orig_sr=16000, target_sr=22050)audio = torch.tensor(audio_data).float()with torch.no_grad():y = torch.FloatTensor(audio).to(self.device)y = y.unsqueeze(0)spec = spectrogram_torch(y, hps.data.filter_length,hps.data.sampling_rate, hps.data.hop_length, hps.data.win_length,center=False).to(self.device)spec_lengths = torch.LongTensor([spec.size(-1)]).to(self.device)audio = self.model.voice_conversion(spec, spec_lengths, sid_src=src_se, sid_tgt=tgt_se, tau=tau)[0][0, 0].data.cpu().float().numpy()# 直接返回音频数据return audio

优化

        如果一切顺利,我相信你已经跑起来了,并可以实时的听到你说出的话被正常采集和变音。不过你会发现CPU利用率会一直很高,因为我们持续不断的在采集和处理麦克风的数据,即使没有任何人在说话。为了解决这一问题,我们需要进行静音检测,在音频能量值(响度)大于某一个阈值时才激活音频转换,这样可以避免不必要的消耗。

        在实现的过程中会发现基于每个音频包的静音检测会有一定的延迟并会导致丢字的现象,原因不难理解,我们上文提到buff的长度未10000,这是一个很大的值,会引入大约0.9s的延迟。如果我们说话的起始音处于buff的靠后位置,这样程序还是会认为我们处于静音状态,直到下一个包才激活,这样就会丢失最开始的一个字。反之亦然,如果结束音处于buff的靠前位置,那么就会丢失末尾的一个字。

        我们需要将10000长度的buff进行拆分,只要存在超过阈值的响度就认为是激活状态,这样就可以避免首字或尾音丢失的问题。在从激活到静音的过程中,我们引入了一个静音包的过渡,这样会更好的保证音频的自然度。

        上述的优化方案很简单,不过实际效果还是很不错,因为时间有限,未进行更深入的研究,希望社区能够共创更优的解决方案。

优化后的完整代码:

import pyaudio
import numpy as np
import torch
import se_extractor
from api import ToneColorConverterckpt_converter = 'checkpoints/converter'
# 使用CPU推理
device = 'cpu'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
# 欲克隆的声音,建议自行录制一段其他人的音频,官方例子是英语,对中文不太友好
reference_speaker = 'resources/example_reference.mp3'
target_se, _ = se_extractor.get_se(reference_speaker, tone_color_converter, target_dir='processed', vad=True)
# 当前说话人的音色文件,可以录音一段然后通过上面的方法生成,在processed文件夹中就可以找到
source_se = torch.load(f'processed/yl3/se.pth').to(device)# 上一次静音状态缓存,用于尾音平滑
lastSilence = True
# 静音阈值(根据实际需求调整)
THRESHOLD_ENERGY = 0.05
# 静音检测
def is_silence(frame):# 将10000的buff拆分为2000,减少延迟,提高灵敏度for i in range(5):# 计算能量energy = np.sum(frame[i*2000 : (i+1)*2000] ** 2) / len(frame[i*2000 : (i+1)*2000])print(energy)# 检测音频帧是否为静音if energy > THRESHOLD_ENERGY:# 只要有一个buff超过阈值就返回不是静音return False# 是静音return Truedef play_and_record():# 设置参数FORMAT = pyaudio.paFloat32CHANNELS = 1  # 单声道RATE = 22050  # 采样率,固定BUFF = 10000  # 每次读取的样本数,每段的音频长度BUFF_OUT = 9984  # 每次播放的样本数,convert后长度有一定变化,避免产生杂音p = pyaudio.PyAudio()# 打开麦克风stream_in = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,input=True,frames_per_buffer=BUFF)# 打开扬声器stream_out = p.open(format=FORMAT,channels=CHANNELS,rate=RATE,output=True,frames_per_buffer=BUFF_OUT) print("开始录音并播放...")try:while True:# 从麦克风读取数据data = stream_in.read(BUFF)# 转换为np数组data = np.frombuffer(data, dtype=np.float32)# 当前静音状态currSilence = is_silence(data)# 当前是静音 且 上次是静音,保证一个包的平滑过渡if currSilence and lastSilence:print("静音")else:print("有声音")# 实时变音audio = tone_color_converter.convertRealTime(audio_data= data, src_se=source_se, tgt_se=target_se)# 将数据写入扬声器以播放stream_out.write(audio.tobytes())# 缓存上次状态lastSilence = currSilenceexcept KeyboardInterrupt:print("停止录音并播放。")stream_in.stop_stream()stream_in.close()stream_out.stop_stream()stream_out.close()p.terminate()if __name__ == '__main__':play_and_record()

*如果本文对您有帮助,求三连(点赞、收藏、关注)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/333914.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亿尚网:撤柜上线电商+直播将成为美妆行业发展的绝佳组合

亿尚网:撤柜上线电商直播将成为美妆行业发展的绝佳组合 来源: 编辑:亿尚风范 时间:2024-01-09 随着社交媒体的兴起,网红经济逐渐成为市场中的一股不可忽视的力量。而在这其中,直播电商的模式更是为网红们…

前端实现搜索功能

最近遇到一个需求,用户在输入框输入关键字之后,点击搜索按钮后进行搜索,如下图,选中的数据在下面,上面展现的是搜索后的数据,现在选中了2条数据: 当用户输入KET后点击搜索,搜出的结果有16条,勾选全选选中后,将选中的16条的数据加到之前已选的2条数据里,于是此时已选…

阿里云云服务器u1实例和e实例有什么区别?

阿里云服务器u1和e实例有什么区别?ECS通用算力型u1实例是企业级独享型云服务器,ECS经济型e实例是共享型云服务器,所以相比较e实例,云服务器u1性能更好一些。e实例为共享型云服务器,共享型实例采用非绑定CPU调度模式&am…

如何启动、停止rocketmq

RocketMQ 是一个分布式消息中间件,启动、停止RocketMQ 实际上意味着重启 RocketMQ 的各个组件,包括 Namesrv、Broker 等。以下是一个通用的重启步骤,具体步骤可能会根据你的部署方式和 RocketMQ 版本有所不同: 1. 停止rocketmq …

prometheus 监控 Hyperledger Fabric 网络

本例中使用的 fabric 版本为 2.4.1 修改 orderer , peer 节点 docker-compose 文件 orderer 节点: environment:- ORDERER_METRICS_PROVIDERprometheus- ORDERER_OPERATIONS_LISTENADDRESS0.0.0.0:8443 ports:- 8443:8443peer 节点: environment:- CO…

ConcurrentLinkedQueue原理探究

ConcurrentLinkedQueue是线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。下面我们来看具体实现。 类图结构 为了能从全局直观地了解ConcurrentLinkedQueue的内部构造,先简单介绍Concu…

步入式汽车零件阳光模拟试验箱

汽车零件阳光模拟试验箱辐照光源采用金属卤素灯管,该产品按照其技术性能规定保证了光谱分布,非常适合运用在 汽车零件阳光模拟系统中。灯具的反光罩使用对称抛物面反射,以保证其高平行光束反射率。灯组由带有实际功率调节功能的电子电源&am…

大前端nestjs入门教程系列(四):如何nestjs整合mysql数据库

经过前面的几篇文章,想必大家已经对nestjs有了基础的了解,那么这篇文章就带大家玩玩数据库,学会了这篇,就离大前端又进了一步 Nest与数据库无关,使你可以轻松地与任何 SQL 或 NoSQL 数据库集成。 根据你的喜好&#xf…

javaweb基础----JDBC(一)

一、什么是JDBC JDBC全称为Java数据库连接(Java Database Connectivity),是一套用于执行SQL语句的Java API。应用程序可以通过这套API连接到关系型数据库,并使用SQL语句完成对数据中数据的查询、增加、更新和删除等操作。 JDBC在应用程序与数据库之间起到了一个桥…

C语言KR圣经笔记 5.6指针数组;指针的指针

5.6 指针数组;指针的指针 因为指针本身也是变量,所以它们也能像其他变量一样保存在数组里面。我们写个程序来说明,该程序将一些文本行按照字母顺序排列,算是 UNIX 程序 sort 的精简版本。 在第三章中,我们介绍了对一…

首个!美创科技助力大型能源央企数据安全能力成熟度评估及规划建设

2024开年,再传捷报。美创科技首个核电行业数据安全治理项目,也是首个大型能源央企数据安全治理项目落地!美创数据安全治理咨询团队,助力用户完成数据安全现状评估、数据安全体系标准设计和落地、数据分类分级落地试点、数据安全三…

性能分析与调优: Linux 使用 iperf3 进行TCP网络吞吐量测试

目录 一、实验 1.环境 2.TCP网络吞吐量的微观基准测试 二、问题 1.iperf参数有哪些 2.iperf如何二进制安装 一、实验 1.环境 (1)主机 表1-1 主机 主机架构组件IP备注prometheus 监测 系统 prometheus、node_exporter 192.168.204.18grafana监测…