diff --git a/AudioSender.js b/AudioSender.js index 833bdb8..154074d 100644 --- a/AudioSender.js +++ b/AudioSender.js @@ -5,12 +5,11 @@ const dispatcher = new Dispatcher('AudioSender', ['begin', 'end', 'send']); let audioContext; let CHANNELS = []; let DESTINATION; -let ON_START; -let ON_STOP; -let ON_SEND; let CHANNEL_OSCILLATORS = []; let WAVE_FORM; +let futureEventIds = []; + let stopOscillatorsTimeoutId; export const addEventListener = dispatcher.addListener; @@ -19,16 +18,10 @@ export const removeEventListener = dispatcher.removeListener; export const changeConfiguration = ({ channels, destination, - startCallback, - stopCallback, - sendCallback, waveForm }) => { CHANNELS = channels; DESTINATION = destination; - ON_START = startCallback; - ON_STOP = stopCallback; - ON_SEND = sendCallback; WAVE_FORM = waveForm; } @@ -85,7 +78,21 @@ export function send(bits, startSeconds) { const hz = channel[isHigh ? 1 : 0]; oscillator.frequency.setValueAtTime(hz, startSeconds); }); - dispatcher.emit('send', {bits: sentBits, startSeconds}); + + // Alghough we program an oscillator of when frequencies + // should change, let's not emit that the data is sent + // until the frequency actually changes in real-time + futureEventIds.push(window.setTimeout( + () => { + dispatcher.emit('send', {bits: sentBits, startSeconds}); + }, delayMs(startSeconds) + )); +} +const delayMs = (seconds) => { + const time = now(); + // now or in the past, no delay + if(time >= seconds) return 0; + return (seconds - time) * 1000; } const stopTimeout = () => { if(stopOscillatorsTimeoutId) { @@ -105,7 +112,7 @@ export function stopAt(streamEndSeconds) { stopTimeout(); stopOscillatorsTimeoutId = window.setTimeout( stop, - (streamEndSeconds - now()) * 1000 + delayMs(streamEndSeconds) ); } export function stop() { @@ -118,6 +125,8 @@ export function stop() { } ) oscillators.length = 0; + futureEventIds.forEach(window.clearTimeout); + futureEventIds.length = 0; dispatcher.emit('end'); stopTimeout(); } diff --git a/Panels/BasePanel.js b/Panels/BasePanel.js new file mode 100644 index 0000000..9f36abe --- /dev/null +++ b/Panels/BasePanel.js @@ -0,0 +1,150 @@ +import Dispatcher from "../Dispatcher"; + +let lastId = 0; +const clamp = (value, min, max) => Math.max(min, Math.min(value, max)); + +class BasePanel { + constructor(title) { + this.dispatcher = new Dispatcher(title); + this.id = `panel-${lastId++}`; + this.panel = document.createElement('div'); + this.panel.id = this.id; + const h2 = document.createElement('h2'); + h2.innerText = title; + this.panel.appendChild(h2); + this.container = document.createElement('div'); + this.panel.appendChild(this.container); + } + getDomElement = () => this.panel; + addSection = text => { + const header = document.createElement('h4'); + header.innerText = text; + this.append(header); + } + addRadios = (name, items) => { + this.addCheckedInputs('radio', name, items); + }; + addCheckboxes = (name, items) => { + this.addCheckedInputs('checkbox', name, items); + }; + addCheckedInputs = (type, name, items, value) => { + items.forEach(({id, text, checked = false, eventName = 'change'}, index)=> { + const label = document.createElement('label'); + const input = document.createElement('input'); + input.type = type; + input.name = name; + input.checked = checked; + input.value = value; + input.id = id; + input.addEventListener('change', e => { + this.dispatcher.emit(eventName, { + panel: this.id, + name, + id, + index, + checked: e.target.checked, + value + }); + }) + label.appendChild(input); + const textNode = document.createTextNode(text); + label.append(textNode); + this.append(label); + const br = document.createElement('br'); + this.append(br); + }); + } + addInputText = (id, value, eventName = 'input') => { + const input = document.createElement('input'); + input.type = 'text'; + input.value = value; + input.id = id; + input.addEventListener('input', e => { + this.dispatcher.emit(eventName, { + panel: this.id, + id, + value + }); + }); + this.append(input); + } + addButton = (id, text, eventName = 'click') => { + const button = document.createElement('button'); + button.id = id; + button.innerText = text; + button.addEventListener('click', e => { + this.dispatcher.emit(eventName, { + panel: this.id, + id, + text + }); + }); + this.append(button); + } + addProgressBar = (id, percent) => { + const progressBar = document.createElement('div'); + progressBar.className = 'progress-container'; + const bar = document.createElement('div'); + bar.id = id; + bar.className = 'progress-bar'; + bar.style.width = `${clamp(percent, 0, 1) * 100}%`; + progressBar.append(bar); + this.append(progressBar); + } + setProgressById = (id, percent) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + element.style.width = `${clamp(percent, 0, 1) * 100}%`; + } + addCode = (id, value, type = '') => { + const code = document.createElement('div'); + code.id = id; + code.innerText = value; + code.className = type === '' ? 'raw-data' : `raw-data-${type}`; + this.append(code); + } + setValueById = (id, value) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + switch(element.tagName) { + case 'INPUT': + case 'SELECT': + element.value = value; + break; + default: + element.innerText = value; + } + } + setHtmlById = (id, html) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + element.innerHTML = html; + } + getValueById = (id) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + switch(element.tagName) { + case 'INPUT': + case 'SELECT': + return element.value; + default: + return element.innerText; + } + } + setCheckedById = (id, checked) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + element.checked = checked; + } + getCheckedById = (id) => { + const element = document.getElementById(id); + if(!element) throw new Error(`Unable to find ${id}`); + return element.checked; + } + append = (element) => this.container.appendChild(element); + clear = () => this.container.innerHTML = ''; + addEventListener = (eventName, callback) => this.dispatcher.addListener(eventName, callback); + removeEventListener = (eventName, callback) => this.dispatcher.removeListener(eventName, callback); +} + +export default BasePanel; \ No newline at end of file diff --git a/Panels/CommunicationsPanel.js b/Panels/CommunicationsPanel.js new file mode 100644 index 0000000..8fccd8a --- /dev/null +++ b/Panels/CommunicationsPanel.js @@ -0,0 +1,24 @@ +import BasePanel from './BasePanel'; + +class CommunicationsPanel extends BasePanel { + constructor() { + super('Communications'); + this.addSection('Send'); + this.addRadios('send-via', [ + {text: 'Analyzer', id: 'send-via-analyzer', eventName: 'sendAnalyzerChange'}, + {text: 'Speakers', id: 'send-via-speaker', eventName: 'sendSpeakersChange'} + ]); + this.addSection('Receive'); + this.addCheckboxes('receive-via', [ + {text: 'Listening', id: 'is-listening-checkbox', eventName: 'listeningChange'} + ]); + } + isListeningChecked = () => { + return this.getCheckedById('is-listening-checkbox'); + } + setListening = checked => this.setCheckedById('is-listening-checkbox', checked); + setSendSpeakers = checked => this.setCheckedById('send-via-speaker', checked); + setSendAnalyzer = checked => this.setCheckedById('send-via-analyzer', checked); +} + +export default CommunicationsPanel; \ No newline at end of file diff --git a/Panels/MessagePanel.js b/Panels/MessagePanel.js new file mode 100644 index 0000000..8252e3f --- /dev/null +++ b/Panels/MessagePanel.js @@ -0,0 +1,20 @@ +import BasePanel from './BasePanel'; + +class MessagePanel extends BasePanel { + constructor() { + super('Message'); + this.addInputText('text-to-send', '', 'messageChange'); + this.addButton('send-button', 'Send', 'send'); + this.addSection('Received'); + this.addProgressBar('received-progress', .50); + this.addCode('decoded-text', '', 'small'); + } + getSendButtonText = () => this.getValueById('send-button'); + setSendButtonText = text => this.setValueById('send-button', text); + setMessage = text => this.setValueById('text-to-send', text); + getMessage = () => this.getValueById('text-to-send'); + setProgress = percent => this.setProgressById('received-progress', percent); + setReceived = (html) => this.setHtmlById('decoded-text', html); +} + +export default MessagePanel; \ No newline at end of file diff --git a/index.html b/index.html index a9761c4..68d8a6f 100644 --- a/index.html +++ b/index.html @@ -9,37 +9,7 @@

Data Over Audio

-
-
-

Communications

-
-

Send

-
-
-

Receive

- - -
-
-
-

Message

-
- -
-

Received

-
-
-
-

- -
-
+

Encoded segments to send

diff --git a/index.js b/index.js index c924660..2cb4186 100644 --- a/index.js +++ b/index.js @@ -7,11 +7,10 @@ import * as Randomizer from './Randomizer'; import * as AudioSender from './AudioSender'; import * as AudioReceiver from './AudioReceiver'; import * as CRC from './CRC.js'; +import CommunicationsPanel from './Panels/CommunicationsPanel'; +import MessagePanel from "./Panels/MessagePanel.js"; var audioContext; -var sendButton; -var textToSend; -var isListeningCheckbox; var microphoneStream; var microphoneNode; var analyser; @@ -23,21 +22,14 @@ var MAX_AMPLITUDE = 300; // Higher than 255 to give us space const MAXIMUM_PACKETIZATION_SIZE_BITS = 16; const CRC_BIT_COUNT = 8; -// bits as they arrived -let RECEIVED_SEGMENT_BITS = []; // RECEIVED_SEGMENT_BITS[packetIndex][segmentIndex][channel] = bit - // bits as they are sent let SENT_ORIGINAL_TEXT = ''; let SENT_ORIGINAL_BITS = []; // original bits let SENT_ENCODED_BITS = []; // bits with error encoding let SENT_TRANSFER_BITS = []; // bits sent in the transfer -// interval and timeout ids -let stopOscillatorsTimeoutId; - let EXCLUDED_CHANNELS = []; -var TEXT_TO_SEND = "U"; var MAX_BITS_DISPLAYED_ON_GRAPH = 79; var SEGMENT_DURATION = 30; var AMPLITUDE_THRESHOLD_PERCENT = .75; @@ -69,12 +61,36 @@ var PAUSE = false; var PAUSE_AFTER_END = true; var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits +const communicationsPanel = new CommunicationsPanel(); +const messagePanel = new MessagePanel(); + function handleWindowLoad() { - TEXT_TO_SEND = Randomizer.text(5); + const panelContainer = document.getElementById('panel-container'); + panelContainer.prepend(messagePanel.getDomElement()); + panelContainer.prepend(communicationsPanel.getDomElement()); + + // Initialize Values + communicationsPanel.setListening(false); + communicationsPanel.setSendSpeakers(false); + communicationsPanel.setSendAnalyzer(true); + + messagePanel.setMessage(Randomizer.text(5)); + messagePanel.setProgress(0); + messagePanel.setReceived(''); + messagePanel.setSendButtonText('Send'); + + // Communications Events + communicationsPanel.addEventListener('listeningChange', handleChangeListening); + communicationsPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); + communicationsPanel.addEventListener('sendAnalyzerChange', handleChangeSendAnalyzer); + + messagePanel.addEventListener('messageChange', configurationChanged); + messagePanel.addEventListener('send', handleSendButtonClick); + // Setup audio sender - AudioSender.addEventListener('begin', () => sendButton.innerText = 'Stop'); + AudioSender.addEventListener('begin', () => messagePanel.setSendButtonText('Stop')); AudioSender.addEventListener('send', handleAudioSenderSend); - AudioSender.addEventListener('end', () => sendButton.innerText = 'Send'); + AudioSender.addEventListener('end', () => messagePanel.setSendButtonText('Send')); // Setup audio receiver AudioReceiver.addEventListener('begin', handleAudioReceiverStart); AudioReceiver.addEventListener('receive', handleAudioReceiverReceive); @@ -83,12 +99,8 @@ function handleWindowLoad() { StreamManager.addEventListener('change', handleStreamManagerChange); // grab dom elements - sendButton = document.getElementById('send-button'); - isListeningCheckbox = document.getElementById('is-listening-checkbox'); receivedDataTextarea = document.getElementById('received-data'); receivedGraph = document.getElementById('received-graph'); - textToSend = document.getElementById('text-to-send'); - textToSend.value = TEXT_TO_SEND; sentDataTextArea = document.getElementById('sent-data'); const receivedChannelGraph = document.getElementById('received-channel-graph'); receivedChannelGraph.addEventListener('mouseover', handleReceivedChannelGraphMouseover); @@ -125,11 +137,6 @@ function handleWindowLoad() { PAUSE_AFTER_END = event.target.checked; if(!PAUSE_AFTER_END) resumeGraph(); }) - document.getElementById('send-via-speaker').checked = SEND_VIA_SPEAKER; - document.getElementById('send-via-speaker').addEventListener('input', event => { - SEND_VIA_SPEAKER = event.target.checked; - configurationChanged(); - }) document.getElementById('frequency-resolution-multiplier').value = FREQUENCY_RESOLUTION_MULTIPLIER; document.getElementById('frequency-resolution-multiplier').addEventListener('input', event => { FREQUENCY_RESOLUTION_MULTIPLIER = parseInt(event.target.value); @@ -185,9 +192,6 @@ function handleWindowLoad() { }); document.getElementById('audio-context-sample-rate').innerText = getAudioContext().sampleRate.toLocaleString(); // wire up events - sendButton.addEventListener('click', handleSendButtonClick); - isListeningCheckbox.addEventListener('click', handleListeningCheckbox); - textToSend.addEventListener('input', configurationChanged); configurationChanged(); } @@ -313,7 +317,7 @@ function updatePacketUtils() { }); } function updatePacketStats() { - const text = textToSend.value; + const text = messagePanel.getMessage(); const bits = textToBits(text); const byteCount = text.length; const bitCount = PacketUtils.getPacketizationBitCountFromBitCount(bits.length);; @@ -553,7 +557,7 @@ function stopGraph() { } function resumeGraph() { - if(isListeningCheckbox.checked) { + if(communicationsPanel.isListeningChecked()) { if(PAUSE) { PAUSE = false; AudioReceiver.start(); @@ -873,12 +877,12 @@ function bitsToText(bits) { return bytesToText(bytes.buffer); } function handleSendButtonClick() { - if(sendButton.innerText === 'Stop') { + if(messagePanel.getSendButtonText() === 'Stop') { AudioSender.stop(); } else { AudioReceiver.reset(); StreamManager.reset(); - const text = document.getElementById('text-to-send').value; + const text = messagePanel.getMessage(); sendBytes(textToBytes(text)); } } @@ -889,7 +893,15 @@ function getAnalyser() { analyser.fftSize = 2 ** FFT_SIZE_POWER; return analyser; } -function handleListeningCheckbox(e) { +function handleChangeSendAnalyzer({checked}) { + SEND_VIA_SPEAKER = !checked; + configurationChanged(); +} +function handleChangeSendSpeakers({checked}) { + SEND_VIA_SPEAKER = checked; + configurationChanged(); +} +function handleChangeListening({checked}) { stopGraph(); var audioContext = getAudioContext(); function handleMicrophoneOn(stream) { @@ -902,7 +914,7 @@ function handleListeningCheckbox(e) { function handleMicrophoneError(error) { console.error('Microphone Error', error); } - if(e.target.checked) { + if(checked) { navigator.mediaDevices .getUserMedia({ audio: {