mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-23 01:05:30 +01:00
Normalize playback volume across tracks (#939)
This commit is contained in:
parent
b40d072488
commit
f54d7caa72
|
@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Muse now normalizes playback volume across tracks. Thanks to @UniversalSuperBox for sponsoring this feature!
|
||||||
|
|
||||||
## [2.2.4] - 2023-04-17
|
## [2.2.4] - 2023-04-17
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -19,6 +19,7 @@ Muse is a **highly-opinionated midwestern self-hosted** Discord music bot **that
|
||||||
- ↔️ Autoconverts playlists / artists / albums / songs from Spotify
|
- ↔️ Autoconverts playlists / artists / albums / songs from Spotify
|
||||||
- ↗️ Users can add custom shortcuts (aliases)
|
- ↗️ Users can add custom shortcuts (aliases)
|
||||||
- 1️⃣ Muse instance supports multiple guilds
|
- 1️⃣ Muse instance supports multiple guilds
|
||||||
|
- 🔊 Normalizes volume across tracks
|
||||||
- ✍️ Written in TypeScript, easily extendable
|
- ✍️ Written in TypeScript, easily extendable
|
||||||
- ❤️ Loyal Packers fan
|
- ❤️ Loyal Packers fan
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {VoiceChannel, Snowflake} from 'discord.js';
|
import {VoiceChannel, Snowflake} from 'discord.js';
|
||||||
import {Readable} from 'stream';
|
import {Readable} from 'stream';
|
||||||
import hasha from 'hasha';
|
import hasha from 'hasha';
|
||||||
import ytdl from 'ytdl-core';
|
import ytdl, {videoFormat} from 'ytdl-core';
|
||||||
import {WriteStream} from 'fs-capacitor';
|
import {WriteStream} from 'fs-capacitor';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import shuffle from 'array-shuffle';
|
import shuffle from 'array-shuffle';
|
||||||
|
@ -56,6 +56,8 @@ export interface PlayerEvents {
|
||||||
statusChange: (oldStatus: STATUS, newStatus: STATUS) => void;
|
statusChange: (oldStatus: STATUS, newStatus: STATUS) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
public voiceConnection: VoiceConnection | null = null;
|
public voiceConnection: VoiceConnection | null = null;
|
||||||
public status = STATUS.PAUSED;
|
public status = STATUS.PAUSED;
|
||||||
|
@ -415,7 +417,7 @@ export default class {
|
||||||
const ffmpegInputOptions: string[] = [];
|
const ffmpegInputOptions: string[] = [];
|
||||||
let shouldCacheVideo = false;
|
let shouldCacheVideo = false;
|
||||||
|
|
||||||
let format: ytdl.videoFormat | undefined;
|
let format: YTDLVideoFormat | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ffmpegInput = await this.fileCache.getPathFor(this.getHashForCache(song.url));
|
ffmpegInput = await this.fileCache.getPathFor(this.getHashForCache(song.url));
|
||||||
|
@ -431,7 +433,7 @@ export default class {
|
||||||
// Not yet cached, must download
|
// Not yet cached, must download
|
||||||
const info = await ytdl.getInfo(song.url);
|
const info = await ytdl.getInfo(song.url);
|
||||||
|
|
||||||
const {formats} = info;
|
const formats = info.formats as YTDLVideoFormat[];
|
||||||
|
|
||||||
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
|
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
|
||||||
|
|
||||||
|
@ -465,12 +467,16 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('Using format', format);
|
||||||
|
|
||||||
ffmpegInput = format.url;
|
ffmpegInput = format.url;
|
||||||
|
|
||||||
// Don't cache livestreams or long videos
|
// Don't cache livestreams or long videos
|
||||||
const MAX_CACHE_LENGTH_SECONDS = 30 * 60; // 30 minutes
|
const MAX_CACHE_LENGTH_SECONDS = 30 * 60; // 30 minutes
|
||||||
shouldCacheVideo = !info.player_response.videoDetails.isLiveContent && parseInt(info.videoDetails.lengthSeconds, 10) < MAX_CACHE_LENGTH_SECONDS && !options.seek;
|
shouldCacheVideo = !info.player_response.videoDetails.isLiveContent && parseInt(info.videoDetails.lengthSeconds, 10) < MAX_CACHE_LENGTH_SECONDS && !options.seek;
|
||||||
|
|
||||||
|
debug(shouldCacheVideo ? 'Caching video' : 'Not caching video');
|
||||||
|
|
||||||
ffmpegInputOptions.push(...[
|
ffmpegInputOptions.push(...[
|
||||||
'-reconnect',
|
'-reconnect',
|
||||||
'1',
|
'1',
|
||||||
|
@ -489,7 +495,11 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createReadStream(ffmpegInput, {ffmpegInputOptions, cache: shouldCacheVideo});
|
return this.createReadStream(ffmpegInput, {
|
||||||
|
ffmpegInputOptions,
|
||||||
|
cache: shouldCacheVideo,
|
||||||
|
volumeAdjustment: format?.loudnessDb ? `${-format.loudnessDb}dB` : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private startTrackingPosition(initalPosition?: number): void {
|
private startTrackingPosition(initalPosition?: number): void {
|
||||||
|
@ -546,7 +556,7 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createReadStream(url: string, options: {ffmpegInputOptions?: string[]; cache?: boolean} = {}): Promise<Readable> {
|
private async createReadStream(url: string, options: {ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string} = {}): Promise<Readable> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const capacitor = new WriteStream();
|
const capacitor = new WriteStream();
|
||||||
|
|
||||||
|
@ -563,6 +573,7 @@ export default class {
|
||||||
.noVideo()
|
.noVideo()
|
||||||
.audioCodec('libopus')
|
.audioCodec('libopus')
|
||||||
.outputFormat('webm')
|
.outputFormat('webm')
|
||||||
|
.addOutputOption(['-filter:a', `volume=${options?.volumeAdjustment ?? '1'}`])
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
if (!hasReturnedStreamClosed) {
|
if (!hasReturnedStreamClosed) {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|
Loading…
Reference in a new issue