elemaudio / website

Elementary Audio Website
https://www.elementary.audio
MIT License
4 stars 5 forks source link

Fallback Limiter in Playground #4

Open nick-thompson opened 7 months ago

nick-thompson commented 7 months ago

Add a default limiter to the Playground which ideally stays out of the way but which would clamp anything going over 0dB. Mostly as a safety/panic mechanism to catch unexpected loud noises while the user is working

mark-mxwl commented 4 months ago

Hi @nick-thompson! Happy to help with this one. Are you intending for the limiter to be applied to the runtime, or as an addition to the editor's default constant?

nick-thompson commented 4 months ago

Awesome, thanks @mark-mxwl! I was imagining it would go right here: https://github.com/elemaudio/website/blob/main/src/components/playground/runtime.js#L46-L49. Basically whatever the user exports from their playground module, we evaluate it and then pass the results through some el.limit() style function. The gotcha is that there is no el.limit in the elementary standard library so this task will include writing a limiter or using the existing el.compress with a suitable ratio. How's that sound?

mark-mxwl commented 4 months ago

Sounds good @nick-thompson! I think dialing in el.compress as a limiter would work just fine (though a dedicated limiter module would be really cool to have at some point). I'll have a PR up sometime this weekend. 🙌

nick-thompson commented 4 months ago

Great, thanks!

mark-mxwl commented 3 months ago

Hey @nick-thompson! I was hoping to make this one easy on you and just knock it out. However, I'm running into an issue. I'm still new to your library, so I may be overlooking the obvious, but here's where I'm at. Looking at el.compress, it accepts an ElemNode for the sidechain and xn parameters. From what I can gather, userOutput returns an object of the type ElemNode, which represents an audio signal within the context of this library. However, when userOutput is passed to the compressor, it throws the following error:

Invariant Violation: Whoops, expecting a Node type here! Got: object
    at invariant (webpack-internal:///./node_modules/invariant/browser.js:38:15)
    at resolve (webpack-internal:///./node_modules/@elemaudio/core/dist/index.js:1121:3)
    at Object.env (webpack-internal:///./node_modules/@elemaudio/core/dist/index.js:1259:59)
    at Object.compress (webpack-internal:///./node_modules/@elemaudio/core/dist/index.js:1572:20)
    at Runtime.runUserCode (webpack-internal:///./src/components/playground/runtime.js:41:74)
    at async eval (webpack-internal:///./src/components/playground/index.js:99:19)

While the Playground editor itself accepts ElemNode as valid input for el.compress, the behavior is different in the runtime--even when userOutput is an identical obj.

Any guidance you could offer would be greatly appreciated!

nick-thompson commented 3 months ago

Hey @mark-mxwl, mind sharing the code you wrote? Without looking at it, my only guess is this: userOutput there is intended to support either an array of output signals (for stereo or multi-channel rendering), or a single output signal (for mono rendering). If you get an array you'll have to unpack it and apply the limiter to both channels:

      const limit = (xn) => el.compress(10, 100, -48, 4, xn, xn);
      const userOutput = render();
      const stats = Array.isArray(userOutput)
        ? await this.core.render(...userOutput.map(limit))
        : await this.core.render(limit(userOutput), limit(userOutput));

Something like that, with more sensible parameters for the atk/rel/thresh/ratio.

The error you're seeing there just suggests that one of the arguments you've passed to el.compress (likely the sidechain argument because we see el.env in the callstack) is not of type ElemNode, so it's not valid type for the graph construction. It might be helpful to drop a debugger or a console log there to inspect the type before you pass it in.

mark-mxwl commented 3 months ago

@nick-thompson, really appreciate the help! Funny, but the code I had was nearly identical, just with the limiter dialed in as so:

      const userOutput = render();
      const limit = (n) => el.compress(1, 10, -3, 20, n, n);
      const stats = Array.isArray(userOutput)
        ? await this.core.render(...userOutput.map(limit))
        : await this.core.render(limit(userOutput), limit(userOutput));

If you log the user output from both the Playground UI and runtime.js, you return the same ElemNode object. But running an isNode check inside the Playground returns true for type NodeRepr_t, and false in the runtime. See below:

True when logging from Playground; false when logging from runtime.js:

Object { symbol: "__ELEM_NODE__", hash: 520300429, kind: "sin", props: {}, children: {…} }
children: Object { hd: {…}, tl: 0 }
hash: 520300429
kind: "sin"
props: Object {  }
symbol: "__ELEM_NODE__"
<prototype>: Object { … }
mark-mxwl commented 3 months ago

Ah, I got it. Just updated @elemaudio/core and @elemaudio/web-renderer to 3.2.x. The problem was the isNode function itself: In the lib. version I was using, isNode was checking for a .TAG value of 4 for symbol, when userOutput.symbol is, of course, a string. The isNode func. in the updated library reflects this with a .TAG value of 1, for string. 🙌

I'll get a PR up in a bit.