Skip to content

Commit 3ac6231

Browse files
committed
added sepia-recorder.js with 'SepiaVoiceRecorder' wrapper
1 parent fb48555 commit 3ac6231

File tree

1 file changed

+365
-0
lines changed

1 file changed

+365
-0
lines changed

src/sepia-recorder.js

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
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

Comments
 (0)