diff --git a/README.md b/README.md index df6668e..f048088 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Performant LavaLink replacement written in Node.js. - [`prism-media`](https://npmjs.com/package/prism-media) - [`opusscript`](https://npmjs.com/package/opusscript) or [`@discordjs/opus`](https://npmjs.com/package/@discordjs/opus) - [`libsodium-wrappers`](https://npmjs.com/package/libsodium-wrappers) or [`sodium-native`](https://npmjs.com/package/sodium-native) or [`tweetnacl`](https://npmjs.com/package/tweetnacl) +- [`ffmpeg`](https://ffmpeg.org/) or [`avconv`](https://libav.org/) or [`ffmpeg-static`](https://npmjs.com/package/ffmpeg-static) + +> [!NOTE] +> For most sources FFmpeg isn't required. It is current required for timescale, seek and endTime filter. Required for `local` and `http` sources. ## Installation diff --git a/config.js b/config.js index 1f4bc45..491e41b 100644 --- a/config.js +++ b/config.js @@ -162,7 +162,8 @@ export default { }, audio: { quality: 'high', - encryption: 'xsalsa20_poly1305_lite' + encryption: 'xsalsa20_poly1305_lite', + resamplingQuality: 'best' // best, medium, fastest, zero order holder, linear }, voiceReceive: { type: 'pcm', // pcm, opus diff --git a/src/filters.js b/src/filters.js index 495bd30..aa57402 100644 --- a/src/filters.js +++ b/src/filters.js @@ -177,19 +177,6 @@ class Filters { getResource(decodedTrack, streamInfo, realTime, currentStream) { return new Promise(async (resolve) => { try { - if (decodedTrack.sourceName === 'deezer') { - debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: 'Filtering does not support Deezer platform.' }) - - return resolve({ - status: 1, - exception: { - message: 'Filtering does not support Deezer platform', - severity: 'fault', - cause: 'Unimplemented feature.' - } - }) - } - const startTime = this.filters.find((filter) => filter.name === 'seek')?.data const endTime = this.filters.find((filter) => filter.name === 'endTime')?.data @@ -234,6 +221,19 @@ class Filters { resolve({ stream }) } } else { + if (decodedTrack.sourceName === 'deezer') { + debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: 'Filtering does not support Deezer platform.' }) + + return resolve({ + status: 1, + exception: { + message: 'Non-native filtering does not support Deezer platform', + severity: 'fault', + cause: 'Unimplemented feature.' + } + }) + } + const ffmpeg = new prism.FFmpeg({ args: [ '-loglevel', '0', diff --git a/src/sources/bandcamp.js b/src/sources/bandcamp.js index 3158001..5d7551d 100644 --- a/src/sources/bandcamp.js +++ b/src/sources/bandcamp.js @@ -183,7 +183,7 @@ async function retrieveStream(uri, title) { return { url: streamURL[0], protocol: 'https', - format: 'arbitrary' + format: 'mp3' } } diff --git a/src/sources/deezer.js b/src/sources/deezer.js index 96254a7..7d5aa78 100644 --- a/src/sources/deezer.js +++ b/src/sources/deezer.js @@ -286,7 +286,7 @@ async function retrieveStream(identifier, title) { return { url: streamData.data[0].media[0].sources[0].url, protocol: 'https', - format: 'arbitrary', + format: streamData.data[0].media[0].format.startsWith('MP3') ? 'mp3' : 'flac', additionalData: trackInfo } } diff --git a/src/voice/utils.js b/src/voice/utils.js index da6994a..232bb95 100644 --- a/src/voice/utils.js +++ b/src/voice/utils.js @@ -1,7 +1,18 @@ +import prism from 'prism-media' +import lame from '@flat/lame' /* libmp3lame bindings */ +import SampleRate from 'node-libsamplerate' /* libsamplerate bindings */ + import config from '../../config.js' import constants from '../../constants.js' -import prism from 'prism-media' +let resamplingQuality = null +switch (config.audio.resamplingQuality) { + case 'best': resamplingQuality = SampleRate.SRC_SINC_BEST_QUALITY; break + case 'medium': resamplingQuality = SampleRate.SRC_SINC_MEDIUM_QUALITY; break + case 'fastest': resamplingQuality = SampleRate.SRC_ZERO_ORDER_HOLD; break + case 'zero order holder': resamplingQuality = SampleRate.SRC_ZERO_ORDER_HOLD; break + case 'linear': resamplingQuality = SampleRate.SRC_LINEAR; break +} class NodeLinkStream { constructor(stream, pipes, ffmpegState) { @@ -95,6 +106,7 @@ function isDecodedInternally(stream, type) { case 'webm/opus': case 'ogg/opus': return 3 + 1 + (stream.ffmpegState === 2 ? -2 : 0) case 'wav': return 2 + 1 + (stream.ffmpegState === 2 ? -2 : 0) + case 'mp3': return 3 + 1 + (stream.ffmpegState === 2 ? -2 : 0) default: return false } } @@ -126,6 +138,26 @@ function createAudioResource(stream, type, additionalPipes = [], ffmpegState = f ], ffmpegState) } + if (type === 'mp3') { + return new NodeLinkStream(stream, [ + new lame.Decoder(), + new SampleRate.SampleRate({ + type: resamplingQuality, + channels: 2, + fromRate: 44100, + fromDepth: 16, + toRate: constants.opus.samplingRate, + toDepth: 16 + }), + ...additionalPipes, + new prism.opus.Encoder({ + rate: constants.opus.samplingRate, + channels: constants.opus.channels, + frameSize: constants.opus.frameSize + }) + ], ffmpegState) + } + const ffmpeg = new prism.FFmpeg({ args: [ '-loglevel', '0',