Client side Voice Visualizer
This commit is contained in:
@@ -118,12 +118,45 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
.visualizer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-visualizer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualizer-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9em;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Sesame AI Voice Chat</h1>
|
<h1>Sesame AI Voice Chat</h1>
|
||||||
<div class="conversation" id="conversation"></div>
|
<div class="conversation" id="conversation"></div>
|
||||||
|
|
||||||
|
<div class="visualizer-container">
|
||||||
|
<canvas id="audioVisualizer" class="audio-visualizer"></canvas>
|
||||||
|
<div id="visualizerLabel" class="visualizer-label">Audio levels will appear here when speaking</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<select id="speakerSelect">
|
<select id="speakerSelect">
|
||||||
<option value="0">Speaker 0</option>
|
<option value="0">Speaker 0</option>
|
||||||
@@ -151,6 +184,15 @@
|
|||||||
const CLIENT_SILENCE_THRESHOLD = 0.01;
|
const CLIENT_SILENCE_THRESHOLD = 0.01;
|
||||||
const CLIENT_SILENCE_DURATION_MS = 1000; // 1 second
|
const CLIENT_SILENCE_DURATION_MS = 1000; // 1 second
|
||||||
|
|
||||||
|
// Add these variables with your existing ones
|
||||||
|
let analyser;
|
||||||
|
let visualizerCanvas;
|
||||||
|
let canvasContext;
|
||||||
|
let visualizerBufferLength;
|
||||||
|
let visualizerDataArray;
|
||||||
|
let visualizerAnimationFrame;
|
||||||
|
const visualizerLabel = document.getElementById('visualizerLabel');
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
const conversationEl = document.getElementById('conversation');
|
const conversationEl = document.getElementById('conversation');
|
||||||
const speakerSelectEl = document.getElementById('speakerSelect');
|
const speakerSelectEl = document.getElementById('speakerSelect');
|
||||||
@@ -163,6 +205,7 @@
|
|||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
setupAudioContext();
|
setupAudioContext();
|
||||||
|
setupVisualizer();
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
streamButton.addEventListener('click', toggleStreaming);
|
streamButton.addEventListener('click', toggleStreaming);
|
||||||
@@ -264,8 +307,27 @@
|
|||||||
|
|
||||||
// Create audio processor node
|
// Create audio processor node
|
||||||
const source = audioContext.createMediaStreamSource(stream);
|
const source = audioContext.createMediaStreamSource(stream);
|
||||||
|
|
||||||
|
// Set up analyser for visualization
|
||||||
|
analyser = audioContext.createAnalyser();
|
||||||
|
analyser.fftSize = 256;
|
||||||
|
visualizerBufferLength = analyser.frequencyBinCount;
|
||||||
|
visualizerDataArray = new Uint8Array(visualizerBufferLength);
|
||||||
|
source.connect(analyser);
|
||||||
|
|
||||||
|
// Hide the label when visualization is active
|
||||||
|
visualizerLabel.style.opacity = '0';
|
||||||
|
|
||||||
|
// Start drawing the visualization
|
||||||
|
drawVisualizer();
|
||||||
|
|
||||||
|
// Set up processor for audio processing
|
||||||
streamProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
streamProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
|
||||||
|
// Connect nodes
|
||||||
|
source.connect(streamProcessor);
|
||||||
|
streamProcessor.connect(audioContext.destination);
|
||||||
|
|
||||||
// Process and send audio data
|
// Process and send audio data
|
||||||
streamProcessor.onaudioprocess = function(e) {
|
streamProcessor.onaudioprocess = function(e) {
|
||||||
const audioData = e.inputBuffer.getChannelData(0);
|
const audioData = e.inputBuffer.getChannelData(0);
|
||||||
@@ -286,10 +348,6 @@
|
|||||||
sendAudioChunk(downsampled, speaker);
|
sendAudioChunk(downsampled, speaker);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect the nodes
|
|
||||||
source.connect(streamProcessor);
|
|
||||||
streamProcessor.connect(audioContext.destination);
|
|
||||||
|
|
||||||
addSystemMessage('Listening - speak naturally and pause when finished');
|
addSystemMessage('Listening - speak naturally and pause when finished');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -389,6 +447,23 @@
|
|||||||
streamProcessor = null;
|
streamProcessor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (analyser) {
|
||||||
|
analyser.disconnect();
|
||||||
|
analyser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the visualization
|
||||||
|
if (visualizerAnimationFrame) {
|
||||||
|
cancelAnimationFrame(visualizerAnimationFrame);
|
||||||
|
visualizerAnimationFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
if (canvasContext) {
|
||||||
|
canvasContext.clearRect(0, 0, visualizerCanvas.width, visualizerCanvas.height);
|
||||||
|
visualizerLabel.style.opacity = '0.7';
|
||||||
|
}
|
||||||
|
|
||||||
// Clear any pending silence timer
|
// Clear any pending silence timer
|
||||||
if (silenceTimer) {
|
if (silenceTimer) {
|
||||||
clearTimeout(silenceTimer);
|
clearTimeout(silenceTimer);
|
||||||
@@ -537,6 +612,64 @@
|
|||||||
conversationEl.appendChild(messageEl);
|
conversationEl.appendChild(messageEl);
|
||||||
conversationEl.scrollTop = conversationEl.scrollHeight;
|
conversationEl.scrollTop = conversationEl.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup the audio visualizer
|
||||||
|
function setupVisualizer() {
|
||||||
|
visualizerCanvas = document.getElementById('audioVisualizer');
|
||||||
|
canvasContext = visualizerCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Set canvas size to match container
|
||||||
|
function resizeCanvas() {
|
||||||
|
const container = visualizerCanvas.parentElement;
|
||||||
|
visualizerCanvas.width = container.clientWidth;
|
||||||
|
visualizerCanvas.height = container.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call initially and on window resize
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the visualization drawing function
|
||||||
|
function drawVisualizer() {
|
||||||
|
if (!isStreaming) {
|
||||||
|
if (visualizerAnimationFrame) {
|
||||||
|
cancelAnimationFrame(visualizerAnimationFrame);
|
||||||
|
visualizerAnimationFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
canvasContext.clearRect(0, 0, visualizerCanvas.width, visualizerCanvas.height);
|
||||||
|
visualizerLabel.style.opacity = '0.7';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visualizerAnimationFrame = requestAnimationFrame(drawVisualizer);
|
||||||
|
|
||||||
|
// Get the frequency data
|
||||||
|
analyser.getByteFrequencyData(visualizerDataArray);
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
canvasContext.clearRect(0, 0, visualizerCanvas.width, visualizerCanvas.height);
|
||||||
|
|
||||||
|
// Calculate bar width based on canvas size and buffer length
|
||||||
|
const barWidth = (visualizerCanvas.width / visualizerBufferLength) * 2.5;
|
||||||
|
let barHeight;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
// Draw bars
|
||||||
|
for (let i = 0; i < visualizerBufferLength; i++) {
|
||||||
|
barHeight = visualizerDataArray[i] / 2; // Scale down to fit in canvas
|
||||||
|
|
||||||
|
// Use a gradient color based on frequency intensity
|
||||||
|
const hue = i / visualizerBufferLength * 180 + 180; // Blue to green spectrum
|
||||||
|
canvasContext.fillStyle = `hsl(${hue}, 100%, ${50 + (barHeight / 2)}%)`;
|
||||||
|
|
||||||
|
canvasContext.fillRect(x, visualizerCanvas.height - barHeight, barWidth, barHeight);
|
||||||
|
|
||||||
|
x += barWidth + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user