feat(analyzer): analyze cuepoint using ffmpeg

- store cue(in|out) as strings
- reraise when executable was not found

BREAKING CHANGE: The analyzer requires 'ffmpeg'. The 'silan' system dependency can be removed.
This commit is contained in:
jo 2022-01-21 09:09:42 +01:00 committed by Kyle Robbertze
parent ceab19271d
commit d93fb44356
5 changed files with 200 additions and 121 deletions

View file

@ -1,7 +1,15 @@
from math import inf
import distro
import pytest
from libretime_analyzer.ffmpeg import compute_replaygain, probe_replaygain
from libretime_analyzer.ffmpeg import (
_SILENCE_DETECT_RE,
compute_replaygain,
compute_silences,
probe_duration,
probe_replaygain,
)
from .fixtures import FILES
@ -28,3 +36,77 @@ def test_compute_replaygain(filepath, replaygain):
tolerance = 5
assert compute_replaygain(filepath) == pytest.approx(replaygain, abs=tolerance)
# Be sure to test a matrix of integer / float, positive / negative values
SILENCE_DETECT_RE_RAW = """
[silencedetect @ 0x563121aee500] silence_start: -0.00154195
[silencedetect @ 0x563121aee500] silence_end: 0.998458 | silence_duration: 1
[silencedetect @ 0x563121aee500] silence_start: 2.99383
[silencedetect @ 0x563121aee500] silence_end: 4.99229 | silence_duration: 1.99846
[silencedetect @ 0x563121aee500] silence_start: 6.98766
[silencedetect @ 0x563121aee500] silence_end: 8.98612 | silence_duration: 1.99846
[silencedetect @ 0x563121aee500] silence_start: 12
[silencedetect @ 0x563121aee500] silence_end: 13 | silence_duration: 1
"""
SILENCE_DETECT_RE_EXPECTED = [
("start", -0.00154195),
("end", 0.998458),
("start", 2.99383),
("end", 4.99229),
("start", 6.98766),
("end", 8.98612),
("start", 12.0),
("end", 13.0),
]
@pytest.mark.parametrize(
"line,expected",
zip(
SILENCE_DETECT_RE_RAW.strip().splitlines(),
SILENCE_DETECT_RE_EXPECTED,
),
)
def test_silence_detect_re(line, expected):
match = _SILENCE_DETECT_RE.search(line)
assert match is not None
assert match.group(1) == expected[0]
assert float(match.group(2)) == expected[1]
@pytest.mark.parametrize(
"filepath,length,cuein,cueout",
map(
lambda i: pytest.param(i.path, i.length, i.cuein, i.cueout, id=i.path.name),
FILES,
),
)
def test_compute_silences(filepath, length, cuein, cueout):
result = compute_silences(filepath)
if cuein != 0.0:
assert len(result) > 0
first = result.pop(0)
assert first[0] == pytest.approx(0.0, abs=0.1)
assert first[1] == pytest.approx(cuein, abs=1)
if cueout != length:
# ffmpeg v3 (bionic) does not warn about silence end when the track ends.
# Check for infinity on last silence ending
if distro.codename() == "bionic":
length = inf
assert len(result) > 0
last = result.pop()
assert last[0] == pytest.approx(cueout, abs=1)
assert last[1] == pytest.approx(length, abs=0.1)
@pytest.mark.parametrize(
"filepath,length",
map(lambda i: pytest.param(i.path, i.length, id=i.path.name), FILES),
)
def test_probe_duration(filepath, length):
assert probe_duration(filepath) == pytest.approx(length, abs=0.05)

View file

@ -1,51 +1,22 @@
from unittest.mock import patch
import distro
import pytest
from libretime_analyzer.steps.analyze_cuepoint import analyze_cuepoint
from ..fixtures import FILE_INVALID_DRM, FILES
from ..fixtures import FILES
@pytest.mark.parametrize(
"filepath,length,cuein,cueout",
map(lambda i: (str(i.path), i.length, i.cuein, i.cueout), FILES),
map(
lambda i: pytest.param(
str(i.path), i.length, i.cuein, i.cueout, id=i.path.name
),
FILES,
),
)
def test_analyze_cuepoint(filepath, length, cuein, cueout):
metadata = analyze_cuepoint(filepath, dict())
assert metadata["length_seconds"] == pytest.approx(length, abs=0.1)
# Silan does not work with m4a files yet
if filepath.endswith("m4a"):
return
# Silan does not work with mp3 on buster, bullseye, focal
if filepath.endswith("mp3") and distro.codename() in (
"buster",
"bullseye",
"focal",
):
return
assert float(metadata["cuein"]) == pytest.approx(cuein, abs=0.5)
assert float(metadata["cueout"]) == pytest.approx(cueout, abs=0.5)
def test_analyze_cuepoint_missing_silan():
with patch(
"libretime_analyzer.steps.analyze_cuepoint.SILAN_EXECUTABLE",
"foobar",
):
analyze_cuepoint(str(FILES[0].path), dict())
def test_analyze_cuepoint_invalid_filepath():
with pytest.raises(KeyError):
test_analyze_cuepoint("non-existent-file", None, None, None)
def test_analyze_cuepoint_invalid_wma():
with pytest.raises(KeyError):
test_analyze_cuepoint(FILE_INVALID_DRM, None, None, None)
assert float(metadata["cuein"]) == pytest.approx(float(cuein), abs=1)
assert float(metadata["cueout"]) == pytest.approx(float(cueout), abs=1)