Tonejs / Tone.js

A Web Audio framework for making interactive music in the browser.
https://tonejs.github.io
MIT License
13.37k stars 976 forks source link

Differences between `Tone.context` and `Tone.getContext()`. How to properly instantiate and dispose the entire Tone instance? #1129

Closed satelllte closed 4 months ago

satelllte commented 1 year ago

Describe the bug

Hi 👋🏻 I'm trying to build some wrapper in my application over the Tone.js for the abstraction. One of the problems I'd like to resolve is to have an ability to instantiate & dispose (i.e. destroy) the core instance of Tone.js.

The weird thing I noticed about Tone.js is the difference between calling Tone.context VS calling Tone.getContext() to get the reference to the Context object. In case I'm using Tone.getContext() I never hear the audio.

To Reproduce

The basic demo in the sandbox: https://stackblitz.com/edit/tone-context-issue (see the private get _context() method of CoreAudio.ts file, if you edit it to return Tone.getContext() instead of Tone.context you will not hear audio anymore)

Expected behavior

What I've tried

See the sandbox for details. Maybe I did something wrong in my CoreAudio wrapper?

braebo commented 1 year ago

Have you learned anything new regarding the global context? I'm trying to set latencyHint to playback without blowing up the whole app's modularity/treeshaking by importing * as Tone from 'tone. Everything I try throws an error.

satelllte commented 1 year ago

Have you learned anything new regarding the global context? I'm trying to set latencyHint to playback without blowing up the whole app's modularity/treeshaking by importing * as Tone from 'tone. Everything I try throws an error.

@FractalHQ no, I didn't have this use-case, maybe you should create a separate issue for this

marcelblum commented 1 year ago

Hello - I am not very familiar with React so admittedly I don't follow the code in your demo very well (specifically - the order in which code is being executed across the different files). The error is happening because some of the code is referencing an already-closed older context (or audio nodes created with the older context). Some things to note about contexts in Tone.js that should help you work around the problem:

andeeplus commented 1 year ago

Hi @satelllte, This is an issue with <StrictMode /> in React and the only solution is not opting in. If you remove the strict mode wrapper in index.tsx your issue is solved. From version 18 the effects are fired twice on load. This is the reason why your context wasn't working, basically it was disposed right after you initialised it.

More Info: StrictMode: Ensuring reusable state

satelllte commented 1 year ago

Hi @satelllte, This is an issue with <StrictMode /> in React and the only solution is not opting in. If you remove the strict mode wrapper in index.tsx your issue is solved. From version 18 the effects are fired twice on load. This is the reason why your context wasn't working, basically it was disposed right after you initialised it.

More Info: StrictMode: Ensuring reusable state

Hi @andeeplus, you're right! When I turn off the React strict mode it works fine. I've also tried to not dispose the context on unmount, which fixes the issue when strict mode is on.


But my main concern is about the difference between Tone.context and Tone.getContext(), why the behaviour is different?

UPD: I see, probably because of this, like @marcelblum described:

Using a new context with Tone is a bit of an edge use case that brings some subtle pitfalls. If you will be creating multiple contexts for use in Tone it's good practice to keep your own global references to those contexts, and always explicitly pass in those references when creating Tone objects (ex: new Tone.Synth({context: myCustomContext1})). That way you never have to worry about which context is referred to by Tone.context and Tone.getContext(), and it's easier to debug.

andeeplus commented 1 year ago

@satelllte Yes, I think what @marcelblum said is the reason about the difference. I've personally run Tone in a large application and I must admit I never had the need for a different context from the main. Anyway has a practice I always access to the destination and the context using getDestination() and getContext().

About strict mode, this is a bit of a pain in react for those kind of situations. Also if you plan to work with other libraries that had issue with strict mode (i.e. wavesurferjs). I didn't find any other solution than didn't opt in the strict mode for the moment (the alternative would be manually opt in all the components that are not using the library that fails).

satelllte commented 1 year ago

Got it, @andeeplus.

Thanks for sharing more about your experience with Tone. And, for sure, that's a good point to use only one way of accessing something at the same project.

Regarding scrict mode, I personally prefer to have it on, so this was one of the reasons to open this issue. But maybe it's really a pain in case the project depends on libraries like Tone, which normally should be instantiated once per session.