Source code for hum.gen.diagnosis_sounds

"""
Sounds that are meant to diagnose audio pipelines
"""
from numpy import (
    array,
    hstack,
    ones,
    ceil,
    zeros,
    floor,
    argmin,
    diff,
    where,
    reshape,
    math,
    arange,
    iinfo,
    int16,
    linspace,
    pi,
    unique,
    tile,
    repeat,
    all,
)
from numpy.random import randint
from itertools import islice, count
from datetime import datetime as dt

second_ms = 1000.0
epoch = dt.utcfromtimestamp(0)


[docs]def utcnow_ms(): """ Returns the current time in UTC in milliseconds """ return (dt.utcnow() - epoch).total_seconds() * second_ms
[docs]def window(seq, n=2): """ Returns a sliding window (of width n) over data from the iterable s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... """ it = iter(seq) result = tuple(islice(it, n)) if len(result) == n: yield result for elem in it: result = result[1:] + (elem,) yield result
[docs]def ums_to_01_array(ums, n_ums_bits): """ Converts ums to an array with length n_ums_bits equivalent to a binary representation of ums assert all(ums_to_01_array(100,1) == [1, 1, 0, 0, 1, 0, 0]) """ ums_bits_str_format = '{:0' + str(n_ums_bits) + 'b}' return array([int(x == '1') for x in ums_bits_str_format.format(ums)])
class BinarySound(object): def __init__( self, redundancy, repetition, nbits=50, header_size_words=1, header_pattern=None, ): """ :param redundancy: num of times a word will be repeated (to implement error correction) :param repetition: num of times to repeat each element of a pattern. :param nbits: num of bits of a pattern. An {0,1}-array to be encoded will have to be this size. Also, if a header_pattern is explicitly given, it will also have to be this size. :param header_size_words: the size (in num of words) of the header :param header_pattern: specifies the header pattern. This pattern will be repeated (i.e. it's elements repeated several times (010-->000011110000) and tiled (header_size_words times). Can be: * None: Default. Will choose a random array of 0s and 1s * an explicit array of nbits 0s and 1s to use for the header * a string indicating a method to generate it (for now, choices are "halfhalf" or "alternating") # TODO: Repair: TypeError: unsupported operand type(s) for *: 'int' and 'map' >>> from numpy import * >>> from numpy.random import randint >>> >>> nbits=50 >>> bs = BinarySound( ... nbits=nbits, redundancy=142, repetition=3, header_size_words=1) >>> utc = randint(0, 2, nbits) >>> wf = bs.mk_phrase(utc) >>> print(bs) {'repetition': 3, 'word_size_frm': 150, 'redundancy': 142, 'phrase_data_frm': 21300} >>> all(utc == bs.decode(wf)) True """ self.nbits = nbits self.header_size_words = header_size_words # repetition: how many times to repeat each bit to make a word self.repetition = repetition # word_size_frm: the size of a word, in frames self.word_size_frm = int(self.nbits * self.repetition) # redundancy: how many times to repeat a word to make (along with header) a phrase self.redundancy = redundancy if header_pattern is None: header_pattern = randint(0, 2, nbits) elif isinstance(header_pattern, str): if header_pattern == 'halfhalf': header_pattern = hstack( (ones(int(ceil(nbits / 2))), zeros(int(floor(nbits / 2)))) ).astype(int) elif header_pattern == 'alternating': header_pattern = array([1, 0] * int(ceil(nbits / 2)))[:nbits] else: raise ValueError( 'header_pattern not recognized: {}'.format(header_pattern) ) else: assert ( len(header_pattern) == nbits ), 'header_pattern must have nbits={}'.format(nbits) assert set(unique(header_pattern).astype(int)) == { 0, 1, }, 'header_pattern must be an array of 0s and 1s' self.header_pattern = header_pattern self.header_word = tile( repeat(self.header_pattern, self.repetition), self.header_size_words, ) self.phrase_data_frm = self.redundancy * self.word_size_frm @classmethod def for_audio_params( cls, nbits=50, freq=3000, chk_size_frm=43008, sr=44100, header_size_words=1, header_pattern=None, ): """ Construct a BinarySound object for a set of audio params :param nbits: num of bits of a word of data we want to encode :param freq: frequency (num of times per second the bits will be repeated -- with sr, will determine repetition) :param chk_size_frm: chunk size (in frames) of the sounds we'll be using (determines :param sr: sample rate of the targeted sound :param header_size_words: header size (increasing it will decrease error rate, but increase computation time) :param header_pattern: specifies the header pattern. This pattern will be repeated (i.e. it's elements repeated several times (010-->000011110000) and tiled (header_size_words times). Can be: * None: Default. Will choose a random array of 0s and 1s * an explicit array of nbits 0s and 1s to use for the header * a string indicating a method to generate it (for now, choices are "halfhalf" or "alternating") :return: a BinarySound object >>> from numpy import * >>> from numpy.random import randint >>> >>> nbits=50 >>> bs = BinarySound.for_audio_params( ... nbits=nbits, freq=6000, chk_size_frm=43008, sr=44100, header_size_words=1) >>> utc = randint(0, 2, nbits) >>> wf = bs.mk_phrase(utc) >>> print(bs) {'repetition': 3, 'word_size_frm': 150, 'redundancy': 142, 'phrase_data_frm': 21300} >>> all(utc == bs.decode(wf)) True """ # repetition: how many times to repeat each bit to make a word repetition = int(floor(sr / (2 * freq))) # word_size_frm: the size of a word, in frames word_size_frm = int(nbits * repetition) # redundancy: how many times to repeat a word to make (along with header) a phrase redundancy = int(floor((chk_size_frm / 2) / word_size_frm) - header_size_words) self = cls( nbits=nbits, redundancy=redundancy, repetition=repetition, header_size_words=header_size_words, header_pattern=header_pattern, ) self.freq = freq self.sr = sr self.chk_size_frm = chk_size_frm self.redundancy = redundancy self.repetition = repetition return self def mk_phrase(self, bit_array): repeated_bit_array = repeat(bit_array, self.repetition) tiled = tile(repeated_bit_array, self.redundancy) wf = hstack((self.header_word, tiled)) return (2 * wf) - 1 def mk_utc_phrases(self, sound_duration_s=12): wf = list() def wf_seconds(wf): return len(wf) / float(self.sr) while wf_seconds(wf) < sound_duration_s: bit_array = ums_to_01_array(ums=int(utcnow_ms()), n_ums_bits=self.nbits) wf += list(self.mk_phrase(bit_array)) return array(wf)[: int(sound_duration_s * self.sr)] def header_position(self, wf): wf = wf > 0 return argmin(slow_mask(wf, self.header_word)) def decode(self, wf): header_pos = self.header_position(wf) header_end_idx = header_pos + len(self.header_word) wf = wf > 0 m = wf[header_end_idx : (header_end_idx + self.phrase_data_frm)] size_to_make_it_a_multiple_of_word_size = len(m) - (len(m) % self.word_size_frm) m = m[:size_to_make_it_a_multiple_of_word_size] m = reshape(m, (-1, self.word_size_frm)) m = m.sum(axis=0).reshape((-1, self.repetition)) m = m.sum(axis=1) return (m / float(self.repetition * self.redundancy) > 0.5).astype(int) def __repr__(self): return str( { 'repetition': self.repetition, 'word_size_frm': self.word_size_frm, 'redundancy': self.redundancy, 'phrase_data_frm': self.phrase_data_frm, } ) def zero_crossing_gaps(wf): w = wf > 0 ww = hstack((w, ~w[-1])) return diff(hstack(([0], 1 + where(diff(ww))[0]))) def slow_mask(arr, msk): msk = array(msk) arr_msk_dist = list() for w in window(arr, len(msk)): arr_msk_dist.append(sum(abs(array(w) - msk))) return arr_msk_dist
[docs]class WfGen(object): """ >>> wfgen = WfGen(sr=44100, buf_size_frm=2048, amplitude=0.5) >>> lookup = wfgen.mk_lookup_table(freq=4400) >>> assert len(lookup) == 10 >>> wfgen.mk_sine_wf(n_frm=5, freq=4400) array([0. , 0.293316 , 0.47508605, 0.47618432, 0.29619315]) """ def __init__(self, sr=44100, buf_size_frm=2048, amplitude=0.5): self.sr = sr self.buf_size_frm = buf_size_frm self.buf_size_s = buf_size_frm / float(self.sr) if amplitude > 1.0: amplitude = 1.0 if amplitude < 0.0: amplitude = 0.0 self.amplitude = float(amplitude) self.lookup_table_freqs = (arange(int(buf_size_frm / 2)) + 1) / self.buf_size_s self.lookup_tables = list(map(self.mk_lookup_table, self.lookup_table_freqs)) def mk_sine_wave_from_lookup_table(self, lookup_table): period = len(lookup_table) return (lookup_table[i % period] for i in count(0)) def mk_sine_wave_iterator(self, freq=440): if isinstance(freq, (int, float)): table_idx = where(self.lookup_table_freqs == freq)[0] if len(table_idx) > 0: table_idx = table_idx[0] lookup_table = self.lookup_tables[table_idx] else: lookup_table = self.mk_lookup_table(freq=freq) return self.mk_sine_wave_from_lookup_table(lookup_table) else: # consider freq to already be a lookup table return self.mk_sine_wave_from_lookup_table(freq) def mk_sine_wf(self, n_frm, freq=440): it = self.mk_sine_wave_iterator(freq) return array([x for x in islice(it, int(n_frm))]) def mk_lookup_table(self, freq=440): freq = float(freq) period = int(self.sr / freq) lookup_table = [ self.amplitude * math.sin( 2.0 * math.pi * float(freq) * (float(i % period) / float(self.sr)) ) for i in range(period) ] return lookup_table def mk_wf_from_freq_weight_array(self, n_frm, freq_weight_array): wf = zeros(n_frm) for i, w in enumerate(freq_weight_array): wf += w * self.mk_sine_wf(n_frm, self.lookup_tables[i]) return wf
[docs]class TimeSound(WfGen): def __init__(self, sr=44100, buf_size_frm=2048, amplitude=0.5, n_ums_bits=30): super(TimeSound, self).__init__( sr=sr, buf_size_frm=buf_size_frm, amplitude=amplitude ) self.n_ums_bits = n_ums_bits self.ums_bits_str_format = '{:0' + str(n_ums_bits) + 'b}' self.n_freqs_per_ums_bit = len(self.lookup_tables) // self.n_ums_bits self.n_freqs_for_ums = self.n_freqs_per_ums_bit * self.n_ums_bits self.buf_size_ms = self.buf_size_s * 1000 def ums_to_01_array(self, ums): return array([int(x == '1') for x in self.ums_bits_str_format.format(ums)]) def freq_weight_array_for_ums(self, ums): return tile(self.ums_to_01_array(ums), self.n_freqs_per_ums_bit) def ums_to_wf(self, ums, n_bufs=1): return self.mk_wf_from_freq_weight_array( n_frm=n_bufs * self.buf_size_frm, freq_weight_array=self.freq_weight_array_for_ums(ums), ) def timestamped_wf(self, offset_ums=0, n_bufs=21, n_bufs_per_tick=1): wf = list() ums = offset_ums for buf_idx in range(n_bufs): wf.extend(list(self.ums_to_wf(ums, n_bufs=n_bufs_per_tick))) ums = int(ums + n_bufs_per_tick * self.buf_size_ms) return array(wf) def spectr_of_time(self, offset_ums=0, n_bufs=21, n_bufs_per_tick=1): wf = list() ums = offset_ums for buf_idx in range(n_bufs): wf.append(list(self.freq_weight_array_for_ums(ums))) if n_bufs_per_tick > 1: wf.append(list(zeros(self.n_freqs_for_ums))) ums = int(ums + n_bufs_per_tick * self.buf_size_ms) wf = array(wf) return hstack( (wf, zeros((wf.shape[0], int(self.buf_size_frm / 2 - wf.shape[1]))),) )
import soundfile as sf DFLT_SR = 44100 DFLT_BLEEP_LOC = 400 DFLT_BLEEP_SPEC = 100
[docs]def mk_some_buzz_wf(sr=44100): """ >>> sr = 10 >>> wf = mk_some_buzz_wf(sr = sr) >>> assert len(wf) == 5*sr """ from scipy import signal # pip install scipy bleep_wf = signal.sawtooth(pi * (sr / 10) * linspace(0, 1, int(5 * sr))) bleep_wf += randint(-1, 1, len(bleep_wf)) return ((bleep_wf / 2) * iinfo(int16).max).astype(int16)
[docs]def wf_with_timed_bleeps( n_samples=DFLT_SR * 2, bleep_loc=DFLT_BLEEP_LOC, bleep_spec=DFLT_BLEEP_SPEC, sr=DFLT_SR, ): """Not sure this works as expected. Docs needed.""" if isinstance(bleep_spec, (int, float)): bleep_size = int(bleep_spec) bleep_spec = mk_some_buzz_wf(sr)[:bleep_size] if isinstance(bleep_loc, (int, float)): bleep_loc = range(0, n_samples, bleep_loc) bleep_size = len(bleep_spec) # bleep_loc = (sr * (array(bleep_loc_ms) / 1000)).astype(int) # max_bleep_loc = max(bleep_size) + len(bleep_spec) wf = zeros(n_samples) for loc_frm in bleep_loc: wf[loc_frm : (loc_frm + bleep_size)] = bleep_spec return wf
def mk_sounds_with_timed_bleeps( n_samples=DFLT_SR * 2, bleep_loc=DFLT_BLEEP_LOC, bleep_spec=DFLT_BLEEP_SPEC, sr=DFLT_SR, save_filepath='bleeps.wav', ): wf = wf_with_timed_bleeps( n_samples=n_samples, bleep_loc=bleep_loc, bleep_spec=bleep_spec, sr=sr ) sf.write(save_filepath, data=wf, samplerate=sr, format='WAV')