@sv443-network/userutils 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 63af1a7: Change default limiter options to be more balanced
8
+
9
+ ## 2.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - b53a946: Added compression to `amplifyMedia()` to prevent audio clipping and distortion and modified return type accordingly:
14
+ - Renamed: `amplify()` to `setGain()` and `getAmpLevel()` to `getGain()`
15
+ - Added properties: `enable()`, `disable()`, `setLimiterOptions()` and `limiterNode`
16
+ - Other changes: Amplification is no longer enabled automatically, `enable()` must now be called manually after initializing
17
+
3
18
  ## 1.2.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -478,47 +478,98 @@ interceptWindowEvent("beforeunload", (event) => {
478
478
  ### amplifyMedia()
479
479
  Usage:
480
480
  ```ts
481
- amplifyMedia(mediaElement: HTMLMediaElement, multiplier?: number): AmplifyMediaResult
481
+ amplifyMedia(mediaElement: HTMLMediaElement, initialMultiplier?: number): AmplifyMediaResult
482
482
  ```
483
483
 
484
484
  Amplifies the gain of a media element (like `<audio>` or `<video>`) by a given multiplier (defaults to 1.0).
485
485
  This is how you can increase the volume of a media element beyond the default maximum volume of 1.0 or 100%.
486
- Make sure to limit the multiplier to a reasonable value ([clamp()](#clamp) is good for this), as it may cause clipping or bleeding eardrums.
486
+ Make sure to limit the multiplier to a reasonable value ([clamp()](#clamp) is good for this), as it may cause bleeding eardrums.
487
+
488
+ This is the processing workflow applied to the media element:
489
+ `MediaElement (source)` => `DynamicsCompressorNode (limiter)` => `GainNode` => `AudioDestination (output)`
490
+
491
+ A limiter (compression) is applied to the audio to prevent clipping.
492
+ Its properties can be changed by calling the returned function `setLimiterOptions()`
493
+ The limiter options set by default are `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }`
487
494
 
488
495
  ⚠️ This function has to be run in response to a user interaction event, else the browser will reject it because of the strict autoplay policy.
496
+ ⚠️ Make sure to call the returned function `enable()` after calling this function to actually enable the amplification.
489
497
 
490
- The returned AmplifyMediaResult object has the following properties:
498
+ The returned object of the type `AmplifyMediaResult` has the following properties:
491
499
  | Property | Description |
492
500
  | :-- | :-- |
493
- | `mediaElement` | The passed media element |
494
- | `amplify()` | A function to change the amplification level |
495
- | `getAmpLevel()` | A function to return the current amplification level |
501
+ | `setGain()` | Used to change the gain multiplier |
502
+ | `getGain()` | Returns the current gain multiplier |
503
+ | `enable()` | Call to enable the amplification for the first time or if it was disabled before |
504
+ | `disable()` | Call to disable amplification |
505
+ | `setLimiterOptions()` | Used for changing the [options of the DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options) - the default is `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }` |
496
506
  | `context` | The AudioContext instance |
497
507
  | `source` | The MediaElementSourceNode instance |
498
- | `gain` | The GainNode instance |
508
+ | `gainNode` | The GainNode instance used for actually boosting the gain |
509
+ | `limiterNode` | The DynamicsCompressorNode instance used for limiting clipping and distortion |
499
510
 
500
511
  <details><summary><b>Example - click to view</b></summary>
501
512
 
502
513
  ```ts
503
- import { amplifyMedia } from "@sv443-network/userutils";
514
+ import { amplifyMedia, clamp } from "@sv443-network/userutils";
515
+ import type { AmplifyMediaResult } from "@sv443-network/userutils";
516
+
517
+ const audioElement = document.querySelector<HTMLAudioElement>("audio");
518
+
519
+ let ampResult: AmplifyMediaResult | undefined;
520
+
521
+ function setGain(newValue: number) {
522
+ if(!ampResult)
523
+ return;
524
+ // constrain the value to between 0 and 3 for safety
525
+ ampResult.setGain(clamp(newValue, 0, 3));
526
+ console.log("Gain set to", ampResult.getGain());
527
+ }
528
+
529
+
530
+ const amplifyButton = document.querySelector<HTMLButtonElement>("button#amplify");
504
531
 
505
- const audio = document.querySelector<HTMLAudioElement>("audio");
506
- const button = document.querySelector<HTMLButtonElement>("button");
532
+ // amplifyMedia() needs to be called in response to a user interaction event:
533
+ amplifyButton.addEventListener("click", () => {
534
+ // only needs to be initialized once, afterwards the returned object
535
+ // can be used to change settings and enable/disable the amplification
536
+ if(!ampResult) {
537
+ // initialize amplification and set gain to 2x
538
+ ampResult = amplifyMedia(audioElement, 2);
539
+ // enable the amplification
540
+ ampResult.enable();
541
+ }
542
+
543
+ setGain(2.5); // set gain to 2.5x
544
+
545
+ console.log(ampResult.getGain()); // 2.5
546
+ });
507
547
 
508
- // amplifyMedia needs to be called in response to a user interaction event:
509
- button.addEventListener("click", () => {
510
- const { amplify, getAmpLevel } = amplifyMedia(audio);
511
548
 
512
- const setGain = (value: number) => {
513
- // constrain the value to between 0 and 5
514
- amplify(clamp(value, 0, 5));
515
- console.log("Gain set to", getAmpLevel());
549
+ const disableButton = document.querySelector<HTMLButtonElement>("button#disable");
550
+
551
+ disableButton.addEventListener("click", () => {
552
+ if(ampResult) {
553
+ // disable the amplification
554
+ ampResult.disable();
516
555
  }
556
+ });
517
557
 
518
- setGain(2); // set gain to 2x
519
- setGain(3.5); // set gain to 3.5x
520
558
 
521
- console.log(getAmpLevel()); // 3.5
559
+ const limiterButton = document.querySelector<HTMLButtonElement>("button#limiter");
560
+
561
+ limiterButton.addEventListener("click", () => {
562
+ if(ampResult) {
563
+ // change the limiter options to a more aggressive setting
564
+ // see https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options
565
+ ampResult.setLimiterOptions({
566
+ threshold: -10,
567
+ knee: 20,
568
+ ratio: 20,
569
+ attack: 0.001,
570
+ release: 0.1,
571
+ });
572
+ }
522
573
  });
523
574
  ```
524
575
 
@@ -1104,7 +1155,7 @@ tr.addLanguage("en", {
1104
1155
 
1105
1156
  /** Returns the custom pluralization identifier for the given number of items (or size of Array/NodeList) */
1106
1157
  function pl(num: number | unknown[] | NodeList) {
1107
- if(Array.isArray(num))
1158
+ if(typeof num !== "number")
1108
1159
  num = num.length;
1109
1160
 
1110
1161
  if(num === 0)
@@ -9,7 +9,7 @@
9
9
  // ==UserLibrary==
10
10
  // @name UserUtils
11
11
  // @description Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more
12
- // @version 1.2.0
12
+ // @version 2.0.1
13
13
  // @license MIT
14
14
  // @copyright Sv443 (https://github.com/Sv443)
15
15
 
@@ -315,22 +315,52 @@ var UserUtils = (function (exports) {
315
315
  function interceptWindowEvent(eventName, predicate) {
316
316
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
317
317
  }
318
- function amplifyMedia(mediaElement, multiplier = 1) {
318
+ function amplifyMedia(mediaElement, initialMultiplier = 1) {
319
319
  const context = new (window.AudioContext || window.webkitAudioContext)();
320
- const result = {
321
- mediaElement,
322
- amplify: (multiplier2) => {
323
- result.gain.gain.value = multiplier2;
320
+ const props = {
321
+ /** Sets the gain multiplier */
322
+ setGain(multiplier) {
323
+ props.gainNode.gain.setValueAtTime(multiplier, props.context.currentTime);
324
+ },
325
+ /** Returns the current gain multiplier */
326
+ getGain() {
327
+ return props.gainNode.gain.value;
328
+ },
329
+ /** Enable the amplification for the first time or if it was disabled before */
330
+ enable() {
331
+ props.source.connect(props.limiterNode);
332
+ props.limiterNode.connect(props.gainNode);
333
+ props.gainNode.connect(props.context.destination);
334
+ },
335
+ /** Disable the amplification */
336
+ disable() {
337
+ props.source.disconnect(props.limiterNode);
338
+ props.limiterNode.disconnect(props.gainNode);
339
+ props.gainNode.disconnect(props.context.destination);
340
+ props.source.connect(props.context.destination);
341
+ },
342
+ /**
343
+ * Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
344
+ * The default is `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }`
345
+ */
346
+ setLimiterOptions(options) {
347
+ for (const [key, val] of Object.entries(options))
348
+ props.limiterNode[key].setValueAtTime(val, props.context.currentTime);
324
349
  },
325
- getAmpLevel: () => result.gain.gain.value,
326
350
  context,
327
351
  source: context.createMediaElementSource(mediaElement),
328
- gain: context.createGain()
352
+ gainNode: context.createGain(),
353
+ limiterNode: context.createDynamicsCompressor()
329
354
  };
330
- result.source.connect(result.gain);
331
- result.gain.connect(context.destination);
332
- result.amplify(multiplier);
333
- return result;
355
+ props.setLimiterOptions({
356
+ threshold: -12,
357
+ knee: 30,
358
+ ratio: 12,
359
+ attack: 3e-3,
360
+ release: 0.25
361
+ });
362
+ props.setGain(initialMultiplier);
363
+ return props;
334
364
  }
335
365
  function isScrollable(element) {
336
366
  const { overflowX, overflowY } = getComputedStyle(element);
package/dist/index.js CHANGED
@@ -294,22 +294,52 @@ function interceptEvent(eventObject, eventName, predicate) {
294
294
  function interceptWindowEvent(eventName, predicate) {
295
295
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
296
296
  }
297
- function amplifyMedia(mediaElement, multiplier = 1) {
297
+ function amplifyMedia(mediaElement, initialMultiplier = 1) {
298
298
  const context = new (window.AudioContext || window.webkitAudioContext)();
299
- const result = {
300
- mediaElement,
301
- amplify: (multiplier2) => {
302
- result.gain.gain.value = multiplier2;
299
+ const props = {
300
+ /** Sets the gain multiplier */
301
+ setGain(multiplier) {
302
+ props.gainNode.gain.setValueAtTime(multiplier, props.context.currentTime);
303
+ },
304
+ /** Returns the current gain multiplier */
305
+ getGain() {
306
+ return props.gainNode.gain.value;
307
+ },
308
+ /** Enable the amplification for the first time or if it was disabled before */
309
+ enable() {
310
+ props.source.connect(props.limiterNode);
311
+ props.limiterNode.connect(props.gainNode);
312
+ props.gainNode.connect(props.context.destination);
313
+ },
314
+ /** Disable the amplification */
315
+ disable() {
316
+ props.source.disconnect(props.limiterNode);
317
+ props.limiterNode.disconnect(props.gainNode);
318
+ props.gainNode.disconnect(props.context.destination);
319
+ props.source.connect(props.context.destination);
320
+ },
321
+ /**
322
+ * Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
323
+ * The default is `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }`
324
+ */
325
+ setLimiterOptions(options) {
326
+ for (const [key, val] of Object.entries(options))
327
+ props.limiterNode[key].setValueAtTime(val, props.context.currentTime);
303
328
  },
304
- getAmpLevel: () => result.gain.gain.value,
305
329
  context,
306
330
  source: context.createMediaElementSource(mediaElement),
307
- gain: context.createGain()
331
+ gainNode: context.createGain(),
332
+ limiterNode: context.createDynamicsCompressor()
308
333
  };
309
- result.source.connect(result.gain);
310
- result.gain.connect(context.destination);
311
- result.amplify(multiplier);
312
- return result;
334
+ props.setLimiterOptions({
335
+ threshold: -12,
336
+ knee: 30,
337
+ ratio: 12,
338
+ attack: 3e-3,
339
+ release: 0.25
340
+ });
341
+ props.setGain(initialMultiplier);
342
+ return props;
313
343
  }
314
344
  function isScrollable(element) {
315
345
  const { overflowX, overflowY } = getComputedStyle(element);
package/dist/index.mjs CHANGED
@@ -292,22 +292,52 @@ function interceptEvent(eventObject, eventName, predicate) {
292
292
  function interceptWindowEvent(eventName, predicate) {
293
293
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
294
294
  }
295
- function amplifyMedia(mediaElement, multiplier = 1) {
295
+ function amplifyMedia(mediaElement, initialMultiplier = 1) {
296
296
  const context = new (window.AudioContext || window.webkitAudioContext)();
297
- const result = {
298
- mediaElement,
299
- amplify: (multiplier2) => {
300
- result.gain.gain.value = multiplier2;
297
+ const props = {
298
+ /** Sets the gain multiplier */
299
+ setGain(multiplier) {
300
+ props.gainNode.gain.setValueAtTime(multiplier, props.context.currentTime);
301
+ },
302
+ /** Returns the current gain multiplier */
303
+ getGain() {
304
+ return props.gainNode.gain.value;
305
+ },
306
+ /** Enable the amplification for the first time or if it was disabled before */
307
+ enable() {
308
+ props.source.connect(props.limiterNode);
309
+ props.limiterNode.connect(props.gainNode);
310
+ props.gainNode.connect(props.context.destination);
311
+ },
312
+ /** Disable the amplification */
313
+ disable() {
314
+ props.source.disconnect(props.limiterNode);
315
+ props.limiterNode.disconnect(props.gainNode);
316
+ props.gainNode.disconnect(props.context.destination);
317
+ props.source.connect(props.context.destination);
318
+ },
319
+ /**
320
+ * Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
321
+ * The default is `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }`
322
+ */
323
+ setLimiterOptions(options) {
324
+ for (const [key, val] of Object.entries(options))
325
+ props.limiterNode[key].setValueAtTime(val, props.context.currentTime);
301
326
  },
302
- getAmpLevel: () => result.gain.gain.value,
303
327
  context,
304
328
  source: context.createMediaElementSource(mediaElement),
305
- gain: context.createGain()
329
+ gainNode: context.createGain(),
330
+ limiterNode: context.createDynamicsCompressor()
306
331
  };
307
- result.source.connect(result.gain);
308
- result.gain.connect(context.destination);
309
- result.amplify(multiplier);
310
- return result;
332
+ props.setLimiterOptions({
333
+ threshold: -12,
334
+ knee: 30,
335
+ ratio: 12,
336
+ attack: 3e-3,
337
+ release: 0.25
338
+ });
339
+ props.setGain(initialMultiplier);
340
+ return props;
311
341
  }
312
342
  function isScrollable(element) {
313
343
  const { overflowX, overflowY } = getComputedStyle(element);
package/dist/lib/dom.d.ts CHANGED
@@ -45,28 +45,51 @@ export declare function interceptEvent<TEvtObj extends EventTarget, TPredicateEv
45
45
  export declare function interceptWindowEvent<TEvtKey extends keyof WindowEventMap>(eventName: TEvtKey, predicate: (event: WindowEventMap[TEvtKey]) => boolean): void;
46
46
  /**
47
47
  * Amplifies the gain of the passed media element's audio by the specified multiplier.
48
- * This function supports any media element like `<audio>` or `<video>`
48
+ * Also applies a limiter to prevent clipping and distortion.
49
+ * This function supports any MediaElement instance like `<audio>` or `<video>`
49
50
  *
50
- * This function has to be run in response to a user interaction event, else the browser will reject it because of the strict autoplay policy.
51
+ * This is the audio processing workflow:
52
+ * `MediaElement (source)` => `DynamicsCompressorNode (limiter)` => `GainNode` => `AudioDestinationNode (output)`
51
53
  *
54
+ * ⚠️ This function has to be run in response to a user interaction event, else the browser will reject it because of the strict autoplay policy.
55
+ * ⚠️ Make sure to call the returned function `enable()` after calling this function to actually enable the amplification.
56
+ *
57
+ * @param mediaElement The media element to amplify (e.g. `<audio>` or `<video>`)
58
+ * @param initialMultiplier The initial gain multiplier to apply (floating point number, default is `1.0`)
52
59
  * @returns Returns an object with the following properties:
53
60
  * | Property | Description |
54
61
  * | :-- | :-- |
55
- * | `mediaElement` | The passed media element |
56
- * | `amplify()` | A function to change the amplification level |
57
- * | `getAmpLevel()` | A function to return the current amplification level |
62
+ * | `setGain()` | Used to change the gain multiplier |
63
+ * | `getGain()` | Returns the current gain multiplier |
64
+ * | `enable()` | Call to enable the amplification for the first time or if it was disabled before |
65
+ * | `disable()` | Call to disable amplification |
66
+ * | `setLimiterOptions()` | Used for changing the [options of the DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options) - the default is `{ threshold: -2, knee: 40, ratio: 12, attack: 0.003, release: 0.25 }` |
58
67
  * | `context` | The AudioContext instance |
59
68
  * | `source` | The MediaElementSourceNode instance |
60
- * | `gain` | The GainNode instance |
69
+ * | `gainNode` | The GainNode instance |
70
+ * | `limiterNode` | The DynamicsCompressorNode instance used for limiting clipping and distortion |
61
71
  */
62
- export declare function amplifyMedia<TElem extends HTMLMediaElement>(mediaElement: TElem, multiplier?: number): {
63
- mediaElement: TElem;
64
- amplify: (multiplier: number) => void;
65
- getAmpLevel: () => number;
72
+ export declare function amplifyMedia<TElem extends HTMLMediaElement>(mediaElement: TElem, initialMultiplier?: number): {
73
+ /** Sets the gain multiplier */
74
+ setGain(multiplier: number): void;
75
+ /** Returns the current gain multiplier */
76
+ getGain(): number;
77
+ /** Enable the amplification for the first time or if it was disabled before */
78
+ enable(): void;
79
+ /** Disable the amplification */
80
+ disable(): void;
81
+ /**
82
+ * Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
83
+ * The default is `{ threshold: -12, knee: 30, ratio: 12, attack: 0.003, release: 0.25 }`
84
+ */
85
+ setLimiterOptions(options: Partial<Record<"threshold" | "knee" | "ratio" | "attack" | "release", number>>): void;
66
86
  context: AudioContext;
67
87
  source: MediaElementAudioSourceNode;
68
- gain: GainNode;
88
+ gainNode: GainNode;
89
+ limiterNode: DynamicsCompressorNode;
69
90
  };
91
+ /** An object which contains the results of `amplifyMedia()` */
92
+ export type AmplifyMediaResult = ReturnType<typeof amplifyMedia>;
70
93
  /** Checks if an element is scrollable in the horizontal and vertical directions */
71
94
  export declare function isScrollable(element: Element): {
72
95
  vertical: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sv443-network/userutils",
3
- "version": "1.2.0",
3
+ "version": "2.0.1",
4
4
  "description": "Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -9,10 +9,10 @@
9
9
  "lint": "tsc --noEmit && eslint .",
10
10
  "build-types": "tsc --emitDeclarationOnly --declaration --outDir dist",
11
11
  "build-common": "tsup lib/index.ts --format cjs,esm --clean --treeshake",
12
- "build-global": "tsup lib/index.ts --format cjs,esm,iife --treeshake --onSuccess \"npm run post-build-global\"",
12
+ "build-global": "tsup lib/index.ts --format cjs,esm,iife --treeshake --onSuccess \"npm run post-build-global && echo Finished building.\"",
13
13
  "build": "npm run build-common -- && npm run build-types",
14
14
  "post-build-global": "npm run node-ts -- ./tools/post-build-global.mts",
15
- "dev": "npm run build-common -- --sourcemap --watch --onSuccess \"npm run build-types\"",
15
+ "dev": "npm run build-common -- --sourcemap --watch --onSuccess \"npm run build-types && echo Finished building.\"",
16
16
  "publish-package": "changeset publish",
17
17
  "node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm"
18
18
  },