add PCM16 audio stream to wave is reday

This commit is contained in:
RockYang 2024-10-14 18:39:50 +08:00
parent 52a3f13f1e
commit 37a9b0e485
10 changed files with 524 additions and 199 deletions

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4125778 */ font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1726622198991') format('woff2'), src: url('iconfont.woff2?t=1728891448746') format('woff2'),
url('iconfont.woff?t=1726622198991') format('woff'), url('iconfont.woff?t=1728891448746') format('woff'),
url('iconfont.ttf?t=1726622198991') format('truetype'); url('iconfont.ttf?t=1728891448746') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-call:before {
content: "\e769";
}
.icon-hung-up:before {
content: "\e609";
}
.icon-paypal:before { .icon-paypal:before {
content: "\e666"; content: "\e666";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,20 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "", "description": "",
"glyphs": [ "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", "icon_id": "7443846",
"name": "PayPal", "name": "PayPal",

Binary file not shown.

View File

@ -324,6 +324,12 @@ const routes = [
meta: {title: '测试页面'}, meta: {title: '测试页面'},
component: () => import('@/views/Test.vue'), component: () => import('@/views/Test.vue'),
}, },
{
name: 'test2',
path: '/test2',
meta: {title: '测试页面'},
component: () => import('@/views/Test2.vue'),
},
{ {
name: 'NotFound', name: 'NotFound',
path: '/:all(.*)', path: '/:all(.*)',

View File

@ -1,23 +1,25 @@
<template> <template>
<div class="video-call-container"> <div class="video-call-container">
<div class="wave-container">
<div class="wave-animation"> <div class="wave-animation">
<div v-for="i in 5" :key="i" class="wave-ellipse"></div> <div v-for="i in 5" :key="i" class="wave-ellipse"></div>
</div> </div>
</div>
<!-- 其余部分保持不变 --> <!-- 其余部分保持不变 -->
<div class="voice-indicators"> <div class="voice-indicators">
<div class="voice-indicator left"> <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>
<div class="voice-indicator right"> <div class="voice-indicator right">
<canvas id="canvas" width="600" height="200"></canvas> <canvas ref="canvasServerRef" width="600" height="200"></canvas>
</div> </div>
</div> </div>
<div class="call-controls"> <div class="call-controls">
<button class="call-button hangup" @click="hangUp"> <button class="call-button hangup" @click="hangUp">
<span class="icon">×</span> <i class="iconfont icon-hung-up"></i>
</button> </button>
<button class="call-button answer" @click="answer"> <button class="call-button answer" @click="answer">
<span class="icon"></span> <i class="iconfont icon-call"></i>
</button> </button>
</div> </div>
</div> </div>
@ -36,48 +38,73 @@ const animateVoice = () => {
}; };
let voiceInterval; let voiceInterval;
const canvasClientRef = ref(null);
const canvasServerRef = ref(null);
onMounted(() => { onMounted(() => {
voiceInterval = setInterval(animateVoice, 500); voiceInterval = setInterval(animateVoice, 500);
const flag = ref(false)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
async function setupAudioProcessing() {
//setupAudioProcessing(canvasServerRef.value, '#2ecc71');
});
const setupAudioProcessing = async (canvas, color) => {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); 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(); const analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
analyser.fftSize = 256; analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount; const bufferLength = analyser.frequencyBinCount;
//
source.connect(analyser);
//
source.connect(audioContext.destination);
source.start(); //
const dataArray = new Uint8Array(bufferLength); const dataArray = new Uint8Array(bufferLength);
const ctx = canvas.getContext('2d')
function draw() { const draw = () => {
analyser.getByteFrequencyData(dataArray); analyser.getByteFrequencyData(dataArray);
if (!flag.value) { //
// ctx.clearRect(0, 0, canvas.width, canvas.height); const maxVolume = Math.max(...dataArray);
// requestAnimationFrame(draw); if (maxVolume < 100) {
//
ctx.clearRect(0, 0, canvas.width, canvas.height);
requestAnimationFrame(draw);
return; return;
} }
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 5; const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0; let x = 0;
for (let i = 0; i < bufferLength; i++) { for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 2; const barHeight = dataArray[i] / 2;
ctx.fillStyle = 'rgba(173, 216, 230, 0.7)'; // ctx.fillStyle = color; //
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 2; x += barWidth + 2;
} }
requestAnimationFrame(draw); requestAnimationFrame(draw);
} }
@ -87,41 +114,32 @@ onMounted(() => {
} }
} }
function setupSpeechRecognition() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const speaker = ref(null)
if (!SpeechRecognition) { // PCM16 Int16Array
alert('您的浏览器不支持语音识别功能'); function playPCM16(pcm16Array, sampleRate = 44100) {
return; // AudioContext
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 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
} }
const recognition = new SpeechRecognition(); // AudioBuffer
recognition.continuous = true; const audioBuffer = audioContext.createBuffer(1, float32Array.length, sampleRate); //
recognition.interimResults = true; audioBuffer.getChannelData(0).set(float32Array); //
recognition.lang = 'zh-CN';
recognition.onresult = (event) => { // AudioBufferSourceNode
const transcript = Array.from(event.results) const source = audioContext.createBufferSource();
.map(result => result[0]) source.buffer = audioBuffer;
.map(result => result.transcript) source.connect(audioContext.destination); //
.join(''); source.start(); //
speaker.value = source
document.getElementById('transcript').textContent = '识别到的文本: ' + transcript;
flag.value = true
};
recognition.onerror = (event) => {
console.error('语音识别错误:', event.error);
flag.value = false
};
recognition.start();
} }
setupAudioProcessing();
setupSpeechRecognition()
});
onUnmounted(() => { onUnmounted(() => {
clearInterval(voiceInterval); clearInterval(voiceInterval);
}); });
@ -132,10 +150,13 @@ const hangUp = () => {
const answer = () => { const answer = () => {
console.log('Call answered'); console.log('Call answered');
setupAudioProcessing(canvasServerRef.value, '#2ecc71');
}; };
</script> </script>
<style scoped> <style scoped lang="stylus">
.video-call-container { .video-call-container {
background: linear-gradient(to right, #2c3e50, #4a5568, #6b46c1); background: linear-gradient(to right, #2c3e50, #4a5568, #6b46c1);
height: 100vh; height: 100vh;
@ -143,19 +164,18 @@ const answer = () => {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 2rem; padding: 0;
}
canvas {
background-color: transparent;
}
.wave-container {
padding 2rem
.wave-animation { .wave-animation {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
}
.wave-ellipse { .wave-ellipse {
width: 40px; width: 40px;
@ -246,7 +266,7 @@ canvas {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 2rem; gap: 2rem;
} padding 2rem
.call-button { .call-button {
width: 60px; width: 60px;
@ -259,8 +279,11 @@ canvas {
font-size: 24px; font-size: 24px;
color: white; color: white;
cursor: pointer; cursor: pointer;
}
.iconfont {
font-size 24px
}
}
.hangup { .hangup {
background-color: #e74c3c; background-color: #e74c3c;
} }
@ -272,4 +295,13 @@ canvas {
.icon { .icon {
font-size: 28px; font-size: 28px;
} }
}
}
canvas {
background-color: transparent;
}
</style> </style>

View File

@ -9,8 +9,7 @@
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import Storage from "good-storage";
const data = ref('abc') const data = ref('abc')
import { RealtimeClient } from '@openai/realtime-api-beta'; import { RealtimeClient } from '@openai/realtime-api-beta';
@ -83,6 +82,9 @@ function playPCM16(pcm16Array, sampleRate = 44100) {
source.buffer = audioBuffer; source.buffer = audioBuffer;
source.connect(audioContext.destination); // source.connect(audioContext.destination); //
source.start(); // source.start(); //
source.onended = () => {
console.log("播放结束")
}
speaker.value = source speaker.value = source
} }

263
web/src/views/Test2.vue Normal file
View 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>