Add ReplayGain apply filter, metadata reader
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
7f46b2713c
commit
2d91a5bd47
|
@ -13,6 +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.
|
||||||
|
|
||||||
# Future improvements
|
# Future improvements
|
||||||
* Allow playback of files by URL, not just by path
|
* Allow playback of files by URL, not just by path
|
||||||
|
|
|
@ -14,6 +14,7 @@ type Config struct {
|
||||||
FallbackPath string `toml:"fallback"`
|
FallbackPath string `toml:"fallback"`
|
||||||
BufferLengthInKiB int `toml:"buffer_len"`
|
BufferLengthInKiB int `toml:"buffer_len"`
|
||||||
BufferSeconds int `toml:"buffer_size"`
|
BufferSeconds int `toml:"buffer_size"`
|
||||||
|
ReplayGain bool `toml:"replaygain"`
|
||||||
} `toml:"queue"`
|
} `toml:"queue"`
|
||||||
Radio struct {
|
Radio struct {
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
|
|
|
@ -19,6 +19,7 @@ host="127.0.0.1"
|
||||||
#
|
#
|
||||||
# The path is the path to an audio file on the filesystem you want MeteorLight to play.
|
# The path is the path to an audio file on the filesystem you want MeteorLight to play.
|
||||||
# Additionally, the "title", "artist" and "art" properties can be included to be used as metadata.
|
# Additionally, the "title", "artist" and "art" properties can be included to be used as metadata.
|
||||||
|
# If "title", "artist" are not specified, file tags may be used.
|
||||||
random_song_api="http://localhost:8012/api/random"
|
random_song_api="http://localhost:8012/api/random"
|
||||||
#
|
#
|
||||||
# An HTTP POST is issued to this URL when MeteorLight starts playing a track. The body
|
# An HTTP POST is issued to this URL when MeteorLight starts playing a track. The body
|
||||||
|
@ -39,6 +40,9 @@ fallback="/tmp/in.flac"
|
||||||
# Maximum 10 seconds.
|
# Maximum 10 seconds.
|
||||||
# Do note buffer is counted from end of frame, not start, for removal purposes. This depends on format and can be a second or so at times.
|
# Do note buffer is counted from end of frame, not start, for removal purposes. This depends on format and can be a second or so at times.
|
||||||
buffer_duration=0
|
buffer_duration=0
|
||||||
|
#
|
||||||
|
# Apply replaygain track tags if existent on files played.
|
||||||
|
replaygain=false
|
||||||
|
|
||||||
[radio]
|
[radio]
|
||||||
#
|
#
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -3,8 +3,9 @@ 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-20220306012302-2f33745b66ee
|
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306150518-7aa672a6166d
|
||||||
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/enriquebris/goconcurrentqueue v0.6.3
|
github.com/enriquebris/goconcurrentqueue v0.6.3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,5 +1,5 @@
|
||||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306012302-2f33745b66ee h1:VGdw3wl1GOXXfGwUlRcaEkJrLyMOxsEYvFnzHZb2JO4=
|
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-20220306012302-2f33745b66ee/go.mod h1:slLvZqRcR9yMu3Ety7AKzyxu87tHfUKR49ae83sCAM8=
|
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220306150518-7aa672a6166d/go.mod h1:slLvZqRcR9yMu3Ety7AKzyxu87tHfUKR49ae83sCAM8=
|
||||||
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=
|
||||||
|
@ -15,6 +15,8 @@ github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozb
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI=
|
github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI=
|
||||||
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
|
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
|
||||||
|
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 h1:/u5RVRk3Nh7Zw1QQnPtUH5kzcc8JmSSRpHSlGU/zGTE=
|
||||||
|
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||||
github.com/edgeware/mp4ff v0.26.1 h1:tH+TIesZZmrA8BN5HuiKWp3sv5NF4N1A2cFxTSCNL8E=
|
github.com/edgeware/mp4ff v0.26.1 h1:tH+TIesZZmrA8BN5HuiKWp3sv5NF4N1A2cFxTSCNL8E=
|
||||||
github.com/edgeware/mp4ff v0.26.1/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
|
github.com/edgeware/mp4ff v0.26.1/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
|
||||||
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
|
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
|
||||||
|
|
117
queue.go
117
queue.go
|
@ -4,16 +4,14 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"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/flac"
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3"
|
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus"
|
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/tta"
|
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/vorbis"
|
|
||||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
||||||
|
"github.com/dhowden/tag"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -27,7 +25,14 @@ const maxBufferSize = 10
|
||||||
type QueueTrackEntry struct {
|
type QueueTrackEntry struct {
|
||||||
QueueIdentifier audio.QueueIdentifier
|
QueueIdentifier audio.QueueIdentifier
|
||||||
Path string
|
Path string
|
||||||
Metadata struct {
|
ReplayGain struct {
|
||||||
|
Apply bool
|
||||||
|
TrackPeak float64
|
||||||
|
TrackGain float64
|
||||||
|
AlbumPeak float64
|
||||||
|
AlbumGain float64
|
||||||
|
}
|
||||||
|
Metadata struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Album string `json:"album"`
|
Album string `json:"album"`
|
||||||
Artist string `json:"artist"`
|
Artist string `json:"artist"`
|
||||||
|
@ -50,28 +55,19 @@ func (e *QueueTrackEntry) Load() error {
|
||||||
//close at end, TODO check if it runs
|
//close at end, TODO check if it runs
|
||||||
runtime.SetFinalizer(f, (*os.File).Close)
|
runtime.SetFinalizer(f, (*os.File).Close)
|
||||||
|
|
||||||
var source audio.Source
|
meta, err := tag.ReadFrom(f)
|
||||||
switch strings.ToLower(path.Ext(e.Path)) {
|
if err != nil {
|
||||||
case ".flac":
|
err = nil
|
||||||
source, err = flacFormat.Open(f)
|
}
|
||||||
case ".tta":
|
if _, err = f.Seek(0, io.SeekStart); err != nil {
|
||||||
source, err = ttaFormat.Open(f)
|
return err
|
||||||
case ".mp3":
|
|
||||||
source, err = mp3Format.Open(f)
|
|
||||||
case ".ogg":
|
|
||||||
if source, err = opusFormat.Open(f); err != nil {
|
|
||||||
//try flac
|
|
||||||
if source, err = flacFormat.Open(f); err != nil {
|
|
||||||
//try vorbis
|
|
||||||
source, err = vorbisFormat.Open(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case ".opus":
|
|
||||||
source, err = opusFormat.Open(f)
|
|
||||||
case ".vorbis":
|
|
||||||
source, err = vorbisFormat.Open(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decoders, err := guess.GetDecoders(f, e.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
source, err := guess.Open(f, decoders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -81,6 +77,52 @@ func (e *QueueTrackEntry) Load() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
e.source = source
|
e.source = source
|
||||||
|
|
||||||
|
//apply tags found on file
|
||||||
|
if meta != nil {
|
||||||
|
if e.Metadata.Title == "" {
|
||||||
|
e.Metadata.Title = meta.Title()
|
||||||
|
}
|
||||||
|
if e.Metadata.Album == "" {
|
||||||
|
e.Metadata.Title = meta.Album()
|
||||||
|
}
|
||||||
|
if e.Metadata.Artist == "" {
|
||||||
|
e.Metadata.Artist = meta.Artist()
|
||||||
|
}
|
||||||
|
if e.Metadata.Artist == "" {
|
||||||
|
e.Metadata.Artist = meta.AlbumArtist()
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := meta.Raw()
|
||||||
|
var strValue string
|
||||||
|
var value interface{}
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if value, ok = tags["replaygain_track_gain"]; ok {
|
||||||
|
if strValue, ok = value.(string); ok {
|
||||||
|
if e.ReplayGain.TrackGain, err = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64); err == nil {
|
||||||
|
e.ReplayGain.Apply = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok = tags["replaygain_track_peak"]; ok {
|
||||||
|
if strValue, ok = value.(string); ok {
|
||||||
|
if e.ReplayGain.TrackPeak, err = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64); err == nil {
|
||||||
|
e.ReplayGain.Apply = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok = tags["replaygain_album_gain"]; ok {
|
||||||
|
if strValue, ok = value.(string); ok {
|
||||||
|
e.ReplayGain.AlbumGain, _ = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok = tags["replaygain_album_peak"]; ok {
|
||||||
|
if strValue, ok = value.(string); ok {
|
||||||
|
e.ReplayGain.AlbumPeak, _ = strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(strValue, "dB")), 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,12 +192,6 @@ func (q *Queue) Wait() {
|
||||||
close(q.NowPlaying)
|
close(q.NowPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
var flacFormat = flac.NewFormat()
|
|
||||||
var ttaFormat = tta.NewFormat()
|
|
||||||
var mp3Format = mp3.NewFormat()
|
|
||||||
var opusFormat = opus.NewFormat()
|
|
||||||
var vorbisFormat = vorbis.NewFormat()
|
|
||||||
|
|
||||||
func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
||||||
|
|
||||||
if err := entry.Load(); err != nil {
|
if err := entry.Load(); err != nil {
|
||||||
|
@ -192,10 +228,21 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
source := entry.source
|
||||||
|
if q.config.Queue.ReplayGain && entry.ReplayGain.Apply {
|
||||||
|
const ReplayGainPreamp = 0 //in dB
|
||||||
|
volume := math.Pow(10, (entry.ReplayGain.TrackGain+ReplayGainPreamp)/20)
|
||||||
|
|
||||||
|
//prevent clipping
|
||||||
|
volume = math.Min(volume, 1/entry.ReplayGain.TrackPeak)
|
||||||
|
|
||||||
|
source = audio.NewVolumeFilter(float32(volume)).Process(source)
|
||||||
|
}
|
||||||
|
|
||||||
if tail {
|
if tail {
|
||||||
entry.QueueIdentifier = q.audioQueue.AddTail(entry.source, startCallback, endCallback, removeCallback)
|
entry.QueueIdentifier = q.audioQueue.AddTail(source, startCallback, endCallback, removeCallback)
|
||||||
} else {
|
} else {
|
||||||
entry.QueueIdentifier = q.audioQueue.AddHead(entry.source, startCallback, endCallback, removeCallback)
|
entry.QueueIdentifier = q.audioQueue.AddHead(source, startCallback, endCallback, removeCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.original["queue_id"] = entry.QueueIdentifier
|
entry.original["queue_id"] = entry.QueueIdentifier
|
||||||
|
|
Loading…
Reference in a new issue