mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	add PCM16 audio stream to wave is reday
This commit is contained in:
		@@ -1,8 +1,8 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "iconfont"; /* Project id 4125778 */
 | 
			
		||||
  src: url('iconfont.woff2?t=1726622198991') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1726622198991') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1726622198991') format('truetype');
 | 
			
		||||
  src: url('iconfont.woff2?t=1728891448746') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1728891448746') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1728891448746') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.iconfont {
 | 
			
		||||
@@ -13,6 +13,14 @@
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-call:before {
 | 
			
		||||
  content: "\e769";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-hung-up:before {
 | 
			
		||||
  content: "\e609";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-paypal:before {
 | 
			
		||||
  content: "\e666";
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -5,6 +5,20 @@
 | 
			
		||||
  "css_prefix_text": "icon-",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "glyphs": [
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "11231556",
 | 
			
		||||
      "name": "打电话",
 | 
			
		||||
      "font_class": "call",
 | 
			
		||||
      "unicode": "e769",
 | 
			
		||||
      "unicode_decimal": 59241
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "21969717",
 | 
			
		||||
      "name": "挂机",
 | 
			
		||||
      "font_class": "hung-up",
 | 
			
		||||
      "unicode": "e609",
 | 
			
		||||
      "unicode_decimal": 58889
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "7443846",
 | 
			
		||||
      "name": "PayPal",
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -324,6 +324,12 @@ const routes = [
 | 
			
		||||
        meta: {title: '测试页面'},
 | 
			
		||||
        component: () => import('@/views/Test.vue'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'test2',
 | 
			
		||||
        path: '/test2',
 | 
			
		||||
        meta: {title: '测试页面'},
 | 
			
		||||
        component: () => import('@/views/Test2.vue'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'NotFound',
 | 
			
		||||
        path: '/:all(.*)',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="video-call-container">
 | 
			
		||||
    <div class="wave-animation">
 | 
			
		||||
      <div v-for="i in 5" :key="i" class="wave-ellipse"></div>
 | 
			
		||||
    <div class="wave-container">
 | 
			
		||||
      <div class="wave-animation">
 | 
			
		||||
        <div v-for="i in 5" :key="i" class="wave-ellipse"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 其余部分保持不变 -->
 | 
			
		||||
    <div class="voice-indicators">
 | 
			
		||||
      <div class="voice-indicator left">
 | 
			
		||||
        <div v-for="i in 3" :key="i" class="bar"></div>
 | 
			
		||||
        <canvas ref="canvasClientRef" width="600" height="200"></canvas>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="voice-indicator right">
 | 
			
		||||
        <canvas id="canvas" width="600" height="200"></canvas>
 | 
			
		||||
        <canvas ref="canvasServerRef" width="600" height="200"></canvas>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="call-controls">
 | 
			
		||||
      <button class="call-button hangup" @click="hangUp">
 | 
			
		||||
        <span class="icon">×</span>
 | 
			
		||||
        <i class="iconfont icon-hung-up"></i>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="call-button answer" @click="answer">
 | 
			
		||||
        <span class="icon">□</span>
 | 
			
		||||
        <i class="iconfont icon-call"></i>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -36,91 +38,107 @@ const animateVoice = () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let voiceInterval;
 | 
			
		||||
const canvasClientRef = ref(null);
 | 
			
		||||
const canvasServerRef = ref(null);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  voiceInterval = setInterval(animateVoice, 500);
 | 
			
		||||
 | 
			
		||||
  const flag = ref(false)
 | 
			
		||||
  const canvas = document.getElementById('canvas');
 | 
			
		||||
  const ctx = canvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
  async function setupAudioProcessing() {
 | 
			
		||||
    try {
 | 
			
		||||
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
 | 
			
		||||
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | 
			
		||||
      const analyser = audioContext.createAnalyser();
 | 
			
		||||
      const source = audioContext.createMediaStreamSource(stream);
 | 
			
		||||
      source.connect(analyser);
 | 
			
		||||
      analyser.fftSize = 256;
 | 
			
		||||
      const bufferLength = analyser.frequencyBinCount;
 | 
			
		||||
      const dataArray = new Uint8Array(bufferLength);
 | 
			
		||||
 | 
			
		||||
      function draw() {
 | 
			
		||||
        analyser.getByteFrequencyData(dataArray);
 | 
			
		||||
 | 
			
		||||
       if (!flag.value) {
 | 
			
		||||
         // ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
         // requestAnimationFrame(draw);
 | 
			
		||||
         return;
 | 
			
		||||
       }
 | 
			
		||||
  //setupAudioProcessing(canvasServerRef.value, '#2ecc71');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const setupAudioProcessing = async (canvas, color) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | 
			
		||||
    const data = JSON.parse(localStorage.getItem("chat_data"))
 | 
			
		||||
 | 
			
		||||
    // 将 Int16Array 转换为 Float32Array (Web Audio API 使用 Float32)
 | 
			
		||||
    let float32Array = new Float32Array(data.length);
 | 
			
		||||
    for (let i = 0; i < data.length; i++) {
 | 
			
		||||
      float32Array[i] = data[i] / 32768; // Int16 转换为 Float32
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 创建 AudioBuffer
 | 
			
		||||
    const audioBuffer = audioContext.createBuffer(1, float32Array.length, 24000); // 单声道
 | 
			
		||||
    audioBuffer.getChannelData(0).set(float32Array); // 设置音频数据
 | 
			
		||||
 | 
			
		||||
    // 创建 AudioBufferSourceNode 并播放音频
 | 
			
		||||
    const source = audioContext.createBufferSource();
 | 
			
		||||
    source.buffer = audioBuffer;
 | 
			
		||||
    const analyser = audioContext.createAnalyser();
 | 
			
		||||
    analyser.fftSize = 256;
 | 
			
		||||
    const bufferLength = analyser.frequencyBinCount;
 | 
			
		||||
    // 连接到输入源(模拟麦克风)
 | 
			
		||||
    source.connect(analyser);
 | 
			
		||||
    // 同时连接到扬声器播放语音
 | 
			
		||||
    source.connect(audioContext.destination);
 | 
			
		||||
    source.start(); // 播放
 | 
			
		||||
    const dataArray = new Uint8Array(bufferLength);
 | 
			
		||||
    const ctx = canvas.getContext('2d')
 | 
			
		||||
 | 
			
		||||
    const draw = () => {
 | 
			
		||||
      analyser.getByteFrequencyData(dataArray);
 | 
			
		||||
 | 
			
		||||
      // 检查音量是否安静
 | 
			
		||||
      const maxVolume = Math.max(...dataArray);
 | 
			
		||||
      if (maxVolume < 100) {
 | 
			
		||||
        // 如果音量很小,则停止绘制
 | 
			
		||||
        ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
        const barWidth = (canvas.width / bufferLength) * 5;
 | 
			
		||||
        let x = 0;
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < bufferLength; i++) {
 | 
			
		||||
          const barHeight = dataArray[i] / 2;
 | 
			
		||||
 | 
			
		||||
          ctx.fillStyle = 'rgba(173, 216, 230, 0.7)'; // 淡蓝色
 | 
			
		||||
          ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
 | 
			
		||||
 | 
			
		||||
          x += barWidth + 2;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        requestAnimationFrame(draw);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      draw();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('获取麦克风权限失败:', err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
      ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
  function setupSpeechRecognition() {
 | 
			
		||||
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
 | 
			
		||||
    if (!SpeechRecognition) {
 | 
			
		||||
      alert('您的浏览器不支持语音识别功能');
 | 
			
		||||
      return;
 | 
			
		||||
      const barWidth = (canvas.width / bufferLength) * 2.5;
 | 
			
		||||
      let x = 0;
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < bufferLength; i++) {
 | 
			
		||||
        const barHeight = dataArray[i] / 2;
 | 
			
		||||
 | 
			
		||||
        ctx.fillStyle = color; // 淡蓝色
 | 
			
		||||
        ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
 | 
			
		||||
 | 
			
		||||
        x += barWidth + 2;
 | 
			
		||||
      }
 | 
			
		||||
      requestAnimationFrame(draw);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const recognition = new SpeechRecognition();
 | 
			
		||||
    recognition.continuous = true;
 | 
			
		||||
    recognition.interimResults = true;
 | 
			
		||||
    recognition.lang = 'zh-CN';
 | 
			
		||||
    draw();
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error('获取麦克风权限失败:', err);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    recognition.onresult = (event) => {
 | 
			
		||||
      const transcript = Array.from(event.results)
 | 
			
		||||
          .map(result => result[0])
 | 
			
		||||
          .map(result => result.transcript)
 | 
			
		||||
          .join('');
 | 
			
		||||
 | 
			
		||||
      document.getElementById('transcript').textContent = '识别到的文本: ' + transcript;
 | 
			
		||||
      flag.value = true
 | 
			
		||||
    };
 | 
			
		||||
const speaker = ref(null)
 | 
			
		||||
// 假设 PCM16 数据已经存储在一个 Int16Array 中
 | 
			
		||||
function playPCM16(pcm16Array, sampleRate = 44100) {
 | 
			
		||||
  // 创建 AudioContext
 | 
			
		||||
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | 
			
		||||
 | 
			
		||||
    recognition.onerror = (event) => {
 | 
			
		||||
      console.error('语音识别错误:', event.error);
 | 
			
		||||
      flag.value = false
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    recognition.start();
 | 
			
		||||
  // 将 Int16Array 转换为 Float32Array (Web Audio API 使用 Float32)
 | 
			
		||||
  let float32Array = new Float32Array(pcm16Array.length);
 | 
			
		||||
  for (let i = 0; i < pcm16Array.length; i++) {
 | 
			
		||||
    float32Array[i] = pcm16Array[i] / 32768; // Int16 转换为 Float32
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建 AudioBuffer
 | 
			
		||||
  const audioBuffer = audioContext.createBuffer(1, float32Array.length, sampleRate); // 单声道
 | 
			
		||||
  audioBuffer.getChannelData(0).set(float32Array); // 设置音频数据
 | 
			
		||||
 | 
			
		||||
  setupAudioProcessing();
 | 
			
		||||
  setupSpeechRecognition()
 | 
			
		||||
});
 | 
			
		||||
  // 创建 AudioBufferSourceNode 并播放音频
 | 
			
		||||
  const source = audioContext.createBufferSource();
 | 
			
		||||
  source.buffer = audioBuffer;
 | 
			
		||||
  source.connect(audioContext.destination); // 连接到扬声器
 | 
			
		||||
  source.start(); // 播放
 | 
			
		||||
  speaker.value = source
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  clearInterval(voiceInterval);
 | 
			
		||||
@@ -132,10 +150,13 @@ const hangUp = () => {
 | 
			
		||||
 | 
			
		||||
const answer = () => {
 | 
			
		||||
  console.log('Call answered');
 | 
			
		||||
  setupAudioProcessing(canvasServerRef.value, '#2ecc71');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
.video-call-container {
 | 
			
		||||
  background: linear-gradient(to right, #2c3e50, #4a5568, #6b46c1);
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
@@ -143,133 +164,144 @@ const answer = () => {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 2rem;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
 | 
			
		||||
  .wave-container {
 | 
			
		||||
    padding 2rem
 | 
			
		||||
    .wave-animation {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    animation: wave 0.8s infinite ease-in-out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(odd) {
 | 
			
		||||
    height: 60px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(even) {
 | 
			
		||||
    height: 80px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes wave {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      transform: scaleY(0.8);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      transform: scaleY(1.2);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(2) {
 | 
			
		||||
    animation-delay: 0.1s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(3) {
 | 
			
		||||
    animation-delay: 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(4) {
 | 
			
		||||
    animation-delay: 0.3s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(5) {
 | 
			
		||||
    animation-delay: 0.4s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 其余样式保持不变 */
 | 
			
		||||
  .voice-indicators {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .bar {
 | 
			
		||||
    width: 10px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    background-color: #3498db;
 | 
			
		||||
    margin: 0 2px;
 | 
			
		||||
    transition: height 0.2s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(1) {
 | 
			
		||||
    height: 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(2) {
 | 
			
		||||
    height: 25px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(3) {
 | 
			
		||||
    height: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(1) {
 | 
			
		||||
    height: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(2) {
 | 
			
		||||
    height: 10px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(3) {
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .call-controls {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: 2rem;
 | 
			
		||||
    padding 2rem
 | 
			
		||||
 | 
			
		||||
    .call-button {
 | 
			
		||||
      width: 60px;
 | 
			
		||||
      height: 60px;
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      border: none;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      font-size: 24px;
 | 
			
		||||
      color: white;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
 | 
			
		||||
      .iconfont {
 | 
			
		||||
        font-size 24px
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .hangup {
 | 
			
		||||
      background-color: #e74c3c;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .answer {
 | 
			
		||||
      background-color: #2ecc71;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .icon {
 | 
			
		||||
      font-size: 28px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
canvas {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-animation {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse {
 | 
			
		||||
  width: 40px;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border-radius: 20px;
 | 
			
		||||
  animation: wave 0.8s infinite ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(odd) {
 | 
			
		||||
  height: 60px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(even) {
 | 
			
		||||
  height: 80px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes wave {
 | 
			
		||||
  0%, 100% {
 | 
			
		||||
    transform: scaleY(0.8);
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: scaleY(1.2);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(2) {
 | 
			
		||||
  animation-delay: 0.1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(3) {
 | 
			
		||||
  animation-delay: 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(4) {
 | 
			
		||||
  animation-delay: 0.3s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wave-ellipse:nth-child(5) {
 | 
			
		||||
  animation-delay: 0.4s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 其余样式保持不变 */
 | 
			
		||||
.voice-indicators {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bar {
 | 
			
		||||
  width: 10px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  background-color: #3498db;
 | 
			
		||||
  margin: 0 2px;
 | 
			
		||||
  transition: height 0.2s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.left .bar:nth-child(1) {
 | 
			
		||||
  height: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.left .bar:nth-child(2) {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.left .bar:nth-child(3) {
 | 
			
		||||
  height: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.right .bar:nth-child(1) {
 | 
			
		||||
  height: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.right .bar:nth-child(2) {
 | 
			
		||||
  height: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.voice-indicator.right .bar:nth-child(3) {
 | 
			
		||||
  height: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.call-controls {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  gap: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.call-button {
 | 
			
		||||
  width: 60px;
 | 
			
		||||
  height: 60px;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  border: none;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  font-size: 24px;
 | 
			
		||||
  color: white;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hangup {
 | 
			
		||||
  background-color: #e74c3c;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.answer {
 | 
			
		||||
  background-color: #2ecc71;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -9,8 +9,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {nextTick, onMounted, ref} from "vue";
 | 
			
		||||
import Storage from "good-storage";
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
 | 
			
		||||
const data = ref('abc')
 | 
			
		||||
import { RealtimeClient } from '@openai/realtime-api-beta';
 | 
			
		||||
@@ -83,6 +82,9 @@ function playPCM16(pcm16Array, sampleRate = 44100) {
 | 
			
		||||
  source.buffer = audioBuffer;
 | 
			
		||||
  source.connect(audioContext.destination); // 连接到扬声器
 | 
			
		||||
  source.start(); // 播放
 | 
			
		||||
  source.onended = () => {
 | 
			
		||||
    console.log("播放结束")
 | 
			
		||||
  }
 | 
			
		||||
  speaker.value = source
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										263
									
								
								web/src/views/Test2.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								web/src/views/Test2.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="video-call-container">
 | 
			
		||||
    <div class="wave-container">
 | 
			
		||||
      <div class="wave-animation">
 | 
			
		||||
        <div v-for="i in 5" :key="i" class="wave-ellipse"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 其余部分保持不变 -->
 | 
			
		||||
    <div class="voice-indicators">
 | 
			
		||||
      <div class="voice-indicator left">
 | 
			
		||||
        <canvas ref="canvasClientRef" width="600" height="200"></canvas>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="voice-indicator right">
 | 
			
		||||
        <canvas ref="canvasServerRef" width="600" height="200"></canvas>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="call-controls">
 | 
			
		||||
      <button class="call-button hangup" @click="hangUp">
 | 
			
		||||
        <i class="iconfont icon-hung-up"></i>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="call-button answer" @click="answer">
 | 
			
		||||
        <i class="iconfont icon-call"></i>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
// Script 部分保持不变
 | 
			
		||||
import {ref, onMounted, onUnmounted} from 'vue';
 | 
			
		||||
 | 
			
		||||
const leftVoiceActive = ref(false);
 | 
			
		||||
const rightVoiceActive = ref(false);
 | 
			
		||||
 | 
			
		||||
const animateVoice = () => {
 | 
			
		||||
  leftVoiceActive.value = Math.random() > 0.5;
 | 
			
		||||
  rightVoiceActive.value = Math.random() > 0.5;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let voiceInterval;
 | 
			
		||||
const canvasClientRef = ref(null);
 | 
			
		||||
const canvasServerRef = ref(null);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  voiceInterval = setInterval(animateVoice, 500);
 | 
			
		||||
 | 
			
		||||
  async function setupAudioProcessing(canvas, color) {
 | 
			
		||||
    try {
 | 
			
		||||
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
 | 
			
		||||
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | 
			
		||||
      const analyser = audioContext.createAnalyser();
 | 
			
		||||
      const source = audioContext.createMediaStreamSource(stream);
 | 
			
		||||
      source.connect(analyser);
 | 
			
		||||
      analyser.fftSize = 256;
 | 
			
		||||
      const bufferLength = analyser.frequencyBinCount;
 | 
			
		||||
      const dataArray = new Uint8Array(bufferLength);
 | 
			
		||||
      const ctx = canvas.getContext('2d')
 | 
			
		||||
 | 
			
		||||
      const draw = () => {
 | 
			
		||||
          analyser.getByteFrequencyData(dataArray);
 | 
			
		||||
 | 
			
		||||
          // 检查音量是否安静
 | 
			
		||||
          const maxVolume = Math.max(...dataArray);
 | 
			
		||||
          if (maxVolume < 100) {
 | 
			
		||||
            // 如果音量很小,则停止绘制
 | 
			
		||||
            ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
            requestAnimationFrame(draw);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
          const barWidth = (canvas.width / bufferLength) * 2.5;
 | 
			
		||||
          let x = 0;
 | 
			
		||||
 | 
			
		||||
          for (let i = 0; i < bufferLength; i++) {
 | 
			
		||||
            const barHeight = dataArray[i] / 2;
 | 
			
		||||
 | 
			
		||||
            ctx.fillStyle = color; // 淡蓝色
 | 
			
		||||
            ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
 | 
			
		||||
 | 
			
		||||
            x += barWidth + 2;
 | 
			
		||||
          }
 | 
			
		||||
        requestAnimationFrame(draw);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      draw();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('获取麦克风权限失败:', err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 // const data = JSON.parse(localStorage.getItem("chat_data"))
 | 
			
		||||
 // setupPCMProcessing(canvasClientRef.value, '#2ecc71', data, 24000);
 | 
			
		||||
  setupAudioProcessing(canvasServerRef.value, '#2ecc71');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  clearInterval(voiceInterval);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const hangUp = () => {
 | 
			
		||||
  console.log('Call hung up');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const answer = () => {
 | 
			
		||||
  console.log('Call answered');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="stylus">
 | 
			
		||||
.video-call-container {
 | 
			
		||||
  background: linear-gradient(to right, #2c3e50, #4a5568, #6b46c1);
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
 | 
			
		||||
  .wave-container {
 | 
			
		||||
    padding 2rem
 | 
			
		||||
    .wave-animation {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    animation: wave 0.8s infinite ease-in-out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(odd) {
 | 
			
		||||
    height: 60px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(even) {
 | 
			
		||||
    height: 80px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes wave {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      transform: scaleY(0.8);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      transform: scaleY(1.2);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(2) {
 | 
			
		||||
    animation-delay: 0.1s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(3) {
 | 
			
		||||
    animation-delay: 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(4) {
 | 
			
		||||
    animation-delay: 0.3s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wave-ellipse:nth-child(5) {
 | 
			
		||||
    animation-delay: 0.4s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 其余样式保持不变 */
 | 
			
		||||
  .voice-indicators {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .bar {
 | 
			
		||||
    width: 10px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    background-color: #3498db;
 | 
			
		||||
    margin: 0 2px;
 | 
			
		||||
    transition: height 0.2s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(1) {
 | 
			
		||||
    height: 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(2) {
 | 
			
		||||
    height: 25px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.left .bar:nth-child(3) {
 | 
			
		||||
    height: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(1) {
 | 
			
		||||
    height: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(2) {
 | 
			
		||||
    height: 10px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .voice-indicator.right .bar:nth-child(3) {
 | 
			
		||||
    height: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .call-controls {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: 2rem;
 | 
			
		||||
    padding 2rem
 | 
			
		||||
 | 
			
		||||
    .call-button {
 | 
			
		||||
      width: 60px;
 | 
			
		||||
      height: 60px;
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      border: none;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      font-size: 24px;
 | 
			
		||||
      color: white;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
 | 
			
		||||
      .iconfont {
 | 
			
		||||
        font-size 24px
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .hangup {
 | 
			
		||||
      background-color: #e74c3c;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .answer {
 | 
			
		||||
      background-color: #2ecc71;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .icon {
 | 
			
		||||
      font-size: 28px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
canvas {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user