Skip to content

Commit

Permalink
Merge pull request #141 from sentriz/only-replaygain
Browse files Browse the repository at this point in the history
add ReplayGain support
  • Loading branch information
tamland authored Dec 29, 2024
2 parents 696c9b7 + 30545f3 commit 5564317
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 4 deletions.
21 changes: 21 additions & 0 deletions src/player/Player.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@
:value="volume" @input="setVolume"
/>
</b-popover>
<b-button title="ReplayGain"
variant="link" class="m-0"
@click="toggleReplayGain">
<IconReplayGain v-if="replayGainMode === ReplayGainMode.None" />
<IconReplayGainTrack v-else-if="replayGainMode === ReplayGainMode.Track" />
<IconReplayGainAlbum v-else-if="replayGainMode === ReplayGainMode.Album" />
</b-button>
<b-button title="Shuffle"
variant="link" class="m-0" :class="{ 'text-primary': shuffleActive }"
@click="toggleShuffle">
Expand Down Expand Up @@ -127,21 +134,29 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ReplayGainMode } from './audio'
import ProgressBar from '@/player/ProgressBar.vue'
import { useFavouriteStore } from '@/library/favourite/store'
import { formatArtists } from '@/shared/utils'
import { BPopover } from 'bootstrap-vue'
import SwitchInput from '@/shared/components/SwitchInput.vue'
import IconReplayGain from '@/shared/components/IconReplayGain.vue'
import IconReplayGainTrack from '@/shared/components/IconReplayGainTrack.vue'
import IconReplayGainAlbum from '@/shared/components/IconReplayGainAlbum.vue'
export default defineComponent({
components: {
SwitchInput,
BPopover,
ProgressBar,
IconReplayGain,
IconReplayGainTrack,
IconReplayGainAlbum,
},
setup() {
return {
favouriteStore: useFavouriteStore(),
ReplayGainMode,
}
},
computed: {
Expand All @@ -154,6 +169,9 @@
isMuted() {
return this.$store.state.player.volume <= 0.0
},
replayGainMode(): ReplayGainMode {
return this.$store.state.player.replayGainMode
},
repeatActive(): boolean {
return this.$store.state.player.repeat
},
Expand Down Expand Up @@ -201,6 +219,9 @@
setVolume(volume: any) {
return this.$store.dispatch('player/setVolume', parseFloat(volume))
},
toggleReplayGain() {
return this.$store.dispatch('player/toggleReplayGain')
},
setPlaybackRate(value: number) {
return this.$store.dispatch('player/setPlaybackRate', value)
},
Expand Down
66 changes: 64 additions & 2 deletions src/player/audio.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import IcecastMetadataStats from 'icecast-metadata-stats'

export enum ReplayGainMode {
None,
Track,
Album,
_Length
}

type ReplayGain = {
trackGain: number // dB
trackPeak: number // 0.0-1.0
albumGain: number // dB
albumPeak: number // 0.0-1.0
}

export class AudioController {
private audio = new Audio()
private handle = -1
private volume = 1.0
private fadeDuration = 200
private buffer = new Audio()
private statsListener : any = null
private replayGainMode = ReplayGainMode.None
private replayGain: ReplayGain | null = null
private preAmp = 0.0

ontimeupdate: (value: number) => void = () => { /* do nothing */ }
ondurationchange: (value: number) => void = () => { /* do nothing */ }
Expand All @@ -30,7 +47,12 @@ export class AudioController {
setVolume(value: number) {
this.cancelFade()
this.volume = value
this.audio.volume = value
this.audio.volume = value * this.replayGainFactor()
}

setReplayGainMode(value: ReplayGainMode) {
this.replayGainMode = value
this.setVolume(this.volume)
}

setPlaybackRate(value: number) {
Expand All @@ -55,7 +77,9 @@ export class AudioController {
await this.fadeIn(this.fadeDuration / 2.0)
}

async changeTrack(options: { url?: string, paused?: boolean, isStream?: boolean, playbackRate?: number }) {
async changeTrack(options: { url?: string, paused?: boolean, replayGain?: ReplayGain, isStream?: boolean, playbackRate?: number }) {
this.replayGain = options.replayGain || null

if (this.audio) {
this.cancelFade()
endPlayback(this.audio, this.fadeDuration)
Expand Down Expand Up @@ -128,6 +152,11 @@ export class AudioController {
private fadeFromTo(from: number, to: number, duration: number) {
console.info(`AudioController: start fade (${from}, ${to}, ${duration})`)
const startTime = Date.now()

const replayGainFactor = this.replayGainFactor()
from *= replayGainFactor
to *= replayGainFactor

const step = (to - from) / duration
if (duration <= 0.0) {
this.audio.volume = to
Expand All @@ -150,6 +179,39 @@ export class AudioController {
run()
})
}

private replayGainFactor(): number {
if (this.replayGainMode === ReplayGainMode.None) {
return 1.0
}
if (!this.replayGain) {
console.warn('AudioController: no ReplayGain information')
return 1.0
}

const gain = this.replayGainMode === ReplayGainMode.Track
? this.replayGain.trackGain
: this.replayGain.albumGain

const peak = this.replayGainMode === ReplayGainMode.Track
? this.replayGain.trackPeak
: this.replayGain.albumPeak

if (!Number.isFinite(gain) || !Number.isFinite(peak) || peak <= 0) {
console.warn('AudioController: invalid ReplayGain settings', this.replayGain)
return 1.0
}

// Implementing min(10^((RG + Gpre-amp)/20), 1/peakamplitude)
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification
const gainFactor = Math.pow(10, (gain + this.preAmp) / 20)
const peakFactor = 1 / peak
const factor = Math.min(gainFactor, peakFactor)

console.info('AudioController: calculated ReplayGain factor', factor)

return factor
}
}

function endPlayback(audio: HTMLAudioElement, duration: number) {
Expand Down
16 changes: 15 additions & 1 deletion src/player/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Vuex, { Store, Module } from 'vuex'
import { shuffle, shuffled, trackListEquals, formatArtists } from '@/shared/utils'
import { API, Track } from '@/shared/api'
import { AudioController } from '@/player/audio'
import { AudioController, ReplayGainMode } from '@/player/audio'
import { useMainStore } from '@/shared/store'
import { ref } from 'vue'

Expand All @@ -10,6 +10,7 @@ localStorage.removeItem('queue')
localStorage.removeItem('queueIndex')

const storedVolume = parseFloat(localStorage.getItem('player.volume') || '1.0')
const storedReplayGainMode = parseInt(localStorage.getItem('player.replayGainMode') ?? '0')
const storedPodcastPlaybackRate = parseFloat(localStorage.getItem('player.podcastPlaybackRate') || '1.0')
const mediaSession: MediaSession | undefined = navigator.mediaSession
const audio = new AudioController()
Expand All @@ -22,6 +23,7 @@ interface State {
duration: number; // duration of current track in seconds
currentTime: number; // position of current track in seconds
streamTitle: string | null;
replayGainMode: ReplayGainMode;
repeat: boolean;
shuffle: boolean;
volume: number; // integer between 0 and 1 representing the volume of the player
Expand All @@ -39,6 +41,7 @@ function createPlayerModule(api: API): Module<State, any> {
duration: 0,
currentTime: 0,
streamTitle: null,
replayGainMode: storedReplayGainMode,
repeat: localStorage.getItem('player.repeat') !== 'false',
shuffle: localStorage.getItem('player.shuffle') === 'true',
volume: storedVolume,
Expand All @@ -58,6 +61,10 @@ function createPlayerModule(api: API): Module<State, any> {
mediaSession.playbackState = 'paused'
}
},
setReplayGainMode(state, mode: ReplayGainMode) {
state.replayGainMode = mode
localStorage.setItem('player.replayGainMode', `${mode}`)
},
setRepeat(state, enable) {
state.repeat = enable
localStorage.setItem('player.repeat', enable)
Expand Down Expand Up @@ -225,6 +232,11 @@ function createPlayerModule(api: API): Module<State, any> {
await audio.changeTrack({ })
}
},
toggleReplayGain({ commit, state }) {
const mode = (state.replayGainMode + 1) % ReplayGainMode._Length
audio.setReplayGainMode(mode)
commit('setReplayGainMode', mode)
},
toggleRepeat({ commit, state }) {
commit('setRepeat', !state.repeat)
},
Expand Down Expand Up @@ -323,7 +335,9 @@ function setupAudio(store: Store<any>, mainStore: ReturnType<typeof useMainStore
mainStore.setError(error)
}

audio.setReplayGainMode(storedReplayGainMode)
audio.setVolume(storedVolume)

const track = store.getters['player/track']
if (track?.url) {
audio.changeTrack({ ...track, paused: true })
Expand Down
4 changes: 3 additions & 1 deletion src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export interface Track {
isStream?: boolean
isPodcast?: boolean
isUnavailable?: boolean
playCount? : number
playCount?: number
replayGain?: {trackGain: number, trackPeak: number, albumGain: number, albumPeak: number}
}

export interface Genre {
Expand Down Expand Up @@ -541,6 +542,7 @@ export class API {
: [{ id: item.artistId, name: item.artist }],
url: this.getStreamUrl(item.id),
image: this.getCoverArtUrl(item),
replayGain: item.replayGain,
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/shared/components/IconReplayGain.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
width="1em" height="1em" fill="currentColor"
viewBox="0 0 24 24"
class="icon bi">
<text opacity="0.5" x="0" y="50%" dominant-baseline="central" text-anchor="start" font-family="Arial, sans-serif" font-weight="bold" font-size="16" fill="currentColor">RG</text>
</svg>
</template>
9 changes: 9 additions & 0 deletions src/shared/components/IconReplayGainAlbum.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
width="1em" height="1em" fill="currentColor"
viewBox="0 0 24 24"
class="icon bi">
<text x="0" y="50%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="16" fill="currentColor">R</text>
<text x="52%" y="72%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="13" fill="currentColor">A</text>
</svg>
</template>
9 changes: 9 additions & 0 deletions src/shared/components/IconReplayGainTrack.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
width="1em" height="1em" fill="currentColor"
viewBox="0 0 24 24"
class="icon bi">
<text x="0" y="50%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="16" fill="currentColor">R</text>
<text x="52%" y="72%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="13" fill="currentColor">T</text>
</svg>
</template>

0 comments on commit 5564317

Please sign in to comment.