Skip to content

Commit

Permalink
feat!: Unify ReplayGain handling
Browse files Browse the repository at this point in the history
Add new metadata.replaygain function to extract unified replay gain
value in dB from metadata.
  - Handles r128_track_gain and replaygain_track_gain internally.
  - Returns single gain value.

Add compute parameter to file.replaygain and enable_replaygain_metadata.
  - Controls whether to compute gain when metadata missing tags.
  - enable_replaygain_metadata calls file.replaygain with compute.

BREAKING CHANGE: Remove unnecessary ebu_r128 parameter from replaygain.
BREAKING CHANGE: Extract replaygain_track_gain always in dB.
  • Loading branch information
vitoyucepi committed Oct 22, 2023
1 parent 0e479d1 commit 7865145
Show file tree
Hide file tree
Showing 9 changed files with 653 additions and 88 deletions.
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

0 comments on commit 7865145

Please sign in to comment.