Recording .mp3 audio in Google Add-ons/Google Apps Script to Google Drive

Matt Knoll (@KnollMatt) recently posed me an interesting question asking how to record audio to a user’s Google Drive using Google Apps Script. Having previously played around with WebRTC I knew capturing audio from the browser was possible but hadn’t looked at processing it. As it turns out skyllo’s blog has a great write-up explaining how you can Record to an Audio File using HTML5 and JS. As part of this Nick unpicks how different providers like Facebook and WhatApp solve the problem. One of the examples Nick highlights is LameJS, a mp3 encoder written in JavaScript. In the LameJS there is a nice example, mic.html, which has a basic interface for recording and download/playback of audio capture from your mic.

I’ve deployed this example as a Google Apps Script Web App. As this is a demo app I haven’t bothered with App Verification so if you would like to test you’ll have to click on the ‘Advanced’ link in the verification flow (more information on this in this Google Support page). The Drive scope is required to save you audio sample to your Google Drive. If you would prefer to deploy the code yourself and test you can open and copy this script project

Example LameJS Google Apps Script Web App (click to try)

Porting the code to Google Apps Script

As this only uses HTML/JS it is relatively easy to port to Google Apps Script are run in HTMLService, the only consideration really is that the sample uses Web Workers to run the mp3 encoding in the background.

More often than not when constructing a Worker() a relative url or the script the worker will execute is provided. In the case of the LameJS example this is defined in on line 15 of mic.js:

var realTimeWorker = new Worker('worker-realtime.js');

As the script has to meet a same-origin policy providing a worker script url is a problem, if not impossible. All’s not lost as it is possible to provide an inline worker. HTML5 Rocks has a post on the basics of Web Workers which includes information on how inline workers can be used:

With Blob(), you can “inline” your worker in the same HTML file as your main logic by creating a URL handle to the worker code as a string. The magic comes with the call to window.URL.createObjectURL(). This method creates a simple URL string which can be used to reference data stored in a DOM File or Blob object.

The post includes an example of how a worker can be defined within a scripttag:


This method works particularly well if you implement one of the many variations of an ‘include’ method which allows you to separate and include different script files. Bruce Mcpherson has an include examplaination and code you can use.

Using Bruce’s include method In the LameJS example we specify the worker-realtime.js and mic.js files that have been copied into the Google Apps Script project:

Including files in mic.html

Modifying the worker-realtime.js script tag to indicate it is a worker file:

Changing script tag to indicate code is worker

We are not entirely out of the woods yet. A feature of Web Workers is the importScripts() method which lets developers import one or more scripts into the worker. This method uses a comma-separated list of urls to identify the scripts to be imported. In the LameJS example there is only one import for a minified version of the LameJS library on line 8 of worker-realtime.js:

importScripts('../lame.min.js');

I tried various ways to include the lame.min.js file but Google Apps Script kept throwing a Malformed HTML content error and in the end the only way I found that worked was to copy the lame.min.js file into worker-realtime.js:

Including lame-min.js in the worker-realtime.js

Saving the encoded mp3 to Google Drive

The final modification was to save the created mp3 audio file to Google Drive. In the LameJS example the created audio file is already handled for playback and download in lines 69 to 76 of mic.html:

As part of this process the audio blob is already converted to a Data URL. This is useful as the google.script.run class can only handle JavaScript primitives like a Number, Boolean and String. As part of this Data URLs are permitted so this can be passed server side and converted back to a blob to save on Google Drive. To do this I modified the mic.html client side code to this:

Which is then handled server side with:

// handles saving the audio file to drive
function invokeSave(dataURI, filename){
  var blob = dataURItoBlob(dataURI, filename);
  var file = DriveApp.createFile(blob);
  return file.getUrl();
}

// https://stackoverflow.com/a/36949118/1027723
function dataURItoBlob(dataURI, filename) {
  // convert base64/URLEncoded data component to raw binary data held in a string
  var byteString;
  if (dataURI.split(',')[0].indexOf('base64') >= 0){
    byteString = Utilities.base64Decode(dataURI.split(',')[1]);
  } else {
    byteString = decodeURI(dataURI.split(',')[1]);
  }
  // separate out the mime component
  var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  return Utilities.newBlob(byteString, mimeString, filename);
}

Summary

A reminder that all the code is in this script project which you can open and copy. Hopefully now you’ve got some boilerplate if you want to record audio with Google Apps Script. I’ve published the code as a web app but there is nothing stopping you using the generated HTMLService in a sidebar or dialog in Google Docs, Sheets, Forms and Slides. This post also highlights some solutions for using Web Workers in Google Apps Script and handling Data URLs to create files in Google Drive. I should however point out that I’ve only tested this code on Chrome so I’m not sure what the cross-browser compatibility is like. I’ve also only recorded modest length audio files so you might want to explore this more if you want to use in production. If you make any useful discoveries or improvements along the way please drop a line via my contact form or in the comments. If you work in education and would like to implement audio feedback using Google Apps Script it might be worth reaching out to Matt Knoll (@KnollMatt) to collaborate with.