package library import ( "context" "math" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // ReplayGain delivery (M464 #4997). // // Clients are handed loudness in the ReplayGain 2.0 form every player already // understands: a gain in dB that brings the track (or album) to the RG2 // reference of -18 LUFS, and a linear peak. A client aiming at another target // adds (target - ReplayGainReferenceLUFS) to the gain. The same numbers go to // the native API, the sync feed and OpenSubsonic's replayGain, so every client // levels a track identically. // ReplayGainReferenceLUFS is the RG2 reference loudness gains are relative to. const ReplayGainReferenceLUFS = -18.0 // ReplayGain is one track's gains. Each field is nil when there is nothing to // say: not yet measured, silent, or (for the album pair) an album still // waiting on a track. AlbumPeak travels only with AlbumGain. type ReplayGain struct { TrackGain *float32 `json:"track_gain,omitempty"` TrackPeak *float32 `json:"track_peak,omitempty"` AlbumGain *float32 `json:"album_gain,omitempty"` AlbumPeak *float32 `json:"album_peak,omitempty"` } // Empty reports whether no gain is known at all. func (g ReplayGain) Empty() bool { return g.TrackGain == nil && g.AlbumGain == nil } // GainDB converts an integrated loudness to its RG2 gain, to 0.01 dB. func GainDB(lufs *float32) *float32 { if lufs == nil { return nil } v := float32(math.Round((ReplayGainReferenceLUFS-float64(*lufs))*100) / 100) return &v } // LinearPeak converts a true peak in dBTP to the linear amplitude ReplayGain // peaks are written in (1.0 is full scale), to 4 decimals. func LinearPeak(dbtp *float32) *float32 { if dbtp == nil { return nil } v := float32(math.Round(math.Pow(10, float64(*dbtp)/20)*10000) / 10000) return &v } // ReplayGainForTracks returns the gains for each of ids that has any. Tracks // with none are absent from the map, so a lookup of a missing key yields the // zero ReplayGain: no adjustment. func ReplayGainForTracks(ctx context.Context, q *dbq.Queries, ids []pgtype.UUID) (map[pgtype.UUID]ReplayGain, error) { out := make(map[pgtype.UUID]ReplayGain, len(ids)) if len(ids) == 0 { return out, nil } rows, err := q.GetReplayGainByTrackIDs(ctx, ids) if err != nil { return nil, err } for _, r := range rows { g := ReplayGain{TrackGain: GainDB(r.TrackLufs)} if g.TrackGain != nil { g.TrackPeak = LinearPeak(r.TrackPeakDbtp) } if g.AlbumGain = GainDB(r.AlbumLufs); g.AlbumGain != nil { g.AlbumPeak = LinearPeak(r.AlbumPeakDbtp) } if !g.Empty() { out[r.ID] = g } } return out, nil } // ReplayGainForAlbums returns the album pair for each of albumIDs that has an // album loudness. The track fields are left nil. func ReplayGainForAlbums(ctx context.Context, q *dbq.Queries, albumIDs []pgtype.UUID) (map[pgtype.UUID]ReplayGain, error) { out := make(map[pgtype.UUID]ReplayGain, len(albumIDs)) if len(albumIDs) == 0 { return out, nil } rows, err := q.GetAlbumLoudnessByIDs(ctx, albumIDs) if err != nil { return nil, err } for _, r := range rows { if g := GainDB(r.IntegratedLufs); g != nil { out[r.AlbumID] = ReplayGain{AlbumGain: g, AlbumPeak: LinearPeak(r.TruePeakDbtp)} } } return out, nil }