diff --git a/detect_test.py b/detect_test.py index 149bc60..b2b023b 100644 --- a/detect_test.py +++ b/detect_test.py @@ -9,7 +9,7 @@ print("Requires a audio path provided. Either file path, or URL.") exit(0) -detect_result = tone_detect(audio_path) +detect_result = tone_detect(audio_path, time_resolution_ms=50, debug=True) if len(detect_result.two_tone_result) == 0 and len(detect_result.long_result) == 0 and len(detect_result.hi_low_result) == 0: print("No tones") diff --git a/pyproject.toml b/pyproject.toml index 7a25ffb..71684dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "icad_tone_detection" -version = "0.7" +version = "0.8" authors = [ {name = "TheGreatCodeholio", email = "ian@icarey.net"}, ] diff --git a/src/icad_tone_detection/main.py b/src/icad_tone_detection/main.py index cd066d1..8725fa4 100644 --- a/src/icad_tone_detection/main.py +++ b/src/icad_tone_detection/main.py @@ -1,6 +1,6 @@ from .audio_loader import load_audio from .frequency_extraction import FrequencyExtraction -from .tone_detection import detect_quickcall, detect_long_tones, detect_warble_tones +from .tone_detection import detect_two_tone, detect_long_tones, detect_warble_tones class ToneDetectionResult: @@ -10,8 +10,8 @@ def __init__(self, two_tone_result, long_result, hi_low_result): self.hi_low_result = hi_low_result -def tone_detect(audio_path, matching_threshold=2, time_resolution_ms=100, hi_low_interval=0.2, - hi_low_min_alternations=2, debug=False): +def tone_detect(audio_path, matching_threshold=2, time_resolution_ms=100, tone_a_min_length=0.8, tone_b_min_length=2.8, hi_low_interval=0.2, + hi_low_min_alternations=3, long_tone_min_length=2.0, debug=False): """ Loads audio from various sources including local path, URL, BytesIO object, or a PyDub AudioSegment. @@ -21,7 +21,10 @@ def tone_detect(audio_path, matching_threshold=2, time_resolution_ms=100, hi_low are considered a match. For example, a threshold of 2 means that two frequencies are considered matching if they are within 2% of each other. - time_resolution_ms (int): The time resolution in milliseconds for the STFT. Default is 100ms. - - hi_low_interval (float): The maximum allowed interval in seconds between two consecutive alternating tones. Default is 0.2 + - tone_a_min_length (float): The minimum length in seconds of an A tone for two tone detections. Default 0.8 Seconds + - tone_b_min_length (float): The minimum length in seconds of a B tone for two tone detections. Default 2.8 Seconds + - long_tone_min_length (float): The minimum length a long tone needs to be to consider it a match. Default 2.0 Seconds + - hi_low_interval (float): The maximum allowed interval in seconds between two consecutive alternating tones. Default is 0.2 Seconds - hi_low_min_alternations (int): The minimum number of alternations for a hi-low warble tone sequence to be considered valid. Default 2 - debug (bool): If debug is enabled, print all tones found in audio file. Default is False @@ -39,7 +42,7 @@ def tone_detect(audio_path, matching_threshold=2, time_resolution_ms=100, hi_low if debug is True: print("Matched frequencies: ", matched_frequencies) - two_tone_result = detect_quickcall(matched_frequencies) - long_result = detect_long_tones(matched_frequencies, two_tone_result) + two_tone_result = detect_two_tone(matched_frequencies, tone_a_min_length, tone_b_min_length) + long_result = detect_long_tones(matched_frequencies, two_tone_result, long_tone_min_length) hi_low_result = detect_warble_tones(matched_frequencies, hi_low_interval, hi_low_min_alternations) return ToneDetectionResult(two_tone_result, long_result, hi_low_result) diff --git a/src/icad_tone_detection/tone_detection.py b/src/icad_tone_detection/tone_detection.py index 779f5f8..ffcdc99 100644 --- a/src/icad_tone_detection/tone_detection.py +++ b/src/icad_tone_detection/tone_detection.py @@ -1,54 +1,86 @@ -def detect_quickcall(frequency_matches): - qc2_matches = [] +# def detect_quickcall(frequency_matches): +# qc2_matches = [] +# tone_id = 0 +# last_set = None +# if not frequency_matches or len(frequency_matches) < 1: +# return qc2_matches +# for x in frequency_matches: +# if last_set is None and len(x[2]) >= 8 and 0 not in x[2] and 0.0 not in x[2]: +# last_set = x +# else: +# if len(x[2]) >= 8 and 0 not in x[2] and 0.0 not in x[2]: +# if len(last_set[2]) <= 12 and len(x[2]) >= 28: +# tone_data = {"tone_id": f'qc_{tone_id + 1}', "detected": [last_set[2][0], x[2][0]], +# "start": last_set[0], "end": x[1]} +# tone_id += 1 +# qc2_matches.append(tone_data) +# last_set = x +# else: +# last_set = x +# +# return qc2_matches + + +def detect_two_tone(frequency_matches, min_tone_a_length=0.8, min_tone_b_length=2.8): + two_tone_matches = [] tone_id = 0 last_set = None if not frequency_matches or len(frequency_matches) < 1: - return qc2_matches - for x in frequency_matches: - if last_set is None and len(x[2]) >= 8 and 0 not in x[2] and 0.0 not in x[2]: - last_set = x - else: - if len(x[2]) >= 8 and 0 not in x[2] and 0.0 not in x[2]: - if len(last_set[2]) <= 12 and len(x[2]) >= 28: - tone_data = {"tone_id": f'qc_{tone_id + 1}', "detected": [last_set[2][0], x[2][0]], - "start": last_set[0], "end": x[1]} + return two_tone_matches + + for current_set in frequency_matches: + if all(f > 0 for f in current_set[2]): # Ensure frequencies are non-zero + current_duration = current_set[1] - current_set[0] # Calculate the duration of the current tone + + if last_set is None: + last_set = current_set + else: + last_duration = last_set[1] - last_set[0] # Calculate the duration of the last tone + # Check if the last tone is a valid A tone and the current is a valid B tone + if last_duration >= min_tone_a_length and current_duration >= min_tone_b_length: + tone_data = { + "tone_id": f'qc_{tone_id + 1}', + "detected": [last_set[2][0], current_set[2][0]], # Frequency values of A and B tones + "start": last_set[0], # Start time of tone A + "end": current_set[1] # End time of tone B + } tone_id += 1 - qc2_matches.append(tone_data) - last_set = x - else: - last_set = x + two_tone_matches.append(tone_data) + # Update last_set to current_set for next iteration + last_set = current_set - return qc2_matches + return two_tone_matches -def detect_long_tones(frequency_matches, detected_quickcall): - tone_id = 0 +def detect_long_tones(frequency_matches, detected_quickcall, min_duration=2.0): long_tone_matches = [] - excluded_frequencies = set([]) + excluded_frequencies = set([0.0]) # Initializing with 0.0 Hz to exclude it - if not frequency_matches or len(frequency_matches) < 1: - return long_tone_matches - - last_set = frequency_matches[0] - # add detected quick call tones to a list, so we can exclude them from long tone matches. - for ttd in detected_quickcall: - excluded_frequencies.update(ttd["detected"][:2]) - - for x in frequency_matches: - if len(x[2]) >= 10: - if 12 >= len(last_set) >= 8 and len(x[2]) >= 20: - last_set = x[2] - elif len(x[2]) >= 15: - if x[2][0] == 0 or x[2][0] == 0.0: - continue - if x[2][0] in excluded_frequencies: - continue - - if x[2][0] > 250: - tone_data = {"tone_id": f'lt_{tone_id + 1}', "detected": x[2][0], "start": round(x[0], 3), - "end": round(x[1], 3)} - tone_id += 1 - long_tone_matches.append(tone_data) + # Add detected quick call tones to the excluded list + for quickcall in detected_quickcall: + excluded_frequencies.update(quickcall["detected"][:2]) + + for start, end, frequencies in frequency_matches: + duration = end - start + if not frequencies: + continue + + current_frequency = frequencies[0] + + # Skip the loop iteration if the current frequency is in the excluded frequencies + if current_frequency in excluded_frequencies or current_frequency <= 500: + continue + + # Check if the duration meets the minimum requirement + if duration >= min_duration: + tone_data = { + "tone_id": f"lt_{len(long_tone_matches) + 1}", + "detected": current_frequency, + "start": start, + "end": end, + "length": duration + } + long_tone_matches.append(tone_data) return long_tone_matches