Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nodelink",
"version": "3.8.0-dev.20260422.1",
"version": "3.8.0",
"scripts": {
"build": "tsc --incremental false",
"start": "node --dns-result-order=ipv4first --import tsx src/index.ts",
Expand Down
22 changes: 9 additions & 13 deletions dist/src/playback/processing/AudioMixer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,23 @@ export class AudioMixer extends Readable {
mixBuffers(mainPCM, layersPCM) {
if (layersPCM.size === 0 || !this.enabled)
return mainPCM;
const outputBuffer = Buffer.allocUnsafe(mainPCM.length);
const mainLen = mainPCM.length >> 1;
const mainView = this._asInt16Array(mainPCM);
const outputView = this._asInt16Array(outputBuffer);
const activeLayerViews = [];
const layerVolumes = [];
for (const layer of layersPCM.values()) {
activeLayerViews.push({
view: this._asInt16Array(layer.buffer),
volume: layer.volume
});
activeLayerViews.push(this._asInt16Array(layer.buffer));
layerVolumes.push(layer.volume);
}
const mainLen = mainView.length;
const numLayers = activeLayerViews.length;
const outputBuffer = Buffer.allocUnsafe(mainPCM.length);
const outputView = this._asInt16Array(outputBuffer);
for (let i = 0; i < mainLen; i++) {
let sample = mainView[i] ?? 0;
for (let j = 0; j < numLayers; j++) {
const layer = activeLayerViews[j];
if (!layer)
continue;
if (i < layer.view.length) {
sample += ((layer.view[i] ?? 0) * layer.volume) | 0;
}
const layerView = activeLayerViews[j];
const volume = layerVolumes[j];
sample += ((layerView[i] ?? 0) * volume) | 0;
}
outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample;
}
Expand Down
45 changes: 32 additions & 13 deletions dist/src/playback/processing/FadeTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,41 @@ export class FadeTransformer extends Transform {
else {
_useBuffer = true;
}
const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0;
if (view) {
for (let i = 0; i < view.length; i++) {
const gain = gainStart + step * i;
const sample = view[i] ?? 0;
const value = sample * gain;
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
if (gainStart === gainEnd) {
if (view) {
for (let i = 0; i < view.length; i++) {
const sample = view[i] ?? 0;
const value = sample * gainStart;
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
}
}
else {
for (let i = 0; i < sampleCount; i++) {
const sample = chunk.readInt16LE(i * 2);
const value = sample * gainStart;
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
chunk.writeInt16LE(clamped, i * 2);
}
}
}
else {
for (let i = 0; i < sampleCount; i++) {
const gain = gainStart + step * i;
const sample = chunk.readInt16LE(i * 2);
const value = sample * gain;
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
chunk.writeInt16LE(clamped, i * 2);
const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0;
if (view) {
for (let i = 0; i < view.length; i++) {
const gain = gainStart + step * i;
const sample = view[i] ?? 0;
const value = sample * gain;
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
}
}
else {
for (let i = 0; i < sampleCount; i++) {
const gain = gainStart + step * i;
const sample = chunk.readInt16LE(i * 2);
const value = sample * gain;
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
chunk.writeInt16LE(clamped, i * 2);
}
}
}
return chunk;
Expand Down
11 changes: 7 additions & 4 deletions dist/src/playback/processing/LoudnessNormalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const INT16_MAX = 32767;
const INT16_MIN = -32768;
const MIN_ENERGY = 1e-12;
const fround = Math.fround;
const INV_32768 = 1 / 32768;
/**
* Implements a standard Biquad filter for audio processing.
*/
Expand Down Expand Up @@ -162,18 +163,20 @@ export class LoudnessNormalizer {
const releaseAlpha = this._releaseAlpha;
const energyAlpha = this._energyAlpha;
const target = this.targetLoudness;
const filters = this.filters;
const numChannels = this.channels;
for (let frameIndex = 0, sampleIndex = 0; frameIndex < frameCount; frameIndex++) {
let energySum = 0.0;
for (let ch = 0; ch < this.channels; ch += 1, sampleIndex += 1) {
const sample = fround((inputView[sampleIndex] ?? 0) / 32768);
const filter = this.filters[ch];
for (let ch = 0; ch < numChannels; ch += 1, sampleIndex += 1) {
const sample = fround((inputView[sampleIndex] ?? 0) * INV_32768);
const filter = filters[ch];
if (filter) {
const filtered = filter.process(sample);
channelBuffer[ch] = filtered;
energySum += filtered * filtered;
}
}
energySum = fround(energySum / this.channels);
energySum = fround(energySum / numChannels);
if (energySum > this._gateThresholdEnergy) {
energyState = fround(energyState * energyAlpha + (1 - energyAlpha) * energySum);
}
Expand Down
72 changes: 54 additions & 18 deletions dist/src/playback/processing/VolumeTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class VolumeTransformer extends Transform {
lookaheadBuffer;
lookaheadIndex;
lookaheadFull;
_reusableOutputBuffer = null;
currentVolume;
targetVolume;
startVolume;
Expand Down Expand Up @@ -141,9 +142,18 @@ export class VolumeTransformer extends Transform {
if (abs <= this._thresholdValue || this._limitHeadroom <= 0)
return value;
const normalizedOvershoot = (abs - this._thresholdValue) / this._limitHeadroom;
const softened = 1 - Math.exp(-normalizedOvershoot * this.limiterSoftness);
// Fast approximation of 1 - exp(-x) for small x
// 1 - exp(-x) \approx x - x^2/2 + x^3/6
const x = normalizedOvershoot * this.limiterSoftness;
let softened;
if (x < 0.1) {
softened = x * (1 - x * 0.5);
}
else {
softened = 1 - Math.exp(-x);
}
const limited = this._thresholdValue + this._limitHeadroom * softened;
return Math.sign(value) * Math.min(INT16_MAX, limited);
return (value < 0 ? -1 : 1) * (limited > INT16_MAX ? INT16_MAX : limited);
}
_clampToInt16(value) {
if (value >= INT16_MAX)
Expand Down Expand Up @@ -186,7 +196,10 @@ export class VolumeTransformer extends Transform {
const gainStep = usableSamples > 1 ? (gainEnd - gainStart) / (usableSamples - 1) : 0;
let gain = gainStart;
if (this.lookaheadSamples > 0) {
const outputBuffer = alignedBufferIfRequired(chunk.length);
if (!this._reusableOutputBuffer || this._reusableOutputBuffer.length < chunk.length) {
this._reusableOutputBuffer = alignedBufferIfRequired(chunk.length);
}
const outputBuffer = this._reusableOutputBuffer.subarray(0, chunk.length);
const outputView = new Int16Array(outputBuffer.buffer, outputBuffer.byteOffset, usableSamples);
if (useBufferOps) {
for (let i = 0; i < usableSamples; i++) {
Expand All @@ -202,36 +215,59 @@ export class VolumeTransformer extends Transform {
}
}
else if (view) {
for (let i = 0; i < view.length; i++) {
const len = view.length;
const lookaheadBuffer = this.lookaheadBuffer;
const lookaheadSamples = this.lookaheadSamples;
let lookaheadIndex = this.lookaheadIndex;
for (let i = 0; i < len; i++) {
const rawSample = view[i] ?? 0;
const scaled = rawSample * gain;
const limited = this._applyLimiter(scaled);
const outputSample = this.lookaheadBuffer[this.lookaheadIndex] ?? 0;
this.lookaheadBuffer[this.lookaheadIndex] = limited;
this.lookaheadIndex =
(this.lookaheadIndex + 1) % this.lookaheadSamples;
const outputSample = lookaheadBuffer[lookaheadIndex] ?? 0;
lookaheadBuffer[lookaheadIndex] = limited;
lookaheadIndex = (lookaheadIndex + 1) % lookaheadSamples;
outputView[i] = this._clampToInt16(outputSample);
gain += gainStep;
}
this.lookaheadIndex = lookaheadIndex;
}
if (this.lookaheadIndex === 0)
this.lookaheadFull = true;
return outputBuffer;
}
if (useBufferOps) {
for (let i = 0; i < usableSamples; i++) {
const scaled = chunk.readInt16LE(i * 2) * gain;
const limited = this._applyLimiter(scaled);
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
gain += gainStep;
if (gainStart === gainEnd) {
for (let i = 0; i < usableSamples; i++) {
const scaled = chunk.readInt16LE(i * 2) * gainStart;
const limited = this._applyLimiter(scaled);
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
}
}
else {
for (let i = 0; i < usableSamples; i++) {
const scaled = chunk.readInt16LE(i * 2) * gain;
const limited = this._applyLimiter(scaled);
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
gain += gainStep;
}
}
}
else if (view) {
for (let i = 0; i < view.length; i++) {
const scaled = (view[i] ?? 0) * gain;
const limited = this._applyLimiter(scaled);
view[i] = this._clampToInt16(limited);
gain += gainStep;
const len = view.length;
if (gainStart === gainEnd) {
for (let i = 0; i < len; i++) {
const scaled = (view[i] ?? 0) * gainStart;
const limited = this._applyLimiter(scaled);
view[i] = this._clampToInt16(limited);
}
}
else {
for (let i = 0; i < len; i++) {
const scaled = (view[i] ?? 0) * gain;
const limited = this._applyLimiter(scaled);
view[i] = this._clampToInt16(limited);
gain += gainStep;
}
}
}
return chunk;
Expand Down
5 changes: 2 additions & 3 deletions dist/src/playback/structs/BufferPool.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ const parsePositiveIntEnv = (key, fallback) => {
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
};
const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 20 * 1024 * 1024 // 20 MB - reduced from 50MB
);
const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 4 // reduced from 8
const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 64 * 1024 * 1024 // 64 MB
);
const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 16);
const IDLE_CLEAR_MS = parsePositiveIntEnv('NODELINK_BUFFER_POOL_IDLE_CLEAR_MS', 60000 // 1 min - reduced from 3 min
);
const CLEANUP_INTERVAL = 60000;
Expand Down
23 changes: 19 additions & 4 deletions dist/src/playback/structs/RingBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,32 @@ export class RingBuffer {
if (bytesToRead === 0)
return null;
const out = Buffer.allocUnsafe(bytesToRead);
this.readTo(out);
return out;
}
/**
* Reads bytes from the ring buffer directly into the target buffer.
* @param target - The target buffer to write into.
* @param targetOffset - Offset in the target buffer.
* @returns The number of bytes actually read.
*/
readTo(target, targetOffset = 0) {
if (!this.buffer)
return 0;
const bytesToRead = Math.min(target.length - targetOffset, this._length);
if (bytesToRead <= 0)
return 0;
const availableAtEnd = this.size - this.readOffset;
if (bytesToRead <= availableAtEnd) {
this.buffer.copy(out, 0, this.readOffset, this.readOffset + bytesToRead);
this.buffer.copy(target, targetOffset, this.readOffset, this.readOffset + bytesToRead);
}
else {
this.buffer.copy(out, 0, this.readOffset, this.size);
this.buffer.copy(out, availableAtEnd, 0, bytesToRead - availableAtEnd);
this.buffer.copy(target, targetOffset, this.readOffset, this.size);
this.buffer.copy(target, targetOffset + availableAtEnd, 0, bytesToRead - availableAtEnd);
}
this.readOffset = (this.readOffset + bytesToRead) % this.size;
this._length -= bytesToRead;
return out;
return bytesToRead;
}
/**
* Skips n bytes in the buffer.
Expand Down
8 changes: 4 additions & 4 deletions dist/src/workers/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1263,10 +1263,10 @@ function startTimers(hibernating = false) {
players: localPlayers,
playingPlayers: localPlayingPlayers,
commandQueueLength: Array.from(guildQueues.values()).reduce((acc, curr) => acc + getHeadQueueLength(curr.queue), 0),
cpu: { nodelinkLoad },
eventLoopLag: eluP50,
eventLoopLagP95: eluP95,
eventLoopLagP99: eluP99,
cpu: { nodelinkLoad: Math.round(nodelinkLoad * 100) / 100 },
eventLoopLag: Math.round(eluP50 * 100) / 100,
eventLoopLagP95: Math.round(eluP95 * 100) / 100,
eventLoopLagP99: Math.round(eluP99 * 100) / 100,
memory: {
used: mem.heapUsed,
allocated: mem.heapTotal
Expand Down
26 changes: 13 additions & 13 deletions src/playback/processing/AudioMixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,29 +98,29 @@ export class AudioMixer extends Readable {
): Buffer {
if (layersPCM.size === 0 || !this.enabled) return mainPCM

const outputBuffer = Buffer.allocUnsafe(mainPCM.length)
const mainLen = mainPCM.length >> 1
const mainView = this._asInt16Array(mainPCM)
const outputView = this._asInt16Array(outputBuffer)

const activeLayerViews: Array<{ view: Int16Array; volume: number }> = []
const activeLayerViews: Int16Array[] = []
const layerVolumes: number[] = []

for (const layer of layersPCM.values()) {
activeLayerViews.push({
view: this._asInt16Array(layer.buffer),
volume: layer.volume
})
activeLayerViews.push(this._asInt16Array(layer.buffer))
layerVolumes.push(layer.volume)
}

const mainLen = mainView.length
const numLayers = activeLayerViews.length
const outputBuffer = Buffer.allocUnsafe(mainPCM.length)
const outputView = this._asInt16Array(outputBuffer)

for (let i = 0; i < mainLen; i++) {
let sample = mainView[i] ?? 0

for (let j = 0; j < numLayers; j++) {
const layer = activeLayerViews[j]
if (!layer) continue
if (i < layer.view.length) {
sample += ((layer.view[i] ?? 0) * layer.volume) | 0
}
const layerView = activeLayerViews[j] as Int16Array
const volume = layerVolumes[j] as number

sample += ((layerView[i] ?? 0) * volume) | 0
}

outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample
Expand Down
Loading
Loading