Extended ReplayGain normalization
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
8927e7b1a6
commit
010e8d73f7
|
@ -13,7 +13,7 @@ Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible).
|
||||||
* Use `queue.buffer_size` to specify number of seconds to buffer (by default 0, automatic per client).
|
* Use `queue.buffer_size` to specify number of seconds to buffer (by default 0, automatic per client).
|
||||||
* Implements `queue.nr` and `/random` (to be deprecated/changed)
|
* Implements `queue.nr` and `/random` (to be deprecated/changed)
|
||||||
* Supports extra encoder bitrate control settings (CBR, VBR, auto, etc.)
|
* Supports extra encoder bitrate control settings (CBR, VBR, auto, etc.)
|
||||||
* Can read and apply ReplayGain tags.
|
* Can read and apply ReplayGain tags, or normalize audio loudness.
|
||||||
* Can have audio sources over HTTP(s) URLs on `path` property, and supports seeking.
|
* Can have audio sources over HTTP(s) URLs on `path` property, and supports seeking.
|
||||||
|
|
||||||
# Future improvements
|
# Future improvements
|
||||||
|
|
|
@ -43,6 +43,14 @@ fallback="/tmp/in.flac"
|
||||||
buffer_duration=0
|
buffer_duration=0
|
||||||
#
|
#
|
||||||
# Apply replaygain track tags if existent on files played.
|
# Apply replaygain track tags if existent on files played.
|
||||||
|
# If ReplayGain tags are not existent, it will normalize audio with a running weighted window of 5 seconds.
|
||||||
|
# ReplayGain tag overrides can be added as properties on track blobs:
|
||||||
|
# {
|
||||||
|
# "replay_gain" : {
|
||||||
|
# "track_peak": 1.0000,
|
||||||
|
# "track_gain": -3.15821
|
||||||
|
# }
|
||||||
|
# }
|
||||||
replaygain=false
|
replaygain=false
|
||||||
|
|
||||||
[radio]
|
[radio]
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -3,13 +3,14 @@ module git.gammaspectra.live/S.O.N.G/MeteorLight
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306150518-7aa672a6166d
|
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220308124222-54bb437c0b50
|
||||||
github.com/BurntSushi/toml v1.0.0
|
github.com/BurntSushi/toml v1.0.0
|
||||||
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63
|
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63
|
||||||
github.com/enriquebris/goconcurrentqueue v0.6.3
|
github.com/enriquebris/goconcurrentqueue v0.6.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220308113719-afad5c6e5c28 // indirect
|
||||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 // indirect
|
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 // indirect
|
||||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba // indirect
|
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba // indirect
|
||||||
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 // indirect
|
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,5 +1,7 @@
|
||||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306150518-7aa672a6166d h1:7Vys7abOvnEeGinIsEGu0KWFZUqQxOcwARBYDAUnJcQ=
|
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220308124222-54bb437c0b50 h1:mncq7NhkVifcjIuNZEKWQ3QtuZNopnP4MWQhAaqLeVM=
|
||||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306150518-7aa672a6166d/go.mod h1:slLvZqRcR9yMu3Ety7AKzyxu87tHfUKR49ae83sCAM8=
|
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220308124222-54bb437c0b50/go.mod h1:S3VhlpN5phBm/HfYqxh9Ik7ZsWj2EAO4+ZgAwX8wAk0=
|
||||||
|
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220308113719-afad5c6e5c28 h1:7YLU2eyGBX8juV445KlBxW71NjFAzbRvfotZBUP16Bs=
|
||||||
|
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220308113719-afad5c6e5c28/go.mod h1:5H4eVW9uknpn8REFr+C3ejhvXdncgm/pbGqKGC43gFY=
|
||||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 h1:MaKiBfXQl0keyfdCi1PxGOKRTiWhIs8PqCal5GhKDi0=
|
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 h1:MaKiBfXQl0keyfdCi1PxGOKRTiWhIs8PqCal5GhKDi0=
|
||||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
|
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
|
||||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU=
|
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU=
|
||||||
|
|
58
queue.go
58
queue.go
|
@ -6,10 +6,10 @@ import (
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
||||||
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/replaygain"
|
||||||
"github.com/dhowden/tag"
|
"github.com/dhowden/tag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -25,18 +25,17 @@ const maxBufferSize = 10
|
||||||
type QueueTrackEntry struct {
|
type QueueTrackEntry struct {
|
||||||
QueueIdentifier audio.QueueIdentifier
|
QueueIdentifier audio.QueueIdentifier
|
||||||
Path string
|
Path string
|
||||||
ReplayGain struct {
|
Metadata struct {
|
||||||
Apply bool
|
Title string `json:"title"`
|
||||||
TrackPeak float64
|
Album string `json:"album"`
|
||||||
TrackGain float64
|
Artist string `json:"artist"`
|
||||||
AlbumPeak float64
|
Art string `json:"art"`
|
||||||
AlbumGain float64
|
ReplayGain struct {
|
||||||
}
|
TrackPeak float64 `json:"track_peak"`
|
||||||
Metadata struct {
|
TrackGain float64 `json:"track_gain"`
|
||||||
Title string `json:"title"`
|
AlbumPeak float64 `json:"album_peak"`
|
||||||
Album string `json:"album"`
|
AlbumGain float64 `json:"album_gain"`
|
||||||
Artist string `json:"artist"`
|
} `json:"replay_gain,omitempty"`
|
||||||
Art string `json:"art"`
|
|
||||||
}
|
}
|
||||||
reader io.ReadSeekCloser
|
reader io.ReadSeekCloser
|
||||||
source audio.Source
|
source audio.Source
|
||||||
|
@ -117,29 +116,30 @@ func (e *QueueTrackEntry) Load() error {
|
||||||
var value interface{}
|
var value interface{}
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if !e.ReplayGain.Apply {
|
getDb := func(strValue string) (ret float64) {
|
||||||
|
ret, _ = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strings.ToLower(strValue), "db")), 64)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Metadata.ReplayGain.TrackPeak == 0 {
|
||||||
if value, ok = tags["replaygain_track_gain"]; ok {
|
if value, ok = tags["replaygain_track_gain"]; ok {
|
||||||
if strValue, ok = value.(string); ok {
|
if strValue, ok = value.(string); ok {
|
||||||
if e.ReplayGain.TrackGain, err = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64); err == nil {
|
e.Metadata.ReplayGain.TrackGain = getDb(strValue)
|
||||||
e.ReplayGain.Apply = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value, ok = tags["replaygain_track_peak"]; ok {
|
if value, ok = tags["replaygain_track_peak"]; ok {
|
||||||
if strValue, ok = value.(string); ok {
|
if strValue, ok = value.(string); ok {
|
||||||
if e.ReplayGain.TrackPeak, err = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64); err == nil {
|
e.Metadata.ReplayGain.TrackPeak = getDb(strValue)
|
||||||
e.ReplayGain.Apply = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value, ok = tags["replaygain_album_gain"]; ok {
|
if value, ok = tags["replaygain_album_gain"]; ok {
|
||||||
if strValue, ok = value.(string); ok {
|
if strValue, ok = value.(string); ok {
|
||||||
e.ReplayGain.AlbumGain, _ = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64)
|
e.Metadata.ReplayGain.AlbumGain = getDb(strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if value, ok = tags["replaygain_album_peak"]; ok {
|
if value, ok = tags["replaygain_album_peak"]; ok {
|
||||||
if strValue, ok = value.(string); ok {
|
if strValue, ok = value.(string); ok {
|
||||||
e.ReplayGain.AlbumPeak, _ = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64)
|
e.Metadata.ReplayGain.AlbumPeak = getDb(strValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,14 +250,12 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
source := entry.source
|
source := entry.source
|
||||||
if q.config.Queue.ReplayGain && entry.ReplayGain.Apply {
|
if q.config.Queue.ReplayGain {
|
||||||
const ReplayGainPreamp = 0 //in dB
|
if entry.Metadata.ReplayGain.TrackPeak != 0 {
|
||||||
volume := math.Pow(10, (entry.ReplayGain.TrackGain+ReplayGainPreamp)/20)
|
source = replaygain.NewReplayGainFilter(entry.Metadata.ReplayGain.TrackGain, entry.Metadata.ReplayGain.TrackPeak, 0).Process(source)
|
||||||
|
} else {
|
||||||
//prevent clipping
|
source = replaygain.NewNormalizationFilter(5).Process(source)
|
||||||
volume = math.Min(volume, 1/entry.ReplayGain.TrackPeak)
|
}
|
||||||
|
|
||||||
source = audio.NewVolumeFilter(float32(volume)).Process(source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tail {
|
if tail {
|
||||||
|
|
Loading…
Reference in a new issue