Add ReplayGain apply filter, metadata reader
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
DataHoarder 2022-03-06 16:29:03 +01:00
parent 7f46b2713c
commit 2d91a5bd47
6 changed files with 94 additions and 38 deletions

View file

@ -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

View file

@ -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"`

View file

@ -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
View file

@ -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
View file

@ -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=

117
queue.go
View file

@ -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,7 +25,14 @@ const maxBufferSize = 10
type QueueTrackEntry struct {
QueueIdentifier audio.QueueIdentifier
Path string
Metadata struct {
ReplayGain struct {
Apply bool
TrackPeak float64
TrackGain float64
AlbumPeak float64
AlbumGain float64
}
Metadata struct {
Title string `json:"title"`
Album string `json:"album"`
Artist string `json:"artist"`
@ -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)
}
}
case ".opus":
source, err = opusFormat.Open(f)
case ".vorbis":
source, err = vorbisFormat.Open(f)
meta, err := tag.ReadFrom(f)
if err != nil {
err = nil
}
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