diff --git a/README.md b/README.md index c2763aa..72016dc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.go b/config.go index b199a9d..57cdd74 100644 --- a/config.go +++ b/config.go @@ -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"` diff --git a/example_config.toml b/example_config.toml index 3ec3412..486df21 100644 --- a/example_config.toml +++ b/example_config.toml @@ -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] # diff --git a/go.mod b/go.mod index 12ea4cb..61324de 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index bb30688..776618b 100644 --- a/go.sum +++ b/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= diff --git a/queue.go b/queue.go index ec54926..37a4942 100644 --- a/queue.go +++ b/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,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