musicmuster/app.py
2021-03-20 12:47:31 +00:00

303 lines
9.9 KiB
Python
Executable File

#!/usr/bin/python3
import sys
import vlc
from datetime import datetime, timedelta
from pydub import AudioSegment
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox
from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog
from PyQt5.uic import loadUi
from threading import Timer
from time import sleep
from timeloop import Timeloop
from tinytag import TinyTag
from main_window_ui import Ui_MainWindow
from dlg_search_database_ui import Ui_Dialog
class RepeatedTimer:
def __init__(self, interval, function, *args, **kwargs):
self._timer = None
self.interval = interval
self.function = function
self.args = args
self.kwargs = kwargsdlg_search_database_ui
self.is_running = False
self.start()
def _run(self):
self.is_running = False
self.start()
self.function(*self.args, **self.kwargs)
def start(self):
if not self.is_running:
self._timer = Timer(self.interval, self._run)
self._timer.start()
self.is_running = True
def stop(self):
self._timer.cancel()
self.is_running = False
class Track:
def __init__(self, path):
self.path = path
audio = self.get_audio_segment(path)
self.start_gap = self.leading_silence(audio)
self.fade_at = self.fade_point(audio)
self.silence_at = self.trailing_silence(audio)
tag = TinyTag.get(path)
self.title = tag.title
self.artist = tag.artist
self.length = tag.duration * 1000
print(
f" path={self.path}"
f" length={self.length}"
f" start_gap={self.start_gap}"
f" fade_at={self.fade_at}"
f" silence_at={sedlg_search_database_uilf.silence_at}"
f" title={self.title}"
f" artist={self.artist}"
)
def get_audio_segment(self, path):
try:
if path.endswith('.mp3'):
return AudioSegment.from_mp3(path)
elif path.endswith('.flac'):
return AudioSegment.from_file(path, "flac")
except:
return None
def leading_silence(self, audio_segment, silence_threshold=-50.0,
chunk_size=10):
"""
Returns the millisecond/index that the leading silence ends.
audio_segment - the segment to find silence in
silence_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
dlg_search_database_ui
https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
"""
trim_ms = 0 # ms
assert chunk_size > 0 # to avoid infinite loop
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS <
silence_threshold and trim_ms < len(audio_segment)):
trim_ms += chunk_size
# if there is no end it should return the length of the segment
return min(trim_ms, len(audio_segment))
def fade_point(self, audio_segment, fade_threshold=-20.0, chunk_size=10):
"""
Returns the millisecond/index of the point where the fade is down to
fade_threshold and doesn't get louder again.
audio_segment - the sdlg_search_database_uiegment to find silence in
fade_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
"""
assert chunk_size > 0 # to avoid infinite loop
segment_length = audio_segment.duration_seconds * 1000 # ms
trim_ms = segment_length - chunk_size
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0):
trim_ms -= chunk_size
# if there is no trailing silence, return lenght of track (it's less
# the chunk_size, but for chunk_size = 10ms, this may be ignored)
return int(trim_ms)
def trailing_silence(self, audio_segment, silence_threshold=-50.0,
chunk_size=10):
return self.fade_point(audio_segment, silence_threshold, chunk_size)
ROOT = "/home/kae/music/"
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.connectSignalsSlots()
self.resize(1599, 981)
self.playlist.setColumnWidth(0, 0)
self.playlist.setColumnWidth(1, 70)
self.playlist.setColumnWidth(2, 381)
self.playlist.setColumnWidth(3, 322)
self.playlist.setColumnWidth(4, 78)
self.playlist.setColumnWidth(5, 68)
self.playlist.setColumnWidth(6, 47)
self.playlist.setColumnWidth(7, 577)
def __del__(self):
for column in range(self.playlist.columnCount()):
print(f"Column {column}: {self.playlist.columnWidth(column)}")
print(f"Window height: {self.height()} Window width: {self.width()}")
def ms_to_mmss(self, ms, decimals=0):
if not ms:
return "-"
if ms < 0:
sign = "-"
else:
sign = ""
minutes, remainder = divmod(ms, 60 * 1000)
seconds = remainder / 1000
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
def connectSignalsSlots(self):
self.fileButton.clicked.connect(self.selectFile)
self.databaseButton.clicked.connect(self.selectDatabase)
# import ipdb; ipdb.set_trace()
# self.playlist.columnResized.connect(self.playlistresize)
# self.playlist.horizontalHeader().sectionResized.connect(self.kae)
# import ipdb; ipdb.set_trace()
# self.playlist.viewport().installEventFilter(self)
# self.playlist.horizontalHeader().sectionClicked.connect(self.kae2)
# x = self.playlist.horizontalHeader()
# import ipdb; ipdb.set_trace()
def selectDatabase(self):
dlg = DbDialog()
dlg.exec()
def selectFile(self):
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFile)
dlg.setViewMode(QFileDialog.Detail)
Gdlg.setDirectory(ROOT)
dlg.setNameFilter("Music files (*.flac *.mp3)")
if dlg.exec_():
for fname in dlg.selectedFiles():
track = Track(fname)
self.add_to_playlist(track)
def add_to_playlist(self, track):
pl = self.playlist
row = pl.rowCount()
pl.insertRow(row)
item = QTableWidgetItem(track.title)
pl.setItem(row, 1, item)
item = QTableWidgetItem(track.artist)
pl.setItem(row, 2, item)
item = QTableWidgetItem(self.ms_to_mmss(track.length))
pl.setItem(row, 3, item)
item = QTableWidgetItem(track.path)
pl.setItem(row, 6, item)
class DbDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
import ipdb; ipdb.set_trace()
# self.lineEdit.clicked.connect(self.selectFile)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
# tl = Timeloop()
#
#
#
## @tl.job(interval=timedelta(seconds=1))
#def update_progress(player, talk_at, silent_at):
# elapsed_time = player.get_time()
# total_time = player.get_length()
# remaining_time = total_time - elapsed_time
# talk_time = remaining_time - (total_time - talk_at)
# silent_time = remaining_time - (total_time - silent_at)
# end_time = (datetime.now() + timedelta(
# milliseconds=remaining_time)).strftime("%H:%M:%S")
# print(
# f"\t{ms_to_mmss(elapsed_time)}/"
# f"{ms_to_mmss(total_time)}\t\t"
# f"Talk in: {ms_to_mmss(talk_time)} "
# f"Silent in: {ms_to_mmss(silent_time)} "
# f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]"
# , end="\r")
#
#
## Print name of current song, print name of next song. Play current when
## return pressed, Pri--current-song-output-lengthnt remaining time every
## second. When it ends, print name of new current and next song.
#
#
#def test():
# track = "wibg.mp3"
# segment = AudioSegment.from_mp3(track)
# print(f"Track: {track}")
# print(f"Leading silence: {ms_to_mmss(leading_silence(segment), decimals=1)}")
# talk_at = significant_fade(segment)
# silent_at = trailing_silence(segment)
# print(f"Talkover fade: {ms_to_mmss(talk_at)}")
# print(f"Track silent from: {ms_to_mmss(silent_at)}")
# p = vlc.MediaPlayer("wibg.mp3")
# _ = input("")
# p.play()
# print()
# rt = RepeatedTimer(0.5, update_progress, p, talk_at, silent_at)
# sleep(1)
# while p.is_playing():
# sleep(1)
# rt.stop() # better in a try/finally block to make sure the program ends!
# print("End")
#def kae2(self, index):
# print(f"table header click, index={index}")
#def kae(self, a, b, c):
# self.data.append(f"a={a}, b={b}, c={c}")
#def mousePressEvent(self, QMouseEvent):
# print("mouse press")
#def mouseReleaseEvent(self, QMouseEvent):
# print("mouse release")
# # QMessageBox.about(
# # self,
# # "About Sample Editor",
# # "\n".join(self.data)
# # )
#def eventFilter(self, obj, event):
# # you could be doing different groups of actions
# # for different types of widgets and either filtering
# # the event or not.
# # Here we just check if its one of the layout widgets
# # if self.layout.indexOf(obj) != -1:
# # print(f"event received: {event.type()}")
# if event.type() == QEvent.MouseButtonPress:
# print("Widget click")
# # if I returned True right here, the event
# # would be filtered and not reach the obj,
# # meaning that I decided to handle it myself
# # regardless, just do the default
# return super().eventFilter(obj, event)