From 631703d0b09737d53d8c49baa28c3af4eba8e1cb Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Thu, 18 Mar 2021 23:36:50 +0000 Subject: [PATCH] File selection and track parsing --- app.py | 263 +++++++++++++++++++++++++++++++++++++ ui/main_window.ui | 326 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 589 insertions(+) create mode 100755 app.py create mode 100644 ui/main_window.ui diff --git a/app.py b/app.py new file mode 100755 index 0000000..6bee8ec --- /dev/null +++ b/app.py @@ -0,0 +1,263 @@ +#!/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 + + +class RepeatedTimer: + def __init__(self, interval, function, *args, **kwargs): + self._timer = None + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + 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.length = int(len(audio)) + self.start_gap = self.leading_silence(audio) + self.fade_at = self.fade_point(audio) + self.silence_at = self.trailing_silence(audio) + + print( + f" path={self.path}" + f" length={self.length}" + f" start_gap={self.start_gap}" + f" fade_at={self.fade_at}" + f" silence_at={self.silence_at}" + ) + + 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 + + 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 segment 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) + + +# tl = Timeloop() +# +#def ms_to_mmss(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}" +# +# +## @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") + + + +ROOT = "/home/kae/music/" + +from main_window_ui import Ui_MainWindow + + +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, 381) + self.playlist.setColumnWidth(2, 322) + self.playlist.setColumnWidth(3, 78) + self.playlist.setColumnWidth(4, 68) + self.playlist.setColumnWidth(5, 47) + self.playlist.setColumnWidth(6, 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 connectSignalsSlots(self): + self.pushButton.clicked.connect(self.btnpush) + # 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 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) + def btnpush(self): + dlg = QFileDialog() + dlg.setFileMode(QFileDialog.ExistingFile) + dlg.setViewMode(QFileDialog.Detail) + dlg.setDirectory(ROOT) + dlg.setNameFilter("Music files (*.flac *.mp3)") + + if dlg.exec_(): + for fname in dlg.selectedFiles(): + track = Track(fname) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + win = Window() + win.show() + sys.exit(app.exec()) diff --git a/ui/main_window.ui b/ui/main_window.ui new file mode 100644 index 0000000..877c1e2 --- /dev/null +++ b/ui/main_window.ui @@ -0,0 +1,326 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + + + + Sans + 20 + + + + background-color: rgb(138, 226, 52); +border: 1px solid rgb(85, 87, 83); + + + After the goldrush - Neil Young [3:46] + + + + + + + 10 + + + + + Elapsed + + + + + + + Sans + 40 + 75 + true + + + + 2:46 + + + + + + + + DejaVu Sans + 16 + + + + 10:17:37 + + + false + + + + + + + + + + Fade at + + + + + + + Sans + 40 + 75 + true + + + + 0:53 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:23 + + + false + + + + + + + + + + background-color: rgb(252, 233, 79); + + + Silent at + + + + + + + Sans + 40 + 75 + true + + + + 0:58 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:28 + + + false + + + + + + + + + + End at + + + + + + + Sans + 40 + 75 + true + + + + 1:00 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:30 + + + false + + + + + + + + + + + + true + + + QAbstractItemView::ContiguousSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + 3 + + + 7 + + + + + + + Skip silence + + + + + Title + + + + + Artist + + + + + Duration + + + + + End time + + + + + Autoplay next + + + + + Path + + + + + + + + + + PushButton + + + + + + + + DejaVu Sans + 25 + 75 + true + + + + 10:20:30 + + + + + current_track + start_box + fade_box + silent_box + end_box + playlist + current_time + pushButton + + + + + + 0 + 0 + 800 + 18 + + + + + Fi&le + + + + + + + background-color: rgb(211, 215, 207); + + + + + +