release / web (push) Successful in 1m44s
release / go (push) Successful in 2m12s
release / govulncheck (push) Successful in 40s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 4m42s
release / integration (push) Successful in 15m33s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m21s
release / Verify release artifacts (tag releases only) (push) Skipped
ReplayGain 2.0 values (gain to -18 LUFS, linear peak) derived from the stored track and album loudness: - Web: GET /api/tracks/replay-gain?ids=... (up to 200), a lookup the player calls for its queue, rather than a field on every TrackRef surface. - Android: track_gain/track_peak and album_gain/album_peak on the sync views, so cached tracks level offline. Storing a measurement logs a track change, and an album's values moving logs an album change, both before the write (#2704), so caches pick the gains up. - OpenSubsonic: replayGain on every song (album, getSong, search3, starred), as a JSON object and an XML element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
package library
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
func TestGainDBAndLinearPeak(t *testing.T) {
|
|
f := func(v float32) *float32 { return &v }
|
|
for _, c := range []struct {
|
|
lufs, want float32
|
|
}{
|
|
{-18, 0}, // at the reference: no change
|
|
{-9.5, -8.5}, // a loud master is turned down
|
|
{-23.456, 5.46}, // a quiet one up, to 0.01 dB
|
|
} {
|
|
if got := GainDB(f(c.lufs)); got == nil || *got != c.want {
|
|
t.Errorf("GainDB(%v) = %v, want %v", c.lufs, got, c.want)
|
|
}
|
|
}
|
|
if GainDB(nil) != nil {
|
|
t.Errorf("GainDB(nil) is not nil")
|
|
}
|
|
for _, c := range []struct {
|
|
dbtp, want float32
|
|
}{
|
|
{0, 1}, // full scale
|
|
{-6.0206, 0.5}, // half amplitude
|
|
{1.2, 1.1482}, // an inter-sample over, above 1.0
|
|
} {
|
|
got := LinearPeak(f(c.dbtp))
|
|
if got == nil || math.Abs(float64(*got-c.want)) > 0.0001 {
|
|
t.Errorf("LinearPeak(%v) = %v, want %v", c.dbtp, got, c.want)
|
|
}
|
|
}
|
|
if LinearPeak(nil) != nil {
|
|
t.Errorf("LinearPeak(nil) is not nil")
|
|
}
|
|
}
|
|
|
|
func TestReplayGainForTracks_Integration(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
dir := t.TempDir()
|
|
|
|
measured, album, artist := seedTrack(t, pool, filepath.Join(dir, "measured.mp3"))
|
|
add := func(name string) dbq.Track {
|
|
t.Helper()
|
|
tr, err := q.UpsertTrack(ctx, dbq.UpsertTrackParams{
|
|
Title: name, AlbumID: album.ID, ArtistID: artist.ID,
|
|
DurationMs: 1000, FilePath: filepath.Join(dir, name+".mp3"), FileSize: 100, FileFormat: "mp3",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("track %s: %v", name, err)
|
|
}
|
|
return tr
|
|
}
|
|
older := add("older")
|
|
unmeasured := add("unmeasured")
|
|
f := func(v float32) *float32 { return &v }
|
|
for _, m := range []struct {
|
|
tr dbq.Track
|
|
lufs float32
|
|
version int16
|
|
}{
|
|
{measured, -12, loudnessVersion},
|
|
// A measurement by an older method is still a better gain than none.
|
|
{older, -20, loudnessVersion - 1},
|
|
} {
|
|
if err := q.UpsertTrackLoudness(ctx, dbq.UpsertTrackLoudnessParams{
|
|
TrackID: m.tr.ID, IntegratedLufs: f(m.lufs), TruePeakDbtp: f(-1), AnalysisVersion: m.version,
|
|
}); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
}
|
|
|
|
gains, err := ReplayGainForTracks(ctx, q, []pgtype.UUID{measured.ID, older.ID, unmeasured.ID})
|
|
if err != nil {
|
|
t.Fatalf("lookup: %v", err)
|
|
}
|
|
if g := gains[measured.ID]; g.TrackGain == nil || *g.TrackGain != -6 || g.TrackPeak == nil {
|
|
t.Errorf("measured track gains = %+v, want track gain -6 with a peak", g)
|
|
}
|
|
if g := gains[older.ID]; g.TrackGain == nil || *g.TrackGain != 2 {
|
|
t.Errorf("older-version track gains = %+v, want track gain 2", g)
|
|
}
|
|
if _, ok := gains[unmeasured.ID]; ok {
|
|
t.Errorf("unmeasured track has an entry; absent means no adjustment")
|
|
}
|
|
// No album value yet (none computed): the album pair is absent.
|
|
if g := gains[measured.ID]; g.AlbumGain != nil || g.AlbumPeak != nil {
|
|
t.Errorf("album pair present before album loudness exists: %+v", g)
|
|
}
|
|
|
|
if err := q.UpsertAlbumLoudness(ctx, dbq.UpsertAlbumLoudnessParams{
|
|
AlbumID: album.ID, IntegratedLufs: f(-14), TruePeakDbtp: f(-0.5),
|
|
TracksTotal: 3, TracksSettled: 3, InputsDigest: "x",
|
|
}); err != nil {
|
|
t.Fatalf("seed album: %v", err)
|
|
}
|
|
gains, err = ReplayGainForTracks(ctx, q, []pgtype.UUID{unmeasured.ID})
|
|
if err != nil {
|
|
t.Fatalf("lookup: %v", err)
|
|
}
|
|
if g := gains[unmeasured.ID]; g.TrackGain != nil || g.AlbumGain == nil || *g.AlbumGain != -4 || g.AlbumPeak == nil {
|
|
t.Errorf("unmeasured track on a leveled album = %+v, want only the album pair (gain -4)", g)
|
|
}
|
|
byAlbum, err := ReplayGainForAlbums(ctx, q, []pgtype.UUID{album.ID})
|
|
if err != nil {
|
|
t.Fatalf("album lookup: %v", err)
|
|
}
|
|
if g := byAlbum[album.ID]; g.AlbumGain == nil || *g.AlbumGain != -4 || g.TrackGain != nil {
|
|
t.Errorf("album gains = %+v, want album gain -4 and no track pair", g)
|
|
}
|
|
}
|