[PYTHON] Essayez d'implémenter un algorithme d'empreinte vocale de type Shazam

Comme Shazam, une technique appelée ** Audio Fingerprinting ** est utilisée pour identifier rapidement les informations musicales qui sont lues à partir de la voix enregistrée sur le smartphone (également dans les grandes lignes de la récupération de musique basée sur le contenu). entrer). Cette technologie de recherche ne semble pas très simple compte tenu de la qualité sonore basse fidélité des smartphones, du bruit tel que les sons environnementaux et de l'énorme base de données musicale, mais en réalité, elle a été mise en pratique dans divers services il y a longtemps. Il a également été installé à Siri. Je pense qu'il existe plusieurs approches différentes de l'empreinte vocale, mais la méthode la plus courante et la plus facile à comprendre consiste à "hacher et enregistrer / récupérer les pics du spectrogramme". On l'appelle ** «Empreinte digitale basée sur un repère» ** dans le quartier. Cette fois, je voudrais mettre en œuvre la technologie des empreintes digitales avec cette méthode. Consultez ce blog pour référence: Audio Fingerprinting with Python and Numpy Et ce papier: Wang, Avery. "An Industrial Strength Audio Search Algorithm." Ismir. Vol. 2003. 2003.

LabROSA a un projet open source appelé AUD FPRINT: AUDFPRINT - Audio fingerprint database creation + query

Conversion de spectrogramme et détection de pic

Commencez par STFT le signal vocal, puis trouvez le point de crête sur l'écran T-F. Ici, recherchez à la manière du traitement d'image. Un filtre maximum scipy est appliqué au spectrogramme, et les points où le résultat et la valeur du spectrogramme d'origine sont égaux sont considérés comme des pics (les points avec des valeurs plus petites sont exclus sans condition). Il peut être bon d'appliquer un filtre passe-bas 2D au prétraitement.

analyze.py


def find_peaks(y,size):
    """
    y:Signal vocal
    size:taille maximale du filtre(Contrôle de la densité des pics)
    """
    sgram = np.abs(stft(y,n_fft=512,hop_length=256))
    sgram_max = ndi.maximum_filter(sgram,size=size,mode="constant")
    maxima = (sgram==sgram_max) & (sgram > 0.2)
    peaks_freq,peaks_time = np.where(maxima)
    return peaks_freq,peaks_time

J'ai pu obtenir une liste des points de crête en utilisant numpy.where. Aussi appelé un point de repère.

Appariement de pics → hachage

Un seul pic n'a que des informations sur la fréquence de ce pic (le timing de la requête est arbitraire, donc la position temporelle du pic n'a pas de sens) et il ne peut pas être utilisé tel quel par hachage. Par conséquent, nous connectons les pics voisins pour créer deux pics en une seule empreinte digitale. Cette paire de pics contient des informations ** (fréquence de pic 1, fréquence de pic 2, différence de temps) **, et vous pouvez concaténer ces valeurs pour créer un hachage moins dupliqué. Je vais. Plus précisément, comme le montre la figure, les points situés dans une certaine plage en avant lorsqu'ils sont vus à partir d'un certain point d'ancrage sont connectés.

shazam1.png

Dans le code ci-dessous, après avoir sélectionné une paire, ** (fréquence de crête 1, différence de fréquence, différence de temps) ** est emballé dans un nombre de 20 bits et haché.

analyze.py


F1_BITS = 8
DF_BITS = 6
DT_BITS = 6

B1_MASK = (1<<F1_BITS) - 1
B1_SHIFT = DF_BITS + DT_BITS
DF_MASK = (1<<DF_BITS) - 1
DF_SHIFT = DT_BITS
DT_MASK = (1<<DT_BITS) - 1

def peaks_to_landmarks(peaks_freq,peaks_time,target_dist=30,target_time=30,target_freq=30):
    peak_mat = np.zeros((np.max(peaks_freq)+target_freq,np.max(peaks_time)+target_dist+target_time),dtype=np.bool)
    peak_mat[peaks_freq,peaks_time] = True
    list_landmarks = []
    for pfreq,ptime in zip(peaks_freq,peaks_time):
        #(pfreq,ptime) -- current anchor
        target_mask = np.zeros(peak_mat.shape,dtype=np.bool)
        target_mask[pfreq-target_freq:pfreq+target_freq,ptime+target_dist:py+target_dist+target_time] = 1
        #target_mask:Spécifiez la plage pour sélectionner le pic cible
        targets_freq,targets_time = np.where(peak_mat & target_mask)
        for pfreq_target,ptime_target in zip(targets_freq,targets_time):
            dtime = ptime_target-ptime
            #Construire un hachage
            hsh = (((pfreq & B1_MASK)<<B1_SHIFT) | (((pfreq_target+target_freq-pfreq)&DF_MASK)<<DF_SHIFT)|(dtime&DT_MASK))
            list_landmarks.append((hsh,ptime))   #Enregistrer le hachage et le pic 1 fois
        
        
    return list_landmarks

Cette valeur de hachage est associée à ** (ID de chanson, position de temps de pic 1) ** et enregistrée dans la table de hachage. J'utilise un objet dictionnaire python.

#hash_table:objet dictionnaire python
for landmark in list_landmarks:
    hsh = landmark[0]
    starttime = landmark[1]
    if hash_table.has_key(hsh):
        hash_table[hsh].append((audio_id,starttime))
    else:
        hash_table[hsh] = [(audio_id,starttime)]

Après avoir enregistré tous les hachages, enregistrez-les avec le module pickle.

Requete

Un total de 497 morceaux utilisés comme ensemble de données pour la reconnaissance de la progression des accords ont été créés dans une base de données. Cela a pris du temps, mais la taille de la table de hachage marinée était d'environ 250 Mo. En guise de test, j'ai joué aux Beatles ** "Back In the U.S.S.R" ** à partir du jeu de données et j'ai fait un enregistrement d'environ 10 secondes avec un enregistreur de poche. C'est un enregistrement assez idéal car il a été enregistré en intérieur, mais je vais essayer. J'ai STFTed la voix enregistrée de la même manière que ci-dessus, détecté le pic et trouvé l'empreinte digitale (valeur de hachage). Alors, comment arrivez-vous à la chanson d'ici? J'ai écrit une classe Database qui gère les tables de hachage et les requêtes.

Database.py


DIFF_RANGE = 2000000

class Database:
    def __init__(self):
        with open("hashtable.pkl","rb") as f:
            self.hash_table = pickle.load(f)
        with open("titlelist.pkl","rb") as f:
            self.title_list = pickle.load(f)
    #Méthode 1
	def query_histogram(self,y,best=5):
		list_landmarks = analyze.extract_landmarks(y)
		id_candidates = self._find_candidate_id(list_landmarks)
		candidate_landmark_histogram = np.zeros(id_candidates.size,dtype=np.int32)
		for landmark in list_landmarks:
			hsh = landmark[0]
			if self.hash_table.has_key(hsh):
				hit_ids = [item[0] for item in self.hash_table[hsh]]
				for i in hit_ids:
					candidate_landmark_histogram[np.where(id_candidates==i)[0]] += 1
			else:
				continue
		
		idxsort = np.argsort(candidate_landmark_histogram)[::-1]
		title_sort = self.title_list[id_candidates[idxsort]]
		fingerprint_histogram_sort = candidate_landmark_histogram[idxsort]
		return title_sort[:best],fingerprint_histogram_sort[:best]

    #Méthode 2
    def query_diff_histogram(self,y,best=5):
        list_landmarks = analyze.extract_landmarks(y)
        id_candidates = self._find_candidate_id(list_landmarks)
        diff_histogram = np.zeros((id_candidates.size,DIFF_RANGE),dtype=np.int32)
        diff_start_offset = np.zeros(id_candidates.size,dtype=np.int32)
        
        for landmark in list_landmarks:
            hsh_query = landmark[0]
            offset_query = landmark[1]
            if self.hash_table.has_key(hsh_query):
                hits = self.hash_table[hsh_query]
                for hit in hits:
                    id_hit = hit[0]
                    offset_hit = hit[1]
                    diff = offset_hit - offset_query
                    if diff >= 0:
                        id_hist = np.where(id_candidates==id_hit)[0]
                        diff_histogram[id_hist,diff] += 1
                        diff_start_offset[id_hist] = min([diff_start_offset[id_hist],offset_query])
            else:
                continue
            
        candidate_match_score = np.max(diff_histogram,axis=1)
        candidate_max_offset = np.argmax(diff_histogram,axis=1)
        idxsort = np.argsort(candidate_match_score)[::-1]
        title_sort = self.title_list[id_candidates[idxsort]]
        match_score_sort = candidate_match_score[idxsort]
        best_match_offset = (candidate_max_offset[idxsort[0]] - diff_start_offset[idxsort[0]]) * 256/11025.0
        return title_sort[:best],match_score_sort[:best],best_match_offset
        
                    
                    
    def _find_candidate_id(self,landmarks):
        candidate = set()
        for landmark in landmarks:
            hsh = landmark[0]
            if self.hash_table.has_key(hsh):
                id_hits = [item[0] for item in self.hash_table[hsh]]
                candidate = candidate.union(id_hits)
            else:
                continue
            
        return np.array(list(candidate),dtype=np.int32)

analy.extract_landmarks (y) intègre la détection des pics et le hachage.

analyze.py


def extract_landmarks(y,peak_dist=20):
    peaks_x,peaks_y = find_peaks(y,peak_dist)
    landmarks = peaks_to_landmarks(peaks_x,peaks_y)
    return landmarks

1. Comptez le nombre de hits

Correspond à Database.query_histogram. Chaque hachage enregistré dans la base de données est associé à un identifiant de morceau (un ou plusieurs). Vérifiez tous les hachages inclus dans l'enregistrement, comptez le nombre de hits de la chanson, et la chanson avec le plus de hits est la bonne réponse. C'est la méthode la plus simple, mais qu'en est-il des performances? J'ai essayé d'afficher les 5 meilleures chansons avec le nombre de hits. plot_hist.png

La requête a pris environ 0,5 seconde (le temps de chargement de la table de hachage n'est pas possible). Le plus de hash correspond à la chanson originale de Back In The U.S.S.R. C'est une réponse correcte, mais la différence avec d'autres chansons n'est pas si perceptible. Si l'enregistrement est un peu pire, il donnera bientôt une réponse incorrecte. Un autre problème est que cette méthode ne vous permet pas de savoir à quelle partie du morceau correspond l'enregistrement. Cela peut ne pas être nécessaire selon l'application, mais j'aimerais quand même le savoir.

2. Comptez les empreintes digitales "alignées"

Sélectionnez la chanson correcte et les deux autres chansons, et tracez la voix de requête et le numéro ** (position temporelle dans la voix d'origine, position temporelle dans l'enregistrement) ** de l'empreinte digitale frappée sur le plan 2D. plot_offset.png

Même si la chanson est incorrecte, le nombre de hits dans le hachage peut être raisonnable, mais la distribution des points est différente dans chaque cas. Par contre, dans la correspondance correcte (graphique du haut), il y a une partie où les points sont alignés en diagonale. Cela signifie que l'empreinte digitale est un hit continu dans un certain laps de temps. Pour cette raison, lorsque vous enregistrez le hachage, vous enregistrez la position temporelle dans le morceau d'origine. Par conséquent, celle avec de nombreuses empreintes digitales "alignées" est considérée comme la bonne réponse. Comme ils sont alignés en diagonale, c'est-à-dire que la différence entre x et y est une constante (sur la ligne droite x-y = b), un histogramme de (x-y) est obtenu pour chaque chanson et la valeur maximale est considérée comme le score. Correspond à Database.query_diff_histogram. Le résultat est là. plot_diff.png

C'est facile à comprendre et il y a une différence. La requête a pris environ 1,8 seconde. Il semble y avoir place à l'amélioration de l'efficacité de la mise en œuvre, mais cela est acceptable comme preuve du principe. Le montant du calcul est N (le nombre de hachages de la voix de requête), donc il ne devrait pas se détériorer beaucoup même si la base de données devient plus grande. Le best_match_offset du code source trouve la position de l'audio de la requête dans la chanson originale. Cela a également résolu le problème de la correspondance.

Autres mesures d'amélioration

Il convient de noter que cette méthode est basée sur l'hypothèse que ** "le morceau original et la requête ont la même vitesse de lecture et aucune modulation" **. Si la vitesse de lecture change ou la modulation globale change, la distance de temps relative entre les points de repère et la distance de fréquence (en raison de la plage de fréquence linéaire) augmente et se contracte, ce qui rend impossible la correspondance avec le hachage. Ce n'est pas un gros problème en pratique, mais une solution a été proposée.

Résumé

J'ai introduit un algorithme d'empreinte vocale simple. audfprint est également basé sur des points de repère, mais la détection des pics et les requêtes semblent également être implémentées d'une manière légèrement différente. Vous voudrez peut-être lire le code source. Si vous pouvez le comprendre, j'aimerais écrire un autre article.

Recommended Posts

Essayez d'implémenter un algorithme d'empreinte vocale de type Shazam
Implémentation d'un algorithme simple en Python 2
[Python] Essayez d'optimiser les paramètres de systole FX avec un algorithme génétique
Essayez de modéliser une distribution multimodale à l'aide de l'algorithme EM