|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ASR WebSocket Testing Client with Sample Rate Analysis</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.container { |
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 20px; |
|
|
padding: 40px; |
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
|
} |
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
color: #333; |
|
|
font-size: 2.5em; |
|
|
font-weight: 300; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.header p { |
|
|
color: #666; |
|
|
font-size: 1.1em; |
|
|
} |
|
|
|
|
|
.connection-section { |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.input-group { |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.input-group label { |
|
|
display: block; |
|
|
margin-bottom: 8px; |
|
|
color: #333; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.input-group input, .input-group select { |
|
|
width: 100%; |
|
|
padding: 12px 16px; |
|
|
border: 2px solid #e1e5e9; |
|
|
border-radius: 10px; |
|
|
font-size: 16px; |
|
|
transition: all 0.3s ease; |
|
|
background: rgba(255, 255, 255, 0.8); |
|
|
} |
|
|
|
|
|
.input-group input:focus, .input-group select:focus { |
|
|
outline: none; |
|
|
border-color: #667eea; |
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 12px 24px; |
|
|
border: none; |
|
|
border-radius: 10px; |
|
|
font-size: 16px; |
|
|
font-weight: 500; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
|
|
|
.btn:disabled { |
|
|
opacity: 0.6; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.btn-connect { |
|
|
background: linear-gradient(135deg, #4CAF50, #45a049); |
|
|
color: white; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.btn-connect:hover:not(:disabled) { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); |
|
|
} |
|
|
|
|
|
.btn-disconnect { |
|
|
background: linear-gradient(135deg, #f44336, #da190b); |
|
|
color: white; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.audio-controls { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 20px; |
|
|
margin: 30px 0; |
|
|
} |
|
|
|
|
|
.btn-mic { |
|
|
background: linear-gradient(135deg, #2196F3, #1976D2); |
|
|
color: white; |
|
|
width: 80px; |
|
|
height: 80px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-size: 24px; |
|
|
} |
|
|
|
|
|
.btn-mic:hover:not(:disabled) { |
|
|
transform: scale(1.1); |
|
|
box-shadow: 0 10px 25px rgba(33, 150, 243, 0.3); |
|
|
} |
|
|
|
|
|
.btn-mic.recording { |
|
|
background: linear-gradient(135deg, #f44336, #da190b); |
|
|
animation: pulse 1.5s infinite; |
|
|
} |
|
|
|
|
|
.btn-stop { |
|
|
background: linear-gradient(135deg, #FF9800, #F57C00); |
|
|
color: white; |
|
|
width: 80px; |
|
|
height: 80px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-size: 24px; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { transform: scale(1); } |
|
|
50% { transform: scale(1.05); } |
|
|
100% { transform: scale(1); } |
|
|
} |
|
|
|
|
|
.status { |
|
|
text-align: center; |
|
|
margin: 20px 0; |
|
|
padding: 12px; |
|
|
border-radius: 10px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.status.connected { |
|
|
background: rgba(76, 175, 80, 0.1); |
|
|
color: #4CAF50; |
|
|
border: 1px solid rgba(76, 175, 80, 0.3); |
|
|
} |
|
|
|
|
|
.status.disconnected { |
|
|
background: rgba(244, 67, 54, 0.1); |
|
|
color: #f44336; |
|
|
border: 1px solid rgba(244, 67, 54, 0.3); |
|
|
} |
|
|
|
|
|
.status.recording { |
|
|
background: rgba(33, 150, 243, 0.1); |
|
|
color: #2196F3; |
|
|
border: 1px solid rgba(33, 150, 243, 0.3); |
|
|
} |
|
|
|
|
|
.stats-section { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr; |
|
|
gap: 20px; |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.stat-box { |
|
|
background: rgba(0, 0, 0, 0.05); |
|
|
border-radius: 10px; |
|
|
padding: 15px; |
|
|
text-align: center; |
|
|
border: 1px solid rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.stat-value { |
|
|
font-size: 2em; |
|
|
font-weight: bold; |
|
|
color: #667eea; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
color: #666; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
|
|
|
.response-section { |
|
|
margin-top: 30px; |
|
|
} |
|
|
|
|
|
.response-box { |
|
|
background: rgba(0, 0, 0, 0.05); |
|
|
border-radius: 10px; |
|
|
padding: 20px; |
|
|
min-height: 200px; |
|
|
max-height: 400px; |
|
|
overflow-y: auto; |
|
|
border: 1px solid rgba(0, 0, 0, 0.1); |
|
|
font-family: 'Courier New', monospace; |
|
|
font-size: 12px; |
|
|
white-space: pre-wrap; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
.debug-section { |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.debug-box { |
|
|
background: rgba(0, 0, 0, 0.8); |
|
|
color: #00ff00; |
|
|
border-radius: 10px; |
|
|
padding: 15px; |
|
|
min-height: 150px; |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
font-family: 'Courier New', monospace; |
|
|
font-size: 11px; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<h1>๐ค ASR Sample Rate Analyzer</h1> |
|
|
<p>WebSocket-based Speech Recognition with Audio Analysis</p> |
|
|
</div> |
|
|
|
|
|
<div class="connection-section"> |
|
|
<div class="input-group"> |
|
|
<label for="websocketUrl">WebSocket URL:</label> |
|
|
<input type="text" id="websocketUrl" value="ws://185.208.206.135:5005" placeholder="ws://185.208.206.135:5005"> |
|
|
</div> |
|
|
|
|
|
<div class="input-group"> |
|
|
<label for="targetSampleRate">Target Sample Rate (Hz):</label> |
|
|
<select id="targetSampleRate"> |
|
|
<option value="8000">8000 Hz (Default)</option> |
|
|
<option value="16000">16000 Hz</option> |
|
|
<option value="22050">22050 Hz</option> |
|
|
<option value="44100">44100 Hz</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="input-group"> |
|
|
<label for="chunkSize">Audio Chunk Size (samples):</label> |
|
|
<select id="chunkSize"> |
|
|
<option value="1024">1024 samples</option> |
|
|
<option value="2048">2048 samples</option> |
|
|
<option value="4096" selected>4096 samples</option> |
|
|
<option value="8192">8192 samples</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="input-group"> |
|
|
<label for="Interim-Results"> Interim-Results:</label> |
|
|
<select id="interimResults"> |
|
|
<option value="true">True</option> |
|
|
<option value="false">False</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div class="input-group"> |
|
|
<label for="language">Language Code:</label> |
|
|
<input type="text" id="language_code" value="en-US" placeholder="en-US"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<button id="connectBtn" class="btn btn-connect">Connect to Debug Server</button> |
|
|
<button id="disconnectBtn" class="btn btn-disconnect" style="display: none;">Disconnect</button> |
|
|
</div> |
|
|
|
|
|
<div id="status" class="status disconnected">Disconnected</div> |
|
|
|
|
|
<div class="stats-section"> |
|
|
<div class="stat-box"> |
|
|
<div class="stat-value" id="actualSampleRate">0</div> |
|
|
<div class="stat-label">Calculated Sample Rate (Hz)</div> |
|
|
</div> |
|
|
<div class="stat-box"> |
|
|
<div class="stat-value" id="bytesSent">0</div> |
|
|
<div class="stat-label">Total Bytes Sent</div> |
|
|
</div> |
|
|
<div class="stat-box"> |
|
|
<div class="stat-value" id="chunksSent">0</div> |
|
|
<div class="stat-label">Audio Chunks Sent</div> |
|
|
</div> |
|
|
<div class="stat-box"> |
|
|
<div class="stat-value" id="duration">0.0s</div> |
|
|
<div class="stat-label">Recording Duration</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="audio-controls"> |
|
|
<button id="micBtn" class="btn btn-mic" disabled title="Start Recording">๐ค</button> |
|
|
<button id="stopBtn" class="btn btn-stop" disabled title="Stop Recording">โน๏ธ</button> |
|
|
</div> |
|
|
|
|
|
<div class="response-section"> |
|
|
<h3>Server Responses:</h3> |
|
|
<div id="responseBox" class="response-box">Waiting for connection...</div> |
|
|
</div> |
|
|
|
|
|
<div class="debug-section"> |
|
|
<h3>Debug Console:</h3> |
|
|
<div id="debugBox" class="debug-box">Ready to connect...</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
class SampleRateAnalyzer { |
|
|
constructor() { |
|
|
this.websocket = null; |
|
|
this.audioContext = null; |
|
|
this.mediaRecorder = null; |
|
|
this.audioStream = null; |
|
|
this.processor = null; |
|
|
this.isRecording = false; |
|
|
this.isConnected = false; |
|
|
|
|
|
|
|
|
this.startTime = null; |
|
|
this.totalBytesSent = 0; |
|
|
this.chunksSent = 0; |
|
|
this.targetSampleRate = 8000; |
|
|
this.chunkSize = 4096; |
|
|
|
|
|
this.initializeElements(); |
|
|
this.attachEventListeners(); |
|
|
} |
|
|
|
|
|
initializeElements() { |
|
|
this.elements = { |
|
|
websocketUrl: document.getElementById('websocketUrl'), |
|
|
targetSampleRate: document.getElementById('targetSampleRate'), |
|
|
chunkSize: document.getElementById('chunkSize'), |
|
|
connectBtn: document.getElementById('connectBtn'), |
|
|
disconnectBtn: document.getElementById('disconnectBtn'), |
|
|
micBtn: document.getElementById('micBtn'), |
|
|
stopBtn: document.getElementById('stopBtn'), |
|
|
status: document.getElementById('status'), |
|
|
responseBox: document.getElementById('responseBox'), |
|
|
debugBox: document.getElementById('debugBox'), |
|
|
actualSampleRate: document.getElementById('actualSampleRate'), |
|
|
bytesSent: document.getElementById('bytesSent'), |
|
|
chunksSent: document.getElementById('chunksSent'), |
|
|
duration: document.getElementById('duration') |
|
|
}; |
|
|
} |
|
|
|
|
|
attachEventListeners() { |
|
|
this.elements.connectBtn.addEventListener('click', () => this.connect()); |
|
|
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect()); |
|
|
this.elements.micBtn.addEventListener('click', () => this.startRecording()); |
|
|
this.elements.stopBtn.addEventListener('click', () => this.stopRecording()); |
|
|
this.elements.targetSampleRate.addEventListener('change', (e) => { |
|
|
this.targetSampleRate = parseInt(e.target.value); |
|
|
this.debugLog(`Target sample rate changed to: ${this.targetSampleRate} Hz`); |
|
|
}); |
|
|
this.elements.chunkSize.addEventListener('change', (e) => { |
|
|
this.chunkSize = parseInt(e.target.value); |
|
|
this.debugLog(`Chunk size changed to: ${this.chunkSize} samples`); |
|
|
}); |
|
|
} |
|
|
|
|
|
debugLog(message) { |
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
const debugBox = this.elements.debugBox; |
|
|
debugBox.innerHTML += `[${timestamp}] ${message}\n`; |
|
|
debugBox.scrollTop = debugBox.scrollHeight; |
|
|
} |
|
|
|
|
|
updateStatus(message, type) { |
|
|
this.elements.status.textContent = message; |
|
|
this.elements.status.className = `status ${type}`; |
|
|
} |
|
|
|
|
|
updateStats() { |
|
|
if (this.startTime) { |
|
|
const elapsed = (Date.now() - this.startTime) / 1000; |
|
|
this.elements.duration.textContent = `${elapsed.toFixed(1)}s`; |
|
|
|
|
|
|
|
|
if (elapsed > 0.5) { |
|
|
const totalSamples = this.totalBytesSent / 2; |
|
|
const calculatedRate = totalSamples / elapsed; |
|
|
this.elements.actualSampleRate.textContent = Math.round(calculatedRate); |
|
|
|
|
|
|
|
|
const difference = Math.abs(calculatedRate - this.targetSampleRate); |
|
|
if (difference > 100) { |
|
|
this.debugLog(`โ ๏ธ Sample rate mismatch! Target: ${this.targetSampleRate}Hz, Actual: ${Math.round(calculatedRate)}Hz`); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
this.elements.bytesSent.textContent = this.totalBytesSent.toLocaleString(); |
|
|
this.elements.chunksSent.textContent = this.chunksSent.toLocaleString(); |
|
|
} |
|
|
|
|
|
async connect() { |
|
|
const url = this.elements.websocketUrl.value.trim(); |
|
|
if (!url) { |
|
|
alert('Please enter a WebSocket URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
this.updateStatus('Connecting...', 'disconnected'); |
|
|
this.elements.connectBtn.disabled = true; |
|
|
this.debugLog(`Attempting to connect to: ${url}`); |
|
|
|
|
|
this.websocket = new WebSocket(url); |
|
|
this.websocket.binaryType = 'arraybuffer'; |
|
|
|
|
|
|
|
|
const connectionTimeout = setTimeout(() => { |
|
|
if (this.websocket.readyState === WebSocket.CONNECTING) { |
|
|
this.websocket.close(); |
|
|
this.debugLog('โ Connection timeout'); |
|
|
this.updateStatus('Connection Timeout', 'disconnected'); |
|
|
this.resetConnection(); |
|
|
} |
|
|
}, 10000); |
|
|
|
|
|
this.websocket.onopen = () => { |
|
|
clearTimeout(connectionTimeout); |
|
|
this.isConnected = true; |
|
|
this.updateStatus('Connected to Debug Server', 'connected'); |
|
|
this.elements.connectBtn.style.display = 'none'; |
|
|
this.elements.disconnectBtn.style.display = 'block'; |
|
|
this.elements.micBtn.disabled = false; |
|
|
this.elements.responseBox.textContent = 'Connected to debug server. Ready to test sample rate...'; |
|
|
this.debugLog('โ
WebSocket connected successfully'); |
|
|
}; |
|
|
|
|
|
this.websocket.onmessage = (event) => { |
|
|
if (typeof event.data === 'string') { |
|
|
try { |
|
|
const response = JSON.parse(event.data); |
|
|
this.displayResponse('JSON Response', response); |
|
|
this.debugLog(`๐จ Received JSON: ${JSON.stringify(response)}`); |
|
|
} catch (e) { |
|
|
this.displayResponse('Text Response', event.data); |
|
|
this.debugLog(`๐จ Received Text: ${event.data}`); |
|
|
} |
|
|
} else { |
|
|
this.displayResponse('Binary Response', `Received binary data: ${event.data.byteLength} bytes`); |
|
|
this.debugLog(`๐จ Received Binary: ${event.data.byteLength} bytes`); |
|
|
} |
|
|
}; |
|
|
|
|
|
this.websocket.onerror = (error) => { |
|
|
clearTimeout(connectionTimeout); |
|
|
console.error('WebSocket error:', error); |
|
|
this.debugLog(`โ WebSocket error: ${error.message || 'Unknown error'}`); |
|
|
this.updateStatus('Connection Error', 'disconnected'); |
|
|
this.resetConnection(); |
|
|
}; |
|
|
|
|
|
this.websocket.onclose = (event) => { |
|
|
clearTimeout(connectionTimeout); |
|
|
this.isConnected = false; |
|
|
const reason = event.reason || 'No reason provided'; |
|
|
this.debugLog(`๐ WebSocket closed: Code ${event.code}, Reason: ${reason}`); |
|
|
this.updateStatus(`Disconnected (Code: ${event.code})`, 'disconnected'); |
|
|
this.resetConnection(); |
|
|
this.displayResponse('Connection Closed', `Code: ${event.code}, Reason: ${reason}`); |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Connection failed:', error); |
|
|
this.debugLog(`โ Connection failed: ${error.message}`); |
|
|
this.updateStatus('Connection Failed', 'disconnected'); |
|
|
this.resetConnection(); |
|
|
} |
|
|
} |
|
|
|
|
|
disconnect() { |
|
|
if (this.isRecording) { |
|
|
this.stopRecording(); |
|
|
} |
|
|
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { |
|
|
this.debugLog('๐ Manually disconnecting...'); |
|
|
this.websocket.close(1000, 'Client disconnect'); |
|
|
} |
|
|
this.resetConnection(); |
|
|
} |
|
|
|
|
|
resetConnection() { |
|
|
this.isConnected = false; |
|
|
this.elements.connectBtn.disabled = false; |
|
|
this.elements.connectBtn.style.display = 'block'; |
|
|
this.elements.disconnectBtn.style.display = 'none'; |
|
|
this.elements.micBtn.disabled = true; |
|
|
this.elements.stopBtn.disabled = true; |
|
|
this.stopRecording(); |
|
|
|
|
|
|
|
|
this.totalBytesSent = 0; |
|
|
this.chunksSent = 0; |
|
|
this.startTime = null; |
|
|
this.updateStats(); |
|
|
} |
|
|
|
|
|
|
|
|
floatTo16BitPCM(float32Array) { |
|
|
const int16Array = new Int16Array(float32Array.length); |
|
|
for (let i = 0; i < float32Array.length; i++) { |
|
|
const clipped = Math.max(-1, Math.min(1, float32Array[i])); |
|
|
int16Array[i] = clipped * 0x7FFF; |
|
|
} |
|
|
return int16Array; |
|
|
} |
|
|
|
|
|
|
|
|
resampleAudio(audioBuffer, sourceSampleRate, targetSampleRate) { |
|
|
if (sourceSampleRate === targetSampleRate) { |
|
|
return audioBuffer; |
|
|
} |
|
|
|
|
|
const ratio = sourceSampleRate / targetSampleRate; |
|
|
const targetLength = Math.round(audioBuffer.length / ratio); |
|
|
const resampled = new Float32Array(targetLength); |
|
|
|
|
|
for (let i = 0; i < targetLength; i++) { |
|
|
const sourceIndex = i * ratio; |
|
|
const sourceIndexFloor = Math.floor(sourceIndex); |
|
|
const sourceIndexCeil = Math.min(sourceIndexFloor + 1, audioBuffer.length - 1); |
|
|
const weight = sourceIndex - sourceIndexFloor; |
|
|
|
|
|
resampled[i] = audioBuffer[sourceIndexFloor] * (1 - weight) + |
|
|
audioBuffer[sourceIndexCeil] * weight; |
|
|
} |
|
|
|
|
|
return resampled; |
|
|
} |
|
|
|
|
|
async startRecording() { |
|
|
if (!this.isConnected) { |
|
|
alert('Please connect to debug server first'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
this.debugLog(`๐ค Starting recording with target sample rate: ${this.targetSampleRate} Hz`); |
|
|
|
|
|
|
|
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
this.debugLog(`๐ Audio context created with sample rate: ${this.audioContext.sampleRate} Hz`); |
|
|
|
|
|
|
|
|
this.audioStream = await navigator.mediaDevices.getUserMedia({ |
|
|
audio: { |
|
|
echoCancellation: false, |
|
|
noiseSuppression: false, |
|
|
autoGainControl: false, |
|
|
channelCount: 1 |
|
|
} |
|
|
}); |
|
|
|
|
|
const source = this.audioContext.createMediaStreamSource(this.audioStream); |
|
|
|
|
|
|
|
|
this.processor = this.audioContext.createScriptProcessor(this.chunkSize, 1, 1); |
|
|
|
|
|
this.processor.onaudioprocess = (event) => { |
|
|
if (!this.isRecording || !this.websocket || this.websocket.readyState !== WebSocket.OPEN) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const inputBuffer = event.inputBuffer; |
|
|
const audioData = inputBuffer.getChannelData(0); |
|
|
|
|
|
|
|
|
const resampled = this.resampleAudio(audioData, this.audioContext.sampleRate, this.targetSampleRate); |
|
|
|
|
|
|
|
|
const pcmData = this.floatTo16BitPCM(resampled); |
|
|
|
|
|
|
|
|
this.websocket.send(pcmData.buffer); |
|
|
|
|
|
|
|
|
this.totalBytesSent += pcmData.buffer.byteLength; |
|
|
this.chunksSent += 1; |
|
|
this.updateStats(); |
|
|
|
|
|
if (this.chunksSent % 10 === 0) { |
|
|
this.debugLog(`๐ Sent ${this.chunksSent} chunks, ${this.totalBytesSent} bytes`); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
source.connect(this.processor); |
|
|
this.processor.connect(this.audioContext.destination); |
|
|
|
|
|
|
|
|
const startMessage = { |
|
|
type: "start", |
|
|
language: this.language_code, |
|
|
format: "raw", |
|
|
encoding: "LINEAR16", |
|
|
interimResults: this.interimResults, |
|
|
sampleRateHz: this.targetSampleRate, |
|
|
options: { |
|
|
testClient: true, |
|
|
chunkSize: this.chunkSize |
|
|
} |
|
|
}; |
|
|
|
|
|
this.websocket.send(JSON.stringify(startMessage)); |
|
|
this.displayResponse('Sent START', startMessage); |
|
|
this.debugLog(`๐ค Sent START message: ${JSON.stringify(startMessage)}`); |
|
|
|
|
|
this.isRecording = true; |
|
|
this.startTime = Date.now(); |
|
|
|
|
|
|
|
|
this.elements.micBtn.classList.add('recording'); |
|
|
this.elements.micBtn.disabled = true; |
|
|
this.elements.stopBtn.disabled = false; |
|
|
this.updateStatus(`Recording @ ${this.targetSampleRate}Hz`, 'recording'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Failed to start recording:', error); |
|
|
this.debugLog(`โ Recording failed: ${error.message}`); |
|
|
alert('Failed to access microphone. Please check permissions.'); |
|
|
this.stopRecording(); |
|
|
} |
|
|
} |
|
|
|
|
|
stopRecording() { |
|
|
if (this.isRecording) { |
|
|
this.isRecording = false; |
|
|
this.debugLog('๐ Stopping recording...'); |
|
|
|
|
|
|
|
|
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { |
|
|
const stopMessage = { type: "stop" }; |
|
|
this.websocket.send(JSON.stringify(stopMessage)); |
|
|
this.displayResponse('Sent STOP', stopMessage); |
|
|
this.debugLog(`๐ค Sent STOP message`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (this.processor) { |
|
|
this.processor.disconnect(); |
|
|
this.processor = null; |
|
|
} |
|
|
|
|
|
if (this.audioContext) { |
|
|
this.audioContext.close().then(() => { |
|
|
this.audioContext = null; |
|
|
this.debugLog('๐ Audio context closed'); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (this.audioStream) { |
|
|
this.audioStream.getTracks().forEach(track => track.stop()); |
|
|
this.audioStream = null; |
|
|
this.debugLog('๐ค Microphone stream stopped'); |
|
|
} |
|
|
|
|
|
|
|
|
this.elements.micBtn.classList.remove('recording'); |
|
|
this.elements.micBtn.disabled = false; |
|
|
this.elements.stopBtn.disabled = true; |
|
|
|
|
|
if (this.isConnected) { |
|
|
this.updateStatus('Connected - Recording stopped', 'connected'); |
|
|
} |
|
|
|
|
|
|
|
|
this.updateStats(); |
|
|
} |
|
|
|
|
|
displayResponse(messageType, response) { |
|
|
const responseBox = this.elements.responseBox; |
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
|
|
|
let content = `[${timestamp}] ${messageType}:\n`; |
|
|
|
|
|
if (typeof response === 'object') { |
|
|
content += JSON.stringify(response, null, 2); |
|
|
} else { |
|
|
content += response; |
|
|
} |
|
|
|
|
|
responseBox.innerHTML += content + '\n\n'; |
|
|
responseBox.scrollTop = responseBox.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new SampleRateAnalyzer(); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |