123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- """
- 新版播放器,包含划词朗读/AI朗读的TTS实时播放和语音合成的音频文件播放
- sudo apt-get install python3-pyqt5.qtmultimedia
- sudo apt-get install libqt5multimedia5-plugins
- """
- import threading
- from queue import Queue
- import sounddevice as sd
- import time
- import os
- import sys
- import config
- from PyQt5.QtWidgets import QApplication
- from libs import lib_tts, lib_tts_dipai
- import lib_tts_grpc
- from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
- from PyQt5.QtCore import QUrl, QObject, pyqtSignal
- # 获取家目录
- def get_home_env():
- if sys.platform == "win32":
- return 'APPDATA'
- return 'HOME'
- # 生成临时音频文件保存路径
- def gen_audio_save_path():
- _dir = os.path.join(os.environ[get_home_env()], "xAssistant", "tts_cache")
- if not os.path.exists(_dir):
- os.makedirs(_dir)
- _fp = f"{int(time.time())}.pcm"
- _audio_fp = os.path.join(_dir, _fp)
- print(_audio_fp)
- return _audio_fp
- # 计算待合成文本的音频时长(预估)
- def calculate_duration(text: str, speed_mode: int):
- """
- :param text: 待合成文本
- :param speed_mode: 语速模式 1:较快 2:较慢 其他为正常
- """
- if speed_mode == 1:
- _speed = 8.145
- elif speed_mode == 2:
- _speed = 4.064
- else:
- _speed = 5.358
- _duration = len(text) / _speed
- return int(_duration)
- # TTS临时文件播放线程
- class PlayTmpFile(threading.Thread):
- def __init__(self, fp, queue):
- super().__init__()
- self.daemon = True
- self._tmp_file = fp
- self._run_flag = True
- self._audio_queue = queue
- def run(self):
- if self._tmp_file is None or not os.path.exists(self._tmp_file):
- return
- self._audio_queue.queue.clear()
- with open(self._tmp_file, "rb") as f:
- while self._run_flag:
- data = f.read(4096)
- if not data:
- self._audio_queue.put("--end--")
- break
- self._audio_queue.put(data)
- time.sleep(0.02)
- def stop(self):
- self._run_flag = False
- # TTS播放器(支持播放、暂停、继续、停止)
- class TTSSteamPlayer(QObject):
- playing_signal = pyqtSignal()
- paused_signal = pyqtSignal()
- stopped_signal = pyqtSignal()
- def __init__(self, cb_fn):
- super().__init__()
- self._audio_queue = Queue()
- self._tmp_file = None
- self._is_paused = False
- # self._select_default_output_device()
- self._player = sd.RawOutputStream(samplerate=16000, channels=1, dtype='int16')
- self._player.start()
- self._tts_thread = None
- self._run_flag = True
- self._callback = cb_fn
- self.engine_type = config.config["ENGINE_TYPE"]
- # 设置默认输出设备
- def _select_default_output_device(self):
- try:
- system_output_device = sd.query_devices(kind='output')
- sd.default.device = system_output_device['index']
- except Exception as e:
- pass
- # 播放
- def play(self, tts_info: dict):
- self.playing_signal.emit()
- if self._is_paused:
- self._is_paused = False
- return
- self._audio_queue.queue.clear()
- if tts_info: # 有文本时,调用语音合成来播放实时音频
- self._tmp_file = gen_audio_save_path()
- text = tts_info.get("text")
- vcn = tts_info.get("vcn")
- speed = tts_info.get("speed")
- # self._tts_thread = lib_tts.TTSManagerNew(text, vcn, speed, self._audio_queue)
- # self._tts_thread = lib_tts_dipai.TTSManagerNew(text, vcn, speed, self._audio_queue)
- # self._tts_thread = lib_tts_grpc.TTSManagerNew(text, vcn, speed, self._audio_queue)
- if self.engine_type == "1":
- self._tts_thread = lib_tts.TTSManagerNew(text, vcn, speed, self._audio_queue)
- elif self.engine_type == "2":
- self._tts_thread = lib_tts_dipai.TTSManagerNew(text, vcn, speed, self._audio_queue)
- else:
- self._tts_thread = lib_tts_grpc.TTSManagerNew(text, vcn, speed, self._audio_queue)
- self._tts_thread.start()
- threading.Thread(target=self._write_stream, name="_write_stream", args=(False,), daemon=True).start()
- else: # 无文本时,播放临时文件
- self._tts_thread = PlayTmpFile(self._tmp_file, self._audio_queue)
- self._tts_thread.start()
- threading.Thread(target=self._write_stream, name="_write_stream", args=(True,), daemon=True).start()
- # 暂停
- def pause(self):
- self.paused_signal.emit()
- self._is_paused = True
- def resume(self):
- self._is_paused = False
- def is_paused(self):
- return self._is_paused
- # 停止
- def stop(self):
- self._is_paused = False
- self._audio_queue.queue.clear()
- self._audio_queue.put("--stop--")
- # 销毁播放器
- def quit(self):
- self._is_paused = False
- self._run_flag = False
- self._audio_queue.queue.clear()
- self._player.stop()
- # 向播放器写入音频流
- def _write_stream(self, is_file=False):
- while self._run_flag:
- if self._is_paused:
- time.sleep(0.02)
- continue
- if not self._audio_queue.empty():
- buf = self._audio_queue.get()
- if isinstance(buf, str):
- if buf == "--end--" or buf == "--stop--":
- if self._tts_thread:
- self._tts_thread.stop()
- self._callback(buf)
- else: # 异常
- self._callback(buf)
- break
- if not is_file:
- self._write_tmp(buf)
- try:
- self._player.write(buf)
- except:
- continue
- self.stopped_signal.emit()
- self._audio_queue.queue.clear()
- # 写入临时文件用于重新播放
- def _write_tmp(self, data):
- with open(self._tmp_file, "ab") as f:
- f.write(data)
- # 音频文件播放器
- class AudioPlayer(QObject):
- playing_signal = pyqtSignal()
- paused_signal = pyqtSignal()
- stopped_signal = pyqtSignal()
- rate_changed_signal = pyqtSignal(dict)
- def __init__(self):
- super().__init__()
- self.duration = 1
- self.position = 0
- self.media_player = QMediaPlayer()
- self.play_state = None
- self.media_player.stateChanged.connect(self.state_changed) # 获取播放状态
- self.media_player.positionChanged.connect(self.position_changed) # 获取播放进度
- self.media_player.durationChanged.connect(self.duration_changed) # 获取音频总时长
- def set_media(self, audio_path: str):
- self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(audio_path.replace('\\', '/'))))
- # 播放状态改变
- def state_changed(self, state):
- if state == QMediaPlayer.State.PlayingState:
- self.play_state = QMediaPlayer.State.PlayingState
- self.playing_signal.emit()
- elif state == QMediaPlayer.State.PausedState:
- self.play_state = QMediaPlayer.State.PausedState
- self.paused_signal.emit()
- elif state == QMediaPlayer.State.StoppedState:
- self.play_state = QMediaPlayer.State.StoppedState
- self.stopped_signal.emit()
- # 播放进度改变
- def position_changed(self, position):
- try:
- self.position = position
- rate = position / self.duration
- # 格式化时间
- # duration = self.format_time(self.duration)
- # position = self.format_time(position)
- info = {"rate": rate, "position": position, "duration": self.duration}
- self.rate_changed_signal.emit(info)
- except ZeroDivisionError:
- pass
- @staticmethod
- def format_time(input_time):
- minute = int(input_time / 60000)
- second = int((input_time - minute * 60000) / 1000)
- # 单数前面加0
- if minute < 10:
- minute = f"0{minute}"
- if second < 10:
- second = f"0{second}"
- output_time = f"{minute}:{second}"
- return output_time
- # 音频总时长改变
- def duration_changed(self, duration):
- self.duration = duration
- # 设置播放进度
- def set_position(self, float_val: float):
- val = float_val * self.duration
- self.media_player.setPosition(val)
- def set_time(self, val):
- self.media_player.setPosition(val)
- # 设置播放倍速
- def set_playback_speed(self, speed: float):
- if speed == 0.5:
- speed = 0.8
- self.media_player.setPlaybackRate(speed)
- # 播放
- def play(self, *args):
- self.media_player.play()
- # 暂停
- def pause(self):
- self.media_player.pause()
- # 继续
- def resume(self):
- self.media_player.play()
- # 停止
- def stop(self):
- try:
- self.media_player.stop()
- except:
- pass
- # 销毁播放器
- def quit(self):
- pass
- def is_playing(self):
- return self.play_state == QMediaPlayer.State.PlayingState
- if __name__ == '__main__':
- app = QApplication([])
- _play_audio_vlc_thread = AudioPlayer()
- _play_audio_vlc_thread.set_media(r"/media/llh/data/test/19.wav")
- _play_audio_vlc_thread.play()
- app.exec_()
|