|
|
@@ -0,0 +1,267 @@
|
|
|
+package cn.iocoder.byzs.module.ai.controller.admin.tts;
|
|
|
+
|
|
|
+import com.alibaba.nls.client.AccessToken;
|
|
|
+import com.alibaba.nls.client.protocol.NlsClient;
|
|
|
+import com.alibaba.nls.client.protocol.OutputFormatEnum;
|
|
|
+import com.alibaba.nls.client.protocol.SampleRateEnum;
|
|
|
+import com.alibaba.nls.client.protocol.tts.StreamInputTts;
|
|
|
+import com.alibaba.nls.client.protocol.tts.StreamInputTtsListener;
|
|
|
+import com.alibaba.nls.client.protocol.tts.StreamInputTtsResponse;
|
|
|
+import jakarta.annotation.PostConstruct;
|
|
|
+import jakarta.annotation.PreDestroy;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.sound.sampled.*;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.ByteBuffer;
|
|
|
+import java.util.concurrent.ConcurrentLinkedQueue;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class StreamTtsService {
|
|
|
+
|
|
|
+ @Value("${ai.tts.appKey:4SUOF4LfaU7FekyW}")
|
|
|
+ private String appKey;
|
|
|
+
|
|
|
+ @Value("${ai.tts.token:20aa8ae2577b414db6c3cbbfcdc287be}")
|
|
|
+ private String token;
|
|
|
+
|
|
|
+ @Value("${ai.tts.url:wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1}")
|
|
|
+ private String url;
|
|
|
+
|
|
|
+
|
|
|
+ @Value("${ai.tts.ALIYUN_AK_ID:LTAI5tQhMPLXtSgXiPiWbw6D}")
|
|
|
+ private String ALIYUN_AK_ID;
|
|
|
+ @Value("${ai.tts.ALIYUN_AK_SECRET:HCXpFYjl4swk0qwfIKa9s2bXx0AWcG}")
|
|
|
+ private String ALIYUN_AK_SECRET;
|
|
|
+
|
|
|
+ private NlsClient client;
|
|
|
+ private PlaybackRunnable playbackRunnable;
|
|
|
+ private Thread playbackThread;
|
|
|
+ private StreamInputTts synthesizer;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ getToken();
|
|
|
+ // 初始化NlsClient
|
|
|
+ client = new NlsClient(url, token);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getToken(){
|
|
|
+ AccessToken accessToken = new AccessToken(ALIYUN_AK_ID, ALIYUN_AK_SECRET);
|
|
|
+ try {
|
|
|
+ accessToken.apply();
|
|
|
+ token = accessToken.getToken();
|
|
|
+ long expireTime = accessToken.getExpireTime();
|
|
|
+ log.info("token: {}, expireTime: {}", token, expireTime);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开始TTS语音合成
|
|
|
+ */
|
|
|
+ public void startTts() {
|
|
|
+ // 初始化播放器
|
|
|
+ playbackRunnable = new PlaybackRunnable(24000);
|
|
|
+ try {
|
|
|
+ playbackRunnable.prepare();
|
|
|
+ } catch (LineUnavailableException e) {
|
|
|
+ log.error("初始化音频播放器失败", e);
|
|
|
+ throw new RuntimeException("初始化音频播放器失败", e);
|
|
|
+ }
|
|
|
+ playbackThread = new Thread(playbackRunnable);
|
|
|
+ playbackThread.start();
|
|
|
+
|
|
|
+ // 创建TTS实例
|
|
|
+ try {
|
|
|
+ synthesizer = new StreamInputTts(client, getSynthesizerListener(playbackRunnable));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("创建TTS实例", e);
|
|
|
+ throw new RuntimeException("创建TTS实例", e);
|
|
|
+ }
|
|
|
+ synthesizer.setAppKey(appKey);
|
|
|
+ synthesizer.setFormat(OutputFormatEnum.PCM);
|
|
|
+ synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_24K);
|
|
|
+ synthesizer.setVoice("longxiaochun");
|
|
|
+ synthesizer.setVolume(50);
|
|
|
+ synthesizer.setPitchRate(0);
|
|
|
+ synthesizer.setSpeechRate(0);
|
|
|
+
|
|
|
+ try {
|
|
|
+ synthesizer.startStreamInputTts();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("启动TTS失败", e);
|
|
|
+ throw new RuntimeException("启动TTS失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送文本到TTS服务进行语音合成
|
|
|
+ */
|
|
|
+ public void sendText(String text) {
|
|
|
+ if (synthesizer == null) {
|
|
|
+ throw new IllegalStateException("TTS服务尚未启动,请先调用startTts()");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ synthesizer.sendStreamInputTts(text);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("发送文本到TTS服务失败", e);
|
|
|
+ throw new RuntimeException("发送文本到TTS服务失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 结束TTS语音合成
|
|
|
+ */
|
|
|
+ public void stopTts() {
|
|
|
+ try {
|
|
|
+ if (synthesizer != null) {
|
|
|
+ synthesizer.stopStreamInputTts();
|
|
|
+ synthesizer.close();
|
|
|
+ }
|
|
|
+ if (playbackRunnable != null) {
|
|
|
+ playbackRunnable.stop();
|
|
|
+ }
|
|
|
+ if (playbackThread != null) {
|
|
|
+ playbackThread.join();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("停止TTS服务失败", e);
|
|
|
+ } finally {
|
|
|
+ synthesizer = null;
|
|
|
+ playbackRunnable = null;
|
|
|
+ playbackThread = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private StreamInputTtsListener getSynthesizerListener(final PlaybackRunnable audioPlayer) {
|
|
|
+ return new StreamInputTtsListener() {
|
|
|
+ private boolean firstRecvBinary = true;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSynthesisStart(StreamInputTtsResponse response) {
|
|
|
+ log.info("TTS合成开始: name={}, status={}", response.getName(), response.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSentenceBegin(StreamInputTtsResponse response) {
|
|
|
+ log.info("句子开始合成: name={}, status={}", response.getName(), response.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSentenceEnd(StreamInputTtsResponse response) {
|
|
|
+ log.info("句子合成结束: name={}, status={}", response.getName(), response.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSynthesisComplete(StreamInputTtsResponse response) {
|
|
|
+ log.info("TTS合成完成: name={}, status={}", response.getName(), response.getStatus());
|
|
|
+ audioPlayer.stop();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAudioData(ByteBuffer message) {
|
|
|
+ if (firstRecvBinary) {
|
|
|
+ firstRecvBinary = false;
|
|
|
+ log.info("收到第一包语音数据");
|
|
|
+ }
|
|
|
+ byte[] bytesArray = new byte[message.remaining()];
|
|
|
+ message.get(bytesArray, 0, bytesArray.length);
|
|
|
+ audioPlayer.put(ByteBuffer.wrap(bytesArray));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSentenceSynthesis(StreamInputTtsResponse response) {
|
|
|
+ log.info("句子合成进度: name={}, status={}", response.getName(), response.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onFail(StreamInputTtsResponse response) {
|
|
|
+ log.error("TTS合成失败: session_id={}, task_id={}, status={}, message={}",
|
|
|
+ getStreamInputTts().getCurrentSessionId(),
|
|
|
+ response.getTaskId(),
|
|
|
+ response.getStatus(),
|
|
|
+ response.getStatusText());
|
|
|
+ audioPlayer.stop();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ class PlaybackRunnable implements Runnable {
|
|
|
+ private AudioFormat af;
|
|
|
+ private DataLine.Info info;
|
|
|
+ private SourceDataLine targetSource;
|
|
|
+ private AtomicBoolean runFlag;
|
|
|
+ private ConcurrentLinkedQueue<ByteBuffer> queue;
|
|
|
+
|
|
|
+ public PlaybackRunnable(int sample_rate) {
|
|
|
+ af = new AudioFormat(sample_rate, 16, 1, true, false);
|
|
|
+ info = new DataLine.Info(SourceDataLine.class, af);
|
|
|
+ targetSource = null;
|
|
|
+ runFlag = new AtomicBoolean(true);
|
|
|
+ queue = new ConcurrentLinkedQueue<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void prepare() throws LineUnavailableException {
|
|
|
+ targetSource = (SourceDataLine) AudioSystem.getLine(info);
|
|
|
+ targetSource.open(af, 4096);
|
|
|
+ targetSource.start();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void put(ByteBuffer buffer) {
|
|
|
+ queue.add(buffer);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void stop() {
|
|
|
+ runFlag.set(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ if (targetSource == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ while (runFlag.get()) {
|
|
|
+ if (queue.isEmpty()) {
|
|
|
+ try {
|
|
|
+ Thread.sleep(10);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ ByteBuffer buffer = queue.poll();
|
|
|
+ if (buffer == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ byte[] data = buffer.array();
|
|
|
+ targetSource.write(data, 0, data.length);
|
|
|
+ }
|
|
|
+ if (!queue.isEmpty()) {
|
|
|
+ ByteBuffer buffer;
|
|
|
+ while ((buffer = queue.poll()) != null) {
|
|
|
+ byte[] data = buffer.array();
|
|
|
+ targetSource.write(data, 0, data.length);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ targetSource.drain();
|
|
|
+ targetSource.stop();
|
|
|
+ targetSource.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreDestroy
|
|
|
+ public void destroy() {
|
|
|
+ if (client != null) {
|
|
|
+ client.shutdown();
|
|
|
+ }
|
|
|
+ stopTts();
|
|
|
+ }
|
|
|
+}
|