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(); } }());