Add voice notes
// FREEBIE
This commit is contained in:
parent
cc15af549b
commit
4f46a164ba
22 changed files with 47762 additions and 14 deletions
1
js/Mp3LameEncoder.min.js
vendored
Symbolic link
1
js/Mp3LameEncoder.min.js
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../components/mp3lameencoder/lib/Mp3LameEncoder.js
|
1
js/WebAudioRecorderMp3.js
Symbolic link
1
js/WebAudioRecorderMp3.js
Symbolic link
|
@ -0,0 +1 @@
|
|||
../components/webaudiorecorder/lib/WebAudioRecorderMp3.js
|
201
js/components.js
201
js/components.js
|
@ -38595,4 +38595,203 @@ if (typeof exports === 'object') {
|
|||
}
|
||||
|
||||
module.exports = autosize;
|
||||
});
|
||||
});
|
||||
(function(window) {
|
||||
// internal: same as jQuery.extend(true, args...)
|
||||
var extend = function() {
|
||||
var target = arguments[0],
|
||||
sources = [].slice.call(arguments, 1);
|
||||
for (var i = 0; i < sources.length; ++i) {
|
||||
var src = sources[i];
|
||||
for (key in src) {
|
||||
var val = src[key];
|
||||
target[key] = typeof val === "object"
|
||||
? extend(typeof target[key] === "object" ? target[key] : {}, val)
|
||||
: val;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
var WORKER_FILE = {
|
||||
wav: "WebAudioRecorderWav.js",
|
||||
ogg: "WebAudioRecorderOgg.js",
|
||||
mp3: "WebAudioRecorderMp3.js"
|
||||
};
|
||||
|
||||
// default configs
|
||||
var CONFIGS = {
|
||||
workerDir: "/", // worker scripts dir (end with /)
|
||||
numChannels: 2, // number of channels
|
||||
encoding: "wav", // encoding (can be changed at runtime)
|
||||
|
||||
// runtime options
|
||||
options: {
|
||||
timeLimit: 300, // recording time limit (sec)
|
||||
encodeAfterRecord: false, // process encoding after recording
|
||||
progressInterval: 1000, // encoding progress report interval (millisec)
|
||||
bufferSize: undefined, // buffer size (use browser default)
|
||||
|
||||
// encoding-specific options
|
||||
wav: {
|
||||
mimeType: "audio/wav"
|
||||
},
|
||||
ogg: {
|
||||
mimeType: "audio/ogg",
|
||||
quality: 0.5 // (VBR only): quality = [-0.1 .. 1]
|
||||
},
|
||||
mp3: {
|
||||
mimeType: "audio/mpeg",
|
||||
bitRate: 160 // (CBR only): bit rate = [64 .. 320]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// constructor
|
||||
var WebAudioRecorder = function(sourceNode, configs) {
|
||||
extend(this, CONFIGS, configs || {});
|
||||
this.context = sourceNode.context;
|
||||
if (this.context.createScriptProcessor == null)
|
||||
this.context.createScriptProcessor = this.context.createJavaScriptNode;
|
||||
this.input = this.context.createGain();
|
||||
sourceNode.connect(this.input);
|
||||
this.buffer = [];
|
||||
this.initWorker();
|
||||
};
|
||||
|
||||
// instance methods
|
||||
extend(WebAudioRecorder.prototype, {
|
||||
isRecording: function() { return this.processor != null; },
|
||||
|
||||
setEncoding: function(encoding) {
|
||||
if (this.isRecording())
|
||||
this.error("setEncoding: cannot set encoding during recording");
|
||||
else if (this.encoding !== encoding) {
|
||||
this.encoding = encoding;
|
||||
this.initWorker();
|
||||
}
|
||||
},
|
||||
|
||||
setOptions: function(options) {
|
||||
if (this.isRecording())
|
||||
this.error("setOptions: cannot set options during recording");
|
||||
else {
|
||||
extend(this.options, options);
|
||||
this.worker.postMessage({ command: "options", options: this.options });
|
||||
}
|
||||
},
|
||||
|
||||
startRecording: function() {
|
||||
if (this.isRecording())
|
||||
this.error("startRecording: previous recording is running");
|
||||
else {
|
||||
var numChannels = this.numChannels,
|
||||
buffer = this.buffer,
|
||||
worker = this.worker;
|
||||
this.processor = this.context.createScriptProcessor(
|
||||
this.options.bufferSize,
|
||||
this.numChannels, this.numChannels);
|
||||
this.input.connect(this.processor);
|
||||
this.processor.connect(this.context.destination);
|
||||
this.processor.onaudioprocess = function(event) {
|
||||
for (var ch = 0; ch < numChannels; ++ch)
|
||||
buffer[ch] = event.inputBuffer.getChannelData(ch);
|
||||
worker.postMessage({ command: "record", buffer: buffer });
|
||||
};
|
||||
this.worker.postMessage({
|
||||
command: "start",
|
||||
bufferSize: this.processor.bufferSize
|
||||
});
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
},
|
||||
|
||||
recordingTime: function() {
|
||||
return this.isRecording() ? (Date.now() - this.startTime) * 0.001 : null;
|
||||
},
|
||||
|
||||
cancelRecording: function() {
|
||||
if (this.isRecording()) {
|
||||
this.input.disconnect();
|
||||
this.processor.disconnect();
|
||||
delete this.processor;
|
||||
this.worker.postMessage({ command: "cancel" });
|
||||
} else
|
||||
this.error("cancelRecording: no recording is running");
|
||||
},
|
||||
|
||||
finishRecording: function() {
|
||||
if (this.isRecording()) {
|
||||
this.input.disconnect();
|
||||
this.processor.disconnect();
|
||||
delete this.processor;
|
||||
this.worker.postMessage({ command: "finish" });
|
||||
} else
|
||||
this.error("finishRecording: no recording is running");
|
||||
},
|
||||
|
||||
cancelEncoding: function() {
|
||||
if (this.options.encodeAfterRecord)
|
||||
if (this.isRecording())
|
||||
this.error("cancelEncoding: recording is not finished");
|
||||
else {
|
||||
this.onEncodingCanceled(this);
|
||||
this.initWorker();
|
||||
}
|
||||
else
|
||||
this.error("cancelEncoding: invalid method call");
|
||||
},
|
||||
|
||||
initWorker: function() {
|
||||
if (this.worker != null)
|
||||
this.worker.terminate();
|
||||
this.onEncoderLoading(this, this.encoding);
|
||||
this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]);
|
||||
var _this = this;
|
||||
this.worker.onmessage = function(event) {
|
||||
var data = event.data;
|
||||
switch (data.command) {
|
||||
case "loaded":
|
||||
_this.onEncoderLoaded(_this, _this.encoding);
|
||||
break;
|
||||
case "timeout":
|
||||
_this.onTimeout(_this);
|
||||
break;
|
||||
case "progress":
|
||||
_this.onEncodingProgress(_this, data.progress);
|
||||
break;
|
||||
case "complete":
|
||||
_this.onComplete(_this, data.blob);
|
||||
break;
|
||||
case "error":
|
||||
_this.error(data.message);
|
||||
}
|
||||
};
|
||||
this.worker.postMessage({
|
||||
command: "init",
|
||||
config: {
|
||||
sampleRate: this.context.sampleRate,
|
||||
numChannels: this.numChannels
|
||||
},
|
||||
options: this.options
|
||||
});
|
||||
},
|
||||
|
||||
error: function(message) {
|
||||
this.onError(this, "WebAudioRecorder.js:" + message);
|
||||
},
|
||||
|
||||
// event handlers
|
||||
onEncoderLoading: function(recorder, encoding) {},
|
||||
onEncoderLoaded: function(recorder, encoding) {},
|
||||
onTimeout: function(recorder) { recorder.finishRecording(); },
|
||||
onEncodingProgress: function (recorder, progress) {},
|
||||
onEncodingCanceled: function(recorder) {},
|
||||
onComplete: function(recorder, blob) {
|
||||
recorder.onError(recorder, "WebAudioRecorder.js: You must override .onComplete event");
|
||||
},
|
||||
onError: function(recorder, message) { console.log(message); }
|
||||
});
|
||||
|
||||
window.WebAudioRecorder = WebAudioRecorder;
|
||||
})(window);
|
||||
|
|
|
@ -101,13 +101,40 @@
|
|||
'click' : 'onClick',
|
||||
'click .bottom-bar': 'focusMessageField',
|
||||
'click .back': 'resetPanel',
|
||||
'click .microphone': 'captureAudio',
|
||||
'focus .send-message': 'focusBottomBar',
|
||||
'change .file-input': 'toggleMicrophone',
|
||||
'blur .send-message': 'unfocusBottomBar',
|
||||
'loadMore .message-list': 'fetchMessages',
|
||||
'close .menu': 'closeMenu',
|
||||
'select .message-list .entry': 'messageDetail',
|
||||
'force-resize': 'forceUpdateMessageFieldSize'
|
||||
},
|
||||
toggleMicrophone: function() {
|
||||
if (this.$('.send-message').val().length > 0 || this.fileInput.hasFiles()) {
|
||||
this.$('.capture-audio').hide();
|
||||
} else {
|
||||
this.$('.capture-audio').show();
|
||||
}
|
||||
},
|
||||
captureAudio: function(e) {
|
||||
e.preventDefault();
|
||||
var view = new Whisper.RecorderView().render();
|
||||
view.on('send', this.handleAudioCapture.bind(this));
|
||||
view.on('closed', this.endCaptureAudio.bind(this));
|
||||
view.$el.appendTo(this.$('.capture-audio'));
|
||||
this.$('.send-message').attr('disabled','disabled');
|
||||
this.$('.microphone').hide();
|
||||
},
|
||||
handleAudioCapture: function(blob) {
|
||||
this.fileInput.file = blob;
|
||||
this.fileInput.previewImages();
|
||||
this.$('.bottom-bar form').submit();
|
||||
},
|
||||
endCaptureAudio: function() {
|
||||
this.$('.send-message').removeAttr('disabled');
|
||||
this.$('.microphone').show();
|
||||
},
|
||||
|
||||
unfocusBottomBar: function() {
|
||||
this.$('.bottom-bar form').removeClass('active');
|
||||
|
@ -295,6 +322,7 @@
|
|||
event.preventDefault();
|
||||
return this.$('.bottom-bar form').submit();
|
||||
}
|
||||
this.toggleMicrophone();
|
||||
|
||||
this.view.measureScrollPosition();
|
||||
window.autosize(this.$messageField);
|
||||
|
|
|
@ -248,6 +248,7 @@
|
|||
this.$input.wrap('<form>').parent('form').trigger('reset');
|
||||
this.$input.unwrap();
|
||||
this.file = null;
|
||||
this.$input.trigger('change');
|
||||
},
|
||||
|
||||
openDropped: function(e) {
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
restartSignal : i18n('restartSignal'),
|
||||
},
|
||||
events: {
|
||||
'click': 'closeMenu',
|
||||
'click': 'onClick',
|
||||
'click #header': 'focusHeader',
|
||||
'click .conversation': 'focusConversation',
|
||||
'click .global-menu .hamburger': 'toggleMenu',
|
||||
|
@ -173,12 +173,22 @@
|
|||
showLightbox: function(e) {
|
||||
this.$el.append(e.target);
|
||||
},
|
||||
closeRecording: function(e) {
|
||||
if (e && this.$(e.target).closest('.capture-audio').length > 0 ) {
|
||||
return;
|
||||
}
|
||||
this.$('.conversation:first .audio-capture').trigger('close');
|
||||
},
|
||||
closeMenu: function(e) {
|
||||
if (e && this.$(e.target).parent('.global-menu').length > 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$('.global-menu .menu-list').hide();
|
||||
},
|
||||
onClick: function(e) {
|
||||
this.closeMenu(e);
|
||||
this.closeRecording(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
74
js/views/recorder_view.js
Normal file
74
js/views/recorder_view.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.RecorderView = Whisper.View.extend({
|
||||
className: 'recorder clearfix',
|
||||
templateName: 'recorder',
|
||||
initialize: function() {
|
||||
this.startTime = Date.now();
|
||||
this.interval = setInterval(this.updateTime.bind(this), 1000);
|
||||
this.start();
|
||||
},
|
||||
events: {
|
||||
'click .close': 'close',
|
||||
'click .finish': 'finish',
|
||||
'close': 'close'
|
||||
},
|
||||
updateTime: function() {
|
||||
var duration = moment.duration(Date.now() - this.startTime, 'ms');
|
||||
var minutes = '' + Math.trunc(duration.asMinutes());
|
||||
var seconds = '' + duration.seconds();
|
||||
if (seconds.length < 2) {
|
||||
seconds = '0' + seconds;
|
||||
}
|
||||
this.$('.time').text(minutes + ':' + seconds);
|
||||
},
|
||||
close: function() {
|
||||
if (this.recorder.isRecording()) {
|
||||
this.recorder.cancelRecording();
|
||||
}
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.source.disconnect();
|
||||
if (this.context) {
|
||||
this.context.close().then(function() {
|
||||
console.log('audio context closed');
|
||||
});
|
||||
}
|
||||
this.remove();
|
||||
this.trigger('closed');
|
||||
},
|
||||
finish: function() {
|
||||
this.recorder.finishRecording();
|
||||
this.close();
|
||||
},
|
||||
handleBlob: function(recorder, blob) {
|
||||
if (blob) {
|
||||
this.trigger('send', blob);
|
||||
}
|
||||
},
|
||||
start: function() {
|
||||
this.context = new AudioContext();
|
||||
this.input = this.context.createGain();
|
||||
this.recorder = new WebAudioRecorder(this.input, {
|
||||
encoding: 'mp3',
|
||||
workerDir: 'js/' // must end with slash
|
||||
});
|
||||
this.recorder.onComplete = this.handleBlob.bind(this);
|
||||
this.recorder.onError = this.onError;
|
||||
navigator.webkitGetUserMedia({ audio: true }, function(stream) {
|
||||
this.source = this.context.createMediaStreamSource(stream);
|
||||
this.source.connect(this.input);
|
||||
}.bind(this), this.onError);
|
||||
this.recorder.startRecording();
|
||||
},
|
||||
onError: function(error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue