aboutsummaryrefslogtreecommitdiff
path: root/web/speaker/speaker.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/speaker/speaker.js')
-rw-r--r--web/speaker/speaker.js289
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