diff options
Diffstat (limited to 'web/speaker/speaker.js')
-rw-r--r-- | web/speaker/speaker.js | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/web/speaker/speaker.js b/web/speaker/speaker.js new file mode 100644 index 0000000..b94180f --- /dev/null +++ b/web/speaker/speaker.js @@ -0,0 +1,289 @@ +import { loadBumble, connectWebSocketTransport } from "../bumble.js"; + +(function () { + 'use strict'; + + let codecText; + let packetsReceivedText; + let bytesReceivedText; + let streamStateText; + let connectionStateText; + let errorText; + let audioOnButton; + let mediaSource; + let sourceBuffer; + let audioElement; + let audioContext; + let audioAnalyzer; + let audioFrequencyBinCount; + let audioFrequencyData; + let packetsReceived = 0; + let bytesReceived = 0; + let audioState = "stopped"; + let streamState = "IDLE"; + let fftCanvas; + let fftCanvasContext; + let bandwidthCanvas; + let bandwidthCanvasContext; + let bandwidthBinCount; + let bandwidthBins = []; + let pyodide; + + const FFT_WIDTH = 800; + const FFT_HEIGHT = 256; + const BANDWIDTH_WIDTH = 500; + const BANDWIDTH_HEIGHT = 100; + + + function init() { + initUI(); + initMediaSource(); + initAudioElement(); + initAnalyzer(); + initBumble(); + } + + function initUI() { + audioOnButton = document.getElementById("audioOnButton"); + codecText = document.getElementById("codecText"); + packetsReceivedText = document.getElementById("packetsReceivedText"); + bytesReceivedText = document.getElementById("bytesReceivedText"); + streamStateText = document.getElementById("streamStateText"); + errorText = document.getElementById("errorText"); + connectionStateText = document.getElementById("connectionStateText"); + + audioOnButton.onclick = () => startAudio(); + + codecText.innerText = "AAC"; + setErrorText(""); + + requestAnimationFrame(onAnimationFrame); + } + + function initMediaSource() { + mediaSource = new MediaSource(); + mediaSource.onsourceopen = onMediaSourceOpen; + mediaSource.onsourceclose = onMediaSourceClose; + mediaSource.onsourceended = onMediaSourceEnd; + } + + function initAudioElement() { + audioElement = document.getElementById("audio"); + audioElement.src = URL.createObjectURL(mediaSource); + // audioElement.controls = true; + } + + function initAnalyzer() { + fftCanvas = document.getElementById("fftCanvas"); + fftCanvas.width = FFT_WIDTH + fftCanvas.height = FFT_HEIGHT + fftCanvasContext = fftCanvas.getContext('2d'); + fftCanvasContext.fillStyle = "rgb(0, 0, 0)"; + fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); + + bandwidthCanvas = document.getElementById("bandwidthCanvas"); + bandwidthCanvas.width = BANDWIDTH_WIDTH + bandwidthCanvas.height = BANDWIDTH_HEIGHT + bandwidthCanvasContext = bandwidthCanvas.getContext('2d'); + bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; + bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); + } + + async function initBumble() { + // Load pyodide + console.log("Loading Pyodide"); + pyodide = await loadPyodide(); + + // Load Bumble + console.log("Loading Bumble"); + const params = (new URL(document.location)).searchParams; + const bumblePackage = params.get("package") || "bumble"; + await loadBumble(pyodide, bumblePackage); + + console.log("Ready!") + + const hciWsUrl = params.get("hci") || "ws://localhost:9922/hci"; + try { + // Create a WebSocket HCI transport + let transport + try { + transport = await connectWebSocketTransport(pyodide, hciWsUrl); + } catch (error) { + console.error(error); + setErrorText(error); + return; + } + + // Run the scanner example + const script = await (await fetch("speaker.py")).text(); + await pyodide.runPythonAsync(script); + const pythonMain = pyodide.globals.get("main"); + console.log("Starting speaker..."); + await pythonMain(transport.packet_source, transport.packet_sink, onEvent); + console.log("Speaker running"); + } catch (err) { + console.log(err); + } + } + + function startAnalyzer() { + // FFT + if (audioElement.captureStream !== undefined) { + audioContext = new AudioContext(); + audioAnalyzer = audioContext.createAnalyser(); + audioAnalyzer.fftSize = 128; + audioFrequencyBinCount = audioAnalyzer.frequencyBinCount; + audioFrequencyData = new Uint8Array(audioFrequencyBinCount); + const stream = audioElement.captureStream(); + const source = audioContext.createMediaStreamSource(stream); + source.connect(audioAnalyzer); + } + + // Bandwidth + bandwidthBinCount = BANDWIDTH_WIDTH / 2; + bandwidthBins = []; + } + + function setErrorText(message) { + errorText.innerText = message; + if (message.length == 0) { + errorText.style.display = "none"; + } else { + errorText.style.display = "inline-block"; + } + } + + function setStreamState(state) { + streamState = state; + streamStateText.innerText = streamState; + } + + function onAnimationFrame() { + // FFT + if (audioAnalyzer !== undefined) { + audioAnalyzer.getByteFrequencyData(audioFrequencyData); + fftCanvasContext.fillStyle = "rgb(0, 0, 0)"; + fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT); + const barCount = audioFrequencyBinCount; + const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1; + for (let bar = 0; bar < barCount; bar++) { + const barHeight = audioFrequencyData[bar]; + fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`; + fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight); + } + } + + // Bandwidth + bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; + bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); + bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`; + for (let t = 0; t < bandwidthBins.length; t++) { + const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT; + bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight); + } + + // Display again at the next frame + requestAnimationFrame(onAnimationFrame); + } + + function onMediaSourceOpen() { + console.log(this.readyState); + sourceBuffer = mediaSource.addSourceBuffer("audio/aac"); + } + + function onMediaSourceClose() { + console.log(this.readyState); + } + + function onMediaSourceEnd() { + console.log(this.readyState); + } + + async function startAudio() { + try { + console.log("starting audio..."); + audioOnButton.disabled = true; + audioState = "starting"; + await audioElement.play(); + console.log("audio started"); + audioState = "playing"; + startAnalyzer(); + } catch (error) { + console.error(`play failed: ${error}`); + audioState = "stopped"; + audioOnButton.disabled = false; + } + } + + async function onEvent(name, params) { + // Dispatch the message. + const handlerName = `on${name.charAt(0).toUpperCase()}${name.slice(1)}` + const handler = eventHandlers[handlerName]; + if (handler !== undefined) { + handler(params); + } else { + console.warn(`unhandled event: ${name}`) + } + } + + function onStart() { + setStreamState("STARTED"); + } + + function onStop() { + setStreamState("STOPPED"); + } + + function onSuspend() { + setStreamState("SUSPENDED"); + } + + function onConnection(params) { + connectionStateText.innerText = `CONNECTED: ${params.get('peer_name')} (${params.get('peer_address')})`; + } + + function onDisconnection(params) { + connectionStateText.innerText = "DISCONNECTED"; + } + + function onAudio(python_packet) { + const packet = python_packet.toJs({create_proxies : false}); + python_packet.destroy(); + if (audioState != "stopped") { + // Queue the audio packet. + sourceBuffer.appendBuffer(packet); + } + + packetsReceived += 1; + packetsReceivedText.innerText = packetsReceived; + bytesReceived += packet.byteLength; + bytesReceivedText.innerText = bytesReceived; + + bandwidthBins[bandwidthBins.length] = packet.byteLength; + if (bandwidthBins.length > bandwidthBinCount) { + bandwidthBins.shift(); + } + } + + function onKeystoreupdate() { + // Sync the FS + pyodide.FS.syncfs(() => { + console.log("FS synced out") + }); + } + + const eventHandlers = { + onStart, + onStop, + onSuspend, + onConnection, + onDisconnection, + onAudio, + onKeystoreupdate + } + + window.onload = (event) => { + init(); + } + +}());
\ No newline at end of file |