Skip to content

Commit

Permalink
Merge pull request #49 from benfmiller/working
Browse files Browse the repository at this point in the history
Added close_seconds_filter
  • Loading branch information
benfmiller authored Apr 12, 2023
2 parents fe1a54a + fc8e786 commit b19a443
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 9 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# Change Log

## [1.2.2] 2023 - 02 -05
## [1.2.3] 2023 - 04 - 11

### Added

- Added close_seconds_filter option to BaseConfig and decorators on recognize and align functions

### Fixed

- Fingerprint, fine_align, single-threaded bug
- VisualRecognizer multiprocessing on macs

## [1.2.2] 2023 - 02 - 05

### Added

Expand Down
66 changes: 64 additions & 2 deletions audalign/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This file serves as the host file for all audalign functions.
recognitions and alignments use recognizer objects which can be configured with
their respective configuration objects.
their respective configuration objects.
There are a number of functions that can be used to enhance the alignments such
as "uniform leveling" and "remove noise" functions. Writing shifts can also be nifty.
Expand All @@ -15,6 +15,7 @@
import os
from functools import wraps
from pprint import PrettyPrinter
from typing import Optional

from pydub.utils import mediainfo

Expand Down Expand Up @@ -55,6 +56,25 @@ def wrapper_decorator(*args, **kwargs):
return wrapper_decorator


def filter_close_seconds(func):
@wraps(func)
def wrapper_decorator(*args, **kwargs):
results = func(*args, **kwargs)
if results is None:
return results
assert results.get("rankings") is not None #This should run after rankings are added
close_seconds_filter = BaseConfig.close_seconds_filter
if kwargs.get("recognizer") is not None:
close_seconds_filter = kwargs.get("recognizer").config.close_seconds_filter
if close_seconds_filter is None:
return results
if "names_and_paths" in results:
return __filter_close_seconds_alignment(results, close_seconds_filter)
else:
return __filter_close_seconds(results, close_seconds_filter)
return wrapper_decorator

@filter_close_seconds
@add_rankings
def recognize(
target_file: str,
Expand All @@ -66,6 +86,7 @@ def recognize(
return recognizer.recognize(target_file, against_path)


@filter_close_seconds
@add_rankings
def align(
directory_path: str,
Expand Down Expand Up @@ -101,6 +122,7 @@ def align(
)


@filter_close_seconds
@add_rankings
def align_files(
filename_a,
Expand Down Expand Up @@ -143,6 +165,7 @@ def align_files(
)


@filter_close_seconds
@add_rankings
def target_align(
target_file: str,
Expand Down Expand Up @@ -183,6 +206,7 @@ def target_align(
)


@filter_close_seconds
@add_rankings
def fine_align(
results,
Expand Down Expand Up @@ -239,7 +263,7 @@ def fine_align(
max_lags_not_set = False
if recognizer.config.max_lags is None:
recognizer.config.max_lags = 2
max_lags_set = True
max_lags_not_set = True
new_results = aligner._align(
recognizer=recognizer,
filename_list=None,
Expand Down Expand Up @@ -515,6 +539,44 @@ def pretty_print_alignment(results, match_keys="both"):
print("No Matches Found")
print()

def __filter_close_seconds(results: dict, close_seconds_filter: float):
results_iterable_keys = []
# all list items in against_filename dictionary values
len_offset_seconds = len(list(results["match_info"].values())[0]["offset_seconds"])
for key, value in list(results["match_info"].values())[0].items():
if isinstance(value, list) and len(value) == len_offset_seconds:
results_iterable_keys.append(key)
for against_dict in results["match_info"].values():
iter_index_pop = []
unfiltered_offset_seconds = []
if len(against_dict["offset_seconds"]) > 0:
unfiltered_offset_seconds.append(against_dict["offset_seconds"][0])
for i, val in enumerate(against_dict["offset_seconds"][1:]):
# values are already sorted by confidence
match = False
for unfiltered_val in unfiltered_offset_seconds:
if abs(abs(val) - abs(unfiltered_val)) <= close_seconds_filter:
iter_index_pop.append(i+1)
match = True
break
if not match:
unfiltered_offset_seconds.append(val)
for key in results_iterable_keys:
temp_list = against_dict[key]
for i in iter_index_pop[::-1]:
temp_list.pop(i)
against_dict[key] = temp_list
return results

def __filter_close_seconds_alignment(results: dict, close_seconds_filter: float):
match_keys = ["match_info"]
if results.get("fine_match_info") is not None:
match_keys += ["fine_match_info"]
for match_key in match_keys:
for value in results.get(match_key).values():
__filter_close_seconds(value, close_seconds_filter)
return results


def recalc_shifts(
results: dict,
Expand Down
3 changes: 3 additions & 0 deletions audalign/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class BaseConfig(ABC):
# Limits number of matches returned. Defaults to 30.
match_len_filter: typing.Optional[int] = None

# if set, filters out each result if is within x seconds of a result with a stronger confidence
close_seconds_filter: typing.Optional[float] = None

# Filters matches to only count within locality. In seconds
# Not all recognizers have to implement locality
locality: typing.Optional[float] = None
Expand Down
9 changes: 5 additions & 4 deletions audalign/recognizers/fingerprint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,11 @@ def _fingerprint_directory(
result = []

for filename in filenames_to_fingerprint:
file_name = os.path.basename(filename)
if file_name in self.file_names:
print(f"{file_name} already fingerprinted, continuing...")
continue
if isinstance(filename, str): # fine alignments are tuples with offsets
file_name = os.path.basename(filename)
if file_name in self.file_names:
print(f"{file_name} already fingerprinted, continuing...")
continue
file_name, hashes = _fingerprint_worker_directory(filename)
if file_name == None:
continue
Expand Down
2 changes: 1 addition & 1 deletion audalign/recognizers/visrecognize/visrecognize.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def _visrecognize(
)

# calculate all mse and ssim values
if use_multiprocessing == True and sys.platform != "linux":
if use_multiprocessing == True and sys.platform not in ["linux", "darwin"]:

try:
nprocesses = num_processes or multiprocessing.cpu_count()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def parse_requirements(requirements):


PACKAGE_NAME = "audalign"
PACKAGE_VERSION = "1.2.2"
PACKAGE_VERSION = "1.2.3"
SUMMARY = "Audio Alignment and Recognition in Python"

REQUIREMENTS = parse_requirements("requirements.txt")
Expand Down
63 changes: 63 additions & 0 deletions tests/test_align.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
test_folder_eig = "test_audio/test_shifts/"


def ensure_close_seconds_filter(result, close_seconds_filter, initial_filter="match_info"):
for target_file in list(result.get(initial_filter).values()):
for against_file in list(target_file["match_info"].values()):
offset_list = sorted(against_file["offset_seconds"])
start = offset_list[0]
for i in offset_list[1:]:
assert i - start > close_seconds_filter # results within close_seconds_filter
start = i

class TestAlign:
fingerprint_recognizer = ad.FingerprintRecognizer(
load_fingerprints_file="tests/test_fingerprints.json"
Expand Down Expand Up @@ -178,6 +187,31 @@ def test_align_files_cor(self, tmpdir):
)
assert result is not None

@pytest.mark.xfail
def test_align_close_seconds_filter_fail(self, tmpdir):
close_seconds_filter = 10
recognizer = ad.FingerprintRecognizer()
recognizer.config.multiprocessing = False
result = ad.align(
"test_audio/test_shifts",
tmpdir,
recognizer=recognizer,
)
assert result
ensure_close_seconds_filter(result, close_seconds_filter)

def test_align_close_seconds_filter(self, tmpdir):
close_seconds_filter = 10
recognizer = ad.FingerprintRecognizer()
recognizer.config.close_seconds_filter = close_seconds_filter
recognizer.config.multiprocessing = False
result = ad.align(
"test_audio/test_shifts",
tmpdir,
recognizer=recognizer,
)
assert result
ensure_close_seconds_filter(result, close_seconds_filter)

class TestTargetAlign:
def test_target_align_vis(self, tmpdir):
Expand Down Expand Up @@ -282,6 +316,7 @@ def test_fine_align_spec(self):
recognizer.config.multiprocessing = False
result = ad.fine_align(
self.align_fing_results,
recognizer=recognizer,
)
assert result is not None

Expand Down Expand Up @@ -336,6 +371,34 @@ def test_fine_align_visual(self, tmpdir):
assert result is not None
ad.pretty_print_alignment(result, match_keys="fine_match_info")

def test_fine_align_close_seconds_filter(self):
close_seconds_filter = 10
recognizer = ad.FingerprintRecognizer(
load_fingerprints_file="tests/test_fingerprints.json"
)
recognizer.config.close_seconds_filter = close_seconds_filter
recognizer.config.multiprocessing = False
result = ad.fine_align(
self.align_fing_results,
recognizer=recognizer,
)
assert result is not None
ensure_close_seconds_filter(result, close_seconds_filter, "fine_match_info")

@pytest.mark.xfail
def test_fine_align_close_seconds_filter_fail(self):
close_seconds_filter = 10
recognizer = ad.FingerprintRecognizer(
load_fingerprints_file="tests/test_fingerprints.json"
)
recognizer.config.multiprocessing = False
result = ad.fine_align(
self.align_fing_results,
recognizer=recognizer,
)
assert result is not None
ensure_close_seconds_filter(result, close_seconds_filter, "fine_match_info")

def test_fine_align_options(self, tmpdir):
recognizer = ad.CorrelationRecognizer()
recognizer.config.sample_rate = 8000
Expand Down

0 comments on commit b19a443

Please sign in to comment.