Skip to content

Commit f023e1e

Browse files
committed
improved audio recorder and fixed dynamic downsampling
1 parent 09a9f61 commit f023e1e

File tree

3 files changed

+106
-42
lines changed

3 files changed

+106
-42
lines changed

www/scripts/recorder.js

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,23 @@ DEALINGS IN THE SOFTWARE.
2222
(function (window) {
2323

2424
var Recorder = function(source, audioProcessor, startFun, stopFun) {
25-
var bufferLen = 4096;
2625
var websocket;
2726
var audioContext = source.context;
27+
var inputSampleRate = (audioContext)? audioContext.sampleRate : source.sampleRate;
28+
var outputSampleRate = 16000;
2829

2930
var recording = false;
3031

3132
function processAudio(inputAudioFrame){
3233
if (!recording) return;
3334

34-
var length = Math.floor(inputAudioFrame.length / 3);
35-
var result = new Float32Array(length);
36-
37-
var index = 0;
38-
var inputIndex = 0;
39-
40-
while (index < length) {
41-
result[index++] = inputAudioFrame[inputIndex];
42-
inputIndex += 3;
43-
}
35+
//downsample
36+
var result = downsampleFloat32(inputAudioFrame, inputSampleRate, outputSampleRate);
4437

4538
var offset = 0;
46-
var buffer = new ArrayBuffer(length * 2);
39+
var buffer = new ArrayBuffer(result.length * 2);
4740
var view = new DataView(buffer);
48-
for (var i = 0; i < result.length; i++, offset += 2) {
49-
var s = Math.max(-1, Math.min(1, result[i]));
50-
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
51-
}
41+
floatTo16BitPCM(view, offset, result);
5242

5343
//console.log('Recorder onaudioprocess - view: ' + view); //DEBUG
5444
if (websocket){
@@ -63,6 +53,7 @@ DEALINGS IN THE SOFTWARE.
6353
}
6454
//Web-Audio
6555
}else if (audioContext){
56+
var bufferLen = 4096;
6657
let processNode;
6758
if ('createScriptProcessor' in audioContext){
6859
processNode = audioContext.createScriptProcessor(bufferLen, 1, 1);
@@ -112,7 +103,6 @@ DEALINGS IN THE SOFTWARE.
112103
this.sendHeader = function(ws){
113104
var sampleLength = 1000000;
114105
var mono = true;
115-
var sampleRate = 16000; //TODO: this might not be true! We cannot guarantee that!
116106
var buffer = new ArrayBuffer(44);
117107
var view = new DataView(buffer);
118108

@@ -131,9 +121,9 @@ DEALINGS IN THE SOFTWARE.
131121
//channel count
132122
view.setUint16(22, mono ? 1 : 2, true);
133123
//sample rate
134-
view.setUint32(24, sampleRate, true);
124+
view.setUint32(24, outputSampleRate, true);
135125
//byte rate (sample rate * block align)
136-
view.setUint32(28, sampleRate * 2, true);
126+
view.setUint32(28, outputSampleRate * 2, true);
137127
//block align (channel count * bytes per sample)
138128
view.setUint16(32, 2, true);
139129
//bits per sample
@@ -146,11 +136,78 @@ DEALINGS IN THE SOFTWARE.
146136
//console.log('Recorder sendHeader - view: ' + view); //DEBUG
147137
ws.send(view);
148138
}
149-
function writeString(view, offset, string){
150-
for (var i = 0; i < string.length; i++){
139+
140+
//--- conversion methods ---
141+
142+
function writeString(view, offset, string){
143+
for (let i = 0; i < string.length; i++){
151144
view.setUint8(offset + i, string.charCodeAt(i));
152145
}
153-
}
146+
}
147+
148+
function floatTo16BitPCM(output, offset, input){
149+
for (let i = 0; i < input.length; i++, offset += 2){
150+
let s = Math.max(-1, Math.min(1, input[i]));
151+
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
152+
}
153+
}
154+
155+
function downsampleAndConvertToInt16(buffer, sampleRate, outSampleRate){
156+
if (outSampleRate == sampleRate){
157+
return buffer;
158+
}
159+
if (outSampleRate > sampleRate){
160+
console.error("Recorder - Downsampling to " + outSampleRate + " failed! Input sampling rate was too low: " + sampleRate);
161+
return buffer;
162+
}
163+
var sampleRateRatio = sampleRate / outSampleRate;
164+
var newLength = Math.round(buffer.length / sampleRateRatio);
165+
var result = new Int16Array(newLength);
166+
var offsetResult = 0;
167+
var offsetBuffer = 0;
168+
while (offsetResult < result.length){
169+
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
170+
var accum = 0, count = 0;
171+
for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++){
172+
accum += buffer[i];
173+
count++;
174+
}
175+
result[offsetResult] = Math.min(1, accum / count)*0x7FFF;
176+
offsetResult++;
177+
offsetBuffer = nextOffsetBuffer;
178+
}
179+
return result.buffer;
180+
}
181+
function downsampleFloat32(array, sampleRate, outSampleRate){
182+
if (outSampleRate == sampleRate){
183+
var result = new Float32Array(array.length);
184+
for (let i = 0 ; i < array.length ; i++){
185+
result.push(array[i]);
186+
}
187+
return array;
188+
}
189+
if (outSampleRate > sampleRate){
190+
console.error("Recorder - Downsampling to " + outSampleRate + " failed! Input sampling rate was too low: " + sampleRate);
191+
return downsampleFloat32(array, sampleRate, sampleRate);
192+
}
193+
var sampleRateRatio = sampleRate / outSampleRate;
194+
var newLength = Math.round(array.length / sampleRateRatio);
195+
var result = new Float32Array(newLength);
196+
var offsetResult = 0;
197+
var offsetBuffer = 0;
198+
while (offsetResult < result.length){
199+
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
200+
var accum = 0, count = 0;
201+
for (var i = offsetBuffer; i < nextOffsetBuffer && i < array.length; i++){
202+
accum += array[i];
203+
count++;
204+
}
205+
result[offsetResult] = accum / count;
206+
offsetResult++;
207+
offsetBuffer = nextOffsetBuffer;
208+
}
209+
return result;
210+
}
154211
};
155212

156213
window.RecorderJS = Recorder;

www/scripts/sepiaFW.audioRecorder.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,22 @@ function sepiaFW_build_audio_recorder(){
113113
broadcastRecorderStopped();
114114
}
115115
function closeAudioContext(audioContext, success, error){
116-
if (audioContext.state == "closed"){
117-
if (success) success();
118-
}else{
119-
audioContext.close().then(function() {
120-
//console.log('CLOSED audio-context');
121-
broadcastRecorderClosed();
116+
if (audioContext){
117+
if (audioContext.state == "closed"){
122118
if (success) success();
123-
}).catch(function(e){
124-
broadcastRecorderError(e);
125-
if (error) error(e);
126-
});
119+
}else{
120+
audioContext.close().then(function() {
121+
//console.log('CLOSED audio-context');
122+
broadcastRecorderClosed();
123+
if (success) success();
124+
}).catch(function(e){
125+
broadcastRecorderError(e);
126+
if (error) error(e);
127+
});
128+
}
129+
}else{
130+
broadcastRecorderClosed();
131+
if (success) success();
127132
}
128133
}
129134

@@ -259,11 +264,12 @@ function sepiaFW_build_audio_recorder(){
259264

260265
//AudioProcessor (replacement for scriptProcessor of AudioContext)
261266
function AudioInputPluginProcessor(){
262-
this.onaudioprocess = function(inputAudioFrame){
267+
var self = this;
268+
self.onaudioprocess = function(inputAudioFrame){
263269
//to be defined by RecorderInstance
264270
};
265-
this.onaudioreceive = function(evt){
266-
this.onaudioprocess(evt.data);
271+
self.onaudioreceive = function(evt){
272+
self.onaudioprocess(evt.data);
267273
};
268274
}
269275

@@ -351,8 +357,8 @@ function sepiaFW_build_audio_recorder(){
351357
bufferSize: 4096,
352358
channels: 1,
353359
format: audioinput.FORMAT.PCM_16BIT,
354-
audioSourceType: audioinput.AUDIOSOURCE_TYPE.UNPROCESSED, //VOICE_COMMUNICATION
355-
normalize: false,
360+
audioSourceType: audioinput.AUDIOSOURCE_TYPE.VOICE_COMMUNICATION, //VOICE_COMMUNICATION UNPROCESSED DEFAULT
361+
normalize: true,
356362
streamToWebAudio: false
357363
};
358364
var audioProc = new AudioInputPluginProcessor();

www/xtensions/picovoice/picovoiceAudioManager.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
let PicovoiceAudioManager = (function() {
2-
const inputBufferLength = 4096;
3-
42
var engine;
53
var processCallback;
64
var isProcessing = false;
@@ -28,10 +26,13 @@ let PicovoiceAudioManager = (function() {
2826
return;
2927
}
3028
//console.log('+');
31-
//-------------------------
29+
30+
//fill inputAudioBuffer
3231
for (let i = 0 ; i < inputAudioFrame.length ; i++) {
33-
inputAudioBuffer.push((inputAudioFrame[i]) * 32767);
32+
inputAudioBuffer.push((inputAudioFrame[i]) * 32767); //0x7FFF
3433
}
34+
35+
//downsample if necessary
3536
while(inputAudioBuffer.length * engine.sampleRate / inputSampleRate > engine.frameLength) {
3637
let result = new Int16Array(engine.frameLength);
3738
let bin = 0;
@@ -56,7 +57,6 @@ let PicovoiceAudioManager = (function() {
5657

5758
processCallback(keywordIndex);
5859
}
59-
//-------------------------
6060
};
6161

6262
//Custom
@@ -66,6 +66,7 @@ let PicovoiceAudioManager = (function() {
6666
}
6767
//Web-Audio
6868
}else if (audioContext){
69+
let inputBufferLength = 4096;
6970
let engineNode = audioContext.createScriptProcessor(inputBufferLength, 1, 1);
7071
engineNode.onaudioprocess = function(ev){
7172
let inputAudioFrame = ev.inputBuffer.getChannelData(0);

0 commit comments

Comments
 (0)