Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Unify ReplayGain handling and calculation #3438

Merged
merged 2 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ New:
- Added `%track.drop` to the `%ffmpeg` encoder to allow partial encoding
of a source's available tracks (#3480)
- Added `let { foo? } = ...` pattern matching (#3481)
- Add `metadata.replaygain` method to extract unified replay gain value from metadata (#3438).
- Add `compute` parameter to `file.replaygain` to control gain calculation (#3438).
- Add `compute` parameter to `enable_replaygain_metadata` to control replay gain calculation (#3438).

Changed:

Expand All @@ -21,6 +24,8 @@ Changed:
mechanism for updating it (#3355).
- Disable output paging when `TERM` environment variable is not set.
- Allow running as `root` user inside `docker` container by default (#3406).
- BREAKING: `replaygain` no longer takes `ebu_r128` parameter (#3438).
- BREAKING: assume `replaygain_track_gain` always stores volume in _dB_ (#3438).

# 2.2.0 (2023-07-21)

Expand Down
18 changes: 18 additions & 0 deletions doc/content/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ close to production. Streaming issues can build up over time. We do our best to
release the most stable possible code but problems can arise from many reasons
so, always best to first to a trial run before putting things to production!

## From 2.2.x to 2.3.x

### Replaygain

- There is a new `metadata.replaygain` function that extracts the replay gain value in _dB_ from the metadata.
It handles both `r128_track_gain` and `replaygain_track_gain` internally and returns a single unified gain value.

- The `file.replaygain` function now takes a new compute parameter:
`file.replaygain(~id=null(), ~compute=true, ~ratio=50., file_name)`.
The compute parameter determines if gain should be calculated when the metadata does not already contain replaygain tags.

- The `enable_replaygain_metadata` function now accepts a compute parameter to control replaygain calculation.

- The `replaygain` function no longer takes an `ebu_r128` parameter. The signature is now simply: `replaygain(~id=null(), s)`.
Previously, `ebu_r128` allowed controlling whether EBU R128 or standard replaygain was used.
However, EBU R128 data is now extracted directly from metadata when available.
So `replaygain` cannot control the gain type via this parameter anymore.

## From 2.1.x to 2.2.x

### References
Expand Down
160 changes: 73 additions & 87 deletions src/libs/replaygain.liq
Original file line number Diff line number Diff line change
Expand Up @@ -3,114 +3,100 @@
# or the `replaygain:` protocol for this.
# @category Source / Audio processing
# @param ~id Force the value of the source ID.
# @param ~ebu_r128 Also amplify according to EBU R128 tags.
# @param s Source to be amplified.
def replaygain(~id=null(), ~ebu_r128=true, s) =
# Normalize opus gain
def f(m) =
def f(m) =
let (k, v) = m
if
k == "r128_track_gain"
then
v = int_of_string(v)
v = v + 5
def replaygain(~id=null(), s) =
amplify(id=id, override="replaygain_track_gain", 1., s)
end

# normalize to -18 dB as usual replaygain instead of -23 dB
v = lin_of_dB(float(v) / 256.)
("replaygain_track_gain", string.float(v))
else
(k, v)
end
end
let file.replaygain = ()

if ebu_r128 then list.map(f, m) else m end
# Compute the ReplayGain for a file (in dB).
# @category File
# @param ~id Force the value of the source ID.
# @param ~ratio Decoding ratio. A value of `50` means try to decode the file `50x` faster than real time, if possible.
# Use this setting to lower CPU peaks when computing replaygain tags.
# @param file_name File name.
# @flag hidden
def file.replaygain.compute(~ratio=50., file_name) =
_request = request.create(resolve_metadata=false, file_name)
if request.resolve(_request) then
_source = source.replaygain.compute(request.once(_request))
source.drop(ratio=ratio, _source)
_source.gain()
else
null()
end
end

s = metadata.map(f, s)
amplify(id=id, override="replaygain_track_gain", 1., s)
# Extract the ReplayGain from the metadata (in dB).
# @category Metadata
# @param _metadata Metadata from which the ReplayGain should be extracted.
def metadata.replaygain(_metadata) =
if list.assoc.mem("r128_track_gain", _metadata) then
try
float_of_string(_metadata["r128_track_gain"]) / 256.
catch _ do
null()
end
elsif list.assoc.mem("replaygain_track_gain", _metadata) then
replaygain_metadata = _metadata["replaygain_track_gain"]
match = r/([+-]?\d*\.?\d*)/.exec(replaygain_metadata)
try
float_of_string(list.assoc(1, match))
catch _ do
null()
end
else
null()
end
end

# Compute the ReplayGain for a file (in dB).
# Get the ReplayGain for a file (in dB).
# @category File
# @param ~ratio Decoding ratio. A value of `50` means try to decode the file `50x` faster than real time, if possible. Use this setting to lower CPU peaks when computing replaygain tags.
# @param fname File name.
def file.replaygain(~id=null(), ~ratio=50., fname) =
# @param ~id Force the value of the source ID.
# @param ~compute Compute ReplayGain if metadata tag is empty.
# @param ~ratio Decoding ratio. A value of `50` means try to decode the file `50x` faster than real time, if possible.
# Use this setting to lower CPU peaks when computing replaygain tags.
# @param file_name File name.
def replaces file.replaygain(~id=null(), ~compute=true, ~ratio=50., file_name) =
id = string.id.default(default="file.replaygain", id)
try
meta = file.metadata(exclude=["replaygain_track_gain"], fname)
replaygain_meta = meta["replaygain_track_gain"]
match = r/([\+\-\d\.]+)\s*dB/i.exec(replaygain_meta)
gain = float_of_string(list.assoc(1, match))
log.important(
label=id,
"Detected track replaygain #{gain} dB for #{string.quote(fname)}"
)

gain
catch _ do
r = request.create(resolve_metadata=false, fname)
if
request.resolve(r)
then
log.important(
label=id,
"Computing replay gain for #{string.quote(fname)}"
)
file_name_quoted = string.quote(file_name)

t = time()
s = source.replaygain.compute(request.once(r))
source.drop(ratio=ratio, s)
gain = s.gain()
log.important(
label=id,
"Computed replay gain #{gain} dB for #{string.quote(fname)} (time: #{
time() - t
} s)."
)
_metadata = file.metadata(exclude=["replaygain_track_gain"], file_name)
gain = metadata.replaygain(_metadata)

gain
else
null()
if gain != null() then
log.info(label=id, "Detected track replaygain #{gain} dB for #{file_name_quoted}.")
gain
elsif compute then
log.info(label=id, "Computing replay gain for #{file_name_quoted}.")
start_time = time()
gain = file.replaygain.compute(ratio=ratio, file_name)
elapsed_time = time() - start_time
if gain != null() then
log.info(label=id, "Computed replay gain #{gain} dB for #{file_name_quoted} (time: #{elapsed_time} s).")
end
gain
else
null()
end
end

# Enable ReplayGain metadata resolver. This resolver will process any file
# decoded by Liquidsoap and add a `replaygain_track_gain` metadata when this
# value could be computed. For a finer-grained replay gain processing, use the
# `replaygain:` protocol.
# @param ~ratio Decoding ratio. A value of `50.` means try to decode the file `50x` faster than real time, if possible. Use this setting to lower CPU peaks when computing replaygain tags.
# @param ~compute Compute replaygain if metadata tag is empty.
# @param ~ratio Decoding ratio. A value of `50.` means try to decode the file `50x` faster than real time, if possible.
# Use this setting to lower CPU peaks when computing replaygain tags.
# @category Liquidsoap
def enable_replaygain_metadata(~ratio=50.) =
def replaygain_metadata(~metadata=_, fname) =
meta = file.metadata(exclude=["replaygain_track_gain"], fname)
replaygain_meta = meta["replaygain_track_gain"]
if
replaygain_meta != ""
then
log.important(
label="decoder.replaygain.metadata",
"Detected replaygain metadata #{replaygain_meta} for #{
string.quote(fname)
}"
)

[("replaygain_track_gain", replaygain_meta)]
def enable_replaygain_metadata(~compute=true, ~ratio=50.) =
def replaygain_metadata(~metadata=_, file_name) =
gain = file.replaygain(compute=compute, ratio=ratio, file_name)
if gain != null() then
[("replaygain_track_gain", "#{null.get(gain)} dB")]
else
gain = file.replaygain(ratio=ratio, fname)
if
null.defined(gain)
then
[
(
"replaygain_track_gain",
"#{null.get(gain)} dB"
)
]
else
[]
end
[]
end
end

Expand Down
12 changes: 12 additions & 0 deletions tests/language/dune.inc
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,18 @@
(:run_test ../run_test.exe))
(action (run %{run_test} regexp.liq liquidsoap %{test_liq} regexp.liq)))

(rule
(alias citest)
(package liquidsoap)
(deps
replaygain.liq
../../src/bin/liquidsoap.exe
(package liquidsoap)
(:stdlib ../../src/libs/stdlib.liq)
(:test_liq ../test.liq)
(:run_test ../run_test.exe))
(action (run %{run_test} replaygain.liq liquidsoap %{test_liq} replaygain.liq)))

(rule
(alias citest)
(package liquidsoap)
Expand Down
38 changes: 38 additions & 0 deletions tests/language/replaygain.liq
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!../../liquidsoap ../test.liq

def test_metadata_replaygain() =
test.equals(metadata.replaygain([("r128_track_gain", "0")]), 0.)
test.equals(metadata.replaygain([("r128_track_gain", "256")]), 1.)
test.equals(metadata.replaygain([("r128_track_gain", "32767")]), 32767./256.)
test.equals(metadata.replaygain([("r128_track_gain", "-32768")]), -32768./256.)

test.equals(metadata.replaygain([("r128_track_gain", "foo")]), null())
test.equals(metadata.replaygain([("r128_track_gain", "0 foo")]), null())
test.equals(metadata.replaygain([("r128_track_gain", "")]), null())

test.equals(metadata.replaygain([("replaygain_track_gain", "1 dB")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "1")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "-1 dB")]), -1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "-1")]), -1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "+0.424046 dB")]), 0.424046)
test.equals(metadata.replaygain([("replaygain_track_gain", "+0.424046")]), 0.424046)
test.equals(metadata.replaygain([("replaygain_track_gain", "-10.38500 dB")]), -10.38500)
test.equals(metadata.replaygain([("replaygain_track_gain", "-10.38500")]), -10.38500)

test.equals(metadata.replaygain([("replaygain_track_gain", "1 dB")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "1 2 dB")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "1 db")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "1 DB")]), 1.)
test.equals(metadata.replaygain([("replaygain_track_gain", "1 foo")]), 1.)

test.equals(metadata.replaygain([("replaygain_track_gain", "")]), null())
test.equals(metadata.replaygain([("replaygain_track_gain", "foo")]), null())

test.equals(metadata.replaygain([]), null())
test.equals(metadata.replaygain([("r128_track_gain", "foo"), ("replaygain_track_gain", "1")]), null())
test.equals(metadata.replaygain([("replaygain_track_gain", "1"), ("r128_track_gain", "foo")]), null())

test.pass()
end

test.check(test_metadata_replaygain)
Loading
Loading