被chatgpt的语音对话迷了眼 ,就想着也做个动画然后通话。但是做好了发现实际上自己想要的是在输入区域内语音、上传文件、输入文本同步处理,动画效果对于这个项目而言完全是多余的。不过都做出来了,就放出来,有需要的小伙伴自己玩玩吧。
注释分别是文件和麦克风的互动,根据自己需要选择取消注释。
<canvas></canvas>
<audio src="" controls></audio>
<button id="audiopbtn" class="btn"> <img src="" alt="audio icon"></button>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: black;
}
.btn {
background-color: white;
color: rgb(15, 14, 14);
font-size: 15px;
height: auto;
padding: 5px 15px;
border-radius: 20px;
margin-top: 10px;
cursor: pointer;
margin-bottom: 10px;
margin: 5px 0;
border: 1px solid #ccc;
box-shadow: 0 4px #999;
}
.btn:hover {
background-color: rgb(212, 210, 210);
box-shadow: 0 2px #666;
transform: translateY(2px);
}
.btn:active {
box-shadow: 0 1px #666;
transform: translateY(4px);
}
canvas {
width: 500px;
height: 400px;
}
audio {
margin-top: 20px;
}
document.addEventListener('DOMContentLoaded', (event) => {
const audioEle = document.querySelector('audio');
const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d');
function initCvs() {
const size = Math.min(window.innerWidth, window.innerHeight) * 0.8; // 80% of viewport
cvs.width = size * devicePixelRatio;
cvs.height = size * devicePixelRatio;
cvs.style.width = `${size}px`;
cvs.style.height = `${size}px`;
}
function draw(datas, maxValue) {
const r = cvs.width / 4 + 20 * devicePixelRatio;
const center = cvs.width / 2;
ctx.clearRect(0, 0, cvs.width, cvs.height);
const hslStep = 360 / datas.length;
const maxLen = cvs.width / 2 - r;
const minLen = 2 * devicePixelRatio;
for (let i = 0; i < datas.length; i++) {
const value = datas[i] / maxValue;
const len = minLen + value * (maxLen - minLen);
const angle = (i * 2 * Math.PI) / datas.length;
const x1 = center + r * Math.cos(angle);
const y1 = center + r * Math.sin(angle);
const x2 = center + (r + len) * Math.cos(angle);
const y2 = center + (r + len) * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = `hsl(${i * hslStep}, 100%, 50%)`;
ctx.lineWidth = 2 * devicePixelRatio;
ctx.stroke();
}
}
initCvs();
// const arr = new Array(100).fill(0).map(() => Math.random() * 255);
draw(new Array(256).fill(0), 255);
let isInit = false, analyser, buffer, ws;
let isMicActive = false; // 麦克风的初始状态为关闭
let stream = null; // 用于存储音频流
let source = null;
// 对于音频来源文件的处理
// audioEle.onplay = function () {
// if (isInit) {
// return;
// }
// isInit = true;
// const audioCtx = new AudioContext();
// const source = audioCtx.createMediaElementSource(audioEle); // 来源节点
// analyser = audioCtx.createAnalyser(); // 分析器节点
// // 分析频域数据
// analyser.fftSize = 512; // 值越大,越精细
// buffer = new Uint8Array(analyser.frequencyBinCount);
// analyser.getByteFrequencyData(buffer); // 获取频域数据
// source.connect(analyser);
// analyser.connect(audioCtx.destination); // 连接到喇叭
// };
// // 对于麦克风音频的处理
// navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
// if (isInit) {
// return;
// }
// isInit = true;
// const audioCtx = new AudioContext();
// const source = audioCtx.createMediaStreamSource(stream); // 来自于流
// analyser = audioCtx.createAnalyser(); // 分析器节点
// // 分析频域数据
// analyser.fftSize = 512; // 值越大,越精细
// buffer = new Uint8Array(analyser.frequencyBinCount);
// analyser.getByteFrequencyData(buffer); // 获取频域数据
// source.connect(analyser);
// // analyser.connect(audioCtx.destination); // 连接到喇叭
// });
function toggleMicrophone() {
if (!isMicActive) {
// 开启麦克风
navigator.mediaDevices.getUserMedia({ audio: true }).then(s => {
isMicActive = true; // 更新麦克风状态
stream = s;
ws = new WebSocket("ws://localhost:9090/api/audio");
ws.onopen = function (event) {
console.log('WebSocket connected');
};
ws.onerror = function (error) {
ws.close();
console.error('WebSocket Error:', error);
};
if (!isInit) {
audioCtx = new AudioContext();
source = audioCtx.createMediaStreamSource(stream); // 来自于流
analyser = audioCtx.createAnalyser(); // 分析器节点
// 分析频域数据
analyser.fftSize = 512; // 值越大,越精细
buffer = new Uint8Array(analyser.frequencyBinCount);
console.log("buffer:", buffer);
analyser.getByteFrequencyData(buffer); // 获取频域数据
source.connect(analyser);
// analyser.connect(audioCtx.destination); // 如果你需要将音频输出到喇叭,取消注释这行
isInit = true;
}
update();
}).catch(error => {
console.error("Error accessing the microphone", error);
});
} else {
// 关闭麦克风
if (stream) {
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
isMicActive = false; // 更新麦克风状态
isInit = false; // 重置初始化状态,以便下次重新初始化
}
// 关闭 WebSocket 连接
if (ws) {
ws.close();
console.log('WebSocket disconnected');
}
}
}
document.getElementById('audiopbtn').addEventListener('click', toggleMicrophone);
function update() {
requestAnimationFrame(update);
if (!isInit || !ws || ws.readyState !== WebSocket.OPEN) {
return;
}
if (ws.readyState === WebSocket.OPEN) {
const arrayBuffer = buffer.buffer; // 获取 Uint8Array 的 ArrayBuffer
ws.send(arrayBuffer);
}
analyser.getByteFrequencyData(buffer);
const offset = Math.floor((buffer.length * 2) / 3);
const data = new Array(offset * 2);
for (let i = 0; i < offset; i++) {
data[i] = data[data.length - i - 1] = buffer[i];
// console.log("data:", data[i]);
}
draw(data, 255);
// console.log(buffer);
}
// update();
});