From 573664999ffe0af1feaf6ca7c0654ddcd105fcd6 Mon Sep 17 00:00:00 2001 From: Jose B Date: Fri, 21 Jul 2023 06:56:29 -0500 Subject: [PATCH 1/6] custom recording waveform --- app/components/CustomRecordPlugin.js | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/app/components/CustomRecordPlugin.js b/app/components/CustomRecordPlugin.js index 6ec25fd4..c8955c58 100644 --- a/app/components/CustomRecordPlugin.js +++ b/app/components/CustomRecordPlugin.js @@ -10,6 +10,83 @@ class CustomRecordPlugin extends RecordPlugin { static create(options) { return new CustomRecordPlugin(options || {}) } + render(stream) { + if (!this.wavesurfer) return () => undefined + + const container = this.wavesurfer.getWrapper() + const canvas = document.createElement('canvas') + canvas.width = container.clientWidth + canvas.height = container.clientHeight + canvas.style.zIndex = '10' + container.appendChild(canvas) + + const canvasCtx = canvas.getContext('2d') + const audioContext = new AudioContext() + const source = audioContext.createMediaStreamSource(stream) + const analyser = audioContext.createAnalyser() + analyser.fftSize = 2 ** 5 + source.connect(analyser) + const bufferLength = analyser.frequencyBinCount + const dataArray = new Uint8Array(bufferLength) + + let animationId, previousTimeStamp; + const BUFFER_SIZE = 2 ** 8 + const dataBuffer = new Array(BUFFER_SIZE).fill(canvas.height) + + const drawWaveform = (timeStamp) => { + if (!canvasCtx) return + + analyser.getByteTimeDomainData(dataArray) + canvasCtx.clearRect(0, 0, canvas.width, canvas.height) + canvasCtx.fillStyle = 'black' + + if (previousTimeStamp === undefined) { + previousTimeStamp = timeStamp + dataBuffer.push(Math.min(...dataArray)) + dataBuffer.splice(0, 1) + } + const elapsed = timeStamp - previousTimeStamp; + if (elapsed > 10) { + previousTimeStamp = timeStamp + dataBuffer.push(Math.min(...dataArray)) + dataBuffer.splice(0, 1) + } + + // Drawing + const sliceWidth = canvas.width / dataBuffer.length + let x = 0 + + for (let i = 0; i < dataBuffer.length; i++) { + const valueNormalized = dataBuffer[i] / canvas.height + const y = valueNormalized * canvas.height / 2 + const sliceHeight = canvas.height + 1 - y * 2 + + canvasCtx.fillRect(x, y, sliceWidth * 2 / 3, sliceHeight) + x += sliceWidth + } + + animationId = requestAnimationFrame(drawWaveform) + } + + drawWaveform() + + return () => { + if (animationId) { + cancelAnimationFrame(animationId) + } + + if (source) { + source.disconnect() + source.mediaStream.getTracks().forEach((track) => track.stop()) + } + + if (audioContext) { + audioContext.close() + } + + canvas?.remove() + } + } startRecording(stream) { this.preventInteraction() this.cleanUp() From e6578d1da2d1ce98ce2fa8ddd0973275a5e2613d Mon Sep 17 00:00:00 2001 From: Jose B Date: Fri, 21 Jul 2023 07:13:21 -0500 Subject: [PATCH 2/6] minor fix --- app/components/record.js | 1 + app/globals.scss | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/components/record.js b/app/components/record.js index de9897cb..6079a920 100644 --- a/app/components/record.js +++ b/app/components/record.js @@ -69,6 +69,7 @@ export default function Recorder(props) { record.stopRecording(); setIsRecording(false); document.getElementById("play-btn").disabled = false; + props.onStop() } else { const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId }, diff --git a/app/globals.scss b/app/globals.scss index a1c78d64..acc5dae4 100644 --- a/app/globals.scss +++ b/app/globals.scss @@ -8,14 +8,6 @@ --background-end-rgb: 255, 255, 255; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 32, 32, 32; - --background-end-rgb: 32, 32, 32; - } -} - body { background: linear-gradient( to bottom, @@ -25,6 +17,7 @@ body { rgb(var(--background-start-rgb)); font-family: "Roboto", sans-serif; + color: var(--foreground-rgb); } .temp-transcription { From 23d563b97a3d98da5103a70ebbb73bc0c4594ae8 Mon Sep 17 00:00:00 2001 From: Jose B Date: Fri, 21 Jul 2023 07:42:17 -0500 Subject: [PATCH 3/6] handle audio input permissions --- app/components/record.js | 63 ++++++++++++++++++++++++++-------------- app/page.js | 2 +- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/components/record.js b/app/components/record.js index 6079a920..92d339aa 100644 --- a/app/components/record.js +++ b/app/components/record.js @@ -7,6 +7,46 @@ import "react-dropdown/style.css"; import CustomRecordPlugin from "./CustomRecordPlugin"; +const queryAndPromptAudio = async () => { + const permissionStatus = await navigator.permissions.query({name: 'microphone'}) + if (permissionStatus.state == 'prompt') { + await navigator.mediaDevices.getUserMedia({ audio: true }) + } +} + +const AudioInputsDropdown = (props) => { + const [ddOptions, setDdOptions] = useState([]); + + useEffect(() => { + const init = async () => { + await queryAndPromptAudio() + + const devices = await navigator.mediaDevices.enumerateDevices() + const audioDevices = devices + .filter((d) => d.kind === "audioinput" && d.deviceId != "") + .map((d) => ({ value: d.deviceId, label: d.label })) + + if (audioDevices.length < 1) return console.log("no audio input devices") + + setDdOptions(audioDevices) + props.setDeviceId(audioDevices[0].value) + } + init() + }, []) + + const handleDropdownChange = (e) => { + props.setDeviceId(e.value); + }; + + return ( + + ) +} + export default function Recorder(props) { const waveformRef = useRef(); const [wavesurfer, setWavesurfer] = useState(null); @@ -14,22 +54,10 @@ export default function Recorder(props) { const [isRecording, setIsRecording] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [deviceId, setDeviceId] = useState(null); - const [ddOptions, setDdOptions] = useState([]); useEffect(() => { document.getElementById("play-btn").disabled = true; - navigator.mediaDevices.enumerateDevices().then((devices) => { - const audioDevices = devices - .filter((d) => d.kind === "audioinput") - .map((d) => ({ value: d.deviceId, label: d.label })); - - if (audioDevices.length < 1) return console.log("no audio input devices"); - - setDdOptions(audioDevices); - setDeviceId(audioDevices[0].value); - }); - if (waveformRef.current) { const _wavesurfer = WaveSurfer.create({ container: waveformRef.current, @@ -84,22 +112,15 @@ export default function Recorder(props) { wavesurfer?.playPause(); }; - const handleDropdownChange = (e) => { - setDeviceId(e.value); - }; - return (
- +   diff --git a/app/page.js b/app/page.js index c0fbfa10..e0ef15f9 100644 --- a/app/page.js +++ b/app/page.js @@ -19,7 +19,7 @@ const App = () => {

Capture The Signal, Not The Noise

- serverData.peer.send(JSON.stringify({ cmd: 'STOP' }))}/> + serverData?.peer?.send(JSON.stringify({ cmd: 'STOP' }))}/> Date: Fri, 21 Jul 2023 07:44:16 -0500 Subject: [PATCH 4/6] disable dropdown when recording --- app/components/record.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/record.js b/app/components/record.js index 92d339aa..56f83e82 100644 --- a/app/components/record.js +++ b/app/components/record.js @@ -43,6 +43,7 @@ const AudioInputsDropdown = (props) => { options={ddOptions} onChange={handleDropdownChange} value={ddOptions[0]} + disabled={props.disabled} /> ) } @@ -115,7 +116,7 @@ export default function Recorder(props) { return (
- +