Skip to content

Commit

Permalink
add: native Deezer and BandCamp playback
Browse files Browse the repository at this point in the history
This commit adds native playback for Deezer and BandCamp, removing the need for FFmpeg.
  • Loading branch information
ThePedroo committed Apr 28, 2024
1 parent 1f3e734 commit 3b2cb5d
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 17 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 13 additions & 13 deletions src/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/sources/bandcamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ async function retrieveStream(uri, title) {
return {
url: streamURL[0],
protocol: 'https',
format: 'arbitrary'
format: 'mp3'
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/sources/deezer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
34 changes: 33 additions & 1 deletion src/voice/utils.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 3b2cb5d

Please sign in to comment.