#!/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)