music pom time
This commit is contained in:
parent
9bc8a460a5
commit
695c1884bd
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
|||||||
|
# My
|
||||||
|
/references
|
||||||
|
/db
|
||||||
|
config.ini
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
49
DBA.py
Normal file
49
DBA.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import sqlite3
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
class DBAccess:
|
||||||
|
def __init__(self, db_name=None):
|
||||||
|
config = ConfigParser()
|
||||||
|
config.read('config.ini')
|
||||||
|
if db_name is None:
|
||||||
|
db_name = config.get('db', 'library')
|
||||||
|
self._conn = sqlite3.connect(db_name)
|
||||||
|
self._cursor = self._conn.cursor()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection(self):
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cursor(self):
|
||||||
|
return self._cursor
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def close(self, commit=True):
|
||||||
|
if commit:
|
||||||
|
self.commit()
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
def execute(self, sql, params):
|
||||||
|
self.cursor.execute(sql, params or ())
|
||||||
|
|
||||||
|
def executemany(self, sql, seq_of_params):
|
||||||
|
self.cursor.executemany(sql, seq_of_params) #sqlite has execute many i guess?
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
return self.cursor.fetchall()
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
|
||||||
|
def query(self, sql, params):
|
||||||
|
self.cursor.execute(sql, params or ())
|
||||||
|
return self.fetchall()
|
||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 billy
|
Copyright (c) 2024 billypom
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
24
components/MusicTable.py
Normal file
24
components/MusicTable.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import DBA
|
||||||
|
from PyQt5.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
from PyQt5.QtWidgets import QTableView
|
||||||
|
|
||||||
|
|
||||||
|
class MusicTable(QTableView):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# Fetch library data
|
||||||
|
with DBA.DBAccess() as db: # returns a tuple, 1 row just for metadata purposes
|
||||||
|
data = db.query('SELECT title, artist, album, genre, codec, album_date, filepath FROM library;', ())
|
||||||
|
headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path']
|
||||||
|
# Create a model
|
||||||
|
model = QStandardItemModel()
|
||||||
|
model.setHorizontalHeaderLabels(headers)
|
||||||
|
# Populate the model
|
||||||
|
for row_data in data:
|
||||||
|
items = [QStandardItem(str(item)) for item in row_data]
|
||||||
|
model.appendRow(items)
|
||||||
|
# Set the model to the tableView
|
||||||
|
self.tableView.setModel(model)
|
||||||
|
# self.tableView.resizeColumnsToContents()
|
||||||
|
self.music_table.clicked.connect(self.set_clicked_cell_filepath)
|
||||||
|
|
||||||
1
components/__init__.py
Normal file
1
components/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .MusicTable import MusicTable
|
||||||
184
main.py
Normal file
184
main.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import DBA
|
||||||
|
from ui import Ui_MainWindow
|
||||||
|
from PyQt5.QtWidgets import QMainWindow, QApplication
|
||||||
|
import qdarktheme
|
||||||
|
from PyQt5.QtCore import QUrl, QTimer, QFile, QTextStream
|
||||||
|
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe
|
||||||
|
from PyQt5.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
from utils import scan_for_music
|
||||||
|
from utils import initialize_library_database
|
||||||
|
from utils import AudioVisualizer
|
||||||
|
from pyqtgraph import mkBrush
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super(ApplicationWindow, self).__init__()
|
||||||
|
self.setupUi(self)
|
||||||
|
self.setWindowTitle('MusicPom')
|
||||||
|
self.selected_song_filepath = None
|
||||||
|
self.current_song_filepath = None
|
||||||
|
|
||||||
|
global stopped
|
||||||
|
stopped = False
|
||||||
|
|
||||||
|
# Initialization
|
||||||
|
self.player = QMediaPlayer(None, QMediaPlayer.VideoSurface) # Audio player
|
||||||
|
self.probe = QAudioProbe() # Get audio data
|
||||||
|
self.timer = QTimer(self) # Audio timing things
|
||||||
|
self.model = QStandardItemModel() # Table library listing
|
||||||
|
self.audio_visualizer = AudioVisualizer(self.player)
|
||||||
|
self.current_volume = 50
|
||||||
|
self.player.setVolume(self.current_volume)
|
||||||
|
|
||||||
|
# Audio probe for processing audio signal in real time
|
||||||
|
# Provides faster updates than move_slider
|
||||||
|
self.probe.setSource(self.player)
|
||||||
|
self.probe.audioBufferProbed.connect(self.process_probe)
|
||||||
|
|
||||||
|
# Probably move all the table logic to its own class
|
||||||
|
# Promote the wigdet to my custom class from utils
|
||||||
|
# inherit from QTableView
|
||||||
|
# then i can handle click events for editing metadata in a class
|
||||||
|
|
||||||
|
# current song should also be its own class
|
||||||
|
# selected song should be its own class too...
|
||||||
|
|
||||||
|
# ______________
|
||||||
|
# | |
|
||||||
|
# | Table making |
|
||||||
|
# | |
|
||||||
|
# |______________|
|
||||||
|
|
||||||
|
# Fetch library data
|
||||||
|
with DBA.DBAccess() as db:
|
||||||
|
data = db.query('SELECT title, artist, album, genre, codec, album_date, filepath FROM library;', ())
|
||||||
|
headers = ['title', 'artist', 'album', 'genre', 'codec', 'year', 'path']
|
||||||
|
self.model.setHorizontalHeaderLabels(headers)
|
||||||
|
for row_data in data: # Populate the model
|
||||||
|
items = [QStandardItem(str(item)) for item in row_data]
|
||||||
|
self.model.appendRow(items)
|
||||||
|
# Set the model to the tableView
|
||||||
|
self.tableView.setModel(self.model)
|
||||||
|
# self.tableView.resizeColumnsToContents()
|
||||||
|
|
||||||
|
|
||||||
|
# Slider Timer (realtime playback feedback horizontal bar)
|
||||||
|
self.timer.start(150) # 150ms update interval solved problem with drag seeking halting playback
|
||||||
|
self.timer.timeout.connect(self.move_slider)
|
||||||
|
|
||||||
|
# Graphics plot
|
||||||
|
self.PlotWidget.setXRange(0,100,padding=0) # x axis range
|
||||||
|
self.PlotWidget.setYRange(0,0.3,padding=0) # y axis range
|
||||||
|
self.PlotWidget.getAxis('bottom').setTicks([]) # Remove x-axis ticks
|
||||||
|
self.PlotWidget.getAxis('bottom').setLabel('') # Remove x-axis label
|
||||||
|
self.PlotWidget.setLogMode(False,False)
|
||||||
|
# Remove y-axis labels and decorations
|
||||||
|
self.PlotWidget.getAxis('left').setTicks([]) # Remove y-axis ticks
|
||||||
|
self.PlotWidget.getAxis('left').setLabel('') # Remove y-axis label
|
||||||
|
|
||||||
|
|
||||||
|
# Connections
|
||||||
|
self.playbackSlider.sliderMoved[int].connect(lambda: self.player.setPosition(self.playbackSlider.value()))
|
||||||
|
self.volumeSlider.sliderMoved[int].connect(lambda: self.volume_changed())
|
||||||
|
self.playButton.clicked.connect(self.on_play_clicked)
|
||||||
|
# self.pauseButton.clicked.connect(self.on_pause_clicked)
|
||||||
|
self.previousButton.clicked.connect(self.on_previous_clicked)
|
||||||
|
self.nextButton.clicked.connect(self.on_next_clicked)
|
||||||
|
self.tableView.clicked.connect(self.set_clicked_cell_filepath)
|
||||||
|
self.actionPreferences.triggered.connect(self.actionPreferencesClicked)
|
||||||
|
self.actionScanLibraries.triggered.connect(self.scan_libraries)
|
||||||
|
self.actionClearDatabase.triggered.connect(initialize_library_database)
|
||||||
|
|
||||||
|
def play_audio_file(self):
|
||||||
|
"""Start playback of selected track & move playback slider"""
|
||||||
|
self.current_song_filepath = self.selected_song_filepath
|
||||||
|
url = QUrl.fromLocalFile(self.current_song_filepath) # read the file
|
||||||
|
content = QMediaContent(url) # load the audio content
|
||||||
|
|
||||||
|
self.player.setMedia(content) # what content to play
|
||||||
|
self.player.play() # play
|
||||||
|
self.move_slider() # mover
|
||||||
|
|
||||||
|
def update_audio_visualization(self):
|
||||||
|
"""Handles upading points on the pyqtgraph visual"""
|
||||||
|
self.clear_audio_visualization()
|
||||||
|
self.y = self.audio_visualizer.get_amplitudes()
|
||||||
|
self.x = [i for i in range(len(self.y))]
|
||||||
|
self.PlotWidget.plot(self.x, self.y, fillLevel=0, fillBrush=mkBrush('b'))
|
||||||
|
self.PlotWidget.show()
|
||||||
|
|
||||||
|
def clear_audio_visualization(self):
|
||||||
|
self.PlotWidget.clear()
|
||||||
|
|
||||||
|
def move_slider(self):
|
||||||
|
"""Handles moving the playback slider"""
|
||||||
|
if stopped:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Update the slider
|
||||||
|
if self.player.state() == QMediaPlayer.PlayingState:
|
||||||
|
self.playbackSlider.setMinimum(0)
|
||||||
|
self.playbackSlider.setMaximum(self.player.duration())
|
||||||
|
slider_position = self.player.position()
|
||||||
|
self.playbackSlider.setValue(slider_position)
|
||||||
|
|
||||||
|
current_minutes, current_seconds = divmod(slider_position / 1000, 60)
|
||||||
|
duration_minutes, duration_seconds = divmod(self.player.duration() / 1000, 60)
|
||||||
|
|
||||||
|
self.startTimeLabel.setText(f"{int(current_minutes):02d}:{int(current_seconds):02d}")
|
||||||
|
self.endTimeLabel.setText(f"{int(duration_minutes):02d}:{int(duration_seconds):02d}")
|
||||||
|
|
||||||
|
def volume_changed(self):
|
||||||
|
"""Handles volume changes"""
|
||||||
|
try:
|
||||||
|
self.current_volume = self.volumeSlider.value()
|
||||||
|
self.player.setVolume(self.current_volume)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Changing volume error: {e}")
|
||||||
|
|
||||||
|
def on_play_clicked(self):
|
||||||
|
"""Updates the Play & Pause buttons when clicked"""
|
||||||
|
if self.player.state() == QMediaPlayer.PlayingState:
|
||||||
|
self.player.pause()
|
||||||
|
self.playButton.setText("▶️")
|
||||||
|
else:
|
||||||
|
if self.player.state() == QMediaPlayer.PausedState:
|
||||||
|
self.player.play()
|
||||||
|
self.playButton.setText("⏸️")
|
||||||
|
else:
|
||||||
|
self.play_audio_file()
|
||||||
|
self.playButton.setText("⏸️")
|
||||||
|
|
||||||
|
def set_clicked_cell_filepath(self):
|
||||||
|
"""Sets the filepath of the currently selected song"""
|
||||||
|
self.selected_song_filepath = self.tableView.currentIndex().siblingAtColumn(6).data()
|
||||||
|
|
||||||
|
def on_previous_clicked(self):
|
||||||
|
""""""
|
||||||
|
print('previous')
|
||||||
|
|
||||||
|
def on_next_clicked(self):
|
||||||
|
print('next')
|
||||||
|
|
||||||
|
def actionPreferencesClicked(self):
|
||||||
|
print('preferences')
|
||||||
|
|
||||||
|
def scan_libraries(self):
|
||||||
|
scan_for_music()
|
||||||
|
# refresh datatable
|
||||||
|
|
||||||
|
def process_probe(self, buff):
|
||||||
|
buff.startTime()
|
||||||
|
self.update_audio_visualization()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
qdarktheme.setup_theme()
|
||||||
|
ui = ApplicationWindow()
|
||||||
|
ui.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
tinytag
|
||||||
|
matplotlib
|
||||||
|
pyqt5
|
||||||
|
pydub
|
||||||
|
pyqtdarktheme
|
||||||
|
pyqtgraph
|
||||||
|
scipy
|
||||||
12
sample_config.ini
Normal file
12
sample_config.ini
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[db]
|
||||||
|
# The library database file
|
||||||
|
library = db/library.db
|
||||||
|
|
||||||
|
[directories]
|
||||||
|
# Useful paths to have stored
|
||||||
|
library = /path/to/music
|
||||||
|
reorganize_destination = /where/to/reorganize/to
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
# Which file types are scanned
|
||||||
|
extensions = mp3,wav,ogg,flac
|
||||||
200
ui.py
Normal file
200
ui.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'ui.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.15.10
|
||||||
|
#
|
||||||
|
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||||
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_MainWindow(object):
|
||||||
|
def setupUi(self, MainWindow):
|
||||||
|
MainWindow.setObjectName("MainWindow")
|
||||||
|
MainWindow.resize(1061, 795)
|
||||||
|
MainWindow.setStatusTip("")
|
||||||
|
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
||||||
|
self.centralwidget.setObjectName("centralwidget")
|
||||||
|
self.frame = QtWidgets.QFrame(self.centralwidget)
|
||||||
|
self.frame.setGeometry(QtCore.QRect(0, 160, 1061, 501))
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth())
|
||||||
|
self.frame.setSizePolicy(sizePolicy)
|
||||||
|
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
|
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
|
self.frame.setObjectName("frame")
|
||||||
|
self.tableView = QtWidgets.QTableView(self.frame)
|
||||||
|
self.tableView.setGeometry(QtCore.QRect(0, 0, 1061, 501))
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
|
||||||
|
self.tableView.setSizePolicy(sizePolicy)
|
||||||
|
self.tableView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||||
|
self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
|
||||||
|
self.tableView.setAlternatingRowColors(True)
|
||||||
|
self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||||
|
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||||
|
self.tableView.setObjectName("tableView")
|
||||||
|
self.tableView.verticalHeader().setVisible(False)
|
||||||
|
self.frame_2 = QtWidgets.QFrame(self.centralwidget)
|
||||||
|
self.frame_2.setGeometry(QtCore.QRect(0, 0, 161, 161))
|
||||||
|
self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
|
self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
|
self.frame_2.setObjectName("frame_2")
|
||||||
|
self.albumGraphicsView = QtWidgets.QGraphicsView(self.frame_2)
|
||||||
|
self.albumGraphicsView.setGeometry(QtCore.QRect(0, 0, 161, 161))
|
||||||
|
self.albumGraphicsView.setObjectName("albumGraphicsView")
|
||||||
|
self.horizontalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
|
||||||
|
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(540, 0, 521, 41))
|
||||||
|
self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
|
||||||
|
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.playbackSlider = QtWidgets.QSlider(self.horizontalLayoutWidget)
|
||||||
|
self.playbackSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.playbackSlider.setObjectName("playbackSlider")
|
||||||
|
self.horizontalLayout.addWidget(self.playbackSlider)
|
||||||
|
self.startTimeLabel = QtWidgets.QLabel(self.horizontalLayoutWidget)
|
||||||
|
self.startTimeLabel.setObjectName("startTimeLabel")
|
||||||
|
self.horizontalLayout.addWidget(self.startTimeLabel)
|
||||||
|
self.slashLabel = QtWidgets.QLabel(self.horizontalLayoutWidget)
|
||||||
|
self.slashLabel.setObjectName("slashLabel")
|
||||||
|
self.horizontalLayout.addWidget(self.slashLabel)
|
||||||
|
self.endTimeLabel = QtWidgets.QLabel(self.horizontalLayoutWidget)
|
||||||
|
self.endTimeLabel.setObjectName("endTimeLabel")
|
||||||
|
self.horizontalLayout.addWidget(self.endTimeLabel)
|
||||||
|
self.frame_4 = QtWidgets.QFrame(self.centralwidget)
|
||||||
|
self.frame_4.setGeometry(QtCore.QRect(540, 40, 521, 121))
|
||||||
|
self.frame_4.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
|
self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
|
self.frame_4.setObjectName("frame_4")
|
||||||
|
self.PlotWidget = PlotWidget(self.frame_4)
|
||||||
|
self.PlotWidget.setGeometry(QtCore.QRect(0, 0, 521, 121))
|
||||||
|
self.PlotWidget.setObjectName("PlotWidget")
|
||||||
|
self.horizontalLayoutWidget_2 = QtWidgets.QWidget(self.centralwidget)
|
||||||
|
self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(0, 660, 1061, 60))
|
||||||
|
self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2")
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
|
||||||
|
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
self.previousButton = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(28)
|
||||||
|
self.previousButton.setFont(font)
|
||||||
|
self.previousButton.setObjectName("previousButton")
|
||||||
|
self.horizontalLayout_2.addWidget(self.previousButton)
|
||||||
|
self.playButton = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(28)
|
||||||
|
self.playButton.setFont(font)
|
||||||
|
self.playButton.setObjectName("playButton")
|
||||||
|
self.horizontalLayout_2.addWidget(self.playButton)
|
||||||
|
self.nextButton = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(28)
|
||||||
|
self.nextButton.setFont(font)
|
||||||
|
self.nextButton.setObjectName("nextButton")
|
||||||
|
self.horizontalLayout_2.addWidget(self.nextButton)
|
||||||
|
self.frame_3 = QtWidgets.QFrame(self.centralwidget)
|
||||||
|
self.frame_3.setGeometry(QtCore.QRect(0, 720, 1061, 31))
|
||||||
|
self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
|
self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
|
self.frame_3.setObjectName("frame_3")
|
||||||
|
self.volumeSlider = QtWidgets.QSlider(self.frame_3)
|
||||||
|
self.volumeSlider.setGeometry(QtCore.QRect(10, 0, 181, 31))
|
||||||
|
self.volumeSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.volumeSlider.setObjectName("volumeSlider")
|
||||||
|
self.frame_5 = QtWidgets.QFrame(self.centralwidget)
|
||||||
|
self.frame_5.setGeometry(QtCore.QRect(160, 0, 381, 161))
|
||||||
|
self.frame_5.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||||
|
self.frame_5.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||||
|
self.frame_5.setObjectName("frame_5")
|
||||||
|
self.verticalLayoutWidget = QtWidgets.QWidget(self.frame_5)
|
||||||
|
self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 381, 161))
|
||||||
|
self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
|
||||||
|
self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
|
||||||
|
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.verticalLayout.setObjectName("verticalLayout")
|
||||||
|
self.artistLabel = QtWidgets.QLabel(self.verticalLayoutWidget)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(24)
|
||||||
|
font.setBold(True)
|
||||||
|
font.setWeight(75)
|
||||||
|
self.artistLabel.setFont(font)
|
||||||
|
self.artistLabel.setObjectName("artistLabel")
|
||||||
|
self.verticalLayout.addWidget(self.artistLabel)
|
||||||
|
self.titleLabel = QtWidgets.QLabel(self.verticalLayoutWidget)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(18)
|
||||||
|
self.titleLabel.setFont(font)
|
||||||
|
self.titleLabel.setObjectName("titleLabel")
|
||||||
|
self.verticalLayout.addWidget(self.titleLabel)
|
||||||
|
self.albumLabel = QtWidgets.QLabel(self.verticalLayoutWidget)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(16)
|
||||||
|
font.setBold(False)
|
||||||
|
font.setItalic(True)
|
||||||
|
font.setWeight(50)
|
||||||
|
self.albumLabel.setFont(font)
|
||||||
|
self.albumLabel.setObjectName("albumLabel")
|
||||||
|
self.verticalLayout.addWidget(self.albumLabel)
|
||||||
|
MainWindow.setCentralWidget(self.centralwidget)
|
||||||
|
self.menubar = QtWidgets.QMenuBar(MainWindow)
|
||||||
|
self.menubar.setGeometry(QtCore.QRect(0, 0, 1061, 24))
|
||||||
|
self.menubar.setObjectName("menubar")
|
||||||
|
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||||
|
self.menuFile.setObjectName("menuFile")
|
||||||
|
self.menuEdit = QtWidgets.QMenu(self.menubar)
|
||||||
|
self.menuEdit.setObjectName("menuEdit")
|
||||||
|
self.menuView = QtWidgets.QMenu(self.menubar)
|
||||||
|
self.menuView.setObjectName("menuView")
|
||||||
|
self.menuQuick_Actions = QtWidgets.QMenu(self.menubar)
|
||||||
|
self.menuQuick_Actions.setObjectName("menuQuick_Actions")
|
||||||
|
MainWindow.setMenuBar(self.menubar)
|
||||||
|
self.statusbar = QtWidgets.QStatusBar(MainWindow)
|
||||||
|
self.statusbar.setObjectName("statusbar")
|
||||||
|
MainWindow.setStatusBar(self.statusbar)
|
||||||
|
self.actionPreferences = QtWidgets.QAction(MainWindow)
|
||||||
|
self.actionPreferences.setObjectName("actionPreferences")
|
||||||
|
self.actionScanLibraries = QtWidgets.QAction(MainWindow)
|
||||||
|
self.actionScanLibraries.setObjectName("actionScanLibraries")
|
||||||
|
self.actionClearDatabase = QtWidgets.QAction(MainWindow)
|
||||||
|
self.actionClearDatabase.setObjectName("actionClearDatabase")
|
||||||
|
self.menuEdit.addAction(self.actionPreferences)
|
||||||
|
self.menuQuick_Actions.addAction(self.actionScanLibraries)
|
||||||
|
self.menuQuick_Actions.addAction(self.actionClearDatabase)
|
||||||
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
|
self.menubar.addAction(self.menuEdit.menuAction())
|
||||||
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
|
self.menubar.addAction(self.menuQuick_Actions.menuAction())
|
||||||
|
|
||||||
|
self.retranslateUi(MainWindow)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||||
|
|
||||||
|
def retranslateUi(self, MainWindow):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
|
||||||
|
self.startTimeLabel.setText(_translate("MainWindow", "00:00"))
|
||||||
|
self.slashLabel.setText(_translate("MainWindow", "/"))
|
||||||
|
self.endTimeLabel.setText(_translate("MainWindow", "00:00"))
|
||||||
|
self.previousButton.setText(_translate("MainWindow", "⏮️"))
|
||||||
|
self.playButton.setText(_translate("MainWindow", "▶️"))
|
||||||
|
self.nextButton.setText(_translate("MainWindow", "⏭️"))
|
||||||
|
self.artistLabel.setText(_translate("MainWindow", "artist"))
|
||||||
|
self.titleLabel.setText(_translate("MainWindow", "song title"))
|
||||||
|
self.albumLabel.setText(_translate("MainWindow", "album"))
|
||||||
|
self.menuFile.setTitle(_translate("MainWindow", "File"))
|
||||||
|
self.menuEdit.setTitle(_translate("MainWindow", "Edit"))
|
||||||
|
self.menuView.setTitle(_translate("MainWindow", "View"))
|
||||||
|
self.menuQuick_Actions.setTitle(_translate("MainWindow", "Quick-Actions"))
|
||||||
|
self.actionPreferences.setText(_translate("MainWindow", "Preferences"))
|
||||||
|
self.actionPreferences.setStatusTip(_translate("MainWindow", "Open preferences"))
|
||||||
|
self.actionScanLibraries.setText(_translate("MainWindow", "Scan libraries"))
|
||||||
|
self.actionClearDatabase.setText(_translate("MainWindow", "Clear Database"))
|
||||||
|
from pyqtgraph import PlotWidget
|
||||||
382
ui.ui
Normal file
382
ui.ui
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>795</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>MainWindow</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<widget class="QFrame" name="frame">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>160</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>501</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<widget class="QTableView" name="tableView">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>501</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="horizontalScrollBarPolicy">
|
||||||
|
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeAdjustPolicy">
|
||||||
|
<enum>QAbstractScrollArea::AdjustToContents</enum>
|
||||||
|
</property>
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<attribute name="verticalHeaderVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QFrame" name="frame_2">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>161</width>
|
||||||
|
<height>161</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<widget class="QGraphicsView" name="albumGraphicsView">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>161</width>
|
||||||
|
<height>161</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="horizontalLayoutWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>540</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>521</width>
|
||||||
|
<height>41</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="playbackSlider">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="startTimeLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>00:00</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="slashLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>/</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="endTimeLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>00:00</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QFrame" name="frame_4">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>540</x>
|
||||||
|
<y>40</y>
|
||||||
|
<width>521</width>
|
||||||
|
<height>121</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<widget class="PlotWidget" name="PlotWidget" native="true">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>521</width>
|
||||||
|
<height>121</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="horizontalLayoutWidget_2">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>660</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>60</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="previousButton">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>28</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>⏮️</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="playButton">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>28</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>▶️</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="nextButton">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>28</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>⏭️</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QFrame" name="frame_3">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>720</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>31</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<widget class="QSlider" name="volumeSlider">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>181</width>
|
||||||
|
<height>31</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QFrame" name="frame_5">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>160</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>381</width>
|
||||||
|
<height>161</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="verticalLayoutWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>381</width>
|
||||||
|
<height>161</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="artistLabel">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>24</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>artist</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="titleLabel">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>18</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>song title</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="albumLabel">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>16</pointsize>
|
||||||
|
<weight>50</weight>
|
||||||
|
<italic>true</italic>
|
||||||
|
<bold>false</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>album</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1061</width>
|
||||||
|
<height>24</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<widget class="QMenu" name="menuFile">
|
||||||
|
<property name="title">
|
||||||
|
<string>File</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuEdit">
|
||||||
|
<property name="title">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionPreferences"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuView">
|
||||||
|
<property name="title">
|
||||||
|
<string>View</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuQuick_Actions">
|
||||||
|
<property name="title">
|
||||||
|
<string>Quick-Actions</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionScanLibraries"/>
|
||||||
|
<addaction name="actionClearDatabase"/>
|
||||||
|
</widget>
|
||||||
|
<addaction name="menuFile"/>
|
||||||
|
<addaction name="menuEdit"/>
|
||||||
|
<addaction name="menuView"/>
|
||||||
|
<addaction name="menuQuick_Actions"/>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
<action name="actionPreferences">
|
||||||
|
<property name="text">
|
||||||
|
<string>Preferences</string>
|
||||||
|
</property>
|
||||||
|
<property name="statusTip">
|
||||||
|
<string>Open preferences</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionScanLibraries">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scan libraries</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionClearDatabase">
|
||||||
|
<property name="text">
|
||||||
|
<string>Clear Database</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>PlotWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>pyqtgraph</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
4
utils/__init__.py
Normal file
4
utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .initialize_library_database import initialize_library_database
|
||||||
|
from .scan_for_music import scan_for_music
|
||||||
|
from .audio_visualizer import AudioVisualizer
|
||||||
|
from.fft_analyser import FFTAnalyser
|
||||||
20
utils/audio_visualizer.py
Normal file
20
utils/audio_visualizer.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .fft_analyser import FFTAnalyser
|
||||||
|
|
||||||
|
class AudioVisualizer(QtWidgets.QWidget):
|
||||||
|
|
||||||
|
def __init__(self, media_player):
|
||||||
|
super().__init__()
|
||||||
|
self.media_player = media_player
|
||||||
|
self.fft_analyser = FFTAnalyser(self.media_player)
|
||||||
|
self.fft_analyser.calculated_visual.connect(self.set_amplitudes)
|
||||||
|
self.fft_analyser.start()
|
||||||
|
self.amps = np.array([])
|
||||||
|
|
||||||
|
def get_amplitudes(self):
|
||||||
|
return self.amps
|
||||||
|
|
||||||
|
def set_amplitudes(self, amps):
|
||||||
|
self.amps = np.array(amps)
|
||||||
15
utils/create_library.sql
Normal file
15
utils/create_library.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
DROP TABLE IF EXISTS library;
|
||||||
|
|
||||||
|
CREATE TABLE library(
|
||||||
|
-- In SQLite, a column with type INTEGER PRIMARY KEY is an alias for the ROWID (except in WITHOUT ROWID tables) which is always a 64-bit signed integer.
|
||||||
|
id integer primary key,
|
||||||
|
filepath varchar(511) UNIQUE,
|
||||||
|
title varchar(255),
|
||||||
|
album varchar(255),
|
||||||
|
artist varchar(255),
|
||||||
|
genre varchar(255),
|
||||||
|
codec varchar(15),
|
||||||
|
album_date date,
|
||||||
|
bitrate int,
|
||||||
|
date_added TIMESTAMP default CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
50
utils/create_waveform_from_file.py
Normal file
50
utils/create_waveform_from_file.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
|
||||||
|
def create_waveform_from_file(file):
|
||||||
|
# Read the MP3 file
|
||||||
|
audio = AudioSegment.from_file(file)
|
||||||
|
# Convert to mono and get frame rate and number of channels
|
||||||
|
num_channels = 1
|
||||||
|
frame_rate = audio.frame_rate
|
||||||
|
downsample = math.ceil(frame_rate * num_channels / 1) # /1 = 1 sample per second
|
||||||
|
# Process audio data
|
||||||
|
process_chunk_size = 600000 - (600000 % frame_rate)
|
||||||
|
signal = None
|
||||||
|
waveform = np.array([])
|
||||||
|
# Convert the audio data to numpy array
|
||||||
|
audio_data = np.array(audio.get_array_of_samples())
|
||||||
|
# Create waveforms
|
||||||
|
start_index = 0
|
||||||
|
while start_index < len(audio_data):
|
||||||
|
end_index = start_index + process_chunk_size
|
||||||
|
signal = audio_data[start_index:end_index].astype(float) # Get chunk and convert to float
|
||||||
|
|
||||||
|
# Take mean of absolute values per 0.5 seconds
|
||||||
|
sub_waveform = np.nanmean(
|
||||||
|
np.pad(np.absolute(signal), (0, ((downsample - (signal.size % downsample)) % downsample)), mode='constant', constant_values=np.NaN).reshape(-1, downsample),
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
|
||||||
|
waveform = np.concatenate((waveform, sub_waveform))
|
||||||
|
start_index = end_index
|
||||||
|
|
||||||
|
# Plot waveforms
|
||||||
|
plt.figure(1)
|
||||||
|
plt.plot(waveform, color='blue')
|
||||||
|
plt.plot(-waveform, color='blue') # Mirrored waveform
|
||||||
|
# Fill in area
|
||||||
|
plt.fill_between(np.arange(len(waveform)), waveform, -waveform, color='blue', alpha=0.5)
|
||||||
|
# Remove decorations, labels, axes, etc
|
||||||
|
plt.axis('off')
|
||||||
|
# Graph goes to ends of pic
|
||||||
|
plt.xlim(0, len(waveform))
|
||||||
|
# Save pic
|
||||||
|
plt.savefig('now_playing_waveform.png', dpi=64, bbox_inches='tight', pad_inches=0)
|
||||||
|
# Show me tho
|
||||||
|
# plt.show()
|
||||||
|
|
||||||
|
# create_waveform_from_file(sys.argv[1])
|
||||||
114
utils/fft_analyser.py
Normal file
114
utils/fft_analyser.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Credit
|
||||||
|
# https://github.com/ravenkls/MilkPlayer/blob/master/audio/fft_analyser.py
|
||||||
|
|
||||||
|
# std
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
# qt
|
||||||
|
from PyQt5 import QtCore
|
||||||
|
|
||||||
|
# other
|
||||||
|
from pydub import AudioSegment
|
||||||
|
import numpy as np
|
||||||
|
from scipy.ndimage.filters import gaussian_filter1d
|
||||||
|
|
||||||
|
|
||||||
|
class FFTAnalyser(QtCore.QThread):
|
||||||
|
"""Analyses a song on a playlist using FFTs."""
|
||||||
|
|
||||||
|
calculated_visual = QtCore.pyqtSignal(np.ndarray)
|
||||||
|
|
||||||
|
def __init__(self, player: 'MusicPlayer'): # noqa: F821
|
||||||
|
super().__init__()
|
||||||
|
self.player = player
|
||||||
|
self.reset_media()
|
||||||
|
self.player.currentMediaChanged.connect(self.reset_media)
|
||||||
|
|
||||||
|
self.resolution = 100
|
||||||
|
self.visual_delta_threshold = 1000
|
||||||
|
self.sensitivity = 5
|
||||||
|
|
||||||
|
def reset_media(self):
|
||||||
|
"""Resets the media to the currently playing song."""
|
||||||
|
audio_file = self.player.currentMedia().canonicalUrl().path()
|
||||||
|
if os.name == 'nt' and audio_file.startswith('/'):
|
||||||
|
audio_file = audio_file[1:]
|
||||||
|
if audio_file:
|
||||||
|
try:
|
||||||
|
self.song = AudioSegment.from_file(audio_file).set_channels(1)
|
||||||
|
except PermissionError:
|
||||||
|
self.start_animate = False
|
||||||
|
else:
|
||||||
|
self.samples = np.array(self.song.get_array_of_samples())
|
||||||
|
|
||||||
|
self.max_sample = self.samples.max()
|
||||||
|
self.points = np.zeros(self.resolution)
|
||||||
|
self.start_animate = True
|
||||||
|
else:
|
||||||
|
self.start_animate = False
|
||||||
|
|
||||||
|
def calculate_amps(self):
|
||||||
|
"""Calculates the amplitudes used for visualising the media."""
|
||||||
|
|
||||||
|
sample_count = int(self.song.frame_rate * 0.05)
|
||||||
|
start_index = int((self.player.position()/1000) * self.song.frame_rate)
|
||||||
|
v_sample = self.samples[start_index:start_index+sample_count] # samples to analyse
|
||||||
|
|
||||||
|
# use FFTs to analyse frequency and amplitudes
|
||||||
|
fourier = np.fft.fft(v_sample)
|
||||||
|
freq = np.fft.fftfreq(fourier.size, d=0.05)
|
||||||
|
amps = 2/v_sample.size * np.abs(fourier)
|
||||||
|
data = np.array([freq, amps]).T
|
||||||
|
|
||||||
|
point_range = 1 / self.resolution
|
||||||
|
point_samples = []
|
||||||
|
|
||||||
|
if not data.size:
|
||||||
|
return
|
||||||
|
|
||||||
|
for n, f in enumerate(np.arange(0, 1, point_range), start=1):
|
||||||
|
# get the amps which are in between the frequency range
|
||||||
|
amps = data[(f - point_range < data[:, 0]) & (data[:, 0] < f)]
|
||||||
|
if not amps.size:
|
||||||
|
point_samples.append(0)
|
||||||
|
else:
|
||||||
|
point_samples.append(amps.max()*((1+self.sensitivity/10+(self.sensitivity-1)/10)**(n/50)))
|
||||||
|
|
||||||
|
# Add the point_samples to the self.points array, the reason we have a separate
|
||||||
|
# array (self.bars) is so that we can fade out the previous amplitudes from
|
||||||
|
# the past
|
||||||
|
for n, amp in enumerate(point_samples):
|
||||||
|
|
||||||
|
amp *= 2
|
||||||
|
|
||||||
|
if (self.points[n] > 0 and amp < self.points[n] or
|
||||||
|
self.player.state() in (self.player.PausedState, self.player.StoppedState)):
|
||||||
|
self.points[n] -= self.points[n] / 10 # fade out
|
||||||
|
elif abs(self.points[n] - amp) > self.visual_delta_threshold:
|
||||||
|
self.points[n] = amp
|
||||||
|
if self.points[n] < 1:
|
||||||
|
self.points[n] = 1e-5
|
||||||
|
|
||||||
|
# interpolate points
|
||||||
|
rs = gaussian_filter1d(self.points, sigma=2)
|
||||||
|
|
||||||
|
# Mirror the amplitudes, these are renamed to 'rs' because we are using them
|
||||||
|
# for polar plotting, which is plotted in terms of r and theta
|
||||||
|
# rs = np.concatenate((rs, np.flip(rs)))
|
||||||
|
# rs = np.concatenate((rs, np.flip(rs)))
|
||||||
|
|
||||||
|
# they are divided by the highest sample in the song to normalise the
|
||||||
|
# amps in terms of decimals from 0 -> 1
|
||||||
|
self.calculated_visual.emit(rs / self.max_sample)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Runs the animate function depending on the song."""
|
||||||
|
while True:
|
||||||
|
if self.start_animate:
|
||||||
|
try:
|
||||||
|
self.calculate_amps()
|
||||||
|
except ValueError:
|
||||||
|
self.calculated_visual.emit(np.zeros(self.resolution))
|
||||||
|
self.start_animate = False
|
||||||
|
time.sleep(0.025)
|
||||||
9
utils/initialize_library_database.py
Normal file
9
utils/initialize_library_database.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import DBA
|
||||||
|
|
||||||
|
def initialize_library_database():
|
||||||
|
with open('utils/create_library.sql', 'r') as file:
|
||||||
|
lines = file.read()
|
||||||
|
for statement in lines.split(';'):
|
||||||
|
print(f'executing [{statement}]')
|
||||||
|
with DBA.DBAccess() as db:
|
||||||
|
db.execute(statement, ())
|
||||||
55
utils/scan_for_music.py
Normal file
55
utils/scan_for_music.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import os
|
||||||
|
import DBA
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from tinytag import TinyTag
|
||||||
|
|
||||||
|
config = ConfigParser()
|
||||||
|
config.read('config.ini')
|
||||||
|
|
||||||
|
|
||||||
|
def scan_for_music():
|
||||||
|
root_dir = config.get('directories', 'library')
|
||||||
|
extensions = config.get('settings', 'extensions').split(',')
|
||||||
|
insert_data = [] # To store data for batch insert
|
||||||
|
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||||
|
for filename in filenames:
|
||||||
|
if any(filename.lower().endswith(ext) for ext in extensions):
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
audio = TinyTag.get(filepath)
|
||||||
|
|
||||||
|
# Append data tuple to insert_data list
|
||||||
|
insert_data.append((
|
||||||
|
filepath,
|
||||||
|
audio.title,
|
||||||
|
audio.album,
|
||||||
|
audio.artist,
|
||||||
|
audio.genre,
|
||||||
|
filename.split('.')[-1],
|
||||||
|
audio.year,
|
||||||
|
audio.bitrate
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check if batch size is reached
|
||||||
|
if len(insert_data) >= 1000:
|
||||||
|
with DBA.DBAccess() as db:
|
||||||
|
db.executemany('INSERT OR IGNORE INTO library (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', insert_data)
|
||||||
|
insert_data = [] # Reset the insert_data list
|
||||||
|
|
||||||
|
# Insert any remaining data
|
||||||
|
if insert_data:
|
||||||
|
with DBA.DBAccess() as db:
|
||||||
|
db.executemany('INSERT OR IGNORE INTO library (filepath, title, album, artist, genre, codec, album_date, bitrate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', insert_data)
|
||||||
|
|
||||||
|
|
||||||
|
# id int unsigned auto_increment,
|
||||||
|
# title varchar(255),
|
||||||
|
# album varchar(255),
|
||||||
|
# artist varchar(255),
|
||||||
|
# genre varchar(255),
|
||||||
|
# codec varchar(15),
|
||||||
|
# album_date date,
|
||||||
|
# bitrate int unsigned,
|
||||||
|
# date_added TIMESTAMP default CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
# scan_for_music(config.get('directories', 'library1'))
|
||||||
36
utils/waveform_from_wav_file.py
Normal file
36
utils/waveform_from_wav_file.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import wave
|
||||||
|
import math
|
||||||
|
|
||||||
|
file = 'output.wav'
|
||||||
|
|
||||||
|
with wave.open(file,'r') as wav_file:
|
||||||
|
num_channels = wav_file.getnchannels()
|
||||||
|
frame_rate = wav_file.getframerate()
|
||||||
|
downsample = math.ceil(frame_rate * num_channels / 2) # Get two samples per second!
|
||||||
|
|
||||||
|
process_chunk_size = 600000 - (600000 % frame_rate)
|
||||||
|
|
||||||
|
signal = None
|
||||||
|
waveform = np.array([])
|
||||||
|
|
||||||
|
while signal is None or signal.size > 0:
|
||||||
|
signal = np.frombuffer(wav_file.readframes(process_chunk_size), dtype='int16')
|
||||||
|
signal = signal.astype(float) # Convert to float
|
||||||
|
# padding_value = 0
|
||||||
|
# Take mean of absolute values per 0.5 seconds
|
||||||
|
sub_waveform = np.nanmean(
|
||||||
|
# np.pad(np.absolute(signal), (0, ((downsample - (signal.size % downsample)) % downsample)), mode='constant', constant_values=padding_value).reshape(-1, downsample),
|
||||||
|
np.pad(np.absolute(signal), (0, ((downsample - (signal.size % downsample)) % downsample)), mode='constant', constant_values=np.NaN).reshape(-1, downsample),
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
|
||||||
|
waveform = np.concatenate((waveform, sub_waveform))
|
||||||
|
|
||||||
|
#Plot
|
||||||
|
plt.figure(1)
|
||||||
|
plt.title('Waveform')
|
||||||
|
plt.plot(waveform)
|
||||||
|
plt.plot(-waveform)
|
||||||
|
plt.show()
|
||||||
Loading…
x
Reference in New Issue
Block a user