diff --git a/Panels/CommunicationsPanel.js b/Panels/CommunicationsPanel.js index 8fccd8a..cf50a13 100644 --- a/Panels/CommunicationsPanel.js +++ b/Panels/CommunicationsPanel.js @@ -8,15 +8,7 @@ class CommunicationsPanel extends BasePanel { {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); } diff --git a/Panels/MicrophonePanel.js b/Panels/MicrophonePanel.js new file mode 100644 index 0000000..6c3492b --- /dev/null +++ b/Panels/MicrophonePanel.js @@ -0,0 +1,150 @@ +import BasePanel from './BasePanel'; + +const media = { + audio: { + mandatory: { + autoGainControl: false, + echoCancellation: false, + noiseSuppression: false, + suppressLocalAudioPlayback: false, + voiceIsolation: false + }, + optional: [] + } +}; + +class MicrophonePanel extends BasePanel { + constructor() { + super('Microphone'); + + this.addCheckboxes('receive-via', [ + {text: 'Listen', id: 'listen', eventName: 'listenChange'} + ]); + this.addCanvas('canvas', 100, 25); + + this.addEventListener('listenChange', (e) => { + if(e.checked) { + this.error = undefined; + navigator.mediaDevices.getUserMedia(media) + .then(stream => { + this.stream = stream; + this.connectStream(); + this.dispatcher.emit('on', stream); + }) + .catch(error => { + this.error = error; + console.log(error); + this.setListening(false); + this.disconnectStream(); + this.stopSampling(); + }) + } else { + if(this.stream) { + this.error = undefined; + this.disconnectStream(); + this.dispatcher.emit('off'); + } + this.stopSampling(); + } + }) + }; + + setAnalyser = analyser => { + if(analyser) { + this.analyser = analyser; + this.connectStream(); + } else { + this.disconnectStream(); + this.analyser = analyser; + } + } + setAudioContext = audioContext => { + if(audioContext) { + this.audioContext = audioContext; + this.connectStream(); + } else { + this.disconnectStream(); + this.audioContext = audioContext; + } + } + connectStream = () => { + if(this.stream) { + if(this.audioContext) { + if(!this.streamNode) { + this.streamNode = this.audioContext.createMediaStreamSource(this.stream); + } + if(this.analyser) { + this.streamNode.connect(this.analyser); + } + if(this.getListening()) { + this.startSampling(); + } + } + } + } + disconnectStream = () => { + if(this.streamNode) { + if(this.analyser) { + this.streamNode.disconnect(this.analyser); + this.streamNode = undefined; + } + } + if(this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + this.stream = undefined; + } + this.stopSampling(); + } + + isSampling = () => !!this.samplingId; + startSampling = () => { + if(this.isSampling()) return; + // nothing to analyse + if(!this.stream) return; + // nothing to analyse with + if(!this.analyser) return; + this.samplingId = window.requestAnimationFrame(this.drawSpectrumAnalyzer); + } + stopSampling = () => { + if(!this.isSampling()) return; + window.cancelAnimationFrame(this.samplingId); + this.samplingId = undefined; + } + + getListening = () => this.getCheckedById('listen'); + setListening = checked => { + if(this.getListening() !== checked) { + this.setCheckedById('listen', checked) + this.dispatcher.emit('listenChange', {checked}); + } + }; + + getStream = () => this.stream; + + drawSpectrumAnalyzer = () => { + const canvas = this.getElement('canvas'); + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + + let frequencyResolution = this.audioContext.sampleRate / this.analyser.fftSize; + let nyquistFrequency = this.audioContext.sampleRate / 2; + let buffer = new Uint8Array(this.analyser.frequencyBinCount); + this.analyser.getByteFrequencyData(buffer); + + ctx.clearRect(0, 0, width, height); + let barWidth = (1/buffer.length) * width; + for(let i = 0; i < buffer.length; i++) { + let x = i * barWidth; + let y = (1 - (buffer[i] / 255)) * height; + const hue = Math.floor(((i * frequencyResolution) / nyquistFrequency) * 360) + ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; + ctx.fillRect(x, y, barWidth, height-y); + } + + this.samplingId = window.requestAnimationFrame(this.drawSpectrumAnalyzer); + } +} + + +export default MicrophonePanel; \ No newline at end of file diff --git a/index.js b/index.js index ca0a93d..a763fdf 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ import { textToBits, textToBytes, } from './converters'; +import MicrophonePanel from "./Panels/MicrophonePanel"; var audioContext; var microphoneStream; var microphoneNode; @@ -68,6 +69,7 @@ const availableFskPairsPanel = new AvailableFskPairsPanel(); const frequencyGraphPanel = new FrequencyGraphPanel(); const graphConfigurationPanel = new GraphConfigurationPanel(); const speedPanel = new SpeedPanel(); +const microphonePanel = new MicrophonePanel(); function handleWindowLoad() { const panelContainer = document.getElementById('panel-container'); @@ -82,9 +84,11 @@ function handleWindowLoad() { panelContainer.prepend(bitsSentPanel.getDomElement()); panelContainer.prepend(messagePanel.getDomElement()); panelContainer.prepend(communicationsPanel.getDomElement()); + panelContainer.prepend(microphonePanel.getDomElement()); // Initialize Values - communicationsPanel.setListening(false); + microphonePanel.setListening(false); + communicationsPanel.setSendSpeakers(false); communicationsPanel.setSendAnalyzer(true); @@ -135,7 +139,6 @@ function handleWindowLoad() { // Events - communicationsPanel.addEventListener('listeningChange', handleChangeListening); communicationsPanel.addEventListener('sendSpeakersChange', handleChangeSendSpeakers); communicationsPanel.addEventListener('sendAnalyzerChange', handleChangeSendAnalyzer); @@ -554,7 +557,7 @@ function stopGraph() { } function resumeGraph() { - if(communicationsPanel.isListeningChecked()) { + if(microphonePanel.getListening()) { if(PAUSE) { PAUSE = false; AudioReceiver.start(); @@ -830,6 +833,7 @@ function getAudioContext() { audioContext = new (window.AudioContext || webkitAudioContext)(); frequencyPanel.setSampleRate(audioContext.sampleRate); availableFskPairsPanel.setSampleRate(audioContext.sampleRate); + microphonePanel.setAudioContext(audioContext); } if(audioContext.state === 'suspended') { audioContext.resume(); @@ -847,6 +851,8 @@ function getAnalyser() { if(analyser) return analyser; analyser = audioContext.createAnalyser(); frequencyGraphPanel.setAnalyser(analyser); + microphonePanel.setAnalyser(analyser); + analyser.smoothingTimeConstant = signalPanel.getSmoothingTimeConstant(); analyser.fftSize = frequencyPanel.getFftSize(); return analyser; @@ -859,51 +865,6 @@ function handleChangeSendSpeakers({checked}) { SEND_VIA_SPEAKER = checked; configurationChanged(); } -function handleChangeListening({checked}) { - stopGraph(); - var audioContext = getAudioContext(); - function handleMicrophoneOn(stream) { - microphoneStream = stream; - microphoneNode = audioContext.createMediaStreamSource(stream); - analyser = getAnalyser(); - microphoneNode.connect(analyser); - resumeGraph(); - } - function handleMicrophoneError(error) { - console.error('Microphone Error', error); - } - if(checked) { - navigator.mediaDevices - .getUserMedia({ - audio: { - mandatory: { - autoGainControl: false, - echoCancellation: false, - noiseSuppression: false, - suppressLocalAudioPlayback: false, - voiceIsolation: false - }, - optional: [] - } - }) - .then(handleMicrophoneOn) - .catch(handleMicrophoneError) - } else { - if(microphoneStream) { - microphoneStream.getTracks().forEach(track => track.stop()); - microphoneStream = undefined; - } - if(analyser && microphoneNode) { - try { - analyser.disconnect(microphoneNode); - } catch(e) { - - } - microphoneNode = undefined; - analyser = undefined; - } - } -} function drawChannelData() { // Do/did we have a stream?