feat: smooth volume transition (#1017)
* g * feat: smooth volume * docs: add smooth volume example * chore: subpath exports
This commit is contained in:
parent
8f5155895a
commit
b0cefa8950
11 changed files with 1139 additions and 1088 deletions
16
README.md
16
README.md
|
@ -166,6 +166,20 @@ These bots are made by the community, they can help you build your own!
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
|
### Smooth Volume
|
||||||
|
|
||||||
|
Discord Player will make volume transition smooth. To enable this, you need to add this line at the top of your main file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// CJS
|
||||||
|
require("discord-player/smoothVolume");
|
||||||
|
|
||||||
|
// ESM
|
||||||
|
import "discord-player/smoothVolume"
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Make sure that line is situated at the **TOP** of your **main** file.
|
||||||
|
|
||||||
### Use cookies
|
### Use cookies
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
@ -215,7 +229,7 @@ const queue = player.createQueue(..., {
|
||||||
// only trap youtube source
|
// only trap youtube source
|
||||||
if (source === "youtube") {
|
if (source === "youtube") {
|
||||||
// track here would be youtube track
|
// track here would be youtube track
|
||||||
return (await playdl.stream(track.url)).stream;
|
return (await playdl.stream(track.url, { discordPlayerCompatibility : true })).stream;
|
||||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const queue = player.createQueue(..., {
|
||||||
// only trap youtube source
|
// only trap youtube source
|
||||||
if (source === "youtube") {
|
if (source === "youtube") {
|
||||||
// track here would be youtube track
|
// track here would be youtube track
|
||||||
return (await playdl.stream(track.url)).stream;
|
return (await playdl.stream(track.url, { discordPlayerCompatibility : true })).stream;
|
||||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
path: __dirname+"/.env"
|
path: __dirname+"/.env"
|
||||||
});
|
});
|
||||||
|
require("discord-player/smoothVolume");
|
||||||
const { Client, GuildMember, Intents } = require("discord.js");
|
const { Client, GuildMember, Intents } = require("discord.js");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { Player, QueryType, QueueRepeatMode } = require("discord-player");
|
const { Player, QueryType, QueueRepeatMode } = require("discord-player");
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.5.3",
|
"@discordjs/opus": "^0.5.3",
|
||||||
"discord-player": "^5.0.2",
|
"discord-player": "^5.2.1",
|
||||||
"discord.js": "^13.0.1",
|
"discord.js": "^13.0.1",
|
||||||
"dotenv": "^10.0.0"
|
"dotenv": "^10.0.0"
|
||||||
}
|
}
|
||||||
|
|
14
package.json
14
package.json
|
@ -9,8 +9,11 @@
|
||||||
],
|
],
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"exports": {
|
"exports": {
|
||||||
"require": "./dist/index.js",
|
".": {
|
||||||
"import": "./dist/index.mjs"
|
"require": "./dist/index.js",
|
||||||
|
"import": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"./smoothVolume": "./dist/smoothVolume.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd example/test && ts-node index.ts",
|
"dev": "cd example/test && ts-node index.ts",
|
||||||
|
@ -60,14 +63,15 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://discord-player.js.org",
|
"homepage": "https://discord-player.js.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.7.5",
|
"@discordjs/voice": "^0.8.0",
|
||||||
"discord-ytdl-core": "^5.0.4",
|
"discord-ytdl-core": "^5.0.4",
|
||||||
"libsodium-wrappers": "^0.7.9",
|
"libsodium-wrappers": "^0.7.9",
|
||||||
|
"prism-media": "https://codeload.github.com/discord-player/prism-media/tar.gz/main",
|
||||||
"soundcloud-scraper": "^5.0.2",
|
"soundcloud-scraper": "^5.0.2",
|
||||||
"spotify-url-info": "^2.2.3",
|
"spotify-url-info": "^2.2.3",
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"youtube-sr": "^4.1.9",
|
"youtube-sr": "^4.1.12",
|
||||||
"ytdl-core": "^4.9.1"
|
"ytdl-core": "^4.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.16.0",
|
"@babel/cli": "^7.16.0",
|
||||||
|
|
|
@ -153,6 +153,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
|
|
||||||
const _meta = queueInitOptions.metadata;
|
const _meta = queueInitOptions.metadata;
|
||||||
delete queueInitOptions["metadata"];
|
delete queueInitOptions["metadata"];
|
||||||
|
queueInitOptions.volumeSmoothness ??= 0.1;
|
||||||
queueInitOptions.ytdlOptions ??= this.options.ytdlOptions;
|
queueInitOptions.ytdlOptions ??= this.options.ytdlOptions;
|
||||||
const queue = new Queue(this, guild, queueInitOptions);
|
const queue = new Queue(this, guild, queueInitOptions);
|
||||||
queue.metadata = _meta;
|
queue.metadata = _meta;
|
||||||
|
|
|
@ -702,6 +702,11 @@ class Queue<T = unknown> {
|
||||||
|
|
||||||
if (options.seek) this._streamTime = options.seek;
|
if (options.seek) this._streamTime = options.seek;
|
||||||
this._filtersUpdate = options.filtersUpdate;
|
this._filtersUpdate = options.filtersUpdate;
|
||||||
|
|
||||||
|
if (resource.volume && typeof this.options.volumeSmoothness === "number") {
|
||||||
|
Reflect.set(resource.volume, "_smoothing", this.options.volumeSmoothness || 0);
|
||||||
|
}
|
||||||
|
|
||||||
this.setVolume(this.options.initialVolume);
|
this.setVolume(this.options.initialVolume);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
116
src/VolumeTransformer.ts
Normal file
116
src/VolumeTransformer.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// prism's volume transformer with smooth volume support
|
||||||
|
|
||||||
|
import { Transform, TransformOptions } from "stream";
|
||||||
|
|
||||||
|
export interface VolumeTransformerOptions extends TransformOptions {
|
||||||
|
type?: "s16le" | "s16be" | "s32le" | "s32be";
|
||||||
|
smoothness?: number;
|
||||||
|
volume?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VolumeTransformer extends Transform {
|
||||||
|
private _bits: number;
|
||||||
|
private _smoothing: number;
|
||||||
|
private _bytes: number;
|
||||||
|
private _extremum: number;
|
||||||
|
private _chunk: Buffer;
|
||||||
|
public volume: number;
|
||||||
|
private _targetVolume: number;
|
||||||
|
constructor(options: VolumeTransformerOptions = {}) {
|
||||||
|
super(options);
|
||||||
|
switch (options.type) {
|
||||||
|
case "s16le":
|
||||||
|
this._readInt = (buffer, index) => buffer.readInt16LE(index);
|
||||||
|
this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index);
|
||||||
|
this._bits = 16;
|
||||||
|
break;
|
||||||
|
case "s16be":
|
||||||
|
this._readInt = (buffer, index) => buffer.readInt16BE(index);
|
||||||
|
this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index);
|
||||||
|
this._bits = 16;
|
||||||
|
break;
|
||||||
|
case "s32le":
|
||||||
|
this._readInt = (buffer, index) => buffer.readInt32LE(index);
|
||||||
|
this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index);
|
||||||
|
this._bits = 32;
|
||||||
|
break;
|
||||||
|
case "s32be":
|
||||||
|
this._readInt = (buffer, index) => buffer.readInt32BE(index);
|
||||||
|
this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index);
|
||||||
|
this._bits = 32;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("VolumeTransformer type should be one of s16le, s16be, s32le, s32be");
|
||||||
|
}
|
||||||
|
this._bytes = this._bits / 8;
|
||||||
|
this._extremum = Math.pow(2, this._bits - 1);
|
||||||
|
this.volume = typeof options.volume === "undefined" ? 1 : options.volume;
|
||||||
|
this._targetVolume = this.volume;
|
||||||
|
this._chunk = Buffer.alloc(0);
|
||||||
|
this._smoothing = options.smoothness || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readInt(buffer: Buffer, index: number) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
_writeInt(buffer: Buffer, int: number, index: number) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(chunk: Buffer, encoding: BufferEncoding, done: () => unknown) {
|
||||||
|
if (this._smoothing > 0 && this.volume !== this._targetVolume) {
|
||||||
|
if (this.volume < this._targetVolume) {
|
||||||
|
this.volume = this.volume + this._smoothing >= this._targetVolume ? this._targetVolume : this.volume + this._smoothing;
|
||||||
|
} else if (this.volume > this._targetVolume) {
|
||||||
|
this.volume = this.volume - this._smoothing <= this._targetVolume ? this._targetVolume : this.volume - this._smoothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.volume === 1) {
|
||||||
|
this.push(chunk);
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _bytes, _extremum } = this;
|
||||||
|
|
||||||
|
chunk = this._chunk = Buffer.concat([this._chunk, chunk]);
|
||||||
|
if (chunk.length < _bytes) return done();
|
||||||
|
|
||||||
|
const complete = Math.floor(chunk.length / _bytes) * _bytes;
|
||||||
|
|
||||||
|
for (let i = 0; i < complete; i += _bytes) {
|
||||||
|
const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i))));
|
||||||
|
this._writeInt(chunk, int, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._chunk = chunk.slice(complete);
|
||||||
|
this.push(chunk.slice(0, complete));
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroy(err: Error, cb: (error: Error) => void) {
|
||||||
|
super._destroy(err, cb);
|
||||||
|
this._chunk = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume: number) {
|
||||||
|
this._targetVolume = volume;
|
||||||
|
if (this._smoothing <= 0) this.volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolumeDecibels(db: number) {
|
||||||
|
this.setVolume(Math.pow(10, db / 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolumeLogarithmic(value: number) {
|
||||||
|
this.setVolume(Math.pow(value, 1.660964));
|
||||||
|
}
|
||||||
|
|
||||||
|
get volumeDecibels() {
|
||||||
|
return Math.log10(this.volume) * 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
get volumeLogarithmic() {
|
||||||
|
return Math.pow(this.volume, 1 / 1.660964);
|
||||||
|
}
|
||||||
|
}
|
4
src/smoothVolume.ts
Normal file
4
src/smoothVolume.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { VolumeTransformer } from "./VolumeTransformer";
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
(require("prism-media") as typeof import("prism-media")).VolumeTransformer = VolumeTransformer;
|
|
@ -135,6 +135,8 @@ export interface PlayerProgressbarOptions {
|
||||||
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
||||||
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
||||||
* @property {boolean} [disableVolume=false] If player should disable inline volume
|
* @property {boolean} [disableVolume=false] If player should disable inline volume
|
||||||
|
* @property {boolean} [volumeSmoothness=0] The volume transition smoothness between volume changes (lower the value to get better result)
|
||||||
|
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
|
||||||
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
||||||
*/
|
*/
|
||||||
export interface PlayerOptions {
|
export interface PlayerOptions {
|
||||||
|
@ -148,6 +150,7 @@ export interface PlayerOptions {
|
||||||
bufferingTimeout?: number;
|
bufferingTimeout?: number;
|
||||||
spotifyBridge?: boolean;
|
spotifyBridge?: boolean;
|
||||||
disableVolume?: boolean;
|
disableVolume?: boolean;
|
||||||
|
volumeSmoothness?: number;
|
||||||
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue