chrisguttandin / standardized-audio-context

A cross-browser wrapper for the Web Audio API which aims to closely follow the standard.
MIT License
680 stars 33 forks source link

context sampleRate handling #968

Closed marcelblum closed 3 years ago

marcelblum commented 3 years ago

I noticed that standardized-audio-context throws an error and won't return a created context if the requested sampleRate doesn't match the resulting native context's sampleRate as per https://github.com/chrisguttandin/standardized-audio-context/blob/50945f4fcaab159fd3535b71cf2e4f2c9b642322/src/factories/audio-context-constructor.ts#L55 even if the browser otherwise successfully instantiates the context.

I guess this is technically following the spec

A NotSupportedError exception MUST be thrown if the specified sample rate is not supported.

But in practice this ends up being less graceful than in some native implementations, for example in Mac Safari which just ignores the sampleRate setting and creates the context at whatever rate it prefers (IMHO better than not creating one at all). OTOH desktop Chrome supports the widest range of all browsers (3000-384000Hz) but does throw a blocking error if sampleRate is outside that range - in fact currently standardized-audio-context fails to catch that error or the similar one from desktop Firefox (which supports 8000-192000Hz). And browser APIs offer no way to probe for supported sample rates. Hmm, wouldn't it be great if this behavior could be... standardized... ;)

Understanding that being able to adjust the sampleRate can be a valuable tool to tweak performance, what do you think about changing the behavior to be more permissive and simply return a browser default sampleRate context if the requested rate is unsupported, with a console warning about mismatched rates? And perhaps even add a try-catch to more gracefully catch some browsers' out of range errors in this situation? The thing is right now I can write something like this in vanilla JS and know that I'll get a usable context in return on a majority of browsers: someContext = new (window.AudioContext || window.webkitAudioContext)({sampleRate: 96000}) but with standardized-audio-context that would end up with a blocking error in Safari.

chrisguttandin commented 3 years ago

Hi @marcelblum, sorry for the delay. You're absolutely right. The goal is to be as spec-compliant as possible. That's why a NotSupportedError is thrown when no AudioContext with the given sampleRate can be created. Safari handles this differently because it doesn't know about the sampleRate option at all yet. But that is about to change. It already works when you switch on the "Modern WebAudio API".

In case you want to create an AudioContext with the default sampleRate of the current audio output device you can just omit the sampleRate option.

I think you can create a little helper that does what you want on top of standardized-audio-context.

import { AudioContext } from 'standardized-audio-context';

const createContextWithSampleRateWhenPossible = (sampleRate) => {
    try {
        return new AudioContext({ sampleRate });
    } catch {
        return new AudioContext();
    }
};

const audioContext = createContextWithSampleRateWhenPossible(96000);
marcelblum commented 3 years ago

Ok thanks, I understand if you want to leave this as is to adhere exactly to the spec since that's the main goal of this project. And thanks for pointing out Safari's 'Modern WebAudio API' switch which I didn't know about, very cool!

One thing to note is that as currently implemented standardized-audio-context actually does not fire the NotSupportedError in certain edge cases because it doesn't use a try/catch on the call to instantiate the native audio context, for example test this in Desktop Firefox (attempts to create a context at 384kHz). The NotSupportedError is properly thrown in Safari but in Firefox an uncaught exception occurs. Not a big deal obviously but just wanted to mention :smirk:

Just to give you some background, Tone.js currently does not permit a new standardized context to be created using a custom sampleRate through Tone's API, and I guess this is in part because of standardized-audio-context's handling of sampleRate and not being able to be guaranteed a working context in return from the constructor. Tone can use a vanilla native context set at a custom rate instead, but many Tone functions won't work without the shims of standardized-audio-context so that path is limiting.

chrisguttandin commented 3 years ago

One thing to note is that as currently implemented standardized-audio-context actually does not fire the NotSupportedError in certain edge cases because it doesn't use a try/catch on the call to instantiate the native audio context, for example test this in Desktop Firefox (attempts to create a context at 384kHz). The NotSupportedError is properly thrown in Safari but in Firefox an uncaught exception occurs. Not a big deal obviously but just wanted to mention 😏

I can't reproduce that. Or maybe I misunderstand the problem. Firefox throws a NotSupportedError when I run your example. standardized-audio-context only throws an error on it's own if the browser itself doesn't do it.

Just to give you some background, Tone.js currently does not permit a new standardized context to be created using a custom sampleRate through Tone's API, and I guess this is in part because of standardized-audio-context's handling of sampleRate and not being able to be guaranteed a working context in return from the constructor. Tone can use a vanilla native context set at a custom rate instead, but many Tone functions won't work without the shims of standardized-audio-context so that path is limiting.

It's a bit hacky but something like this should work:

import * as Tone from 'tone';

console.log(Tone.getContext().sampleRate);

Tone.setContext(new Tone.context.rawContext.constructor({ sampleRate: 96000 }));

console.log(Tone.getContext().sampleRate);
marcelblum commented 3 years ago

I can't reproduce that. Or maybe I misunderstand the problem. Firefox throws a NotSupportedError

Apologies, this was just me misunderstanding how Firefox's console prints exception objects, my bad! You are absolutely right, if I catch that exception then exception.name === "NotSupportedError" in Firefox.

Tone.setContext(new Tone.context.rawContext.constructor({ sampleRate: 96000 }));

Thank you so much for this bit of code-fu, I've been wondering how to access the constructor for Tone's bundled standardized-audio-context directly and this is a brilliant way to do it. Actually in order for this to work and have the standardized context wrapped within Tone's own context frame, so that it has all the properties Tone expects and all Tone functions work, this seems to be what's needed:

Tone.setContext(new Tone.Context(new Tone.context.rawContext.constructor({ sampleRate: 96000 })))

chrisguttandin commented 3 years ago

I'm glad you found a way to make it work. I'm closing the issue now. Please feel free to reopen it or to create another one if there is anything else.