-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from Shryansh107/molecule/voiceRecorder
Adds voice recorder molecule
- Loading branch information
Showing
11 changed files
with
286 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"theme": { | ||
"primaryColor": { | ||
"value": "#ffa500", | ||
"allowOverride": true | ||
}, | ||
"secondaryColor": { | ||
"value": "#1E232C", | ||
"allowOverride": true | ||
} | ||
}, | ||
"component": { | ||
"allowOverride": false, | ||
"startIcon": "src/molecules/VoiceRecorder/assets/startIcon.png", | ||
"stopIcon": "src/molecules/VoiceRecorder/assets/stop.gif", | ||
"errorIcon": "src/molecules/VoiceRecorder/assets/error.gif", | ||
"processingIcon": "src/molecules/VoiceRecorder/assets/process.gif", | ||
"voiceMinDecibels":-35, | ||
"delayBetweenDialogs": 2500, | ||
"dialogMaxLength":60000, | ||
"isRecording":false, | ||
"recorderErrorMessage": "Your question was not recognised. Pls try speaking more clearly.", | ||
"waitMessage": "Please wait while we process your request..." | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import { useState } from 'react' | ||
import styles from './styles.module.css' | ||
import toast from 'react-hot-toast' | ||
import config from './config.json' | ||
|
||
interface VoiceRecorder { | ||
setInputMsg: (msg: string) => void | ||
tapToSpeak: boolean | ||
includeDiv?: boolean | ||
} | ||
|
||
const VoiceRecorder: React.FC<VoiceRecorder> = ({ | ||
setInputMsg, | ||
tapToSpeak, | ||
includeDiv = false, | ||
}) => { | ||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null) | ||
const [isErrorClicked, setIsErrorClicked] = useState(false) | ||
const [recorderStatus, setRecorderStatus] = useState('idle') | ||
|
||
const voiceMinDecibels: number = config.component.voiceMinDecibels | ||
const delayBetweenDialogs: number = config.component.delayBetweenDialogs | ||
const dialogMaxLength: number = config.component.dialogMaxLength | ||
const [isRecording,setIsRecording] = useState(config.component.isRecording) | ||
|
||
const startRecording = () => { | ||
if(!isRecording){ | ||
setIsRecording(true) | ||
record() | ||
} | ||
} | ||
|
||
const stopRecording = () => { | ||
if(isRecording){ | ||
if (mediaRecorder !== null) { | ||
mediaRecorder.stop() | ||
setIsRecording(false) | ||
setMediaRecorder(null) | ||
} | ||
} | ||
} | ||
|
||
function record() { | ||
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { | ||
//start recording: | ||
const recorder = new MediaRecorder(stream) | ||
recorder.start() | ||
setMediaRecorder(recorder) | ||
|
||
//save audio chunks: | ||
const audioChunks: BlobPart[] = [] | ||
recorder.addEventListener('dataavailable', (event) => { | ||
audioChunks.push(event.data) | ||
}) | ||
|
||
//analysis: | ||
const audioContext = new AudioContext() | ||
const audioStreamSource = audioContext.createMediaStreamSource(stream) | ||
const analyser = audioContext.createAnalyser() | ||
analyser.minDecibels = voiceMinDecibels | ||
audioStreamSource.connect(analyser) | ||
const bufferLength = analyser.frequencyBinCount | ||
const domainData = new Uint8Array(bufferLength) | ||
|
||
//loop: | ||
let time: Date = new Date() | ||
let startTime: number | ||
let lastDetectedTime: number = time.getTime() | ||
let anySoundDetected: boolean = false | ||
const detectSound = () => { | ||
//recording stopped by user: | ||
if (!isRecording) return | ||
|
||
time = new Date() | ||
const currentTime = time.getTime() | ||
|
||
//time out: | ||
if (currentTime > startTime + dialogMaxLength) { | ||
recorder.stop() | ||
return | ||
} | ||
|
||
//a dialog detected: | ||
if ( | ||
anySoundDetected === true && | ||
currentTime > lastDetectedTime + delayBetweenDialogs | ||
) { | ||
recorder.stop() | ||
return | ||
} | ||
|
||
//check for detection: | ||
analyser.getByteFrequencyData(domainData) | ||
for (let i = 0; i < bufferLength; i++) | ||
if (domainData[i] > 0) { | ||
anySoundDetected = true | ||
time = new Date() | ||
lastDetectedTime = time.getTime() | ||
} | ||
|
||
//continue the loop: | ||
window?.requestAnimationFrame(detectSound) | ||
} | ||
window?.requestAnimationFrame(detectSound) | ||
|
||
//stop event: | ||
recorder.addEventListener('stop', () => { | ||
//stop all the tracks: | ||
stream.getTracks().forEach((track) => track.stop()) | ||
if (!anySoundDetected) return | ||
|
||
//send to server: | ||
const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' }) | ||
makeComputeAPICall(audioBlob) | ||
}) | ||
}) | ||
} | ||
const makeComputeAPICall = async (blob: Blob) => { | ||
try { | ||
setRecorderStatus('processing') | ||
toast.success(`${config.component.waitMessage}`) | ||
// Define the API endpoint and make api call here | ||
if(blob){ | ||
//set api result in setInputMsg | ||
setInputMsg('') | ||
} | ||
|
||
} catch (error) { | ||
console.error(error) | ||
setRecorderStatus('error') | ||
toast.error(`${config.component.recorderErrorMessage}`) | ||
// Set isErrorClicked to true when an error occurs | ||
setIsErrorClicked(false) | ||
setTimeout(() => { | ||
// Check if the user has not clicked the error icon again | ||
if (!isErrorClicked) { | ||
setRecorderStatus('idle') | ||
} | ||
}, 2500) | ||
|
||
} | ||
} | ||
|
||
return ( | ||
<div> | ||
<div> | ||
{mediaRecorder && mediaRecorder.state === 'recording' ? ( | ||
<div className={styles.center}> | ||
<RecorderControl | ||
icon={config.component.stopIcon} | ||
onClick={stopRecording} | ||
includeDiv={includeDiv} | ||
/> | ||
</div> | ||
) : ( | ||
<div className={styles.center}> | ||
{recorderStatus === 'processing' ? ( | ||
<RecorderControl icon={config.component.processingIcon} onClick={()=>{}} /> | ||
) : recorderStatus === 'error' ? ( | ||
<RecorderControl | ||
icon={config.component.errorIcon} | ||
onClick={() => { | ||
setIsErrorClicked(true); | ||
startRecording(); | ||
}} | ||
includeDiv={includeDiv} | ||
/> | ||
) : ( | ||
<div className={styles.center}> | ||
<RecorderControl | ||
icon={config.component.startIcon} | ||
onClick={() => { | ||
setIsErrorClicked(true); | ||
startRecording(); | ||
}} | ||
includeDiv={includeDiv} | ||
tapToSpeak={tapToSpeak} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
// includeDiv is being checked in render Function | ||
const RecorderControl: React.FC<{ | ||
icon: string; | ||
onClick?: () => void; | ||
includeDiv?: boolean; | ||
tapToSpeak?: boolean; | ||
}> = ({ icon, onClick, includeDiv = true, tapToSpeak= false }) => { | ||
const handleClick = () => { | ||
if(onClick){ | ||
onClick(); | ||
} | ||
}; | ||
|
||
return includeDiv ? ( | ||
<div className={styles.imgContainer}> | ||
<img | ||
src={icon} | ||
alt='icon' | ||
onClick={handleClick} | ||
style={{ cursor: 'pointer', height: '40px', width: '40px' }} | ||
/> | ||
{tapToSpeak && ( | ||
<p style={{ color: 'black', fontSize: '12px', marginTop: '4px' }}> | ||
{'label.tap_to_speak'} | ||
</p> | ||
)} | ||
</div> | ||
) : ( | ||
<img | ||
src={icon} | ||
alt='icon' | ||
onClick={handleClick} | ||
style={{ cursor: 'pointer', height: '40px', width: '40px' }} | ||
/> | ||
); | ||
}; | ||
|
||
export default VoiceRecorder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.center { | ||
display: block; | ||
height: 100%; | ||
width: 100%; | ||
-webkit-tap-highlight-color: transparent; | ||
-webkit-touch-callout: none; | ||
-webkit-user-select: none; | ||
-khtml-user-select: none; | ||
-moz-user-select: none; | ||
-ms-user-select: none; | ||
user-select: none; | ||
} | ||
|
||
.imgContainer { | ||
position: relative; | ||
overflow: hidden; | ||
margin: auto; | ||
width: 4rem; | ||
height: 4rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters