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).
|
||||
* Implements `queue.nr` and `/random` (to be deprecated/changed)
|
||||
* Supports extra encoder bitrate control settings (CBR, VBR, auto, etc.)
|
||||
* Can read and apply ReplayGain tags.
|
||||
|
||||
# Future improvements
|
||||
* Allow playback of files by URL, not just by path
|
||||
|
|
|
@ -14,6 +14,7 @@ type Config struct {
|
|||
FallbackPath string `toml:"fallback"`
|
||||
BufferLengthInKiB int `toml:"buffer_len"`
|
||||
BufferSeconds int `toml:"buffer_size"`
|
||||
ReplayGain bool `toml:"replaygain"`
|
||||
} `toml:"queue"`
|
||||
Radio struct {
|
||||
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.
|
||||
# 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"
|
||||
#
|
||||
# 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.
|
||||
# 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
|
||||
#
|
||||
# Apply replaygain track tags if existent on files played.
|
||||
replaygain=false
|
||||
|
||||
[radio]
|
||||
#
|
||||
|
|
3
go.mod
3
go.mod
|
@ -3,8 +3,9 @@ module git.gammaspectra.live/S.O.N.G/MeteorLight
|
|||
go 1.18
|
||||
|
||||
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/dhowden/tag v0.0.0-20201120070457-d52dcb253c63
|
||||
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-20220306012302-2f33745b66ee/go.mod h1:slLvZqRcR9yMu3Ety7AKzyxu87tHfUKR49ae83sCAM8=
|
||||
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-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/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=
|
||||
|
@ -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/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI=
|
||||
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/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
|
||||
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
|
||||
|
|
113
queue.go
113
queue.go
|
@ -4,16 +4,14 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"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/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/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
||||
"github.com/dhowden/tag"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -27,6 +25,13 @@ const maxBufferSize = 10
|
|||
type QueueTrackEntry struct {
|
||||
QueueIdentifier audio.QueueIdentifier
|
||||
Path string
|
||||
ReplayGain struct {
|
||||
Apply bool
|
||||
TrackPeak float64
|
||||
TrackGain float64
|
||||
AlbumPeak float64
|
||||
AlbumGain float64
|
||||
}
|
||||
Metadata struct {
|
||||
Title string `json:"title"`
|
||||
Album string `json:"album"`
|
||||
|
@ -50,28 +55,19 @@ func (e *QueueTrackEntry) Load() error {
|
|||
//close at end, TODO check if it runs
|
||||
runtime.SetFinalizer(f, (*os.File).Close)
|
||||
|
||||
var source audio.Source
|
||||
switch strings.ToLower(path.Ext(e.Path)) {
|
||||
case ".flac":
|
||||
source, err = flacFormat.Open(f)
|
||||
case ".tta":
|
||||
source, err = ttaFormat.Open(f)
|
||||
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)
|
||||
meta, err := tag.ReadFrom(f)
|
||||
if err != nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
case ".opus":
|
||||
source, err = opusFormat.Open(f)
|
||||
case ".vorbis":
|
||||
source, err = vorbisFormat.Open(f)
|
||||
if _, err = f.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decoders, err := guess.GetDecoders(f, e.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := guess.Open(f, decoders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -81,6 +77,52 @@ func (e *QueueTrackEntry) Load() error {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -150,12 +192,6 @@ func (q *Queue) Wait() {
|
|||
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 {
|
||||
|
||||
if err := entry.Load(); err != nil {
|
||||
|
@ -192,10 +228,21 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
|||
q.mutex.Lock()
|
||||
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 {
|
||||
entry.QueueIdentifier = q.audioQueue.AddTail(entry.source, startCallback, endCallback, removeCallback)
|
||||
entry.QueueIdentifier = q.audioQueue.AddTail(source, startCallback, endCallback, removeCallback)
|
||||
} 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
|
||||
|
|
Loading…
Reference in a new issue