add audio transcription module
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Justice Yen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Audio Transcription
|
||||
|
||||
Audio Transcription is a Chrome extension that allows users to capture any audio playing on the current tab and transcribe it using OpenAI-whisper in real time. Users will have the option to do voice activity detection as well to not send audio to server when there is no speech.
|
||||
|
||||
## Loading the Extension
|
||||
- Open the Google Chrome browser.
|
||||
- Type chrome://extensions in the address bar and press Enter.
|
||||
- Enable the Developer mode toggle switch located in the top right corner.
|
||||
- Clone this repository
|
||||
- Click the Load unpacked button.
|
||||
- Browse to the location where you cloned the repository files and select the ```Audio Transcription``` folder.
|
||||
- The extension should now be loaded and visible on the extensions page.
|
||||
|
||||
|
||||
## Real time transcription with OpenAI-whisper
|
||||
This Chrome extension allows you to send audio from your browser to a server for transcribing the audio in real time. It can also incorporate voice activity detection on the client side to detect when speech is present, and it continuously receives transcriptions of the spoken content from the server. You can select from the options menu if you want to run the speech recognition.
|
||||
|
||||
|
||||
## Running the Whisper-live server
|
||||
For a detailed overview of how to run the server to leverage real time transcriptions with OpenAI-whisper, use [whisper-live](https://github.com/collabora/whisper-live) to setup your own server.
|
||||
|
||||
|
||||
## Options
|
||||
Several options are able to be changed in the extension:
|
||||
- 'Mute tabs that are being captured' allows the extension to force any tabs currently being captured to be muted on the system's audio output, but still have its audio captured and encoded to the resulting file.
|
||||
- 'Maximum capture time' changes the amount of time the extension will capture audio for before timing out, and has a limit to prevent exceeding Chrome's memory limit.
|
||||
- 'Output file format' allows users to choose whether the resulting file will be encoded into .wav or .mp3
|
||||
- 'MP3 Quality' is only applicable for .mp3 encodings, and will change the bitrate of the encode. (Low: 96 kbps, Medium: 192 kbps, High: 320 kbps)
|
||||
- 'Enable Voice Activity detection' allows users to run a voice activity detection model before sending audio to a server hosting the OpenAI-whisper model for transcriptions.
|
||||
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Capturing Audio
|
||||
To capture the audio in the current tab, I used the chrome `tabCapture` API to obtain a `MediaStream` object of the current tab. Next I used the `MediaStream` object to initialize a recorder that will encode the stream into a .wav file using the `Recorder.js` library.
|
||||
|
||||
### Audio Transcription
|
||||
We use OpenAI-whisper model to process the audio continuously and send the transcription back to the client. We apply a few optimizations on top of OpenAI's implementation to improve performance and run it faster in a real-time manner. To this end, we used [faster-whisper](https://github.com/guillaumekln/faster-whisper) which is 4x faster than OpenAI's implementation.
|
||||
|
||||
### Voice Activity Detection
|
||||
For VAD, we use [silero-vad](https://github.com/snakers4/silero-vad) which is both efficient and accurate for detecting voice activity. It takes around ```1ms``` to process a single audio chunk of ```30ms```. We use the ONNX model in the browser to only send the audio to the server when there is a voice activity.
|
||||
|
||||
### Tab Management
|
||||
To allow audio capture on multiple tabs simultaneously, I stored the `tabId` of each tab being captured into the `sessionStorage` object. When a `stopCapture` command is issued, the extension will check whether the current tab is the same as the tab that the capture was started on, and only stop the specific instance of the capture on the current tab.
|
||||
|
||||
|
||||
### Audio Playback During Capture
|
||||
By default, using `tabCapture` will mute the audio on the current tab in order for the capture to take place. To allow audio to continue playing during the capture, I created an `Audio` object which has its source linked to the ongoing stream that is being captured. In the options menu, users will have the option to keep the tab muted or unmuted during the capture.
|
||||
|
||||
|
||||
## Limitations
|
||||
This extension requires an internet connection to stream audio and receive transcriptions. The accuracy of the transcriptions may vary depending on the audio quality and the performance of the server-side transcription service. The extension may consume additional system resources while running, especially when streaming audio.
|
||||
|
||||
## License
|
||||
This extension is provided as-is, without any warranty or guarantee of its performance or suitability for any particular purpose. The developers of this extension shall not be held responsible for any damages or losses incurred while using this extension.
|
||||
This extension uses LAME MP3 encoder, licensed LGPL.
|
||||
Everything else is under the MIT License.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
|
||||
<script src="background.js"></script>
|
||||
<script src="worker.js"></script>
|
||||
@@ -0,0 +1,363 @@
|
||||
const extend = function() { //helper function to merge objects
|
||||
let target = arguments[0],
|
||||
sources = [].slice.call(arguments, 1);
|
||||
for (let i = 0; i < sources.length; ++i) {
|
||||
let src = sources[i];
|
||||
for (key in src) {
|
||||
let val = src[key];
|
||||
target[key] = typeof val === "object"
|
||||
? extend(typeof target[key] === "object" ? target[key] : {}, val)
|
||||
: val;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const WORKER_FILE = {
|
||||
wav: "WavWorker.js",
|
||||
mp3: "Mp3Worker.js"
|
||||
};
|
||||
|
||||
// default configs
|
||||
const CONFIGS = {
|
||||
workerDir: "/workers/", // worker scripts dir (end with /)
|
||||
numChannels: 2, // number of channels
|
||||
encoding: "wav", // encoding (can be changed at runtime)
|
||||
|
||||
// runtime options
|
||||
options: {
|
||||
timeLimit: 1200, // recording time limit (sec)
|
||||
encodeAfterRecord: true, // process encoding after recording
|
||||
progressInterval: 1000, // encoding progress report interval (millisec)
|
||||
bufferSize: 4096, // buffer size (use browser default)
|
||||
|
||||
// encoding-specific options
|
||||
wav: {
|
||||
mimeType: "audio/wav"
|
||||
},
|
||||
mp3: {
|
||||
mimeType: "audio/mpeg",
|
||||
bitRate: 192 // (CBR only): bit rate = [64 .. 320]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Recorder {
|
||||
|
||||
constructor(source, configs) { //creates audio context from the source and connects it to the worker
|
||||
extend(this, CONFIGS, configs || {});
|
||||
this.speech_threshold = 0.4
|
||||
this.context = source.context;
|
||||
if (this.context.createScriptProcessor == null)
|
||||
this.context.createScriptProcessor = this.context.createJavaScriptNode;
|
||||
this.input = this.context.createGain();
|
||||
source.connect(this.input);
|
||||
this.buffer = [];
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
isRecording() {
|
||||
return this.processor != null;
|
||||
}
|
||||
|
||||
setEncoding(encoding) {
|
||||
if(!this.isRecording() && this.encoding !== encoding) {
|
||||
this.encoding = encoding;
|
||||
this.initWorker();
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (!this.isRecording()) {
|
||||
extend(this.options, options);
|
||||
this.worker.postMessage({ command: "options", options: this.options});
|
||||
}
|
||||
}
|
||||
|
||||
async startRecording(doVad) {
|
||||
if(!this.isRecording()) {
|
||||
let numChannels = this.numChannels;
|
||||
let buffer = this.buffer;
|
||||
let worker = this.worker;
|
||||
|
||||
// initialize onnx model
|
||||
const session = await ort.InferenceSession.create('./silero_vad.onnx');
|
||||
var h = new Array(128);
|
||||
for (let i = 0; i < h.length; i++) {
|
||||
h[i] = 0;
|
||||
}
|
||||
var c = new Array(128);
|
||||
for (let i = 0; i < h.length; i++) {
|
||||
c[i] = 0;
|
||||
}
|
||||
|
||||
const sr = new BigInt64Array(1)
|
||||
sr[0] = BigInt(16000);
|
||||
const srate = new ort.Tensor('int64', sr, [1]);
|
||||
let speech_prob = undefined;
|
||||
const vad_infer = async (feed_dict) => {
|
||||
// feed inputs and run
|
||||
try{
|
||||
const results = await session.run(feed_dict);
|
||||
|
||||
// update states
|
||||
h = results.hn.data
|
||||
c = results.cn.data
|
||||
speech_prob = results.output.data
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// voice activity detection inference
|
||||
const audioBuffer = new ort.Tensor('float32', buffer[0], [1, buffer[0].length]);
|
||||
const hh = new ort.Tensor('float32', h, [2, 1, 64]);
|
||||
const hc = new ort.Tensor('float32', c, [2, 1, 64]);
|
||||
const feeds = { input: audioBuffer, sr: srate, h: hh, c: hc};
|
||||
|
||||
// feed inputs and run
|
||||
if (doVad) {
|
||||
vad_infer(feeds)
|
||||
if (speech_prob > 0.4) {
|
||||
worker.postMessage({ command: "record", buffer: buffer });
|
||||
}
|
||||
else
|
||||
console.log("no speech found: " + speech_prob)
|
||||
}
|
||||
else
|
||||
worker.postMessage({ command: "record", buffer: buffer });
|
||||
};
|
||||
this.worker.postMessage({
|
||||
command: "start",
|
||||
bufferSize: this.processor.bufferSize
|
||||
});
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
cancelRecording() {
|
||||
if(this.isRecording()) {
|
||||
this.input.disconnect();
|
||||
this.processor.disconnect();
|
||||
delete this.processor;
|
||||
this.worker.postMessage({ command: "cancel" });
|
||||
}
|
||||
}
|
||||
|
||||
finishRecording() {
|
||||
if (this.isRecording()) {
|
||||
this.input.disconnect();
|
||||
this.processor.disconnect();
|
||||
delete this.processor;
|
||||
this.worker.postMessage({ command: "finish" });
|
||||
}
|
||||
}
|
||||
|
||||
cancelEncoding() {
|
||||
if (this.options.encodeAfterRecord)
|
||||
if (!this.isRecording()) {
|
||||
this.onEncodingCanceled(this);
|
||||
this.initWorker();
|
||||
}
|
||||
}
|
||||
|
||||
initWorker() {
|
||||
if (this.worker != null)
|
||||
this.worker.terminate();
|
||||
this.onEncoderLoading(this, this.encoding);
|
||||
this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]);
|
||||
let _this = this;
|
||||
this.worker.onmessage = function(event) {
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
this.worker.postMessage({
|
||||
command: "init",
|
||||
config: {
|
||||
sampleRate: this.context.sampleRate,
|
||||
numChannels: this.numChannels
|
||||
},
|
||||
options: this.options
|
||||
});
|
||||
}
|
||||
|
||||
onEncoderLoading(recorder, encoding) {}
|
||||
onEncoderLoaded(recorder, encoding) {}
|
||||
onTimeout(recorder) {}
|
||||
onEncodingProgress(recorder, progress) {}
|
||||
onEncodingCanceled(recorder) {}
|
||||
onComplete(recorder, blob) {}
|
||||
|
||||
}
|
||||
|
||||
const audioCapture = (timeLimit, muteTab, format, quality, limitRemoved, doVad) => {
|
||||
chrome.tabCapture.capture({audio: true}, (stream) => { // sets up stream for capture
|
||||
let startTabId; //tab when the capture is started
|
||||
let timeout;
|
||||
let completeTabID; //tab when the capture is stopped
|
||||
let audioURL = null; //resulting object when encoding is completed
|
||||
chrome.tabs.query({active:true, currentWindow: true}, (tabs) => startTabId = tabs[0].id) //saves start tab
|
||||
const liveStream = stream;
|
||||
const audioCtx = new AudioContext({sampleRate: 16000});
|
||||
const source = audioCtx.createMediaStreamSource(stream);
|
||||
let mediaRecorder = new Recorder(source); //initiates the recorder based on the current stream
|
||||
mediaRecorder.setEncoding(format); //sets encoding based on options
|
||||
if(limitRemoved) { //removes time limit
|
||||
mediaRecorder.setOptions({timeLimit: 10800});
|
||||
} else {
|
||||
mediaRecorder.setOptions({timeLimit: timeLimit/1000});
|
||||
}
|
||||
if(format === "mp3") {
|
||||
mediaRecorder.setOptions({mp3: {bitRate: quality}});
|
||||
}
|
||||
mediaRecorder.startRecording(doVad);
|
||||
|
||||
function onStopCommand(command) { //keypress
|
||||
if (command === "stop") {
|
||||
stopCapture();
|
||||
}
|
||||
}
|
||||
function onStopClick(request) { //click on popup
|
||||
if(request === "stopCapture") {
|
||||
stopCapture();
|
||||
} else if (request === "cancelCapture") {
|
||||
cancelCapture();
|
||||
} else if (request.cancelEncodeID) {
|
||||
if(request.cancelEncodeID === startTabId && mediaRecorder) {
|
||||
mediaRecorder.cancelEncoding();
|
||||
}
|
||||
}
|
||||
}
|
||||
chrome.commands.onCommand.addListener(onStopCommand);
|
||||
chrome.runtime.onMessage.addListener(onStopClick);
|
||||
mediaRecorder.onComplete = (recorder, blob) => {
|
||||
audioURL = window.URL.createObjectURL(blob);
|
||||
if(completeTabID) {
|
||||
chrome.tabs.sendMessage(completeTabID, {type: "encodingComplete", audioURL});
|
||||
}
|
||||
mediaRecorder = null;
|
||||
}
|
||||
mediaRecorder.onEncodingProgress = (recorder, progress) => {
|
||||
if(completeTabID) {
|
||||
chrome.tabs.sendMessage(completeTabID, {type: "encodingProgress", progress: progress});
|
||||
}
|
||||
}
|
||||
|
||||
const stopCapture = function() {
|
||||
let endTabId;
|
||||
//check to make sure the current tab is the tab being captured
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
endTabId = tabs[0].id;
|
||||
if(mediaRecorder && startTabId === endTabId){
|
||||
mediaRecorder.finishRecording();
|
||||
chrome.tabs.create({url: "complete.html"}, (tab) => {
|
||||
completeTabID = tab.id;
|
||||
let completeCallback = () => {
|
||||
chrome.tabs.sendMessage(tab.id, {type: "createTab", format: format, audioURL, startID: startTabId});
|
||||
}
|
||||
setTimeout(completeCallback, 500);
|
||||
});
|
||||
closeStream(endTabId);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const cancelCapture = function() {
|
||||
let endTabId;
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
endTabId = tabs[0].id;
|
||||
if(mediaRecorder && startTabId === endTabId){
|
||||
mediaRecorder.cancelRecording();
|
||||
closeStream(endTabId);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//removes the audio context and closes recorder to save memory
|
||||
const closeStream = function(endTabId) {
|
||||
chrome.commands.onCommand.removeListener(onStopCommand);
|
||||
chrome.runtime.onMessage.removeListener(onStopClick);
|
||||
mediaRecorder.onTimeout = () => {};
|
||||
audioCtx.close();
|
||||
liveStream.getAudioTracks()[0].stop();
|
||||
sessionStorage.removeItem(endTabId);
|
||||
chrome.runtime.sendMessage({captureStopped: endTabId});
|
||||
}
|
||||
|
||||
mediaRecorder.onTimeout = stopCapture;
|
||||
|
||||
if(!muteTab) {
|
||||
let audio = new Audio();
|
||||
audio.srcObject = liveStream;
|
||||
audio.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
//sends reponses to and from the popup menu
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.currentTab && sessionStorage.getItem(request.currentTab)) {
|
||||
sendResponse(sessionStorage.getItem(request.currentTab));
|
||||
} else if (request.currentTab){
|
||||
sendResponse(false);
|
||||
} else if (request === "startCapture") {
|
||||
startCapture();
|
||||
}
|
||||
});
|
||||
|
||||
const startCapture = function() {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
// CODE TO BLOCK CAPTURE ON YOUTUBE, DO NOT REMOVE
|
||||
// if(tabs[0].url.toLowerCase().includes("youtube")) {
|
||||
// chrome.tabs.create({url: "error.html"});
|
||||
// } else {
|
||||
if(!sessionStorage.getItem(tabs[0].id)) {
|
||||
sessionStorage.setItem(tabs[0].id, Date.now());
|
||||
chrome.storage.sync.get({
|
||||
maxTime: 1200000,
|
||||
muteTab: false,
|
||||
format: "mp3",
|
||||
quality: 192,
|
||||
limitRemoved: false,
|
||||
doVad: false
|
||||
}, (options) => {
|
||||
let time = options.maxTime;
|
||||
if(time > 1200000) {
|
||||
time = 1200000
|
||||
}
|
||||
audioCapture(time, options.muteTab, options.format, options.quality, options.limitRemoved, options.doVad);
|
||||
});
|
||||
chrome.runtime.sendMessage({captureStarted: tabs[0].id, startTime: Date.now()});
|
||||
}
|
||||
// }
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
chrome.commands.onCommand.addListener((command) => {
|
||||
if (command === "start") {
|
||||
startCapture();
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,71 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 15px;
|
||||
padding-left: 20px;
|
||||
border-bottom: 2px solid darkred;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 50px 20px 10px 20px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
#progressContainer {
|
||||
margin-left: 5px;
|
||||
width: 500px;
|
||||
height: 18px;
|
||||
background-color: lavenderblush;
|
||||
}
|
||||
|
||||
#encodeProgress {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#saveCapture {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 30px;
|
||||
margin-left: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px;
|
||||
border: 2px solid darkred;
|
||||
font-size: 16px;
|
||||
background-color: lavenderblush;
|
||||
font-weight: bold;
|
||||
margin: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: red;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#review {
|
||||
color: blue;
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Transcription Options</title>
|
||||
<script src="complete.js"></script>
|
||||
<link rel="stylesheet" href="complete.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><img src="./collabora.png"/> <h1 class="header-title">Audio Transcription</h1></div>
|
||||
<div class="inner">
|
||||
<div class="progress">
|
||||
<label for="progrssContainer">Encoding Progress:</label>
|
||||
<div id="progressContainer">
|
||||
<div id="encodeProgress"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="status"></p>
|
||||
</div>
|
||||
<div class="buttonContainer">
|
||||
<div class="button" id="saveCapture">Save Capture</div>
|
||||
<div class="button" id="close">Close</div>
|
||||
</div>
|
||||
<div class="notes">Thank you for using the extension! Please go <div id="review">here</div> to leave a review!</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const encodeProgress = document.getElementById('encodeProgress');
|
||||
const saveButton = document.getElementById('saveCapture');
|
||||
const closeButton = document.getElementById('close');
|
||||
const review = document.getElementById('review');
|
||||
const status = document.getElementById('status');
|
||||
let format;
|
||||
let audioURL;
|
||||
let encoding = false;
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if(request.type === "createTab") {
|
||||
format = request.format;
|
||||
let startID = request.startID;
|
||||
status.innerHTML = "Please wait..."
|
||||
closeButton.onclick = () => {
|
||||
chrome.runtime.sendMessage({cancelEncodeID: startID});
|
||||
chrome.tabs.getCurrent((tab) => {
|
||||
chrome.tabs.remove(tab.id);
|
||||
});
|
||||
}
|
||||
|
||||
//if the encoding completed before the page has loaded
|
||||
if(request.audioURL) {
|
||||
encodeProgress.style.width = '100%';
|
||||
status.innerHTML = "File is ready!"
|
||||
generateSave(request.audioURL);
|
||||
} else {
|
||||
encoding = true;
|
||||
}
|
||||
}
|
||||
|
||||
//when encoding completes
|
||||
if(request.type === "encodingComplete" && encoding) {
|
||||
encoding = false;
|
||||
status.innerHTML = "File is ready!";
|
||||
encodeProgress.style.width = '100%';
|
||||
generateSave(request.audioURL);
|
||||
}
|
||||
//updates encoding process bar upon messages
|
||||
if(request.type === "encodingProgress" && encoding) {
|
||||
encodeProgress.style.width = `${request.progress * 100}%`;
|
||||
}
|
||||
function generateSave(url) { //creates the save button
|
||||
const currentDate = new Date(Date.now()).toDateString();
|
||||
saveButton.onclick = () => {
|
||||
chrome.downloads.download({url: url, filename: `${currentDate}.${format}`, saveAs: true});
|
||||
};
|
||||
saveButton.style.display = "inline-block";
|
||||
}
|
||||
});
|
||||
review.onclick = () => {
|
||||
chrome.tabs.create({url: "https://chrome.google.com/webstore/detail/chrome-audio-capture/kfokdmfpdnokpmpbjhjbcabgligoelgp/reviews"});
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
+14
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
(function(n){var a=Math.min,s=Math.max;var e=function(n,a,e){var s=e.length;for(var t=0;t<s;++t)n.setUint8(a+t,e.charCodeAt(t))};var t=function(t,e){this.sampleRate=t;this.numChannels=e;this.numSamples=0;this.dataViews=[]};t.prototype.encode=function(r){var t=r[0].length,u=this.numChannels,h=new DataView(new ArrayBuffer(t*u*2)),o=0;for(var e=0;e<t;++e)for(var n=0;n<u;++n){var i=r[n][e]*32767;h.setInt16(o,i<0?s(i,-32768):a(i,32767),true);o+=2}this.dataViews.push(h);this.numSamples+=t};t.prototype.finish=function(s){var n=this.numChannels*this.numSamples*2,t=new DataView(new ArrayBuffer(44));e(t,0,"RIFF");t.setUint32(4,36+n,true);e(t,8,"WAVE");e(t,12,"fmt ");t.setUint32(16,16,true);t.setUint16(20,1,true);t.setUint16(22,this.numChannels,true);t.setUint32(24,this.sampleRate,true);t.setUint32(28,this.sampleRate*4,true);t.setUint16(32,this.numChannels*2,true);t.setUint16(34,16,true);e(t,36,"data");t.setUint32(40,n,true);this.dataViews.unshift(t);var a=new Blob(this.dataViews,{type:"audio/wav"});this.cleanup();return a};t.prototype.cancel=t.prototype.cleanup=function(){delete this.dataViews};n.WavAudioEncoder=t})(self);
|
||||
@@ -0,0 +1,23 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid darkred;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 64px;
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Transcription Options</title>
|
||||
<link rel="stylesheet" href="error.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><img src="./collabora.png"/> <h1>Audio Transcription</h1></div>
|
||||
<div class="inner">
|
||||
<h2>Sorry, capture on YouTube is disabled!</h2>
|
||||
<p>Chrome Web Store does not allow extensions to capture audio from YouTube due to copyright reasons.</p>
|
||||
<p>Sorry for the inconvenience, please use the extension on other websites.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
|
||||
"name": "Audio Transcription",
|
||||
"description": "This extension captures the audio on the current tab and saves the output file on your computer when the capture is complete",
|
||||
"version": "1.1.1",
|
||||
"icons": {
|
||||
"128":"collabora.png"
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": "collabora.png",
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Open Audio Transcription interface"
|
||||
},
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"page" : "background.html",
|
||||
"persistent": true
|
||||
},
|
||||
"content_security_policy": "script-src 'self' https://cdn.jsdelivr.net 'wasm-eval'; object-src 'self'",
|
||||
"permissions": [
|
||||
"tabCapture",
|
||||
"downloads",
|
||||
"storage"
|
||||
],
|
||||
"commands": {
|
||||
"start": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+S",
|
||||
"mac": "Command+Shift+U"
|
||||
},
|
||||
"description": "Start Capture"
|
||||
},
|
||||
"stop": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+X",
|
||||
"mac": "MacCtrl+Shift+X"
|
||||
},
|
||||
"description": "Stop Capture"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 15px;
|
||||
padding-left: 20px;
|
||||
border-bottom: 2px solid darkred;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 64px;
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.options-list li {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.limit {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.radioChoice {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px;
|
||||
border: 2px solid darkred;
|
||||
font-size: 16px;
|
||||
background-color: lavenderblush;
|
||||
font-weight: bold;
|
||||
margin: 10px 20px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
width: 110px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: red;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#save {
|
||||
font-size: 16px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
#status {
|
||||
color: red;
|
||||
margin-top: 8px;
|
||||
margin-left: 50px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#qualityLi {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#maxTime {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Transcription Options</title>
|
||||
<script src="options.js"></script>
|
||||
<link rel="stylesheet" href="options.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><img src="./collabora.png"/> <h1>Audio Transcription</h1></div>
|
||||
<div class="inner">
|
||||
<h2>Options</h2>
|
||||
<ul class="options-list">
|
||||
<li><input type="checkbox" id="mute"><label for="mute">Mute tabs that are being captured</label></li>
|
||||
<li class="time"><label for="maxTime">Maximum capture time <p class="limit">(enter value from 1 - 20)</p>: </label><input type="text" id="maxTime"> min(s)</li>
|
||||
<li><input type="checkbox" id="removeLimit"><label for="removeLimit">Remove capture time limit (not recommended)</label></li>
|
||||
<li id="outputType"><label for="outputType">Output file format:</label>
|
||||
<input class="radioChoice" id="mp3" type="radio" name="format" value="mp3"> <label for="mp3">.mp3</label>
|
||||
<input class="radioChoice" id="wav" type="radio" name="format" value="wav"> <label for="wav">.wav</label>
|
||||
</li>
|
||||
<li id="qualityLi">
|
||||
<label for="quality">MP3 Quality: </label>
|
||||
<select id="quality">
|
||||
<option value="96">Low</option>
|
||||
<option value="192">Medium</option>
|
||||
<option value="320">High</option>
|
||||
</select>
|
||||
</li>
|
||||
<li><input type="checkbox" id="doVad"><label for="doVad">Enable Voice Activity detection.</label></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="button" id="save">Save Settings</div>
|
||||
<div id="status"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mute = document.getElementById('mute');
|
||||
const maxTime = document.getElementById('maxTime');
|
||||
const save = document.getElementById('save');
|
||||
const status = document.getElementById('status');
|
||||
const mp3Select = document.getElementById('mp3');
|
||||
const wavSelect = document.getElementById('wav');
|
||||
const quality = document.getElementById("quality");
|
||||
const qualityLi = document.getElementById("qualityLi");
|
||||
const limitRemoved = document.getElementById("removeLimit");
|
||||
const doVad = document.getElementById("doVad");
|
||||
let currentFormat;
|
||||
//initial settings
|
||||
chrome.storage.sync.get({
|
||||
muteTab: false,
|
||||
maxTime: 1200000,
|
||||
format: "mp3",
|
||||
quality: 192,
|
||||
limitRemoved: false,
|
||||
asr: false,
|
||||
doVad: false
|
||||
}, (options) => {
|
||||
mute.checked = options.muteTab;
|
||||
limitRemoved.checked = options.limitRemoved;
|
||||
maxTime.disabled = options.limitRemoved;
|
||||
maxTime.value = options.maxTime/60000;
|
||||
currentFormat = options.format;
|
||||
doVad.checked = options.doVad;
|
||||
if (options.format === "mp3") {
|
||||
mp3Select.checked = true;
|
||||
qualityLi.style.display = "block";
|
||||
} else {
|
||||
wavSelect.checked = true;
|
||||
}
|
||||
if (options.quality === "96") {
|
||||
quality.selectedIndex = 0;
|
||||
} else if(options.quality === "192") {
|
||||
quality.selectedIndex = 1;
|
||||
} else {
|
||||
quality.selectedIndex = 2;
|
||||
}
|
||||
});
|
||||
|
||||
mute.onchange = () => {
|
||||
status.innerHTML = "";
|
||||
}
|
||||
|
||||
doVad.onchange = () => {
|
||||
status.innerHTML = "";
|
||||
}
|
||||
|
||||
maxTime.onchange = () => {
|
||||
status.innerHTML = "";
|
||||
if(maxTime.value > 20) {
|
||||
maxTime.value = 20;
|
||||
} else if (maxTime.value < 1) {
|
||||
maxTime.value = 1;
|
||||
} else if (isNaN(maxTime.value)) {
|
||||
maxTime.value = 20;
|
||||
}
|
||||
}
|
||||
|
||||
mp3Select.onclick = () => {
|
||||
currentFormat = "mp3";
|
||||
qualityLi.style.display = "block";
|
||||
status.innerHTML = "";
|
||||
}
|
||||
|
||||
wavSelect.onclick = () => {
|
||||
currentFormat = "wav";
|
||||
qualityLi.style.display = "none";
|
||||
status.innerHTML = "";
|
||||
}
|
||||
|
||||
quality.onchange = (e) => {
|
||||
status.innerHTML = "";
|
||||
}
|
||||
|
||||
limitRemoved.onchange = () => {
|
||||
if(limitRemoved.checked) {
|
||||
maxTime.disabled = true;
|
||||
status.innerHTML = "WARNING: Recordings that are too long may not save properly!"
|
||||
} else {
|
||||
maxTime.disabled = false;
|
||||
status.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
save.onclick = () => {
|
||||
chrome.storage.sync.set({
|
||||
muteTab: mute.checked,
|
||||
maxTime: maxTime.value*60000,
|
||||
format: currentFormat,
|
||||
quality: quality.value,
|
||||
limitRemoved: limitRemoved.checked,
|
||||
doVad: doVad.checked
|
||||
});
|
||||
status.innerHTML = "Settings saved!"
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
body, h2 {
|
||||
font-family: Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 10px 15px 5px 15px;
|
||||
}
|
||||
|
||||
#status, #timeRem {
|
||||
margin: 10px auto;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid darkred;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header img {
|
||||
height: 64px;
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
|
||||
.extra {
|
||||
font-family: sans-serif;
|
||||
white-space: nowrap;
|
||||
font-style: italic;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.links p {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border: 2px solid darkred;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background-color: lavenderblush;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: red;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
.notes {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Transcription</title>
|
||||
<script src="popup.js"></script>
|
||||
<link rel="stylesheet" href="popup.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><img src="./collabora.png"/> <h1>Audio Transcription</h1></div>
|
||||
<div id="status"></div>
|
||||
<div id="timeRem"></div>
|
||||
<div class="buttonContainer">
|
||||
<div class="button" id="start">Start Capture</div>
|
||||
<div class="button" id="finish">Save Capture</div>
|
||||
<div class="button" id="cancel">Cancel Capture</div>
|
||||
</div>
|
||||
<div class="notes">After capture is finished, a new tab will be opened automatically for you to
|
||||
name and save the file. Please do not close the tab before saving the file!</div>
|
||||
<ul> Hotkeys:
|
||||
<li id="startKey">Ctrl/Command + Shift + to start capture on current tab</li>
|
||||
<li id="endKey">Ctrl/Command + Shift + X to stop capture on current tab</li>
|
||||
</ul>
|
||||
<p class="extra">Hotkeys may not work if another extension is using the same hotkeys</p>
|
||||
<p class="extra">Currently the max capture time is 20 minutes due to Chrome memory contraints</p>
|
||||
<div class="links">
|
||||
<p id="options">Options</p>
|
||||
<p id="GitHub">GitHub</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
let interval;
|
||||
let timeLeft;
|
||||
|
||||
const displayStatus = function() { //function to handle the display of time and buttons
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
const status = document.getElementById("status");
|
||||
const timeRem = document.getElementById("timeRem");
|
||||
const startButton = document.getElementById('start');
|
||||
const finishButton = document.getElementById('finish');
|
||||
const cancelButton = document.getElementById('cancel');
|
||||
//CODE TO BLOCK CAPTURE ON YOUTUBE, DO NOT DELETE
|
||||
// if(tabs[0].url.toLowerCase().includes("youtube")) {
|
||||
// status.innerHTML = "Capture is disabled on this site due to copyright";
|
||||
// } else {
|
||||
chrome.runtime.sendMessage({currentTab: tabs[0].id}, (response) => {
|
||||
if(response) {
|
||||
chrome.storage.sync.get({
|
||||
maxTime: 1200000,
|
||||
limitRemoved: false
|
||||
}, (options) => {
|
||||
if(options.maxTime > 1200000) {
|
||||
chrome.storage.sync.set({
|
||||
maxTime: 1200000
|
||||
});
|
||||
timeLeft = 1200000 - (Date.now() - response)
|
||||
} else {
|
||||
timeLeft = options.maxTime - (Date.now() - response)
|
||||
}
|
||||
status.innerHTML = "Tab is currently being captured";
|
||||
if(options.limitRemoved) {
|
||||
timeRem.innerHTML = `${parseTime(Date.now() - response)}`;
|
||||
interval = setInterval(() => {
|
||||
timeRem.innerHTML = `${parseTime(Date.now() - response)}`;
|
||||
});
|
||||
} else {
|
||||
timeRem.innerHTML = `${parseTime(timeLeft)} remaining`;
|
||||
interval = setInterval(() => {
|
||||
timeLeft = timeLeft - 1000;
|
||||
timeRem.innerHTML = `${parseTime(timeLeft)} remaining`;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
finishButton.style.display = "block";
|
||||
cancelButton.style.display = "block";
|
||||
} else {
|
||||
startButton.style.display = "block";
|
||||
}
|
||||
});
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
const parseTime = function(time) { //function to display time remaining or time elapsed
|
||||
let minutes = Math.floor((time/1000)/60);
|
||||
let seconds = Math.floor((time/1000) % 60);
|
||||
if (minutes < 10 && minutes >= 0) {
|
||||
minutes = '0' + minutes;
|
||||
} else if (minutes < 0) {
|
||||
minutes = '00';
|
||||
}
|
||||
if (seconds < 10 && seconds >= 0) {
|
||||
seconds = '0' + seconds;
|
||||
} else if (seconds < 0) {
|
||||
seconds = '00';
|
||||
}
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
//manipulation of the displayed buttons upon message from background
|
||||
chrome.runtime.onMessage.addListener((request, sender) => {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
const status = document.getElementById("status");
|
||||
const timeRem = document.getElementById("timeRem");
|
||||
const buttons = document.getElementById("buttons");
|
||||
const startButton = document.getElementById('start');
|
||||
const finishButton = document.getElementById('finish');
|
||||
const cancelButton = document.getElementById('cancel');
|
||||
if(request.captureStarted && request.captureStarted === tabs[0].id) {
|
||||
chrome.storage.sync.get({
|
||||
maxTime: 1200000,
|
||||
limitRemoved: false
|
||||
}, (options) => {
|
||||
if(options.maxTime > 1200000) {
|
||||
chrome.storage.sync.set({
|
||||
maxTime: 1200000
|
||||
});
|
||||
timeLeft = 1200000 - (Date.now() - request.startTime)
|
||||
} else {
|
||||
timeLeft = options.maxTime - (Date.now() - request.startTime)
|
||||
}
|
||||
status.innerHTML = "Tab is currently being captured";
|
||||
if(options.limitRemoved) {
|
||||
timeRem.innerHTML = `${parseTime(Date.now() - request.startTime)}`;
|
||||
interval = setInterval(() => {
|
||||
timeRem.innerHTML = `${parseTime(Date.now() - request.startTime)}`
|
||||
}, 1000);
|
||||
} else {
|
||||
timeRem.innerHTML = `${parseTime(timeLeft)} remaining`;
|
||||
interval = setInterval(() => {
|
||||
timeLeft = timeLeft - 1000;
|
||||
timeRem.innerHTML = `${parseTime(timeLeft)} remaining`;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
finishButton.style.display = "block";
|
||||
cancelButton.style.display = "block";
|
||||
startButton.style.display = "none";
|
||||
} else if(request.captureStopped && request.captureStopped === tabs[0].id) {
|
||||
status.innerHTML = "";
|
||||
finishButton.style.display = "none";
|
||||
cancelButton.style.display = "none";
|
||||
startButton.style.display = "block";
|
||||
timeRem.innerHTML = "";
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//initial display for popup menu when opened
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
displayStatus();
|
||||
const startKey = document.getElementById("startKey");
|
||||
const endKey = document.getElementById("endKey");
|
||||
const startButton = document.getElementById('start');
|
||||
const finishButton = document.getElementById('finish');
|
||||
const cancelButton = document.getElementById('cancel');
|
||||
startButton.onclick = () => {chrome.runtime.sendMessage("startCapture")};
|
||||
finishButton.onclick = () => {chrome.runtime.sendMessage("stopCapture")};
|
||||
cancelButton.onclick = () => {chrome.runtime.sendMessage("cancelCapture")};
|
||||
chrome.runtime.getPlatformInfo((info) => {
|
||||
if(info.os === "mac") {
|
||||
startKey.innerHTML = "Command + Shift + U to start capture on current tab";
|
||||
endKey.innerHTML = "Command + Shift + X to stop capture on current tab";
|
||||
} else {
|
||||
startKey.innerHTML = "Ctrl + Shift + S to start capture on current tab";
|
||||
endKey.innerHTML = "Ctrl + Shift + X to stop capture on current tab";
|
||||
}
|
||||
})
|
||||
const options = document.getElementById("options");
|
||||
options.onclick = () => {chrome.runtime.openOptionsPage()};
|
||||
const git = document.getElementById("GitHub");
|
||||
git.onclick = () => {chrome.tabs.create({url: "https://github.com/arblast/Chrome-Audio-Capturer"})};
|
||||
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,148 @@
|
||||
let recLength = 0,
|
||||
recBuffers = [],
|
||||
sampleRate,
|
||||
numChannels;
|
||||
|
||||
onmessage = function(e) {
|
||||
switch (e.data.command) {
|
||||
case 'init':
|
||||
init(e.data.config);
|
||||
break;
|
||||
case 'record':
|
||||
record(e.data.buffer);
|
||||
break;
|
||||
case 'exportWAV':
|
||||
exportWAV(e.data.type);
|
||||
break;
|
||||
case 'getBuffer':
|
||||
getBuffer();
|
||||
break;
|
||||
case 'clear':
|
||||
clear();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function init(config) {
|
||||
sampleRate = config.sampleRate;
|
||||
numChannels = config.numChannels;
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function record(inputBuffer) {
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel].push(inputBuffer[channel]);
|
||||
}
|
||||
recLength += inputBuffer[0].length;
|
||||
}
|
||||
|
||||
function exportWAV(type) {
|
||||
let buffers = [];
|
||||
for (let channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
let interleaved;
|
||||
if (numChannels === 2) {
|
||||
interleaved = interleave(buffers[0], buffers[1]);
|
||||
} else {
|
||||
interleaved = buffers[0];
|
||||
}
|
||||
let dataview = encodeWAV(interleaved);
|
||||
let audioBlob = new Blob([dataview], {type: type});
|
||||
|
||||
this.postMessage({command: 'exportWAV', data: audioBlob});
|
||||
}
|
||||
|
||||
function getBuffer() {
|
||||
let buffers = [];
|
||||
for (let channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
this.postMessage({command: 'getBuffer', data: buffers});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
recLength = 0;
|
||||
recBuffers = [];
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function initBuffers() {
|
||||
for (let channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBuffers(recBuffers, recLength) {
|
||||
let result = new Float32Array(recLength);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < recBuffers.length; i++) {
|
||||
result.set(recBuffers[i], offset);
|
||||
offset += recBuffers[i].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function interleave(inputL, inputR) {
|
||||
let length = inputL.length + inputR.length;
|
||||
let result = new Float32Array(length);
|
||||
|
||||
let index = 0,
|
||||
inputIndex = 0;
|
||||
|
||||
while (index < length) {
|
||||
result[index++] = inputL[inputIndex];
|
||||
result[index++] = inputR[inputIndex];
|
||||
inputIndex++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input) {
|
||||
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||
let s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWAV(samples) {
|
||||
let buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
let view = new DataView(buffer);
|
||||
|
||||
/* RIFF identifier */
|
||||
writeString(view, 0, 'RIFF');
|
||||
/* RIFF chunk length */
|
||||
view.setUint32(4, 36 + samples.length * 2, true);
|
||||
/* RIFF type */
|
||||
writeString(view, 8, 'WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString(view, 12, 'fmt ');
|
||||
/* format chunk length */
|
||||
view.setUint32(16, 16, true);
|
||||
/* sample format (raw) */
|
||||
view.setUint16(20, 1, true);
|
||||
/* channel count */
|
||||
view.setUint16(22, numChannels, true);
|
||||
/* sample rate */
|
||||
view.setUint32(24, sampleRate, true);
|
||||
/* byte rate (sample rate * block align) */
|
||||
view.setUint32(28, sampleRate * 4, true);
|
||||
/* block align (channel count * bytes per sample) */
|
||||
view.setUint16(32, numChannels * 2, true);
|
||||
/* bits per sample */
|
||||
view.setUint16(34, 16, true);
|
||||
/* data chunk identifier */
|
||||
writeString(view, 36, 'data');
|
||||
/* data chunk length */
|
||||
view.setUint32(40, samples.length * 2, true);
|
||||
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
importScripts("/../encoders/Mp3Encoder.min.js");
|
||||
|
||||
let NUM_CH = 2, // constant
|
||||
sampleRate = 16000,
|
||||
options = undefined,
|
||||
maxBuffers = undefined,
|
||||
encoder = undefined,
|
||||
recBuffers = undefined,
|
||||
socket = undefined,
|
||||
bufferCount = 0;
|
||||
|
||||
function error(message) {
|
||||
self.postMessage({ command: "error", message: "mp3: " + message });
|
||||
}
|
||||
|
||||
function init(data) {
|
||||
if (data.config.numChannels === NUM_CH) {
|
||||
sampleRate = data.config.sampleRate;
|
||||
options = data.options;
|
||||
} else
|
||||
error("numChannels must be " + NUM_CH);
|
||||
};
|
||||
|
||||
function setOptions(opt) {
|
||||
if (encoder || recBuffers)
|
||||
error("cannot set options during recording");
|
||||
else
|
||||
options = opt;
|
||||
}
|
||||
|
||||
function start(bufferSize) {
|
||||
maxBuffers = Math.ceil(options.timeLimit * sampleRate / bufferSize);
|
||||
// TODO: get server address from user
|
||||
socket = new WebSocket("ws://localhost:9090/");
|
||||
socket.onopen = function(e) {
|
||||
socket.send("handshake");
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
console.log(event.data);
|
||||
};
|
||||
|
||||
if (options.encodeAfterRecord)
|
||||
recBuffers = [];
|
||||
else
|
||||
encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate);
|
||||
}
|
||||
|
||||
function record(buffer) {
|
||||
if (bufferCount++ < maxBuffers){
|
||||
if (encoder)
|
||||
encoder.encode(buffer);
|
||||
else
|
||||
recBuffers.push(buffer);
|
||||
|
||||
// send buffer to server
|
||||
socket.send(buffer[0]);
|
||||
}
|
||||
else
|
||||
self.postMessage({ command: "timeout" });
|
||||
};
|
||||
|
||||
function postProgress(progress) {
|
||||
self.postMessage({ command: "progress", progress: progress });
|
||||
};
|
||||
|
||||
function finish() {
|
||||
if (recBuffers) {
|
||||
postProgress(0);
|
||||
encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate);
|
||||
let timeout = Date.now() + options.progressInterval;
|
||||
while (recBuffers.length > 0) {
|
||||
encoder.encode(recBuffers.shift());
|
||||
let now = Date.now();
|
||||
if (now > timeout) {
|
||||
postProgress((bufferCount - recBuffers.length) / bufferCount);
|
||||
timeout = now + options.progressInterval;
|
||||
}
|
||||
}
|
||||
postProgress(1);
|
||||
}
|
||||
self.postMessage({
|
||||
command: "complete",
|
||||
blob: encoder.finish(options.mp3.mimeType)
|
||||
});
|
||||
cleanup();
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
encoder = recBuffers = undefined;
|
||||
bufferCount = 0;
|
||||
socket.close();
|
||||
}
|
||||
|
||||
self.onmessage = function(event) {
|
||||
let data = event.data;
|
||||
switch (data.command) {
|
||||
case "init": init(data); break;
|
||||
case "options": setOptions(data.options); break;
|
||||
case "start": start(data.bufferSize); break;
|
||||
case "record": record(data.buffer); break;
|
||||
case "finish": finish(); break;
|
||||
case "cancel": cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
self.postMessage({ command: "loaded" });
|
||||
@@ -0,0 +1,105 @@
|
||||
importScripts("/../encoders/WavEncoder.min.js");
|
||||
|
||||
|
||||
let sampleRate = 16000,
|
||||
numChannels = 2,
|
||||
options = undefined,
|
||||
maxBuffers = undefined,
|
||||
encoder = undefined,
|
||||
recBuffers = undefined,
|
||||
socket = undefined,
|
||||
bufferCount = 0;
|
||||
|
||||
function error(message) {
|
||||
self.postMessage({ command: "error", message: "wav: " + message });
|
||||
}
|
||||
|
||||
function init(data) {
|
||||
sampleRate = data.config.sampleRate;
|
||||
numChannels = data.config.numChannels;
|
||||
options = data.options;
|
||||
};
|
||||
|
||||
function setOptions(opt) {
|
||||
if (encoder || recBuffers)
|
||||
error("cannot set options during recording");
|
||||
else
|
||||
options = opt;
|
||||
}
|
||||
|
||||
function start(bufferSize) {
|
||||
maxBuffers = Math.ceil(options.timeLimit * sampleRate / bufferSize);
|
||||
// TODO: get server address from user
|
||||
socket = new WebSocket("ws://localhost:9090/");
|
||||
socket.onopen = function(e) {
|
||||
socket.send("handshake");
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
console.log(event.data);
|
||||
};
|
||||
|
||||
if (options.encodeAfterRecord)
|
||||
recBuffers = [];
|
||||
else
|
||||
encoder = new WavAudioEncoder(sampleRate, numChannels);
|
||||
}
|
||||
|
||||
function record(buffer) {
|
||||
if (bufferCount++ < maxBuffers){
|
||||
if (encoder)
|
||||
encoder.encode(buffer);
|
||||
else if(recBuffers)
|
||||
recBuffers.push(buffer);
|
||||
|
||||
// send buffer to server
|
||||
socket.send(buffer[0]);
|
||||
}
|
||||
else
|
||||
self.postMessage({ command: "timeout" });
|
||||
};
|
||||
|
||||
function postProgress(progress) {
|
||||
self.postMessage({ command: "progress", progress: progress });
|
||||
};
|
||||
|
||||
function finish() {
|
||||
if (recBuffers) {
|
||||
postProgress(0);
|
||||
encoder = new WavAudioEncoder(sampleRate, numChannels);
|
||||
var timeout = Date.now() + options.progressInterval;
|
||||
while (recBuffers.length > 0) {
|
||||
encoder.encode(recBuffers.shift());
|
||||
var now = Date.now();
|
||||
if (now > timeout) {
|
||||
postProgress((bufferCount - recBuffers.length) / bufferCount);
|
||||
timeout = now + options.progressInterval;
|
||||
}
|
||||
}
|
||||
postProgress(1);
|
||||
}
|
||||
self.postMessage({
|
||||
command: "complete",
|
||||
blob: encoder.finish(options.wav.mimeType)
|
||||
});
|
||||
cleanup();
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
encoder = recBuffers = undefined;
|
||||
bufferCount = 0;
|
||||
socket.close();
|
||||
}
|
||||
|
||||
self.onmessage = function(event) {
|
||||
var data = event.data;
|
||||
switch (data.command) {
|
||||
case "init": init(data); break;
|
||||
case "options": setOptions(data.options); break;
|
||||
case "start": start(data.bufferSize); break;
|
||||
case "record": record(data.buffer); break;
|
||||
case "finish": finish(); break;
|
||||
case "cancel": cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
self.postMessage({ command: "loaded" });
|
||||
Reference in New Issue
Block a user