1+ //Simple voice recorder with fixed parameters using SEPIA Web Audio Lib
2+ ( function ( ) {
3+ var SepiaVoiceRecorder = { } ;
4+
5+ //callbacks (defined once because we can have only one instance):
6+
7+ SepiaVoiceRecorder . onProcessorReady = function ( info ) {
8+ console . log ( "SepiaVoiceRecorder - onProcessorReady" , info ) ;
9+ }
10+ SepiaVoiceRecorder . onProcessorInitError = function ( err ) {
11+ console . error ( "SepiaVoiceRecorder - onProcessorInitError" , err ) ;
12+ }
13+
14+ SepiaVoiceRecorder . onAudioStart = function ( info ) {
15+ console . log ( "SepiaVoiceRecorder - onAudioStart" ) ;
16+ }
17+ SepiaVoiceRecorder . onAudioEnd = function ( info ) {
18+ console . log ( "SepiaVoiceRecorder - onAudioEnd" ) ;
19+ }
20+ SepiaVoiceRecorder . onProcessorError = function ( err ) {
21+ console . error ( "SepiaVoiceRecorder - onProcessorError" , err ) ;
22+ }
23+
24+ SepiaVoiceRecorder . onProcessorRelease = function ( info ) {
25+ console . log ( "SepiaVoiceRecorder - onProcessorRelease" ) ;
26+ }
27+
28+ SepiaVoiceRecorder . onDebugLog = function ( msg ) {
29+ console . log ( "debugLog" , msg ) ;
30+ }
31+
32+ //Resampler events
33+ SepiaVoiceRecorder . onResamplerData = function ( data ) {
34+ console . log ( "SepiaVoiceRecorder - onResamplerData" , data ) ;
35+ }
36+
37+ //Wave encoder events
38+ SepiaVoiceRecorder . onWaveEncoderStateChange = function ( state ) {
39+ console . log ( "SepiaVoiceRecorder - onWaveEncoderStateChange" , state ) ;
40+ }
41+ SepiaVoiceRecorder . onWaveEncoderAudioData = function ( waveData ) {
42+ console . log ( "SepiaVoiceRecorder - onWaveEncoderAudioData" , waveData ) ;
43+ //SepiaVoiceRecorder.addAudioElementToPage(targetEle, waveData, "audio/wav");
44+ }
45+ function onWaveEncoderData ( data ) {
46+ if ( data . output && data . output . wav ) {
47+ SepiaVoiceRecorder . onWaveEncoderAudioData ( data . output . wav ) ;
48+
49+ } else if ( data . output && data . output . buffer ) {
50+ //plotData(data.output.buffer);
51+ //console.log("waveEncoder", "buffer output length: " + data.output.buffer.length);
52+ }
53+ if ( data . gate ) {
54+ SepiaVoiceRecorder . onWaveEncoderStateChange ( data . gate ) ;
55+ if ( data . gate . isOpen === true ) {
56+ waveEncoderIsBuffering = true ;
57+
58+ } else if ( data . gate . isOpen === false ) {
59+ if ( waveEncoderIsBuffering ) {
60+ waveEncoderGetWave ( ) ; //we use this by default?
61+ }
62+ waveEncoderIsBuffering = false ;
63+ }
64+ }
65+ }
66+ var waveEncoderIsBuffering = false ;
67+
68+ //SpeechRecognition events
69+ SepiaVoiceRecorder . onSpeechRecognitionStateChange = function ( ev ) {
70+ console . log ( "SepiaVoiceRecorder - onSpeechRecognitionStateChange" , ev ) ;
71+ }
72+ SepiaVoiceRecorder . onSpeechRecognitionEvent = function ( data ) {
73+ console . log ( "SepiaVoiceRecorder - onSpeechRecognitionEvent" , data ) ;
74+ }
75+ function onSpeechRecognitionData ( msg ) {
76+ if ( ! msg ) return ;
77+ if ( msg . gate ) {
78+ //gate closed
79+ if ( msg . gate . isOpen == false && asrModuleGateIsOpen ) {
80+ asrModuleGateIsOpen = false ;
81+ //STATE: streamend
82+ SepiaVoiceRecorder . onSpeechRecognitionStateChange ( {
83+ state : "onStreamEnd" ,
84+ bufferOrTimeLimit : msg . gate . bufferOrTimeLimit
85+ } ) ;
86+ //gate opened
87+ } else if ( msg . gate . isOpen == true && ! asrModuleGateIsOpen ) {
88+ //STATE: streamstart
89+ SepiaVoiceRecorder . onSpeechRecognitionStateChange ( {
90+ state : "onStreamStart"
91+ } ) ;
92+ asrModuleGateIsOpen = true ;
93+ }
94+ }
95+ if ( msg . recognitionEvent ) {
96+ SepiaVoiceRecorder . onSpeechRecognitionEvent ( msg . recognitionEvent ) ;
97+ }
98+ if ( msg . connectionEvent ) {
99+ //TODO: use? - type: open, ready, close
100+ }
101+ //In debug or test-mode the module might send the recording:
102+ if ( msg . output && msg . output . wav ) {
103+ SepiaVoiceRecorder . onWaveEncoderAudioData ( msg . output . wav ) ;
104+ }
105+ }
106+ var asrModuleGateIsOpen = false ;
107+
108+ //recorder processor:
109+
110+ var sepiaWebAudioProcessor ;
111+ var targetSampleRate = 16000 ;
112+ var resamplerBufferSize = 512 ;
113+
114+ async function createRecorder ( options ) {
115+ if ( ! options ) options = { } ;
116+ else {
117+ //overwrite shared defaults?
118+ if ( options . targetSampleRate ) targetSampleRate = options . targetSampleRate ;
119+ if ( options . resamplerBufferSize ) resamplerBufferSize = options . resamplerBufferSize ;
120+ }
121+ var useRecognitionModule = ! ! options . asr ;
122+ if ( ! options . asr ) options . asr = { } ;
123+ //audio source
124+ var customSource = undefined ;
125+ if ( options . fileUrl ) {
126+ //customSourceNode: file audio buffer
127+ try {
128+ customSource = await SepiaFW . webAudio . createFileSource ( options . fileUrl , {
129+ targetSampleRate : targetSampleRate
130+ } ) ;
131+ } catch ( err ) {
132+ SepiaVoiceRecorder . onProcessorInitError ( err ) ;
133+ return ;
134+ }
135+ }
136+
137+ var resampler = {
138+ name : 'speex-resample-switch' ,
139+ settings : {
140+ onmessage : SepiaVoiceRecorder . onResamplerData ,
141+ sendToModules : [ ] , //index given to processor - 0: source, 1: module 1, ...
142+ options : {
143+ processorOptions : {
144+ targetSampleRate : targetSampleRate ,
145+ resampleQuality : options . resampleQuality || 4 , //1 [low] - 10 [best],
146+ bufferSize : resamplerBufferSize ,
147+ passThroughMode : 0 , //0: none, 1: original (float32), 2: 16Bit PCM - NOTE: NOT resampled
148+ calculateRmsVolume : true ,
149+ gain : options . gain || 1.0
150+ }
151+ }
152+ }
153+ } ;
154+ var resamplerIndex ;
155+
156+ var waveEncoder = {
157+ name : 'wave-encoder' ,
158+ type : 'worker' ,
159+ handle : { } , //will be updated on init. with ref. to node.
160+ settings : {
161+ onmessage : onWaveEncoderData ,
162+ options : {
163+ setup : {
164+ inputSampleRate : targetSampleRate ,
165+ inputSampleSize : resamplerBufferSize ,
166+ lookbackBufferMs : 0 ,
167+ recordBufferLimitKb : 500 , //default: 5MB (overwritten by ms limit), good value e.g. 600
168+ recordBufferLimitMs : options . recordingLimitMs ,
169+ doDebug : false
170+ }
171+ }
172+ }
173+ } ;
174+ var waveEncoderIndex ;
175+
176+ var sttServerModule = {
177+ name : 'stt-socket' ,
178+ type : 'worker' ,
179+ handle : { } , //will be updated on init. with ref. to node.
180+ settings : {
181+ onmessage : onSpeechRecognitionData ,
182+ options : {
183+ setup : {
184+ //rec. options
185+ inputSampleRate : targetSampleRate ,
186+ inputSampleSize : resamplerBufferSize ,
187+ lookbackBufferMs : 0 ,
188+ recordBufferLimitKb : 500 , //default: 5MB (overwritten by ms limit), good value e.g. 600
189+ recordBufferLimitMs : options . recordingLimitMs , //NOTE: will not apply in 'continous' mode (but buffer will not grow larger)
190+ //ASR server options
191+ serverUrl : options . asr . serverUrl , //NOTE: if set to 'debug' it will trigger "dry run" (wav file + pseudo res.)
192+ clientId : options . asr . clientId ,
193+ accessToken : options . asr . accessToken ,
194+ //ASR engine common options
195+ messageFormat : options . asr . messageFormat || "webSpeechApi" , //use events in 'webSpeechApi' compatible format
196+ language : options . asr . language || "" ,
197+ model : options . asr . model || "" ,
198+ continuous : ( options . asr . continuous != undefined ? options . asr . continuous : false ) , //one final result only?
199+ optimizeFinalResult : options . asr . optimizeFinalResult , //try to optimize result e.g. by converting text to numbers etc.
200+ //ASR engine specific options (can include commons but will be overwritten with above)
201+ engineOptions : options . asr . engineOptions || { } , //e.g. ASR model, alternatives, ...
202+ //other
203+ returnAudioFile : options . asr . returnAudioFile || false , //NOTE: can be enabled via "dry run" mode
204+ doDebug : false
205+ }
206+ }
207+ }
208+ } ;
209+ var sttServerModuleIndex ;
210+
211+ //put together modules
212+ var activeModules = [ ] ;
213+
214+ //- resampler is required
215+ activeModules . push ( resampler ) ;
216+ resamplerIndex = activeModules . length ;
217+
218+ //- use either speech-recognition (ASR) or wave-encoder
219+ if ( useRecognitionModule ) {
220+ activeModules . push ( sttServerModule ) ;
221+ sttServerModuleIndex = activeModules . length ;
222+ SepiaVoiceRecorder . sttServerModule = sttServerModule ;
223+ resampler . settings . sendToModules . push ( sttServerModuleIndex ) ; //add to resampler
224+ } else {
225+ activeModules . push ( waveEncoder ) ;
226+ waveEncoderIndex = activeModules . length ;
227+ SepiaVoiceRecorder . waveEncoder = waveEncoder ;
228+ resampler . settings . sendToModules . push ( waveEncoderIndex ) ; //add to resampler
229+ }
230+
231+ //create processor
232+ sepiaWebAudioProcessor = new SepiaFW . webAudio . Processor ( {
233+ onaudiostart : SepiaVoiceRecorder . onAudioStart ,
234+ onaudioend : SepiaVoiceRecorder . onAudioEnd ,
235+ onrelease : SepiaVoiceRecorder . onProcessorRelease ,
236+ onerror : SepiaVoiceRecorder . onProcessorError ,
237+ targetSampleRate : targetSampleRate ,
238+ //targetBufferSize: 512,
239+ modules : activeModules ,
240+ destinationNode : undefined , //defaults to: new "blind" destination (mic) or audioContext.destination (stream)
241+ startSuspended : true ,
242+ debugLog : SepiaVoiceRecorder . onDebugLog ,
243+ customSource : customSource
244+
245+ } , function ( msg ) {
246+ //Init. ready
247+ SepiaVoiceRecorder . onProcessorReady ( msg ) ;
248+
249+ } , function ( err ) {
250+ //Init. error
251+ SepiaVoiceRecorder . onProcessorInitError ( err ) ;
252+ } ) ;
253+ }
254+
255+ //Interface:
256+
257+ SepiaVoiceRecorder . create = function ( options ) {
258+ if ( sepiaWebAudioProcessor ) {
259+ SepiaVoiceRecorder . onProcessorInitError ( { name : "ProcessorInitError" , message : "SepiaVoiceRecorder already exists. Release old one before creating new." } ) ;
260+ return ;
261+ }
262+ if ( ! options ) options = { } ;
263+ createRecorder ( options ) ;
264+ }
265+
266+ SepiaVoiceRecorder . isReady = function ( ) {
267+ return ( ! ! sepiaWebAudioProcessor && sepiaWebAudioProcessor . isInitialized ( ) ) ;
268+ }
269+ SepiaVoiceRecorder . isActive = function ( ) {
270+ return ( ! ! sepiaWebAudioProcessor && sepiaWebAudioProcessor . isInitialized ( ) && sepiaWebAudioProcessor . isProcessing ( ) ) ;
271+ }
272+ SepiaVoiceRecorder . start = function ( successCallback , noopCallback , errorCallback ) {
273+ if ( sepiaWebAudioProcessor ) {
274+ sepiaWebAudioProcessor . start ( function ( ) {
275+ waveEncoderSetGate ( "open" ) ; //start recording
276+ speechRecognitionModuleSetGate ( "open" ) ; //start recognition
277+ if ( successCallback ) successCallback ( ) ;
278+ } , noopCallback , errorCallback ) ;
279+ } else {
280+ if ( errorCallback ) errorCallback ( { name : "ProcessorInitError" , message : "SepiaVoiceRecorder doesn't exist yet." } ) ;
281+ }
282+ }
283+ SepiaVoiceRecorder . stop = function ( stopCallback , noopCallback , errorCallback ) {
284+ if ( sepiaWebAudioProcessor ) {
285+ sepiaWebAudioProcessor . stop ( function ( info ) {
286+ waveEncoderSetGate ( "close" ) ; //stop recording
287+ speechRecognitionModuleSetGate ( "close" ) ; //stop recognition
288+ if ( stopCallback ) stopCallback ( info ) ;
289+ } , noopCallback , errorCallback ) ;
290+ } else {
291+ if ( noopCallback ) noopCallback ( ) ;
292+ }
293+ }
294+ SepiaVoiceRecorder . release = function ( releaseCallback , noopCallback , errorCallback ) {
295+ if ( sepiaWebAudioProcessor ) {
296+ sepiaWebAudioProcessor . release ( function ( ) {
297+ sepiaWebAudioProcessor = undefined ;
298+ if ( releaseCallback ) releaseCallback ( ) ;
299+ } , function ( ) {
300+ sepiaWebAudioProcessor = undefined ;
301+ if ( noopCallback ) noopCallback ( ) ;
302+ } , function ( err ) {
303+ sepiaWebAudioProcessor = undefined ;
304+ if ( errorCallback ) errorCallback ( err ) ;
305+ } ) ;
306+ } else {
307+ if ( noopCallback ) noopCallback ( ) ;
308+ }
309+ }
310+ //stop and release if possible or confirm right away
311+ SepiaVoiceRecorder . stopIfActive = function ( callback ) {
312+ if ( SepiaVoiceRecorder . isActive ( ) ) {
313+ SepiaVoiceRecorder . stop ( callback , callback , undefined ) ;
314+ } else {
315+ if ( callback ) callback ( ) ;
316+ }
317+ }
318+ SepiaVoiceRecorder . stopAndReleaseIfActive = function ( callback ) {
319+ SepiaVoiceRecorder . stopIfActive ( function ( ) {
320+ if ( SepiaVoiceRecorder . isReady ( ) ) {
321+ SepiaVoiceRecorder . release ( callback , callback , undefined ) ;
322+ } else {
323+ sepiaWebAudioProcessor = undefined ;
324+ if ( callback ) callback ( ) ;
325+ }
326+ } ) ;
327+ }
328+
329+ //Extras:
330+
331+ function waveEncoderSetGate ( state ) {
332+ if ( sepiaWebAudioProcessor && SepiaVoiceRecorder . waveEncoder ) {
333+ SepiaVoiceRecorder . waveEncoder . handle . sendToModule ( { gate : state } ) ; //"open", "close"
334+ }
335+ }
336+ function waveEncoderGetWave ( ) {
337+ if ( sepiaWebAudioProcessor && SepiaVoiceRecorder . waveEncoder ) {
338+ SepiaVoiceRecorder . waveEncoder . handle . sendToModule ( { request : { get : "wave" } } ) ;
339+ }
340+ }
341+ function speechRecognitionModuleSetGate ( state ) {
342+ if ( sepiaWebAudioProcessor && SepiaVoiceRecorder . sttServerModule ) {
343+ SepiaVoiceRecorder . sttServerModule . handle . sendToModule ( { gate : state } ) ; //"open", "close"
344+ }
345+ }
346+
347+ //Decode audio file to audio buffer and then to 16bit PCM mono
348+ SepiaVoiceRecorder . decodeAudioFileToInt16Mono = function ( fileUrl , sampleRate , channels , successCallback , errorCallback ) {
349+ if ( ! sampleRate ) sampleRate = 16000 ;
350+ if ( channels && channels > 1 ) {
351+ console . error ( "SepiaVoiceRecorder.decodeAudioFileToInt16Mono - Channels > 1 not supported. Result will only contain data of channel 0." ) ;
352+ }
353+ if ( ! successCallback ) successCallback = console . log ;
354+ if ( ! errorCallback ) errorCallback = console . error ;
355+ SepiaFW . webAudio . decodeAudioFileToInt16Mono ( fileUrl , sampleRate , successCallback , errorCallback ) ;
356+ }
357+
358+ //Add audio data as audio element to page
359+ SepiaVoiceRecorder . addAudioElementToPage = function ( targetEle , audioData , audioType ) {
360+ return SepiaFW . webAudio . addAudioElementToPage ( targetEle , audioData , audioType ) ;
361+ }
362+
363+ //export
364+ window . SepiaVoiceRecorder = SepiaVoiceRecorder ;
365+ } ) ( ) ;
0 commit comments