Прослушивание песен и знание песен — реализация музыкального поиска в Python

база данных Python алгоритм искусственный интеллект


Посетите flyai.club, чтобы создать свой проект ИИ одним щелчком мыши



Автор | Идеализм – это непросто @blog garden

http://www.cnblogs.com/chuxiuhong/p/6063602.html

Прослушайте песню, чтобы узнать песню, как следует из названия, используйте устройство, чтобы «прослушать» песню, и тогда оно скажет вам, что это за песня. И в девяти случаях из десяти он должен сыграть для вас эту песню. Такие функции уже давно появились в таких приложениях, как QQ Music. Давайте сегодня сами послушаем песни

Общая блок-схема, которую мы разработали, проста:

часть записи

Если мы хотим «слушать», мы должны сначала иметь процесс записи. В наших экспериментах наша музыкальная библиотека также записывается с помощью нашего кода записи, а затем функции извлекаются и сохраняются в базе данных. Мы используем следующие идеи для записи


# coding=utf8import waveimport pyaudioclass recode():
    def recode(self, CHUNK=44100, FORMAT=pyaudio.paInt16, CHANNELS=2, RATE=44100, RECORD_SECONDS=200,
               WAVE_OUTPUT_FILENAME="record.wav"):
        '''        :param CHUNK: 缓冲区大小        :param FORMAT: 采样大小        :param CHANNELS:通道数        :param RATE:采样率        :param RECORD_SECONDS:录的时间        :param WAVE_OUTPUT_FILENAME:输出文件路径        :return:        '''
        p = pyaudio.PyAudio()
        stream = p.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,                        input=True,
                        frames_per_buffer=CHUNK)
        frames = []        for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
            data = stream.read(CHUNK)
            frames.append(data)
        stream.stop_stream()
        stream.close()
        p.terminate()
        wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(p.get_sample_size(FORMAT))
        wf.setframerate(RATE)
        wf.writeframes(''.join(frames))
        wf.close()if __name__ == '__main__':
    a = recode()
    a.recode(RECORD_SECONDS=30, WAVE_OUTPUT_FILENAME='record_pianai.wav')

В каком формате мы записали песню?

Если вы посмотрите только на один канал, это одномерный массив, который выглядит так

Мы рисуем его в соответствии со значением индекса на горизонтальной оси, что является формой звука, которую мы часто видим.

секция обработки звука

Здесь мы собираемся написать наш основной код. Ключ "как определить песню". Подумайте о том, как мы, люди, различаем песни? Думаете ли вы об одномерном массиве, подобном приведенному выше? Это громкость песни? ни один.

Мы состоим из определенных частот, которые мы слышим ушами.последовательностьЧтобы запоминать песни, поэтому, если мы хотим писать и слушать песни, мы должны побеспокоиться о частотной последовательности звука.

Узнайте, что такое преобразование Фурье. Класс блоггера «Сигнал и система» очень хорош, но хотя конкретная форма преобразования не записана в классе, все же есть перцептивное понимание.

Суть преобразования Фурье заключается в преобразовании сигнала во временной области в сигнал в частотной области. То есть исходные оси X и Y - это индексы нашего массива и элементы массива соответственно, но теперь они становятся частотой (это не точно говорить, но здесь правильно понимать) и размером компонента на этой частоте .


Выше две фотографии от Zhihu, большое спасибо за статью, написанную Генрихом (http://daily.zhihu.com/story/3935067)

Как понять частотную область? Для тех из нас, кто мало что знает об обработке сигналов, самое главное — изменить свое понимание того, что представляет собой звук. Первоначально мы думали, что звук похож на форму волны, с которой мы начали, с амплитудой в каждый момент времени и последовательностью различных амплитуд, составляющих наш специфический звук. А теперь мы думаем о звуке как о смеси различных частотных сигналов, каждый из которыхот начала и до концасохраняя это. И они вносят свой вклад в соответствии со своими спроецированными компонентами.

Давайте посмотрим, как это выглядит, чтобы преобразовать песню в частотную область?

Мы можем заметить, что компоненты этих частот не являются средними, а различия очень велики. В какой-то степени можно думать, что очевидные пики на рисунке — это частотные сигналы с большой выходной энергией, а это означает, что этот сигнал занимает высокое положение на данной звуковой частоте. Поэтому мы выбираем такой сигнал, чтобы извлечь особенности песни.

Но не забывайте, мы говорили о частоте раньшепоследовательность, преобразование Фурье, мы можем знать только информацию о частоте всей песни, тогда мы теряем связь времени, и мы не можем говорить о «последовательности». Поэтому мы приняли более скомпрометированный метод, разделив звук на небольшие фрагменты по времени, здесь я разделил 40 фрагментов в секунду.

Оставив вопрос здесь: зачем использовать маленькие блоки вместо больших, например, по одному в секунду?

Мы преобразуем Фурье каждый блок, а затем по модулю получаем массив. Берем индекс с наибольшей длиной по модулю в значениях индекса (0,40), (40,80), (80,120), (120,180) соответственно и синтезируем четверку, которая и является нашим ядром. отпечатки пальцев».

Извлеченный нами «отпечаток пальца» выглядит следующим образом.

(39, 65, 110, 131), (15, 66, 108, 161), (3, 63, 118, 146), (11, 62, 82, 158), (15, 41, 95, 140), (2, 71, 106, 143), (15, 44, 80, 133), (36, 43, 80, 135), (22, 58, 80, 120), (29, 52, 89, 126), (15, 59, 89, 126), (37, 59, 89, 126), (37, 59, 89, 126), (37, 67, 119, 126)

Класс обработки звука имеет три метода: загрузка данных, преобразование Фурье и воспроизведение музыки.

следующее:

# coding=utf8import osimport reimport waveimport numpy as npimport pyaudioclass voice():
    def loaddata(self, filepath):
        '''        :param filepath: 文件路径,为wav文件        :return: 如果无异常则返回True,如果有异常退出并返回False        self.wave_data内储存着多通道的音频数据,其中self.wave_data[0]代表第一通道        具体有几通道,看self.nchannels        '''
        if type(filepath) != str:            raise TypeError, 'the type of filepath must be string'
        p1 = re.compile('\.wav')        if p1.findall(filepath) is None:            raise IOError, 'the suffix of file must be .wav'
        try:
            f = wave.open(filepath, 'rb')
            params = f.getparams()            self.nchannels, self.sampwidth, self.framerate, self.nframes = params[:4]
            str_data = f.readframes(self.nframes)            self.wave_data = np.fromstring(str_data, dtype=np.short)            self.wave_data.shape = -1, self.sampwidth            self.wave_data = self.wave_data.T
            f.close()            self.name = os.path.basename(filepath)  # 记录下文件名
            return True
        except:            raise IOError, 'File Error'

    def fft(self, frames=40):
        '''        整体指纹提取的核心方法,将整个音频分块后分别对每块进行傅里叶变换,之后分子带抽取高能量点的下标        :param frames: frames是指定每秒钟分块数        :return:        '''
        block = []
        fft_blocks = []        self.high_point = []
        blocks_size = self.framerate / frames  # block_size为每一块的frame数量
        blocks_num = self.nframes / blocks_size  # 将音频分块的数量
        for i in xrange(0, len(self.wave_data[0]) - blocks_size, blocks_size):
            block.append(self.wave_data[0][i:i + blocks_size])
            fft_blocks.append(np.abs(np.fft.fft(self.wave_data[0][i:i + blocks_size])))            self.high_point.append((np.argmax(fft_blocks[-1][:40]),
                                    np.argmax(fft_blocks[-1][40:80]) + 40,
                                    np.argmax(fft_blocks[-1][80:120]) + 80,
                                    np.argmax(fft_blocks[-1][120:180]) + 120,                                    # np.argmax(fft_blocks[-1][180:300]) + 180,
                                    ))    def play(self, filepath):
        '''        音频播放方法        :param filepath:文件路径        :return:        '''
        chunk = 1024
        wf = wave.open(filepath, 'rb')
        p = pyaudio.PyAudio()        # 打开声音输出流
        stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                        channels=wf.getnchannels(),
                        rate=wf.getframerate(),
                        output=True)        # 写声音输出流进行播放
        while True:
            data = wf.readframes(chunk)            if data == "": break
            stream.write(data)
        stream.close()
        p.terminate()if __name__ == '__main__':
    p = voice()
    p.play('the_mess.wav')    print p.name

Здесь self.high_point — это основные данные будущих приложений. Тип списка, где элементы представлены в виде описанных выше отпечатков пальцев.

Секция хранения и поиска данных

Поскольку мы заранее подготовили музыкальную библиотеку для ожидания извлечения, у нас должен быть соответствующий метод сохранения. Что я использую, так это напрямую использовать базу данных mysql для хранения отпечатков пальцев, соответствующих нашим песням, что имеет преимущество: экономия времени на написание кода.

Мы храним отпечатки пальцев и песни вот так:

Кстати: почему отпечатки пальцев первых нескольких песен одинаковы? (Конечно, последнее должно сильно отличаться.) На самом деле нет смысла с сильной энергетикой в ​​промежутке времени до начала музыки, а так как частота дискретизации у нас 44100 относительно высокая, то повторений в моменте будет много. начало, не волнуйся.

Как мы совпадаем? Мы можем напрямую искать такое же количество звуковых отпечатков, но при этом теряется то, о чем мы говорили раньше.последовательность, мы должны использовать временной ряд. В противном случае, чем длиннее песня, тем легче ее будет сопоставить, а эта песня, как дикая трава, занимает первое место во всех результатах поиска по аудио. И теоретически информация, содержащаяся в аудиозаписи, отражается в последовательности, точно так же, как предложение может выражать собственное значение определенной последовательностью фраз и слов. Просто глядя на количество лексических совпадений в двух предложениях, нельзя определить, похожи ли два предложения. Мы используем следующий алгоритм, но это всего лишь экспериментальный код, конструкция алгоритма очень проста, а эффективность невысока. Студентам, которые хотят добиться лучших результатов, рекомендуется использовать улучшенный алгоритм DTW.

Мы перемещаем последовательность отпечатков пальцев в процессе сопоставления и каждый раз сравниваем строку шаблона и соответствующую подстроку исходной строки.Если отпечатки пальцев в соответствующих позициях совпадают, значение сходства этого сравнения увеличивается на единицу, и мы принять максимальное сходство, полученное в процессе скольжения, как сходство двух песен.

Пример:

Последовательность отпечатков песен в библиотеке: [fp13, fp20, fp10, fp29, fp14, fp25, fp13, fp13, fp20, fp33, fp14]

Получить последовательность музыки по отпечатку пальца: [fp14, fp25, fp13, fp17]

Процесс сравнения:








Окончательное совпадающее значение сходства равно 3.

Код реализации поисковой части хранилища

# coding=utf-8import osimport MySQLdbimport my_audioclass memory():
    def __init__(self, host, port, user, passwd, db):
        '''        初始化的方法,主要是存储连接数据库的参数        :param host:        :param port:        :param user:        :param passwd:        :param db:        '''
        self.host = host        self.port = port        self.user = user        self.passwd = passwd        self.db = db    def addsong(self, path):
        '''        添加歌曲方法,将歌曲名和歌曲特征指纹存到数据库        :param path: 歌曲路径        :return:        '''
        if type(path) != str:            raise TypeError, 'path need string'
        basename = os.path.basename(path)        try:
            conn = MySQLdb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
                                   charset='utf8')        except:            print 'DataBase error'
            return None
        cur = conn.cursor()
        namecount = cur.execute("select * from fingerprint.musicdata WHERE song_name = '%s'" % basename)        if namecount > 0:            print 'the song has been record!'
            return None
        v = my_audio.voice()
        v.loaddata(path)
        v.fft()
        cur.execute("insert into fingerprint.musicdata VALUES('%s','%s')" % (basename, v.high_point.__str__()))
        conn.commit()
        cur.close()
        conn.close()    def fp_compare(self, search_fp, match_fp):
        '''        :param search_fp: 查询指纹        :param match_fp: 库中指纹        :return:最大相似值 float        '''
        if len(search_fp) > len(match_fp):            return 0
        max_similar = 0
        search_fp_len = len(search_fp)
        match_fp_len = len(match_fp)        for i in range(match_fp_len - search_fp_len):
            temp = 0
            for j in range(search_fp_len):                if match_fp[i + j] == search_fp[j]:
                    temp += 1
            if temp > max_similar:
                max_similar = temp        return max_similar    def search(self, path):
        '''        搜索方法,输入为文件路径        :param path: 待检索文件路径        :return: 按照相似度排序后的列表,元素类型为tuple,二元组,歌曲名和相似匹配值        '''
        #先计算出来我们的音频指纹
        v = my_audio.voice()
        v.loaddata(path)
        v.fft()        #尝试连接数据库
        try:
            conn = MySQLdb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
                                   charset='utf8')        except:            raise IOError, 'DataBase error'
        cur = conn.cursor()
        cur.execute("SELECT * FROM fingerprint.musicdata")
        result = cur.fetchall()
        compare_res = []        for i in result:
            compare_res.append((self.fp_compare(v.high_point[:-1], eval(i[1])), i[0]))
        compare_res.sort(reverse=True)
        cur.close()
        conn.close()        print compare_res        return compare_res    def search_and_play(self, path):
        '''        搜索方法顺带了播放方法        :param path:文件路径        :return:        '''
        v = my_audio.voice()
        v.loaddata(path)
        v.fft()        try:
            conn = MySQLdb.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd, db=self.db,
                                   charset='utf8')        except:            print 'DataBase error'
            return None
        cur = conn.cursor()
        cur.execute("SELECT * FROM fingerprint.musicdata")
        result = cur.fetchall()
        compare_res = []        for i in result:
            compare_res.append((self.fp_compare(v.high_point[:-1], eval(i[1])), i[0]))
        compare_res.sort(reverse=True)
        cur.close()
        conn.close()        print compare_res
        v.play(compare_res[0][1])        return compare_resif __name__ == '__main__':
    sss = memory('localhost', 3306, 'root', '', 'fingerprint')
    sss.addsong('taiyangzhaochangshengqi.wav')
    sss.addsong('beiyiwangdeshiguang.wav')
    sss.addsong('xiaozezhenger.wav')
    sss.addsong('nverqing.wav')
    sss.addsong('the_mess.wav')
    sss.addsong('windmill.wav')
    sss.addsong('end_of_world.wav')
    sss.addsong('pianai.wav')

    sss.search_and_play('record_pianai.wav')

Суммировать

Многие части нашего эксперимента грубы, и основным алгоритмом является идея «отпечатка пальца», взятая из алгоритма, предложенного Shazam. Я надеюсь, что читатели могут сделать ценные предложения.

- Конец -