Skip to content

Commit 0e131ec

Browse files
committed
add: fading configuration and processing for audio playback
1 parent 8911c99 commit 0e131ec

6 files changed

Lines changed: 419 additions & 7 deletions

File tree

config.default.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,35 @@ export default {
374374
audio: {
375375
quality: 'high', // high, medium, low, lowest
376376
encryption: 'aead_aes256_gcm_rtpsize',
377-
resamplingQuality: 'best' // best, medium, fastest, zero order holder, linear
377+
resamplingQuality: 'best', // best, medium, fastest, zero order holder, linear
378+
fading: {
379+
enabled: false,
380+
// curve meanings:
381+
// linear = constant rate, exponential = slow start then faster,
382+
// logarithmic = fast start then slower, s-curve = smooth start/end
383+
trackStart: {
384+
duration: 0,
385+
curve: 'linear'
386+
},
387+
trackEnd: {
388+
duration: 0,
389+
curve: 'linear'
390+
},
391+
trackStop: {
392+
duration: 0,
393+
curve: 'linear'
394+
},
395+
seek: {
396+
duration: 0,
397+
curve: 'linear'
398+
},
399+
ducking: {
400+
enabled: false,
401+
duration: 0,
402+
targetVolume: 0.3,
403+
curve: 'linear'
404+
}
405+
}
378406
},
379407
voiceReceive: {
380408
enabled: false,

src/api/sessions.id.players.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const updatePlayerSchema = myzod
3030
volume: myzod.number().min(0).max(1000).optional(),
3131
paused: myzod.boolean().optional(),
3232
filters: filtersSchema.optional(),
33+
fading: myzod.unknown().optional(),
3334
voice: voiceStateSchema.optional(),
3435
guildId: myzod.string().optional()
3536
})
@@ -52,6 +53,59 @@ const pathSchema = myzod.object({
5253
.optional()
5354
})
5455

56+
const sanitizeFadingConfig = (raw) => {
57+
const safe = {
58+
enabled: false,
59+
trackStart: { duration: 0, curve: 'linear' },
60+
trackEnd: { duration: 0, curve: 'linear' },
61+
trackStop: { duration: 0, curve: 'linear' },
62+
seek: { duration: 0, curve: 'linear' },
63+
ducking: {
64+
enabled: false,
65+
duration: 0,
66+
targetVolume: 0.3,
67+
curve: 'linear'
68+
}
69+
}
70+
71+
if (!raw || typeof raw !== 'object') return safe
72+
safe.enabled = raw.enabled === true
73+
74+
const updateSection = (key) => {
75+
const section = raw[key]
76+
if (!section || typeof section !== 'object') return
77+
if (Number.isFinite(section.duration)) {
78+
safe[key].duration = Math.max(0, section.duration)
79+
}
80+
if (typeof section.curve === 'string') {
81+
safe[key].curve = section.curve
82+
}
83+
}
84+
85+
updateSection('trackStart')
86+
updateSection('trackEnd')
87+
updateSection('trackStop')
88+
updateSection('seek')
89+
90+
if (raw.ducking && typeof raw.ducking === 'object') {
91+
safe.ducking.enabled = raw.ducking.enabled === true
92+
if (Number.isFinite(raw.ducking.duration)) {
93+
safe.ducking.duration = Math.max(0, raw.ducking.duration)
94+
}
95+
if (Number.isFinite(raw.ducking.targetVolume)) {
96+
safe.ducking.targetVolume = Math.max(
97+
0,
98+
Math.min(1, raw.ducking.targetVolume)
99+
)
100+
}
101+
if (typeof raw.ducking.curve === 'string') {
102+
safe.ducking.curve = raw.ducking.curve
103+
}
104+
}
105+
106+
return safe
107+
}
108+
55109
async function handler(nodelink, req, res, sendResponse, parsedUrl) {
56110
const parts = parsedUrl.pathname.split('/')
57111
const pathParams = {
@@ -426,6 +480,16 @@ async function handler(nodelink, req, res, sendResponse, parsedUrl) {
426480
await session.players.setFilters(guildId, payload)
427481
}
428482

483+
if (payload.fading !== undefined) {
484+
logger(
485+
'debug',
486+
'PlayerUpdate',
487+
`Setting fading for guild ${guildId}`
488+
)
489+
const sanitizedFading = sanitizeFadingConfig(payload.fading)
490+
await session.players.setFading(guildId, sanitizedFading)
491+
}
492+
429493
const playerJson = await session.players.toJSON(guildId)
430494
return sendResponse(req, res, playerJson, 200)
431495
}

src/managers/playerManager.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,41 @@ export default class PlayerManager {
341341
return player.setFilters(filtersPayload)
342342
}
343343

344+
async setFading(guildId, fadingConfig) {
345+
const interception = await this._runInterceptors(
346+
'setFading',
347+
guildId,
348+
fadingConfig
349+
)
350+
if (interception?.handled) return interception.result
351+
352+
const session = this.nodelink.sessions.get(this.sessionId)
353+
const playerKey = `${this.sessionId}:${guildId}`
354+
355+
if (this.isCluster) {
356+
const worker = this.nodelink.workerManager.getWorkerForGuild(playerKey)
357+
if (!worker) throw new Error('Player not assigned to a worker.')
358+
const result = await this.nodelink.workerManager.execute(
359+
worker,
360+
'playerCommand',
361+
{
362+
sessionId: this.sessionId,
363+
guildId,
364+
userId: session.userId,
365+
command: 'setFading',
366+
args: [fadingConfig]
367+
}
368+
)
369+
if (result?.playerNotFound) {
370+
throw new Error('Player not found.')
371+
}
372+
return result
373+
}
374+
const player = this.players.get(playerKey)
375+
if (!player) throw new Error('Player not found locally.')
376+
return player.setFading(fadingConfig)
377+
}
378+
344379
async updateVoice(guildId, voicePayload) {
345380
const interception = await this._runInterceptors(
346381
'updateVoice',

src/playback/player.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export class Player {
4747
this.lastManualReconnect = 0
4848
this.audioMixer = null
4949
this._initAudioMixer()
50+
this.fading = this.nodelink.options?.audio?.fading
51+
this._fadeTimers = { trackEnd: null, pause: null, stop: null }
52+
this._isResuming = false
53+
this._pendingTrackStartFade = false
5054

5155
this.isLyricsSubscribed = false
5256
this.currentLyrics = null
@@ -292,6 +296,7 @@ export class Player {
292296
this.isPaused = false
293297

294298
if (!wasResuming && !this._isRestoring) {
299+
this._fading('trackStart')
295300
this._emitTrackStart()
296301
}
297302
} else if (state.status === 'paused') {
@@ -391,6 +396,7 @@ export class Player {
391396
this.position = 0
392397
this.currentLyrics = null
393398
this.lyricsLineIndex = -1
399+
this._fading('reset')
394400
}
395401

396402
async _emitTrackStart() {
@@ -720,6 +726,8 @@ export class Player {
720726
if (this.volumePercent !== 100) {
721727
resource.setVolume(this.volumePercent / 100)
722728
}
729+
this._fading('trackStartArm', { resource })
730+
this._fading('trackEndSchedule', { startPosition: startTime || 0 })
723731

724732
this.setFilters(this.filters)
725733

@@ -775,6 +783,7 @@ export class Player {
775783
}
776784

777785
this.track = { encoded, info, endTime, userData, audioTrackId }
786+
this._fading('reset')
778787

779788
if (!this.voice.endpoint || !this.voice.token) {
780789
logger(
@@ -897,6 +906,8 @@ export class Player {
897906
if (result) {
898907
this.emitEvent(GatewayEvents.SEEK, { position: this.position })
899908
if (this.isLyricsSubscribed) this._recalculateLyricsIndex()
909+
this._fading('seek')
910+
this._fading('trackEndSchedule', { startPosition: this.position })
900911
}
901912
return result
902913
} finally {
@@ -975,6 +986,7 @@ export class Player {
975986
if (this.volumePercent !== 100) {
976987
resource.setVolume(this.volumePercent / 100)
977988
}
989+
this._fading('seekPrepare', { resource })
978990

979991
this.setFilters(this.filters)
980992

@@ -1032,6 +1044,7 @@ export class Player {
10321044
if (this.volumePercent !== 100) {
10331045
resource.setVolume(this.volumePercent / 100)
10341046
}
1047+
this._fading('seekPrepare', { resource })
10351048
resource.setFilters(this.filters)
10361049

10371050
const oldStream = this.connection.play(resource)
@@ -1140,6 +1153,7 @@ export class Player {
11401153
if (this.volumePercent !== 100) {
11411154
resource.setVolume(this.volumePercent / 100)
11421155
}
1156+
this._fading('seekPrepare', { resource })
11431157

11441158
this.setFilters(this.filters)
11451159

@@ -1160,6 +1174,7 @@ export class Player {
11601174
if (this.destroying || !this.track) return false
11611175
if (this.connection && this.connStatus !== 'destroyed') {
11621176
if (this.connection.audioStream) {
1177+
if (this._fading('trackStop')) return true
11631178
this.connection.stop(EndReasons.STOPPED)
11641179
} else {
11651180
this._emitTrackEnd(EndReasons.STOPPED)
@@ -1211,6 +1226,11 @@ export class Player {
12111226
return true
12121227
}
12131228

1229+
setFading(config) {
1230+
this.fading = config
1231+
return true
1232+
}
1233+
12141234
setFilters(filters) {
12151235
if (this.destroying || !this.track) return false
12161236
logger(
@@ -1553,6 +1573,7 @@ export class Player {
15531573
guildId: this.guildId,
15541574
track: this.track,
15551575
volume: this.volumePercent,
1576+
fading: this.fading,
15561577
paused: this.isPaused,
15571578
filters: this.filters,
15581579
state: {
@@ -1564,4 +1585,118 @@ export class Player {
15641585
voice: { ...this.voice }
15651586
}
15661587
}
1588+
1589+
_fading(action, payload = {}) {
1590+
const timers = this._fadeTimers
1591+
if (!timers) return false
1592+
1593+
if (action === 'reset') {
1594+
if (timers.trackEnd) clearTimeout(timers.trackEnd)
1595+
if (timers.pause) clearTimeout(timers.pause)
1596+
if (timers.stop) clearTimeout(timers.stop)
1597+
timers.trackEnd = null
1598+
timers.pause = null
1599+
timers.stop = null
1600+
this._pendingTrackStartFade = false
1601+
return false
1602+
}
1603+
1604+
if (action === 'trackEndSchedule' && timers.trackEnd) {
1605+
clearTimeout(timers.trackEnd)
1606+
timers.trackEnd = null
1607+
}
1608+
1609+
if (!this.fading || this.fading.enabled !== true) return false
1610+
1611+
let section = null
1612+
if (action === 'trackStart' || action === 'trackStartArm')
1613+
section = this.fading.trackStart
1614+
else if (action === 'trackEndSchedule') section = this.fading.trackEnd
1615+
else if (action === 'trackStop') section = this.fading.trackStop
1616+
else if (action === 'seek') section = this.fading.seek
1617+
else if (action === 'seekPrepare') section = this.fading.seek
1618+
else return false
1619+
1620+
if (
1621+
!section ||
1622+
!Number.isFinite(section.duration) ||
1623+
section.duration <= 0
1624+
)
1625+
return false
1626+
1627+
if (action === 'trackStartArm') {
1628+
const resource = payload.resource
1629+
if (!resource?.setFadeVolume) return false
1630+
resource.setFadeVolume(0)
1631+
this._pendingTrackStartFade = true
1632+
return true
1633+
}
1634+
1635+
if (action === 'trackStart') {
1636+
if (!this._pendingTrackStartFade) return false
1637+
const stream = payload.resource || this.connection?.audioStream
1638+
if (!stream?.fadeTo) return false
1639+
this._pendingTrackStartFade = false
1640+
stream.fadeTo(1, section.duration, section.curve)
1641+
return true
1642+
}
1643+
1644+
if (action === 'seekPrepare') {
1645+
const resource = payload.resource
1646+
if (!resource?.setFadeVolume) return false
1647+
resource.setFadeVolume(0)
1648+
return true
1649+
}
1650+
1651+
if (action === 'seek') {
1652+
const stream = this.connection?.audioStream
1653+
if (!stream?.setFadeVolume) return false
1654+
stream.setFadeVolume(0)
1655+
stream.fadeTo(1, section.duration, section.curve)
1656+
return true
1657+
}
1658+
1659+
if (action === 'trackStop') {
1660+
const stream = this.connection?.audioStream
1661+
if (!stream?.fadeTo) return false
1662+
if (timers.stop) clearTimeout(timers.stop)
1663+
stream.fadeTo(0, section.duration, section.curve)
1664+
timers.stop = setTimeout(() => {
1665+
this.connection?.stop(EndReasons.STOPPED)
1666+
if (timers.stop) {
1667+
clearTimeout(timers.stop)
1668+
timers.stop = null
1669+
}
1670+
}, section.duration)
1671+
return true
1672+
}
1673+
1674+
if (action === 'trackEndSchedule') {
1675+
if (!this.track?.info) return false
1676+
const total =
1677+
this.track.endTime && this.track.endTime > 0
1678+
? this.track.endTime
1679+
: this.track.info.length || 0
1680+
if (!Number.isFinite(total) || total <= 0) return false
1681+
1682+
const startPosition = payload.startPosition || 0
1683+
const remaining = Math.max(0, total - startPosition)
1684+
const fadeDuration = Math.min(section.duration, remaining)
1685+
const delay = Math.max(0, remaining - fadeDuration)
1686+
1687+
timers.trackEnd = setTimeout(() => {
1688+
const stream = this.connection?.audioStream
1689+
if (stream?.fadeTo) {
1690+
stream.fadeTo(0, fadeDuration, section.curve)
1691+
}
1692+
if (timers.trackEnd) {
1693+
clearTimeout(timers.trackEnd)
1694+
timers.trackEnd = null
1695+
}
1696+
}, delay)
1697+
return true
1698+
}
1699+
1700+
return false
1701+
}
15671702
}

0 commit comments

Comments
 (0)