Skip to content

Commit

Permalink
Add audiofile.has_video() (#153)
Browse files Browse the repository at this point in the history
* Add audiofile.has_video()

* Fix tests

* Add missing video test assest

* Fix return type

* Raise RuntimeError if file does not exist

* Only check for non SND files
  • Loading branch information
hagenw authored Jul 26, 2024
1 parent a8a130b commit 027e21b
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 25 deletions.
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

0 comments on commit 027e21b

Please sign in to comment.