From acaaec0c401b666ed12bd7e5a3ee79907d8a25a6 Mon Sep 17 00:00:00 2001 From: Jose B Date: Wed, 19 Jul 2023 01:44:36 -0500 Subject: [PATCH] use wavesurfer, extend class, improve front --- app/components/CustomRecordPlugin.js | 44 ++++++++++ app/components/audioVisualizer.js | 63 -------------- app/components/record.js | 124 +++++++++++++++++++++------ app/globals.css | 4 +- app/page.js | 44 ++-------- package.json | 4 +- public/button.css | 2 +- yarn.lock | 17 ++++ 8 files changed, 170 insertions(+), 132 deletions(-) create mode 100644 app/components/CustomRecordPlugin.js delete mode 100644 app/components/audioVisualizer.js diff --git a/app/components/CustomRecordPlugin.js b/app/components/CustomRecordPlugin.js new file mode 100644 index 00000000..6ec25fd4 --- /dev/null +++ b/app/components/CustomRecordPlugin.js @@ -0,0 +1,44 @@ +// Override the startRecording method so we can pass the desired stream +// Checkout: https://github.com/katspaugh/wavesurfer.js/blob/fa2bcfe/src/plugins/record.ts + +import RecordPlugin from "wavesurfer.js/dist/plugins/record" + +const MIME_TYPES = ['audio/webm', 'audio/wav', 'audio/mpeg', 'audio/mp4', 'audio/mp3'] +const findSupportedMimeType = () => MIME_TYPES.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) + +class CustomRecordPlugin extends RecordPlugin { + static create(options) { + return new CustomRecordPlugin(options || {}) + } + startRecording(stream) { + this.preventInteraction() + this.cleanUp() + + const onStop = this.render(stream) + const mediaRecorder = new MediaRecorder(stream, { + mimeType: this.options.mimeType || findSupportedMimeType(), + audioBitsPerSecond: this.options.audioBitsPerSecond, + }) + const recordedChunks = [] + + mediaRecorder.addEventListener('dataavailable', (event) => { + if (event.data.size > 0) { + recordedChunks.push(event.data) + } + }) + + mediaRecorder.addEventListener('stop', () => { + onStop() + this.loadBlob(recordedChunks, mediaRecorder.mimeType) + this.emit('stopRecording') + }) + + mediaRecorder.start() + + this.emit('startRecording') + + this.mediaRecorder = mediaRecorder + } +} + +export default CustomRecordPlugin; \ No newline at end of file diff --git a/app/components/audioVisualizer.js b/app/components/audioVisualizer.js deleted file mode 100644 index fedaa5da..00000000 --- a/app/components/audioVisualizer.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useRef, useEffect } from "react"; - -function AudioVisualizer(props) { - const canvasRef = useRef(null); - - useEffect(() => { - let animationFrameId; - - const canvas = canvasRef.current; - const context = canvas.getContext("2d"); - const analyser = new AnalyserNode(new AudioContext()); - - navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { - const audioContext = new (window.AudioContext || - window.webkitAudioContext)(); - const source = audioContext.createMediaStreamSource(stream); - const analyser = audioContext.createAnalyser(); - analyser.fftSize = 2048; - source.connect(analyser); - - const bufferLength = analyser.frequencyBinCount; - const dataArray = new Uint8Array(bufferLength); - const barWidth = (canvas.width / bufferLength) * 2.5; - let barHeight; - let x = 0; - - function renderFrame() { - x = 0; - analyser.getByteFrequencyData(dataArray); - context.fillStyle = "#000"; - context.fillRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < bufferLength; i++) { - barHeight = dataArray[i]; - - const red = 255; - const green = 250 * (i / bufferLength); - const blue = barHeight + 25 * (i / bufferLength); - - context.fillStyle = `rgb(${red},${green},${blue})`; - context.fillRect( - x, - canvas.height - barHeight / 2, - barWidth, - barHeight / 2, - ); - - x += barWidth + 1; - } - animationFrameId = requestAnimationFrame(renderFrame); - } - renderFrame(); - }); - - return () => cancelAnimationFrame(animationFrameId); - }, []); - - return ( - - ); -} - -export default AudioVisualizer; diff --git a/app/components/record.js b/app/components/record.js index 26bcc984..3e5a17f5 100644 --- a/app/components/record.js +++ b/app/components/record.js @@ -1,36 +1,106 @@ -import AudioVisualizer from "./audioVisualizer.js"; +import React, { useRef, useEffect, useState } from "react"; + +import WaveSurfer from "wavesurfer.js"; + +import Dropdown from 'react-dropdown' +import 'react-dropdown/style.css' + +import CustomRecordPlugin from './CustomRecordPlugin' + export default function Recorder(props) { - let mediaRecorder = null; // mediaRecorder instance + const waveformRef = useRef() + const [wavesurfer, setWavesurfer] = useState(null) + const [record, setRecord] = useState(null) + const [isRecording, setIsRecording] = useState(false) + const [isPlaying, setIsPlaying] = useState(false) + const [deviceId, setDeviceId] = useState(null) + const [ddOptions, setDdOptions] = useState([]) - const startRecording = () => { - navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { - mediaRecorder = new MediaRecorder(stream); - mediaRecorder.start(); - props.onRecord(true); - }); - }; + useEffect(() => { + document.getElementById('play-btn').disabled = true - const stopRecording = () => { - if (mediaRecorder) { - mediaRecorder.stop(); - props.onRecord(false); + 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, + waveColor: "#333", + progressColor: "#0178FF", + cursorColor: "OrangeRed", + hideScrollbar: true, + autoCenter: true, + barWidth: 2, + }) + const wsWrapper = _wavesurfer.getWrapper() + wsWrapper.style.cursor = 'pointer' + wsWrapper.style.backgroundColor = 'lightgray' + wsWrapper.style.borderRadius = '15px' + + _wavesurfer.on('play', () => { + setIsPlaying(true) + }) + _wavesurfer.on('pause', () => { + setIsPlaying(false) + }) + + setRecord(_wavesurfer.registerPlugin(CustomRecordPlugin.create())) + setWavesurfer(_wavesurfer) + return () => { + _wavesurfer.destroy() + setIsRecording(false) + setIsPlaying(false) + } } - }; + }, []) + + const handleRecClick = async () => { + if (!record) return console.log("no record") + + if(record?.isRecording()) { + record.stopRecording() + setIsRecording(false) + document.getElementById('play-btn').disabled = false + } else { + const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId } }) + await record.startRecording(stream) + props.setStream(stream) + setIsRecording(true) + } + } + + const handlePlayClick = () => { + wavesurfer?.playPause() + } + + const handleDropdownChange = (e) => { + setDeviceId(e.value) + } return ( -
- {props.isRecording && } - - {props.isRecording ? ( - - ) : ( - - )} +
+
+ +   + +   +
- ); +
+ {/* TODO: Download audio tag */} +
+ ) } diff --git a/app/globals.css b/app/globals.css index 168f6514..d45723bd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -11,8 +11,8 @@ @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background-start-rgb: 32, 32, 32; + --background-end-rgb: 32, 32, 32; } } diff --git a/app/page.js b/app/page.js index 5130b47a..0b837da3 100644 --- a/app/page.js +++ b/app/page.js @@ -1,39 +1,16 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import Recorder from "./components/record.js"; import { Dashboard } from "./components/dashboard.js"; import useWebRTC from "./components/webrtc.js"; import "../public/button.css"; const App = () => { - const [isRecording, setIsRecording] = useState(false); - const [splashScreen, setSplashScreen] = useState(true); - - const handleRecord = (recording) => { - console.log("handleRecord", recording); - - setIsRecording(recording); - setSplashScreen(false); - - if (recording) { - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then(setStream) - .catch((err) => console.error(err)); - } else if (!recording) { - if (stream) { - const tracks = stream.getTracks(); - tracks.forEach((track) => track.stop()); - setStream(null); - } - - setIsRecording(false); - } - }; - const [stream, setStream] = useState(null); + + // This is where you'd send the stream and receive the data from the server. + // transcription, summary, etc const serverData = useWebRTC(stream); - console.log(serverData); return (
@@ -42,17 +19,8 @@ const App = () => {

Capture The Signal, Not The Noise

- handleRecord(recording)} - /> - - {!splashScreen && ( - handleRecord(recording)} - /> - )} + +