from numpy import fft, array as nparray, maximum, log, hanning, mean, abs, round
from typing import Dict, List, Set, Sequence, Union, Optional, Any
from struct import pack, unpack
from enum import IntEnum
from copy import copy
HANNING_MATRIX = hanning(2050)[1:-1]
from signature_format import DecodedMessage, FrequencyPeak, RawSignatureHeader, FrequencyBand
class RingBuffer(list):
def __init__(self, buffer_size : int, default_value : Any = None):
if default_value is not None:
list.__init__(self, [copy(default_value) for position in range(buffer_size)])
else:
list.__init__(self, [None] * buffer_size)
self.position : int = 0
self.buffer_size : int = buffer_size
self.num_written : int = 0
def append(self, value : Any):
self[self.position] = value
self.position += 1
self.position %= self.buffer_size
self.num_written += 1
class SignatureGenerator:
def __init__(self):
self.input_pending_processing : List[int] = []
self.samples_processed : int = 0
self.ring_buffer_of_samples : RingBuffer[int] = RingBuffer(buffer_size = 2048, default_value = 0)
self.fft_outputs : RingBuffer[List[float]] = RingBuffer(buffer_size = 256, default_value = [0. * 1025])
self.spread_ffts_output : RingBuffer[List[float]] = RingBuffer(buffer_size = 256, default_value = [0] * 1025)
self.MAX_TIME_SECONDS = 3.1
self.MAX_PEAKS = 255
self.next_signature = DecodedMessage()
self.next_signature.sample_rate_hz = 16000
self.next_signature.number_samples = 0
self.next_signature.frequency_band_to_sound_peaks = {}
def feed_input(self, s16le_mono_samples : List[int]):
self.input_pending_processing += s16le_mono_samples
def get_next_signature(self) -> Optional[DecodedMessage]:
if len(self.input_pending_processing) - self.samples_processed < 128:
return None
while (len(self.input_pending_processing) - self.samples_processed >= 128 and
(self.next_signature.number_samples / self.next_signature.sample_rate_hz < self.MAX_TIME_SECONDS or
sum(len(peaks) for peaks in self.next_signature.frequency_band_to_sound_peaks.values()) < self.MAX_PEAKS
)):
self.process_input(self.input_pending_processing[self.samples_processed:self.samples_processed + 128])
self.samples_processed += 128
returned_signature = self.next_signature
self.next_signature = DecodedMessage()
self.next_signature.sample_rate_hz = 16000
self.next_signature.number_samples = 0
self.next_signature.frequency_band_to_sound_peaks = {}
self.ring_buffer_of_samples : RingBuffer[int] = RingBuffer(buffer_size = 2048, default_value = 0)
self.fft_outputs : RingBuffer[List[float]] = RingBuffer(buffer_size = 256, default_value = [0. * 1025])
self.spread_ffts_output : RingBuffer[List[float]] = RingBuffer(buffer_size = 256, default_value = [0] * 1025)
return returned_signature
def process_input(self, s16le_mono_samples : List[int]):
self.next_signature.number_samples += len(s16le_mono_samples)
for position_of_chunk in range(0, len(s16le_mono_samples), 128):
self.do_fft(s16le_mono_samples[position_of_chunk:position_of_chunk + 128])
self.do_peak_spreading_and_recognition()
def do_fft(self, batch_of_128_s16le_mono_samples):
self.ring_buffer_of_samples[
self.ring_buffer_of_samples.position:
self.ring_buffer_of_samples.position + len(batch_of_128_s16le_mono_samples)
] = batch_of_128_s16le_mono_samples
self.ring_buffer_of_samples.position += len(batch_of_128_s16le_mono_samples)
self.ring_buffer_of_samples.position %= 2048
self.ring_buffer_of_samples.num_written += len(batch_of_128_s16le_mono_samples)
excerpt_from_ring_buffer : list = (
self.ring_buffer_of_samples[self.ring_buffer_of_samples.position:] +
self.ring_buffer_of_samples[:self.ring_buffer_of_samples.position]
)
fft_results : nparray = fft.rfft(HANNING_MATRIX * excerpt_from_ring_buffer)
assert len(fft_results) == 1025 and len(excerpt_from_ring_buffer) == 2048 == len(HANNING_MATRIX)
fft_results = (fft_results.real ** 2 + fft_results.imag ** 2) / (1 << 17)
fft_results = maximum(fft_results, 0.0000000001)
self.fft_outputs.append(fft_results)
def do_peak_spreading_and_recognition(self):
self.do_peak_spreading()
if self.spread_ffts_output.num_written >= 46:
self.do_peak_recognition()
def do_peak_spreading(self):
origin_last_fft : List[float] = self.fft_outputs[self.fft_outputs.position - 1]
spread_last_fft : List[float] = list(origin_last_fft)
for position in range(1025):
if position < 1023:
spread_last_fft[position] = max(spread_last_fft[position:position + 3])
max_value = spread_last_fft[position]
for former_fft_num in [-1, -3, -6]:
former_fft_output = self.spread_ffts_output[(self.spread_ffts_output.position + former_fft_num) % self.spread_ffts_output.buffer_size]
former_fft_output[position] = max_value = max(former_fft_output[position], max_value)
self.spread_ffts_output.append(spread_last_fft)
pass
def do_peak_recognition(self):
fft_minus_46 = self.fft_outputs[(self.fft_outputs.position - 46) % self.fft_outputs.buffer_size]
fft_minus_49 = self.spread_ffts_output[(self.spread_ffts_output.position - 49) % self.spread_ffts_output.buffer_size]
fft_minus_53 = self.spread_ffts_output[(self.spread_ffts_output.position - 53) % self.spread_ffts_output.buffer_size]
fft_minus_45 = self.spread_ffts_output[(self.spread_ffts_output.position - 45) % self.spread_ffts_output.buffer_size]
for bin_position in range(10, 1015):
if (fft_minus_46[bin_position] >= 1 / 64 and
fft_minus_46[bin_position] >= fft_minus_49[bin_position - 1]):
max_neighbor_in_fft_minus_49 = 0
for neighbor_offset in [*range(-10, -3, 3), -3, 1, *range(2, 9, 3)]:
max_neighbor_in_fft_minus_49 = max(fft_minus_49[bin_position + neighbor_offset], max_neighbor_in_fft_minus_49)
if fft_minus_46[bin_position] > max_neighbor_in_fft_minus_49:
max_neighbor_in_other_adjacent_ffts = max_neighbor_in_fft_minus_49
for other_offset in [-53, -45, *range(165, 201, 7), *range(214, 250, 7)]:
max_neighbor_in_other_adjacent_ffts = max(
self.spread_ffts_output[(self.spread_ffts_output.position + other_offset) % self.spread_ffts_output.buffer_size][bin_position - 1],
max_neighbor_in_other_adjacent_ffts
)
if fft_minus_46[bin_position] > max_neighbor_in_other_adjacent_ffts:
fft_number = self.spread_ffts_output.num_written - 46
peak_magnitude = log(max(1 / 64, fft_minus_46[bin_position])) * 1477.3 + 6144
peak_magnitude_before = log(max(1 / 64, fft_minus_46[bin_position - 1])) * 1477.3 + 6144
peak_magnitude_after = log(max(1 / 64, fft_minus_46[bin_position + 1])) * 1477.3 + 6144
peak_variation_1 = peak_magnitude * 2 - peak_magnitude_before - peak_magnitude_after
peak_variation_2 = (peak_magnitude_after - peak_magnitude_before) * 32 / peak_variation_1
corrected_peak_frequency_bin = bin_position * 64 + peak_variation_2
assert peak_variation_1 > 0
frequency_hz = corrected_peak_frequency_bin * (16000 / 2 / 1024 / 64)
if frequency_hz < 250:
continue
elif frequency_hz < 520:
band = FrequencyBand._250_520
elif frequency_hz < 1450:
band = FrequencyBand._520_1450
elif frequency_hz < 3500:
band = FrequencyBand._1450_3500
elif frequency_hz <= 5500:
band = FrequencyBand._3500_5500
else:
continue
if band not in self.next_signature.frequency_band_to_sound_peaks:
self.next_signature.frequency_band_to_sound_peaks[band] = []
self.next_signature.frequency_band_to_sound_peaks[band].append(
FrequencyPeak(fft_number, int(peak_magnitude), int(corrected_peak_frequency_bin), 16000)
)