From 5e3a9a0801ff9da7637e42132380c0b967452086 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:41:06 +0300 Subject: [PATCH 01/10] Add files via upload --- converter.ipynb | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 converter.ipynb diff --git a/converter.ipynb b/converter.ipynb new file mode 100644 index 0000000..812d565 --- /dev/null +++ b/converter.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "id": "41f37bc9", + "metadata": {}, + "outputs": [], + "source": [ + "from pydub import AudioSegment" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b02f7833", + "metadata": {}, + "outputs": [], + "source": [ + "input = \"convert.mp3\"\n", + "output = \"test2.wav\"" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "f0dcd4a1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<_io.BufferedRandom name='test2.wav'>" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sound = AudioSegment.from_mp3(input)\n", + "sound.set_channels(1)\n", + "sound.export(output, format='wav')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d31aa2a8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 842f10bef2409cf95f475d5ea911e58eea0f4998 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:43:49 +0300 Subject: [PATCH 02/10] Delete converter.ipynb --- converter.ipynb | 77 ------------------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 converter.ipynb diff --git a/converter.ipynb b/converter.ipynb deleted file mode 100644 index 812d565..0000000 --- a/converter.ipynb +++ /dev/null @@ -1,77 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 14, - "id": "41f37bc9", - "metadata": {}, - "outputs": [], - "source": [ - "from pydub import AudioSegment" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "b02f7833", - "metadata": {}, - "outputs": [], - "source": [ - "input = \"convert.mp3\"\n", - "output = \"test2.wav\"" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "f0dcd4a1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "<_io.BufferedRandom name='test2.wav'>" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sound = AudioSegment.from_mp3(input)\n", - "sound.set_channels(1)\n", - "sound.export(output, format='wav')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d31aa2a8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 19208a79778011f60eab2d8d0f1d7b0e61e6e450 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Wed, 12 Apr 2023 16:12:00 +0300 Subject: [PATCH 03/10] Add files via upload --- bot.py | 84 +++++++++++++++++++++++++++++++++++++++++++ matching.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++ spectr.py | 62 ++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 bot.py create mode 100644 matching.py create mode 100644 spectr.py diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..1ce2a26 --- /dev/null +++ b/bot.py @@ -0,0 +1,84 @@ +import logging +from aiogram import Bot, Dispatcher, types, executor +from dotenv import load_dotenv +from pathlib import Path +from spectr import create_spectrogram +from matching import finish +import os + +dotenv_path = Path('.env.txt') +load_dotenv(dotenv_path=dotenv_path) + +# logging so don't miss important messages +logging.basicConfig(level=logging.INFO) +bot_token = os.getenv("BOT_TOKEN") +# bot's object +bot = Bot(token=bot_token) +dp = Dispatcher(bot) + + +# handler for /start +@dp.message_handler(commands='start') +async def cmd_start(message: types.Message): + await message.answer('Hello!') + + +user_home_dir = os.path.expanduser('~') +user = os.path.split(user_home_dir)[-1] +disk = os.path.split(user_home_dir)[0] +common_path = os.path.join(disk, user, 'Desktop') + +try: + if not os.path.isdir('Downloaded audio'): + os.chdir(common_path) + os.mkdir('Downloaded audio"') +except: + print() + + +# handler for audio messages, voice messages and photos +@dp.message_handler(content_types=['audio', 'voice', 'photo']) +async def show_spectrogram(message: types.Message): + unique_id = path_to_file = format_file = file_info = '' + # getting a directory for saving a file + directory = os.path.join(disk, user, 'Desktop', 'Downloaded audio') + # checking a type of message + if message.audio is not None: + audio_id = message.audio.file_id + unique_id = message.audio.file_unique_id + file_info = await bot.get_file(audio_id) + path_to_file = os.path.join(directory, f'{unique_id}.mp3') + elif message.voice is not None: + audio_id = message.voice.file_id + unique_id = message.voice.file_unique_id + file_info = await bot.get_file(audio_id) + path_to_file = os.path.join(directory, f'{unique_id}.ogg') + + # downloading a file + try: + await bot.download_file(file_info.file_path, + destination=os.path.join(path_to_file)) + await message.answer("Your audio is successfully downloaded") + except: + await message.answer("Error downloading") + + create_spectrogram(path_to_file, unique_id) + + path_to_photo = os.path.join(disk, user, 'Desktop', \ + 'Created spectrograms', f'{unique_id}.png') + # showing spectrogram + try: + with open(path_to_photo, 'rb') as photo: + await message.answer_photo(photo, caption='Your spectrogram') + photo.close() + except: + await message.answer("Error showing") + + try: + await message.answer(finish()) + except: + pass + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) \ No newline at end of file diff --git a/matching.py b/matching.py new file mode 100644 index 0000000..4fa128d --- /dev/null +++ b/matching.py @@ -0,0 +1,100 @@ +import numpy as np +from scipy.io import wavfile +from scipy.signal import spectrogram +import os +import spectr + + +# function to generate a fingerprint of an audio file +def finger_print(audio_file): + # read audio file + samplerate, data = wavfile.read(audio_file) + # generate spectrogram + try: + f, t, Sxx = spectrogram(data[:, 0], samplerate) + except: + f, t, Sxx = spectrogram(data, samplerate) + # replace zeros with epsilon to handle divide by zero errors + Sxx[Sxx == 0] = np.finfo(float).eps + # take logarithm of spectrogram + log_spectrogram = np.log(Sxx) + # flatten spectrogram to generate fingerprint + fingerprint = log_spectrogram.flatten() + return fingerprint + + +# function to calculate cosine distance between two fingerprints +def cosine_distance(x, y): + # pad the smaller fingerprint with zeros to match the size of the larger fingerprint + if len(x) < len(y): + x = np.pad(x, (0, len(y) - len(x)), mode='constant') + elif len(y) < len(x): + y = np.pad(y, (0, len(x) - len(y)), mode='constant') + # calculate cosine distance + return np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y)) + + +# function to find the closest match for an audio file in a given set of fingerprints +def find_closest_match(audio_file, fingerprint_maps): + # generate fingerprint for query audio file + query_fp = finger_print(audio_file) + # initialize maximum distance and closest match + max_distance = 0 + closest_match = '' + # iterate through fingerprint maps and find closest match + for key in fingerprint_maps: + # calculate cosine distance between query fingerprint and current fingerprint + distance = cosine_distance(query_fp, fingerprint_maps[key]) + # update maximum distance and closest match if a closer match is found + if distance > max_distance: + max_distance = distance + closest_match = key + # return closest match and its distance from query audio file + return closest_match, max_distance + + +# prompt user to enter directory path containing audio files +# path = input("Enter the directory path where your music files are located: ") +# # validate directory path +# while True: +# try: +# if not os.path.exists(path): +# raise FileNotFoundError +# break +# except FileNotFoundError: +# print("Invalid directory path!") +# path = input("Enter the directory path where your music files are located: ") + +path = "D:\\PyCharm\\PyCharm Community Edition 2023.1\\pythonProject\\music" + +# generate fingerprints for all audio files in directory +fingerprint_maps = {} +for file_name in os.listdir(path): + if file_name.endswith('.wav'): + audio_file_path = os.path.join(path, file_name) + try: + fp = finger_print(audio_file_path) + fingerprint_maps[file_name] = fp + except Exception as e: + print(f"Error processing file {audio_file_path}: {e}") + + +# prompt user to enter path of audio file to match +# audio = input("Enter the path of the audio file you want to match: ") +# # validate audio file path +# while True: +# try: +# if not os.path.exists(audio): +# raise FileNotFoundError +# break +# except FileNotFoundError: +# print("Invalid audio file path!") +# audio = input("Enter the path of the audio file you want to match: ") + +def finish(): + audio = os.path.join(spectr.output_file) + # find closest match for query audio file + closest_match, max_distance = find_closest_match(audio, fingerprint_maps) + + # print closest match and its distance from query audio file + return f"Closest match found: {closest_match} with distance {max_distance}" diff --git a/spectr.py b/spectr.py new file mode 100644 index 0000000..5f84946 --- /dev/null +++ b/spectr.py @@ -0,0 +1,62 @@ +from pydub import AudioSegment +import matplotlib.pyplot as plt +from scipy.io import wavfile +from scipy.signal import spectrogram +import numpy as np +from matching import finish +import os + +user_home_dir = os.path.expanduser('~') +user = os.path.split(user_home_dir)[-1] +disk = os.path.split(user_home_dir)[0] +common_path = os.path.join(disk, user, 'Desktop') + +FRAME_SIZE = 2048 +HOP_SIZE = 512 + +input_file = '' +output_file = '' + + +def create_spectrogram(path_to_file: str, unique_id: str): + global input_file, output_file + input_file = f'{path_to_file}' + output_file = f'{path_to_file}.wav' + + if input_file[-3:] == 'mp3': + sound = AudioSegment.from_mp3(input_file) + sound.export(output_file, format='wav') + elif input_file[-3:] == 'ogg': + sound = AudioSegment.from_ogg(input_file) + sound.export(output_file, format='wav') + + # output_file_mono = f'{output_file}_mono.wav' + + # stereo_audio = AudioSegment.from_file(output_file, format='wav') + # mono_audios = stereo_audio.split_to_mono() + # mono_left = mono_audios[0].export(output_file_mono, format='wav') + + samplerate, data = wavfile.read(output_file) + try: + f, t, Sxx = spectrogram(data[:, 0], samplerate, nperseg=FRAME_SIZE, \ + noverlap=HOP_SIZE, window='hamming') + except: + f, t, Sxx = spectrogram(data, samplerate, nperseg=FRAME_SIZE, \ + noverlap=HOP_SIZE, window='hamming') + Sxx[Sxx == 0] = np.finfo(float).eps + + plt.pcolormesh(t, f / 1000, 10 * np.log10(Sxx / Sxx.max()), vmin=-120, \ + vmax=0, cmap='inferno') + plt.ylabel('Frequency [kHz]') + plt.xlabel('Time [s]') + plt.colorbar() + img = f'{unique_id}.png' + try: + if not os.path.isdir('Created spectrograms'): + os.chdir(common_path) + os.mkdir('Created spectrograms') + except: + print() + path = os.path.join(disk, user, 'Desktop', 'Created spectrograms', img) + plt.savefig(path) + plt.clf() \ No newline at end of file From 5a222a637a2f23e2edba4faa8393db742afbc4e2 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:31:15 +0300 Subject: [PATCH 04/10] Add files via upload --- bot.py | 122 ++++++++++++++++++++++++++++++++++++++-------- fingerprinting.py | 46 +++++++++++++++++ matching.py | 100 ++++++++++++++++++------------------- spectr.py | 40 ++++++++------- 4 files changed, 218 insertions(+), 90 deletions(-) create mode 100644 fingerprinting.py diff --git a/bot.py b/bot.py index 1ce2a26..6872af6 100644 --- a/bot.py +++ b/bot.py @@ -1,9 +1,13 @@ import logging +import threading + from aiogram import Bot, Dispatcher, types, executor from dotenv import load_dotenv from pathlib import Path +from pyglet.media import Player, load + from spectr import create_spectrogram -from matching import finish +from matching import finish, finish_debug, get_song import os dotenv_path = Path('.env.txt') @@ -16,11 +20,73 @@ bot = Bot(token=bot_token) dp = Dispatcher(bot) +command = '' + # handler for /start -@dp.message_handler(commands='start') +@dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): - await message.answer('Hello!') + await message.answer("Hello! I'm music recognizing bot. Waiting for your commands...") + + +@dp.message_handler(commands=['run']) +async def run(message: types.Message): + global command + command = 'run' + await message.answer("Send me an audiofile or voice message. I'll show you its title") + + +@dp.message_handler(commands=['debug']) +async def debug(message: types.Message): + global command + command = 'debug' + await message.answer("Send me an audiofile or voice message. I'll show you its title " + "and some additional info") + + +player = None + + +def play_audio(audio_file): + global player + player = Player() + source = load(audio_file) + player.queue(source) + player.play() + + +def stop_audio(): + global player + if player is not None: + player.pause() + player.delete() + player = None + + +@dp.callback_query_handler(lambda c: c.data == 'play') +async def play_callback_handler(callback_query: types.CallbackQuery): + audio_file = get_song() + threading.Thread(target=play_audio, args=(audio_file,)).start() + + +@dp.callback_query_handler(lambda c: c.data == 'stop') +async def stop_callback_handler(callback_query: types.CallbackQuery): + stop_audio() + + +@dp.message_handler(commands=['play']) +async def play_music(message: types.Message): + audio_file = get_song() + await bot.send_audio(message.chat.id, audio=open(audio_file, 'rb')) + + +@dp.message_handler(commands=['help']) +async def help(message: types.Message): + await message.answer('All commands:\n' + '/start - run bot\n' + '/run - waiting for audio and shows its title\n' + '/debug - gives an additional info of your file\n' + '/play - sends an audio player') user_home_dir = os.path.expanduser('~') @@ -31,15 +97,16 @@ async def cmd_start(message: types.Message): try: if not os.path.isdir('Downloaded audio'): os.chdir(common_path) - os.mkdir('Downloaded audio"') + os.mkdir('Downloaded audio') except: print() # handler for audio messages, voice messages and photos -@dp.message_handler(content_types=['audio', 'voice', 'photo']) +@dp.message_handler(content_types=['audio', 'voice']) async def show_spectrogram(message: types.Message): - unique_id = path_to_file = format_file = file_info = '' + global command + unique_id = path_to_file = file_info = '' # getting a directory for saving a file directory = os.path.join(disk, user, 'Desktop', 'Downloaded audio') # checking a type of message @@ -62,23 +129,36 @@ async def show_spectrogram(message: types.Message): except: await message.answer("Error downloading") - create_spectrogram(path_to_file, unique_id) + print(command) + if command == 'run': + create_spectrogram(path_to_file, unique_id) - path_to_photo = os.path.join(disk, user, 'Desktop', \ - 'Created spectrograms', f'{unique_id}.png') - # showing spectrogram - try: - with open(path_to_photo, 'rb') as photo: - await message.answer_photo(photo, caption='Your spectrogram') - photo.close() - except: - await message.answer("Error showing") + path_to_photo = os.path.join(disk, user, 'Desktop', + 'Created spectrograms', f'{unique_id}.png') + try: + await message.answer(finish()) + except: + await message.answer("I'm Sorry. Something went wrong...") + elif command == 'debug': + create_spectrogram(path_to_file, unique_id) - try: - await message.answer(finish()) - except: - pass + path_to_photo = os.path.join(disk, user, 'Desktop', + 'Created spectrograms', f'{unique_id}.png') + # showing spectrogram + try: + with open(path_to_photo, 'rb') as photo: + await message.answer_photo(photo, caption='Your spectrogram') + photo.close() + except: + await message.answer("Error showing") + + try: + await message.answer(finish_debug()) + except: + await message.answer("I'm Sorry. Something went wrong...") + else: + await message.answer('Try some existed commands, please') if __name__ == '__main__': - executor.start_polling(dp, skip_updates=True) \ No newline at end of file + executor.start_polling(dp, skip_updates=True) diff --git a/fingerprinting.py b/fingerprinting.py new file mode 100644 index 0000000..d7234a2 --- /dev/null +++ b/fingerprinting.py @@ -0,0 +1,46 @@ +from scipy.io import wavfile +from scipy.signal import spectrogram +import numpy as np +from scipy.ndimage.morphology import (generate_binary_structure, iterate_structure, + binary_erosion) +from scipy.ndimage.filters import maximum_filter + + +# function to generate a fingerprint of an audio file +def finger_print(audio_file): + # read audio file + samplerate, data = wavfile.read(audio_file) + # generate spectrogram + try: + f, t, Sxx = spectrogram(data[:, 0], samplerate) + except: + f, t, Sxx = spectrogram(data, samplerate) + # replace zeros with epsilon to handle divide by zero errors + Sxx[Sxx == 0] = np.finfo(float).eps + # take logarithm of spectrogram + log_spectrogram = np.log(Sxx) + fingerprint = log_spectrogram.flatten() + return fingerprint + + +def get_peaks(spectrogram, no_of_iteration=10, min_amplitude=10): + structure = generate_binary_structure(2, 2) + neighborhood = iterate_structure(structure, no_of_iteration) + + local_max = maximum_filter(spectrogram, footprint=neighborhood) == spectrogram + background = (spectrogram == 0) + eroded_background = binary_erosion(background, structure=neighborhood, border_value=1) + + # applying XOR between matrices to get boolean mask of spectrogram + detected_peaks = local_max ^ eroded_background + + # getting peaks, their freqs and times + peaks = spectrogram[detected_peaks].flatten() + peak_freqs, peak_times = np.where(detected_peaks) + + peak_idx = np.where(peaks > min_amplitude) + + freqs = peak_freqs[peak_idx] + times = peak_times[peak_idx] + + return list(zip(freqs, times)) diff --git a/matching.py b/matching.py index 4fa128d..0f8c1e6 100644 --- a/matching.py +++ b/matching.py @@ -1,29 +1,11 @@ import numpy as np -from scipy.io import wavfile -from scipy.signal import spectrogram import os import spectr +from fingerprinting import finger_print +from pydub import AudioSegment +from mutagen.mp3 import MP3 -# function to generate a fingerprint of an audio file -def finger_print(audio_file): - # read audio file - samplerate, data = wavfile.read(audio_file) - # generate spectrogram - try: - f, t, Sxx = spectrogram(data[:, 0], samplerate) - except: - f, t, Sxx = spectrogram(data, samplerate) - # replace zeros with epsilon to handle divide by zero errors - Sxx[Sxx == 0] = np.finfo(float).eps - # take logarithm of spectrogram - log_spectrogram = np.log(Sxx) - # flatten spectrogram to generate fingerprint - fingerprint = log_spectrogram.flatten() - return fingerprint - - -# function to calculate cosine distance between two fingerprints def cosine_distance(x, y): # pad the smaller fingerprint with zeros to match the size of the larger fingerprint if len(x) < len(y): @@ -31,7 +13,10 @@ def cosine_distance(x, y): elif len(y) < len(x): y = np.pad(y, (0, len(x) - len(y)), mode='constant') # calculate cosine distance - return np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y)) + dot_product = np.dot(x, y) + norm_x = np.linalg.norm(x) + norm_y = np.linalg.norm(y) + return dot_product / (norm_x * norm_y) # function to find the closest match for an audio file in a given set of fingerprints @@ -53,48 +38,59 @@ def find_closest_match(audio_file, fingerprint_maps): return closest_match, max_distance -# prompt user to enter directory path containing audio files -# path = input("Enter the directory path where your music files are located: ") -# # validate directory path -# while True: -# try: -# if not os.path.exists(path): -# raise FileNotFoundError -# break -# except FileNotFoundError: -# print("Invalid directory path!") -# path = input("Enter the directory path where your music files are located: ") - path = "D:\\PyCharm\\PyCharm Community Edition 2023.1\\pythonProject\\music" # generate fingerprints for all audio files in directory fingerprint_maps = {} for file_name in os.listdir(path): - if file_name.endswith('.wav'): - audio_file_path = os.path.join(path, file_name) + file_path = os.path.join(path, file_name) + if file_path.endswith(".mp3"): + # convert mp3 file to wav format + output_file = os.path.join(path, f"{os.path.splitext(file_name)[0]}.wav") + sound = AudioSegment.from_mp3(file_path) + sound.export(output_file, format='wav') try: - fp = finger_print(audio_file_path) + fp = finger_print(output_file) fingerprint_maps[file_name] = fp except Exception as e: - print(f"Error processing file {audio_file_path}: {e}") + print(f"Error processing file {output_file}: {e}") + finally: + # delete the wav file + os.remove(output_file) -# prompt user to enter path of audio file to match -# audio = input("Enter the path of the audio file you want to match: ") -# # validate audio file path -# while True: -# try: -# if not os.path.exists(audio): -# raise FileNotFoundError -# break -# except FileNotFoundError: -# print("Invalid audio file path!") -# audio = input("Enter the path of the audio file you want to match: ") +def get_duration(audio_file): + audio_ = MP3(audio_file) + duration_ = audio_.info.length + minutes = int(duration_ // 60) + seconds = int(duration_ % 60) + duration_format = f"{minutes:02d}:{seconds:02d}" + return duration_format + def finish(): audio = os.path.join(spectr.output_file) # find closest match for query audio file - closest_match, max_distance = find_closest_match(audio, fingerprint_maps) + closest_match_, max_distance_ = find_closest_match(audio, fingerprint_maps) + path_to_file = os.path.join(path, closest_match_) + duration = get_duration(path_to_file) + # print closest match + return f"Song: {closest_match_[:-4]}\nDuration: {duration}" + - # print closest match and its distance from query audio file - return f"Closest match found: {closest_match} with distance {max_distance}" +def finish_debug(): + audio = os.path.join(spectr.output_file) + # find closest match for query audio file + closest_match_, max_distance_ = find_closest_match(audio, fingerprint_maps) + path_to_file = os.path.join(path, closest_match_) + duration = get_duration(path_to_file) + # print closest match + return f"Song: {closest_match_[:-4]}\nDuration: {duration}\nDistance: {max_distance_}" + + +def get_song(): + audio = os.path.join(spectr.output_file) + # find closest match for query audio file + closest_match_, max_distance_ = find_closest_match(audio, fingerprint_maps) + path_to_file = os.path.join(path, closest_match_) + return path_to_file diff --git a/spectr.py b/spectr.py index 5f84946..dbd9561 100644 --- a/spectr.py +++ b/spectr.py @@ -1,18 +1,18 @@ -from pydub import AudioSegment import matplotlib.pyplot as plt from scipy.io import wavfile from scipy.signal import spectrogram import numpy as np -from matching import finish import os +from fingerprinting import get_peaks +from pydub import AudioSegment user_home_dir = os.path.expanduser('~') user = os.path.split(user_home_dir)[-1] disk = os.path.split(user_home_dir)[0] common_path = os.path.join(disk, user, 'Desktop') -FRAME_SIZE = 2048 -HOP_SIZE = 512 +FRAME_SIZE = 2 ** 12 +HOP_SIZE = int(FRAME_SIZE * 0.5) input_file = '' output_file = '' @@ -30,26 +30,31 @@ def create_spectrogram(path_to_file: str, unique_id: str): sound = AudioSegment.from_ogg(input_file) sound.export(output_file, format='wav') - # output_file_mono = f'{output_file}_mono.wav' - - # stereo_audio = AudioSegment.from_file(output_file, format='wav') - # mono_audios = stereo_audio.split_to_mono() - # mono_left = mono_audios[0].export(output_file_mono, format='wav') - samplerate, data = wavfile.read(output_file) + try: - f, t, Sxx = spectrogram(data[:, 0], samplerate, nperseg=FRAME_SIZE, \ - noverlap=HOP_SIZE, window='hamming') + f, t, Sxx = spectrogram(data[:, 0], samplerate, nperseg=FRAME_SIZE, + noverlap=HOP_SIZE, window='hamming') except: - f, t, Sxx = spectrogram(data, samplerate, nperseg=FRAME_SIZE, \ + f, t, Sxx = spectrogram(data, samplerate, nperseg=FRAME_SIZE, noverlap=HOP_SIZE, window='hamming') + Sxx[Sxx == 0] = np.finfo(float).eps - plt.pcolormesh(t, f / 1000, 10 * np.log10(Sxx / Sxx.max()), vmin=-120, \ - vmax=0, cmap='inferno') - plt.ylabel('Frequency [kHz]') - plt.xlabel('Time [s]') + max_time = int(np.max(t)) + max_freq = int(np.max(f)) + + spec_peaks = get_peaks(Sxx[:max_freq, :max_time]) + plot_peaks = np.array(spec_peaks) + plot_peaks[:, 0] = (plot_peaks[:, 0] * max_freq) / len(f) + + plt.pcolormesh(t, f, 10 * np.log10(Sxx / Sxx.max()), vmin=-120, + vmax=0, cmap='inferno') plt.colorbar() + plt.scatter(plot_peaks[:, 1], plot_peaks[:, 0], s=10, color='white') + plt.ylabel('Frequency [Hz]') + plt.xlabel('Time [s]') + img = f'{unique_id}.png' try: if not os.path.isdir('Created spectrograms'): @@ -57,6 +62,7 @@ def create_spectrogram(path_to_file: str, unique_id: str): os.mkdir('Created spectrograms') except: print() + path = os.path.join(disk, user, 'Desktop', 'Created spectrograms', img) plt.savefig(path) plt.clf() \ No newline at end of file From 2b650d7f6708575c5d0a37b911f2edfa99ae27ab Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:34:38 +0300 Subject: [PATCH 05/10] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45a883a..b9effd1 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# MusBot \ No newline at end of file +# MusBot +# Телеграм бот для распознавания From 713ddb38a021a2fc50f38ca636a1bb0e47aed516 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:35:07 +0300 Subject: [PATCH 06/10] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b9effd1..efef226 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # MusBot # Телеграм бот для распознавания +## Задача From a161ffdf686460d2dceb7498120a92620978d96b Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:37:24 +0300 Subject: [PATCH 07/10] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index efef226..1a61e1e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # MusBot -# Телеграм бот для распознавания +# Телеграм бот для распознавания музыки ## Задача +Разработать аналог приложения "Shazam" - Телеграм бота для распознавания музыки через голосовые или музыкальные файлы формата MP3. From 6787c82da13239af8adfcf63e07bc8359c835895 Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:54:28 +0300 Subject: [PATCH 08/10] Update README.md --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a61e1e..cb3b179 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,27 @@ -# MusBot -# Телеграм бот для распознавания музыки +# MusBot. Телеграм бот для распознавания музыки ## Задача Разработать аналог приложения "Shazam" - Телеграм бота для распознавания музыки через голосовые или музыкальные файлы формата MP3. +## Анализ предметной области +Для того, чтобы представить музыку в удобном для Shazam или любого другого сервиса виде, мы выделили следующие шаги преобразования: +1. Анализ частот. Преобразование Фурье; +2. Создание «карты созвездий»; +3. Устранение времени. +## Анализ частот. Преобразование Фурье +Преобразование Фурье - математический метод, позволяющий представить музыку в её первоначальном виде – до того, как все частоты смешались в один поток. + +![image](https://github.com/miroutom/MusBot/assets/78901500/77eaa2fc-dc6d-461c-bad0-95892ac39333) + + +Использование данного метода позволяет нам получить график, именуемый спектрограммой, показывающий зависимость частот (ось Y) от времени (ось X). Ниже представлена реализация метода Фурье с помощью библиотеки scipy. + +![image](https://github.com/miroutom/MusBot/assets/78901500/9a675d4c-bb5a-4753-83e9-867a3ac7f747) + + +## Создание "карты созвездий" +Создание "карты созвездий" или "отпечатка" композиции - это процесс анализа спектрограммы и выделения на ней самых ярких областей в каждый момент времени. Использование данной карты позволяет сократить размер композиции, а также удалить все лишние звуки. + + +![image](https://github.com/miroutom/MusBot/assets/78901500/81934d68-afd0-425c-a8bf-1d1200152929) + + +## Устранение времени From 119440e2ca36a35abb69c204ab9ed3ba0428d96f Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 18:59:04 +0300 Subject: [PATCH 09/10] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cb3b179..f9f416a 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,6 @@ ## Устранение времени +На данном этапе возникла довольно серьёзная проблема - записанный пользователем фрагмент может оказаться в любом моменте песни: будь то начало, середина или конец. В итоге, алгоритм примет совершенно другую карту. Поэтому на данном шаге было принято решение найти какую-то опорную точку O и связать её с несколькими другими точками. Так, вместо конкретных частот с привязкой ко времени, мы храним информацию о том, как эти частоты связаны между собой. + +## Обработка аудио. Создание спектрограммы From 745fefddf31743c93fcb4996ec6758e268991f8f Mon Sep 17 00:00:00 2001 From: cc30f <78901500+miroutom@users.noreply.github.com> Date: Mon, 15 May 2023 19:18:30 +0300 Subject: [PATCH 10/10] Update README.md --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f9f416a..f485f24 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,92 @@ 1. Анализ частот. Преобразование Фурье; 2. Создание «карты созвездий»; 3. Устранение времени. -## Анализ частот. Преобразование Фурье +### Анализ частот. Преобразование Фурье Преобразование Фурье - математический метод, позволяющий представить музыку в её первоначальном виде – до того, как все частоты смешались в один поток. ![image](https://github.com/miroutom/MusBot/assets/78901500/77eaa2fc-dc6d-461c-bad0-95892ac39333) -Использование данного метода позволяет нам получить график, именуемый спектрограммой, показывающий зависимость частот (ось Y) от времени (ось X). Ниже представлена реализация метода Фурье с помощью библиотеки scipy. +Использование данного метода позволяет нам получить график, именуемый спектрограммой, показывающий зависимость частот (ось Y) от времени (ось X). Ниже представлена реализация метода Фурье с помощью библиотеки `scipy`. ![image](https://github.com/miroutom/MusBot/assets/78901500/9a675d4c-bb5a-4753-83e9-867a3ac7f747) -## Создание "карты созвездий" +### Создание "карты созвездий" Создание "карты созвездий" или "отпечатка" композиции - это процесс анализа спектрограммы и выделения на ней самых ярких областей в каждый момент времени. Использование данной карты позволяет сократить размер композиции, а также удалить все лишние звуки. ![image](https://github.com/miroutom/MusBot/assets/78901500/81934d68-afd0-425c-a8bf-1d1200152929) -## Устранение времени +### Устранение времени На данном этапе возникла довольно серьёзная проблема - записанный пользователем фрагмент может оказаться в любом моменте песни: будь то начало, середина или конец. В итоге, алгоритм примет совершенно другую карту. Поэтому на данном шаге было принято решение найти какую-то опорную точку O и связать её с несколькими другими точками. Так, вместо конкретных частот с привязкой ко времени, мы храним информацию о том, как эти частоты связаны между собой. -## Обработка аудио. Создание спектрограммы +## Реализация алгоритма + +### Обработка аудио. Создание спектрограммы +Для создания спектрограммы сперва нужно было выбрать необходимую библиотеку. Мы предпочли `scipy`, так как в сравнении с её не менее известным аналогом `librosa`, она гораздо быстрее. +```Python +from scipy.io import wavfile +from scipy.signal import spectrogram +``` +Данная библиотека обрабатывает аудиофайлы в формате WAV. Но поскольку мы решили, что бот будет принимать форматы MP3 и OGG, необходимо было сделать предварительную обработку. +```Python + if input_file[-3:] == 'mp3': + sound = AudioSegment.from_mp3(input_file) + sound.export(output_file, format='wav') + elif input_file[-3:] == 'ogg': + sound = AudioSegment.from_ogg(input_file) + sound.export(output_file, format='wav') +``` + +После чего файл проверяется на количество каналов в нём (`scipy` обрабатывает только одноканальную музыку) и исходя из этого создаётся спектрограмма. +```Python + try: + f, t, Sxx = spectrogram(data[:, 0], samplerate, nperseg=FRAME_SIZE, + noverlap=HOP_SIZE, window='hamming') + except: + f, t, Sxx = spectrogram(data, samplerate, nperseg=FRAME_SIZE, + noverlap=HOP_SIZE, window='hamming') +``` + +Затем данная картинка отображается при помощи использования библиотеки `matplotlib`. + +![image](https://github.com/miroutom/MusBot/assets/78901500/89943812-6d59-4afa-a537-08c473e9eb5c) + + +### Создание "карты созвездий" +Данный этап был значительно упрощён в виду того, что библиотеки, реализующие `fingerprinting` оказались устаревшими или изменёнными, поэтому было принятно решение создать карту созвездий с использованием `np.array` и функции `flatten`, преобразовывающей многомерный массив в одномерный. +```Python + # read audio file + samplerate, data = wavfile.read(audio_file) + # generate spectrogram + try: + f, t, Sxx = spectrogram(data[:, 0], samplerate) + except: + f, t, Sxx = spectrogram(data, samplerate) + # replace zeros with epsilon to handle divide by zero errors + Sxx[Sxx == 0] = np.finfo(float).eps + # take logarithm of spectrogram + log_spectrogram = np.log(Sxx) + fingerprint = log_spectrogram.flatten() + return fingerprint +``` + +### Нахождение наибольшего совпадения +Для нахождения наибольшего совпадения мы написали функцию косинусного расстояния между векторами, работающей по следующему принципу: +1. Уравнять размеры векторов, заполнив меньший нулями +2. Вычислить скалярное произведение и норму векторов +3. Рассчитать косинусное расстояние как скалярное произведение, поделённое на произведение норм +```Python + # pad the smaller fingerprint with zeros to match the size of the larger fingerprint + if len(x) < len(y): + x = np.pad(x, (0, len(y) - len(x)), mode='constant') + elif len(y) < len(x): + y = np.pad(y, (0, len(x) - len(y)), mode='constant') + # calculate cosine distance + dot_product = np.dot(x, y) + norm_x = np.linalg.norm(x) + norm_y = np.linalg.norm(y) + return dot_product / (norm_x * norm_y) +```