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

Add audiofile.has_video() #153

Merged
merged 6 commits into from
Jul 26, 2024
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
1 change: 1 addition & 0 deletions audiofile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from audiofile.core.info import bit_depth
from audiofile.core.info import channels
from audiofile.core.info import duration
from audiofile.core.info import has_video
from audiofile.core.info import samples
from audiofile.core.info import sampling_rate
from audiofile.core.io import convert_to_wav
Expand Down
41 changes: 41 additions & 0 deletions audiofile/core/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,47 @@ def duration(file: str, sloppy=False) -> float:
return samples(file) / sampling_rate(file)


def has_video(file: str) -> bool:
"""If file contains video.

For WAV, FLAC, MP3, or OGG files
``False`` is returned.
All other files are probed by mediainfo.

Args:
file: file name of input file

Returns:
``True`` if file contains video data

Raises:
FileNotFoundError: if mediainfo binary is needed,
but cannot be found
RuntimeError: if ``file`` is missing
and does not end with ``"wav"``, ``"flac"``, ``"mp3"``, ``"ogg"``

Examples:
>>> has_video("stereo.wav")
False

"""
if file_extension(file) in SNDFORMATS:
return False
else:
try:
path = audeer.path(file)
if not os.path.exists(path):
raise RuntimeError(f"'{file}' does not exist.")
cmd = ["mediainfo", "--Inform=Video;%Format%", path]
video_format = run(cmd)
if len(video_format) > 0:
return True
else:
return False
except FileNotFoundError:
raise binary_missing_error("mediainfo")


def samples(file: str) -> int:
"""Number of samples in audio file.

Expand Down
1 change: 1 addition & 0 deletions docs/api-src/audiofile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ audiofile
bit_depth
channels
duration
has_video
samples
sampling_rate
convert_to_wav
14 changes: 12 additions & 2 deletions tests/assets/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Non SND format audio test files
# Test files

## Non SND format audio test files

This folder contains files to test file formats
that cannot be stored with `audiofile.write()`
Expand All @@ -10,5 +12,13 @@ They are excerpts
from "Furious Freak"
and "Galway",
Kevin MacLeod (incompetech.com),
Licensed under Creative Commons:
licensed under Creative Commons:
[CC-BY-3.0](http://creativecommons.org/licenses/by/3.0/).

## Video test files

The folder contains the video file `video.mp4`,
which is a short excerpt from "Big Bunny"
from Blender Foundation | www.blender.org,
licensed under Creative Commons:
[CC-BY-3.0](http://creativecommons.org/licenses/by/3.0/).
Binary file added tests/assets/video.mp4
Binary file not shown.
64 changes: 41 additions & 23 deletions tests/test_audiofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ def test_missing_binaries(tmpdir, hide_system_path, empty_file):
af.duration(empty_file)
with pytest.raises(expected_error, match="mediainfo"):
af.duration(empty_file, sloppy=True)
with pytest.raises(expected_error, match="mediainfo"):
af.has_video(empty_file)
with pytest.raises(expected_error, match="ffmpeg"):
af.samples(empty_file)
with pytest.raises(expected_error, match="mediainfo"):
Expand Down Expand Up @@ -247,6 +249,22 @@ def test_missing_file(tmpdir, ext):
af.convert_to_wav(missing_file, converted_file)


@pytest.mark.parametrize(
"file, expected_error, expected_error_message",
[
("missing_file.bin", RuntimeError, "'missing_file.bin' does not exist"),
("missing_file.mp4", RuntimeError, "'missing_file.mp4' does not exist"),
("missing_file.wav", None, None),
],
)
def test_missing_file_has_video(file, expected_error, expected_error_message):
if expected_error is not None:
with pytest.raises(expected_error, match=expected_error_message):
af.has_video(file)
else:
assert af.has_video(file) is False


@pytest.mark.parametrize(
"non_audio_file",
("bin", "mp4", "wav"),
Expand Down Expand Up @@ -488,40 +506,40 @@ def test_file_type(tmpdir, file_type, magnitude, sampling_rate, channels):
if file_type in ["mp3", "ogg"]:
bit_depth = None
assert af.bit_depth(file) == bit_depth
assert af.has_video(file) is False


def test_other_formats():
files = [
"gs-16b-1c-44100hz.opus",
"gs-16b-1c-8000hz.amr",
"gs-16b-1c-44100hz.m4a",
"gs-16b-1c-44100hz.aac",
]
header_durations = [ # as given by mediainfo
15.839,
15.840000,
15.833,
None,
]
files = [os.path.join(ASSETS_DIR, f) for f in files]
for file, header_duration in zip(files, header_durations):
signal, sampling_rate = af.read(file)
assert af.channels(file) == _channels(signal)
assert af.sampling_rate(file) == sampling_rate
assert af.samples(file) == _samples(signal)
@pytest.mark.parametrize(
"file, header_duration, audio, video", # header duration as given by mediainfo
[
("gs-16b-1c-44100hz.opus", 15.839, True, False),
("gs-16b-1c-8000hz.amr", 15.840000, True, False),
("gs-16b-1c-44100hz.m4a", 15.833, True, False),
("gs-16b-1c-44100hz.aac", None, True, False),
("video.mp4", None, False, True),
],
)
def test_other_formats(file, header_duration, audio, video):
path = os.path.join(ASSETS_DIR, file)
if audio:
signal, sampling_rate = af.read(path)
assert af.channels(path) == _channels(signal)
assert af.sampling_rate(path) == sampling_rate
assert af.samples(path) == _samples(signal)
duration = _duration(signal, sampling_rate)
assert af.duration(file) == duration
assert af.duration(path) == duration
if header_duration is None:
# Here we expect samplewise precision
assert af.duration(file, sloppy=True) == duration
assert af.duration(path, sloppy=True) == duration
else:
# Here we expect limited precision
# as the results differ between soxi and mediainfo
precision = 1
sloppy_duration = round(af.duration(file, sloppy=True), precision)
sloppy_duration = round(af.duration(path, sloppy=True), precision)
header_duration = round(header_duration, precision)
assert sloppy_duration == header_duration
assert af.bit_depth(file) is None
assert af.bit_depth(path) is None
assert af.has_video(path) is video


@pytest.mark.parametrize(
Expand Down