Skip to content

Commit 5564317

Browse files
authored
Merge pull request #141 from sentriz/only-replaygain
add ReplayGain support
2 parents 696c9b7 + 30545f3 commit 5564317

File tree

7 files changed

+129
-4
lines changed

7 files changed

+129
-4
lines changed

src/player/Player.vue

+21
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@
6868
:value="volume" @input="setVolume"
6969
/>
7070
</b-popover>
71+
<b-button title="ReplayGain"
72+
variant="link" class="m-0"
73+
@click="toggleReplayGain">
74+
<IconReplayGain v-if="replayGainMode === ReplayGainMode.None" />
75+
<IconReplayGainTrack v-else-if="replayGainMode === ReplayGainMode.Track" />
76+
<IconReplayGainAlbum v-else-if="replayGainMode === ReplayGainMode.Album" />
77+
</b-button>
7178
<b-button title="Shuffle"
7279
variant="link" class="m-0" :class="{ 'text-primary': shuffleActive }"
7380
@click="toggleShuffle">
@@ -127,21 +134,29 @@
127134
</template>
128135
<script lang="ts">
129136
import { defineComponent } from 'vue'
137+
import { ReplayGainMode } from './audio'
130138
import ProgressBar from '@/player/ProgressBar.vue'
131139
import { useFavouriteStore } from '@/library/favourite/store'
132140
import { formatArtists } from '@/shared/utils'
133141
import { BPopover } from 'bootstrap-vue'
134142
import SwitchInput from '@/shared/components/SwitchInput.vue'
143+
import IconReplayGain from '@/shared/components/IconReplayGain.vue'
144+
import IconReplayGainTrack from '@/shared/components/IconReplayGainTrack.vue'
145+
import IconReplayGainAlbum from '@/shared/components/IconReplayGainAlbum.vue'
135146
136147
export default defineComponent({
137148
components: {
138149
SwitchInput,
139150
BPopover,
140151
ProgressBar,
152+
IconReplayGain,
153+
IconReplayGainTrack,
154+
IconReplayGainAlbum,
141155
},
142156
setup() {
143157
return {
144158
favouriteStore: useFavouriteStore(),
159+
ReplayGainMode,
145160
}
146161
},
147162
computed: {
@@ -154,6 +169,9 @@
154169
isMuted() {
155170
return this.$store.state.player.volume <= 0.0
156171
},
172+
replayGainMode(): ReplayGainMode {
173+
return this.$store.state.player.replayGainMode
174+
},
157175
repeatActive(): boolean {
158176
return this.$store.state.player.repeat
159177
},
@@ -201,6 +219,9 @@
201219
setVolume(volume: any) {
202220
return this.$store.dispatch('player/setVolume', parseFloat(volume))
203221
},
222+
toggleReplayGain() {
223+
return this.$store.dispatch('player/toggleReplayGain')
224+
},
204225
setPlaybackRate(value: number) {
205226
return this.$store.dispatch('player/setPlaybackRate', value)
206227
},

src/player/audio.ts

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import IcecastMetadataStats from 'icecast-metadata-stats'
22

3+
export enum ReplayGainMode {
4+
None,
5+
Track,
6+
Album,
7+
_Length
8+
}
9+
10+
type ReplayGain = {
11+
trackGain: number // dB
12+
trackPeak: number // 0.0-1.0
13+
albumGain: number // dB
14+
albumPeak: number // 0.0-1.0
15+
}
16+
317
export class AudioController {
418
private audio = new Audio()
519
private handle = -1
620
private volume = 1.0
721
private fadeDuration = 200
822
private buffer = new Audio()
923
private statsListener : any = null
24+
private replayGainMode = ReplayGainMode.None
25+
private replayGain: ReplayGain | null = null
26+
private preAmp = 0.0
1027

1128
ontimeupdate: (value: number) => void = () => { /* do nothing */ }
1229
ondurationchange: (value: number) => void = () => { /* do nothing */ }
@@ -30,7 +47,12 @@ export class AudioController {
3047
setVolume(value: number) {
3148
this.cancelFade()
3249
this.volume = value
33-
this.audio.volume = value
50+
this.audio.volume = value * this.replayGainFactor()
51+
}
52+
53+
setReplayGainMode(value: ReplayGainMode) {
54+
this.replayGainMode = value
55+
this.setVolume(this.volume)
3456
}
3557

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

58-
async changeTrack(options: { url?: string, paused?: boolean, isStream?: boolean, playbackRate?: number }) {
80+
async changeTrack(options: { url?: string, paused?: boolean, replayGain?: ReplayGain, isStream?: boolean, playbackRate?: number }) {
81+
this.replayGain = options.replayGain || null
82+
5983
if (this.audio) {
6084
this.cancelFade()
6185
endPlayback(this.audio, this.fadeDuration)
@@ -128,6 +152,11 @@ export class AudioController {
128152
private fadeFromTo(from: number, to: number, duration: number) {
129153
console.info(`AudioController: start fade (${from}, ${to}, ${duration})`)
130154
const startTime = Date.now()
155+
156+
const replayGainFactor = this.replayGainFactor()
157+
from *= replayGainFactor
158+
to *= replayGainFactor
159+
131160
const step = (to - from) / duration
132161
if (duration <= 0.0) {
133162
this.audio.volume = to
@@ -150,6 +179,39 @@ export class AudioController {
150179
run()
151180
})
152181
}
182+
183+
private replayGainFactor(): number {
184+
if (this.replayGainMode === ReplayGainMode.None) {
185+
return 1.0
186+
}
187+
if (!this.replayGain) {
188+
console.warn('AudioController: no ReplayGain information')
189+
return 1.0
190+
}
191+
192+
const gain = this.replayGainMode === ReplayGainMode.Track
193+
? this.replayGain.trackGain
194+
: this.replayGain.albumGain
195+
196+
const peak = this.replayGainMode === ReplayGainMode.Track
197+
? this.replayGain.trackPeak
198+
: this.replayGain.albumPeak
199+
200+
if (!Number.isFinite(gain) || !Number.isFinite(peak) || peak <= 0) {
201+
console.warn('AudioController: invalid ReplayGain settings', this.replayGain)
202+
return 1.0
203+
}
204+
205+
// Implementing min(10^((RG + Gpre-amp)/20), 1/peakamplitude)
206+
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification
207+
const gainFactor = Math.pow(10, (gain + this.preAmp) / 20)
208+
const peakFactor = 1 / peak
209+
const factor = Math.min(gainFactor, peakFactor)
210+
211+
console.info('AudioController: calculated ReplayGain factor', factor)
212+
213+
return factor
214+
}
153215
}
154216

155217
function endPlayback(audio: HTMLAudioElement, duration: number) {

src/player/store.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Vuex, { Store, Module } from 'vuex'
22
import { shuffle, shuffled, trackListEquals, formatArtists } from '@/shared/utils'
33
import { API, Track } from '@/shared/api'
4-
import { AudioController } from '@/player/audio'
4+
import { AudioController, ReplayGainMode } from '@/player/audio'
55
import { useMainStore } from '@/shared/store'
66
import { ref } from 'vue'
77

@@ -10,6 +10,7 @@ localStorage.removeItem('queue')
1010
localStorage.removeItem('queueIndex')
1111

1212
const storedVolume = parseFloat(localStorage.getItem('player.volume') || '1.0')
13+
const storedReplayGainMode = parseInt(localStorage.getItem('player.replayGainMode') ?? '0')
1314
const storedPodcastPlaybackRate = parseFloat(localStorage.getItem('player.podcastPlaybackRate') || '1.0')
1415
const mediaSession: MediaSession | undefined = navigator.mediaSession
1516
const audio = new AudioController()
@@ -22,6 +23,7 @@ interface State {
2223
duration: number; // duration of current track in seconds
2324
currentTime: number; // position of current track in seconds
2425
streamTitle: string | null;
26+
replayGainMode: ReplayGainMode;
2527
repeat: boolean;
2628
shuffle: boolean;
2729
volume: number; // integer between 0 and 1 representing the volume of the player
@@ -39,6 +41,7 @@ function createPlayerModule(api: API): Module<State, any> {
3941
duration: 0,
4042
currentTime: 0,
4143
streamTitle: null,
44+
replayGainMode: storedReplayGainMode,
4245
repeat: localStorage.getItem('player.repeat') !== 'false',
4346
shuffle: localStorage.getItem('player.shuffle') === 'true',
4447
volume: storedVolume,
@@ -58,6 +61,10 @@ function createPlayerModule(api: API): Module<State, any> {
5861
mediaSession.playbackState = 'paused'
5962
}
6063
},
64+
setReplayGainMode(state, mode: ReplayGainMode) {
65+
state.replayGainMode = mode
66+
localStorage.setItem('player.replayGainMode', `${mode}`)
67+
},
6168
setRepeat(state, enable) {
6269
state.repeat = enable
6370
localStorage.setItem('player.repeat', enable)
@@ -225,6 +232,11 @@ function createPlayerModule(api: API): Module<State, any> {
225232
await audio.changeTrack({ })
226233
}
227234
},
235+
toggleReplayGain({ commit, state }) {
236+
const mode = (state.replayGainMode + 1) % ReplayGainMode._Length
237+
audio.setReplayGainMode(mode)
238+
commit('setReplayGainMode', mode)
239+
},
228240
toggleRepeat({ commit, state }) {
229241
commit('setRepeat', !state.repeat)
230242
},
@@ -323,7 +335,9 @@ function setupAudio(store: Store<any>, mainStore: ReturnType<typeof useMainStore
323335
mainStore.setError(error)
324336
}
325337

338+
audio.setReplayGainMode(storedReplayGainMode)
326339
audio.setVolume(storedVolume)
340+
327341
const track = store.getters['player/track']
328342
if (track?.url) {
329343
audio.changeTrack({ ...track, paused: true })

src/shared/api.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export interface Track {
2323
isStream?: boolean
2424
isPodcast?: boolean
2525
isUnavailable?: boolean
26-
playCount? : number
26+
playCount?: number
27+
replayGain?: {trackGain: number, trackPeak: number, albumGain: number, albumPeak: number}
2728
}
2829

2930
export interface Genre {
@@ -541,6 +542,7 @@ export class API {
541542
: [{ id: item.artistId, name: item.artist }],
542543
url: this.getStreamUrl(item.id),
543544
image: this.getCoverArtUrl(item),
545+
replayGain: item.replayGain,
544546
}
545547
}
546548

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
3+
width="1em" height="1em" fill="currentColor"
4+
viewBox="0 0 24 24"
5+
class="icon bi">
6+
<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>
7+
</svg>
8+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
3+
width="1em" height="1em" fill="currentColor"
4+
viewBox="0 0 24 24"
5+
class="icon bi">
6+
<text x="0" y="50%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="16" fill="currentColor">R</text>
7+
<text x="52%" y="72%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="13" fill="currentColor">A</text>
8+
</svg>
9+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true"
3+
width="1em" height="1em" fill="currentColor"
4+
viewBox="0 0 24 24"
5+
class="icon bi">
6+
<text x="0" y="50%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="16" fill="currentColor">R</text>
7+
<text x="52%" y="72%" dominant-baseline="central" font-family="Arial, sans-serif" font-weight="bold" font-size="13" fill="currentColor">T</text>
8+
</svg>
9+
</template>

0 commit comments

Comments
 (0)