GoogleChromeLabs/audioworklet-polyfill

'currentTime' and 'sampleRate' are not set correctly before and between 'process' calls

Closed this issue · 1 comments

This is a follow-up of #7 which was claimed to have been fixed in eb0840c. Unfortunately, the problem with the current approach is that sampleRate is not defined until the first process call, so any instantiation-time use of it will yield an incorrect outcome (e.g. you can't use it in the constructor).

The same is true for currentTime, and although it's less of a problem here, it's still tied to process calls directly, which is incorrect. For example, for a case where the main-thread AudioWorkletNode posts messages to the AudioWorkletProcessor, which is then supposed to respond with a message based on currentTime but without process being called (such as when disconnected), the outcome will be incorrect.


Instead, the Realm must be configured correctly with getters that return directly from the working AudioContext, something like the following (which was working fine for my use-case, but I'm unsure of what side-effects it has):

realm.js

export function Realm (scope, parentElement) {
  const frame = document.createElement('iframe');
  frame.style.cssText = 'position:absolute;left:0;top:-999px;width:1px;height:1px;';
  parentElement.appendChild(frame);
  const win = frame.contentWindow;
  const doc = win.document;
  let vars = 'var window,$hook';
+ let accessors = "";
  for (const i in win) {
    if (!(i in scope) && i !== 'eval') {
      vars += ',';
      vars += i;
    }
  }
  for (const i in scope) {
-   vars += ',';
-   vars += i;
-   vars += '=self.';
-   vars += i;
+   accessors += `Object.defineProperty(this,'${i}',{`;
+   accessors += `    get: function(){return self.${i};},`;
+   accessors += `    set: function(v){self.${i}=v;}`;
+   accessors += `});`;
  }
  const script = doc.createElement('script');
  script.appendChild(doc.createTextNode(
    `function $hook(self,console) {"use strict";
-       ${vars};return function() {return eval(arguments[0])}}`
+       ${vars};${accessors}return function() {return eval(arguments[0])}}`
  ));
  doc.body.appendChild(script);
  this.exec = win.$hook(scope, console);
}

polyfill.js

    addModule (url, options) {
      return fetch(url).then(r => {
        if (!r.ok) throw Error(r.status);
        return r.text();
      }).then(code => {
        const context = {
          sampleRate: 0,
          currentTime: 0,
          AudioWorkletProcessor () {
            this.port = nextPort;
          },
          registerProcessor: (name, Processor) => {
            const processors = getProcessorsForContext(this.$$context);
            processors[name] = {
              realm,
              context,
              Processor,
              properties: Processor.parameterDescriptors || []
            };
          }
        };

+       Object.defineProperty(context, "sampleRate", {
+         get: () => this.$$context.sampleRate
+       });
+
+       Object.defineProperty(context, "currentTime", {
+         get: () => this.$$context.currentTime
+       });

        context.self = context;
        const realm = new Realm(context, document.documentElement);
        realm.exec(((options && options.transpile) || String)(code));
        return null;
      });
    }

This will throw errors when trying to assign to sampleRate or currentTime (which I believe is correct), but let other assignments through.

Your explanation makes sense. The this / self issue can also be corrected more simply:

-  this.exec = win.$hook(scope, console);
+  this.exec = win.$hook.call(scope, scope, console);