diff --git a/Pipfile b/Pipfile index cff5b2b..6e4d2ed 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ pipfile-requirements = "*" pylama = "*" ffmpeg = "*" pip = "*" +ffmpeg-python = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 7d1d3d3..7d192f9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a59428c79a266fc1237cdd4e74b0803bb5f39803495f1fc6c134df7333cdc2bc" + "sha256": "6c12b28b456a859b8139dd5175876beeec58f47abcfbcb6d98dbfdc3a955320d" }, "pipfile-spec": 6, "requires": { @@ -31,6 +31,14 @@ "index": "pypi", "version": "==1.4" }, + "ffmpeg-python": { + "hashes": [ + "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", + "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5" + ], + "index": "pypi", + "version": "==0.2.0" + }, "flask": { "hashes": [ "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", @@ -39,6 +47,13 @@ "index": "pypi", "version": "==1.1.2" }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", diff --git a/bash_commands/connect_sound.sh b/bash_commands/connect_sound.sh new file mode 100644 index 0000000..98cc6e7 --- /dev/null +++ b/bash_commands/connect_sound.sh @@ -0,0 +1,5 @@ +#!/bin/bash +tmp_dir=$1 +audio=$2 + +ffmpeg -i $tmp_dir/video/video.mp4 -i $audio -ac 1 -tune stillimage ./out/video-and-audio.mp4 -y diff --git a/bash_commands/create_raw_files.sh b/bash_commands/create_raw_files.sh new file mode 100644 index 0000000..5df355d --- /dev/null +++ b/bash_commands/create_raw_files.sh @@ -0,0 +1,8 @@ +#!/bin/bash +tmp_dir=$1 +aresample=$2 + +ffmpeg -i $tmp_dir/left.wav -ac 1 -filter:a aresample=$aresample -map 0:a -c:a pcm_u8 -f data - > $tmp_dir/leftraw & +ffmpeg -i $tmp_dir/right.wav -ac 1 -filter:a aresample=$aresample -map 0:a -c:a pcm_u8 -f data - > $tmp_dir/rightraw & + +wait; diff --git a/bash_commands/generate_video_by_demuxer.sh b/bash_commands/generate_video_by_demuxer.sh new file mode 100644 index 0000000..44dea13 --- /dev/null +++ b/bash_commands/generate_video_by_demuxer.sh @@ -0,0 +1,5 @@ +#!/bin/bash +tmp_dir=$1 +tmp_video_dir=$2 + +ffmpeg -y -f concat -safe 0 -i demuxer.txt -r 30 -tune stillimage -vsync vfr -pix_fmt yuv420p $tmp_video_dir/video.mp4 \ No newline at end of file diff --git a/bash_commands/split_channels_to_two_ways.sh b/bash_commands/split_channels_to_two_ways.sh new file mode 100644 index 0000000..38e3d30 --- /dev/null +++ b/bash_commands/split_channels_to_two_ways.sh @@ -0,0 +1,5 @@ +#!/bin/bash +tmp_dir=$1 +both_channels=$2 + +ffmpeg -i $both_channels -map_channel 0.0.0 $tmp_dir/left.wav -map_channel 0.0.1 $tmp_dir/right.wav \ No newline at end of file diff --git a/demuxer.txt b/demuxer.txt new file mode 100644 index 0000000..bfc4f62 --- /dev/null +++ b/demuxer.txt @@ -0,0 +1,33 @@ +file /tmp/tmpz9izgjti/pics/none.png +duration 0.286 +file /tmp/tmpz9izgjti/pics/right.png +duration 3.47975 +file /tmp/tmpz9izgjti/pics/none.png +duration 0.427375 +file /tmp/tmpz9izgjti/pics/right.png +duration 0.300375 +file /tmp/tmpz9izgjti/pics/none.png +duration 0.24225 +file /tmp/tmpz9izgjti/pics/right.png +duration 1.837375 +file /tmp/tmpz9izgjti/pics/both.png +duration 1.1325 +file /tmp/tmpz9izgjti/pics/left.png +duration 0.199 +file /tmp/tmpz9izgjti/pics/both.png +duration 3.920625 +file /tmp/tmpz9izgjti/pics/right.png +duration 0.7935 +file /tmp/tmpz9izgjti/pics/both.png +duration 2.446375 +file /tmp/tmpz9izgjti/pics/right.png +duration 0.331875 +file /tmp/tmpz9izgjti/pics/both.png +duration 0.58725 +file /tmp/tmpz9izgjti/pics/right.png +duration 0.082 +file /tmp/tmpz9izgjti/pics/both.png +duration 3.36025 +file /tmp/tmpz9izgjti/pics/right.png +duration 0.1615 +file /tmp/tmpz9izgjti/pics/right.png diff --git a/find_loudness.py b/find_loudness.py new file mode 100644 index 0000000..a27e32d --- /dev/null +++ b/find_loudness.py @@ -0,0 +1,52 @@ + +def createReadStream(file): + """Create Bytes Stream""" + data = open(file, 'rb') + return data + + +def process_find_loudness(file_path: str, threshold_at_point: int, inertia_samples: float, label: str): + + print("Start process to find loudness in:") + print( + F"\tfile_path: {file_path}\n" + F"\tthreshold_at_point: {threshold_at_point}\n" + F"\tinertia_samples: {inertia_samples}\n" + F"\tlabel: {label}\n" + ) + stream = createReadStream(file_path) + position = 0 + results = [] + last_swap_position = 0 + keep_loud_until = -1 + was_loud_last_time = False + print("Read chunks") + chunks = stream.read() + print(F"Length: {len(chunks)}") + + try: + for i, byte in enumerate(chunks): + position += 1 + volume = abs(byte - 128) + + if position >= keep_loud_until: + is_loud = volume > threshold_at_point + + if is_loud != was_loud_last_time: + swap_point = { + 'position_start': last_swap_position, + 'duration': position - last_swap_position, + 'loud': was_loud_last_time, + 'label': label + } + results.append(swap_point) + last_swap_position = position + was_loud_last_time = is_loud + + if volume > threshold_at_point: + keep_loud_until = position + inertia_samples + + return results + + except Exception as err: + print(err) diff --git a/generate_demuxer.py b/generate_demuxer.py new file mode 100644 index 0000000..54a3b86 --- /dev/null +++ b/generate_demuxer.py @@ -0,0 +1,91 @@ +import math + +from find_loudness import process_find_loudness + +graph_density = 8000 +threshold_at_point = 7 +inertia_s = 0.3 +inertia_samples = inertia_s * graph_density + + +def s(n: int): + global graph_density + return n / graph_density + + +def seconds(units: int): + return math.floor(s(units) % 60) + + +def minutes(units: int): + return math.floor(s(units) / 60) + + +def hours(units: int): + return math.floor(s(units) / 60 / 60) + + +def formatTime(units: int): + return f"{hours(units)}:{minutes(units)}:{seconds(units)}" + + +def new_mode(m, s): + data = m + data[s['label']] = s['loud'] + return data + + +def mode_to_string(mode): + if mode['left'] and mode['right']: + return 'both' + elif mode['left']: + return 'left' + elif mode['right']: + return 'right' + else: + return "none" + + +def run(tmp_dir): + global inertia_samples + out_demuxer = 'demuxer.txt' + + with open(out_demuxer, 'w') as demuxer: + # Execute process_find_loudness for left and right side + left_loudness = process_find_loudness( + tmp_dir + "/audio/leftraw", + threshold_at_point=threshold_at_point, + inertia_samples=inertia_samples, + label="left") + + right_loudness = process_find_loudness( + tmp_dir + "/audio/rightraw", + threshold_at_point=threshold_at_point, + inertia_samples=inertia_samples, + label="right") + + merged = [*left_loudness, *right_loudness] + sorted_list = sorted(merged, key=lambda x: x['position_start']) + + demuxer.write(F"file {tmp_dir}/pics/none.png\n") + + last_point = 0 + mode = {'left': False, 'right': False} + last_file = '' + total = 0 + + for i in range(2, len(sorted_list)): + point = sorted_list[i] + mode = new_mode(m=mode, s=point) + + file = F"{tmp_dir}/pics/{mode_to_string(mode)}.png" + duration = (point['position_start'] - last_point) / graph_density + demuxer.write(F"duration {duration}\n") + demuxer.write(F"file {file}\n") + last_point = point['position_start'] + last_file = file + total += duration * graph_density + + demuxer.write(F"duration {sorted_list[len(sorted_list) - 1]['duration'] / graph_density}\n") + demuxer.write(F"file {last_file}\n") + print(F"{total} {formatTime(total)}\n") diff --git a/src/gui/images/icons/32x32.png b/src/gui/images/icons/32x32.png new file mode 100644 index 0000000..3a4f469 Binary files /dev/null and b/src/gui/images/icons/32x32.png differ diff --git a/src/gui/images/icons/45x45.png b/src/gui/images/icons/45x45.png new file mode 100644 index 0000000..e72fbe0 Binary files /dev/null and b/src/gui/images/icons/45x45.png differ diff --git a/src/gui/images/icons/60x60.png b/src/gui/images/icons/60x60.png new file mode 100644 index 0000000..c69cb13 Binary files /dev/null and b/src/gui/images/icons/60x60.png differ diff --git a/src/gui/images/icons/generowanie.png b/src/gui/images/icons/generowanie.png new file mode 100644 index 0000000..0d755ef Binary files /dev/null and b/src/gui/images/icons/generowanie.png differ diff --git a/src/gui/images/icons/info.png b/src/gui/images/icons/info.png new file mode 100644 index 0000000..753714b Binary files /dev/null and b/src/gui/images/icons/info.png differ diff --git a/src/gui/images/icons/otworz.png b/src/gui/images/icons/otworz.png new file mode 100644 index 0000000..9a48054 Binary files /dev/null and b/src/gui/images/icons/otworz.png differ diff --git a/src/gui/images/icons/podglad.png b/src/gui/images/icons/podglad.png new file mode 100644 index 0000000..d59f882 Binary files /dev/null and b/src/gui/images/icons/podglad.png differ diff --git a/src/gui/images/icons/ustawienia.png b/src/gui/images/icons/ustawienia.png new file mode 100644 index 0000000..8320c8a Binary files /dev/null and b/src/gui/images/icons/ustawienia.png differ diff --git a/src/gui/images/icons/zapis-n.png b/src/gui/images/icons/zapis-n.png new file mode 100644 index 0000000..9e187d8 Binary files /dev/null and b/src/gui/images/icons/zapis-n.png differ diff --git a/src/gui/images/icons/zapis.png b/src/gui/images/icons/zapis.png new file mode 100644 index 0000000..ccd578a Binary files /dev/null and b/src/gui/images/icons/zapis.png differ diff --git a/src/python/classes/mainwindow.py b/src/python/classes/mainwindow.py index f2b1dca..23366ed 100644 --- a/src/python/classes/mainwindow.py +++ b/src/python/classes/mainwindow.py @@ -2,14 +2,18 @@ import os import shutil import subprocess import tempfile -import resources_rc + +# import resources_rc from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QMainWindow, QLabel, QFileDialog, QDialog, QDialogButtonBox, QVBoxLayout, QApplication + from src.python.classes.settings_dialog import SettingsDialog from src.python.ui.mainwindow_ui import Ui_MainWindow from src.python.classes.translate import Translator +import generate_demuxer + class MainWindow(QMainWindow, QApplication, Ui_MainWindow): def __init__(self, parent=None): @@ -22,7 +26,7 @@ class MainWindow(QMainWindow, QApplication, Ui_MainWindow): self.setup_logic() self.setup_detail() self.retranslateUi(self.window()) - self.aresample = 8000 + self.aresample = "8000" self.test_data() def setup_logic(self): @@ -176,36 +180,96 @@ class MainWindow(QMainWindow, QApplication, Ui_MainWindow): def generate_video_podcast(self): # TODO: Change to pure python """Generate podcast based on values from UI.""" - audio_1 = self.line_edit_audio_1.text() - audio_2 = self.line_edit_audio_2.text() + connected_channels = self.check_box_connected_channels.isChecked() + # Setup images + image_files = { + 'both': self.preview_label_avatar_1.property('path'), + 'none': self.preview_label_avatar_2.property('path'), + 'left': self.preview_label_avatar_3.property('path'), + 'right': self.preview_label_avatar_4.property('path') + } + audio_files = [] - both_image = self.preview_label_avatar_1.property('path') - none_image = self.preview_label_avatar_2.property('path') - left_image = self.preview_label_avatar_3.property('path') - right_image = self.preview_label_avatar_4.property('path') + if not connected_channels: + audio_files.append({'file': self.line_edit_audio_1.text()}) + audio_files.append({'file': self.line_edit_audio_2.text()}) + + else: + audio_files.append({'file': self.line_edit_audio_1.text()}) + + # Split name and extension of the file + for dictionary in audio_files: + dictionary['ext'] = dictionary['file'].rsplit('.')[-1] with tempfile.TemporaryDirectory() as tmp_dir_name: - print('[*] Created temporary directory', tmp_dir_name) - pics_dir = tmp_dir_name + "/pics/" + print(tmp_dir_name) + print(f'[*] Create temporary directory: {tmp_dir_name}') + tmp_out_dir = tmp_dir_name + "/out" + pics_dir = tmp_dir_name + "/pics" + audio_dir = tmp_dir_name + "/audio" + video_dir = tmp_dir_name + "/video" - print(f'[*] Created pics directory in {tmp_dir_name}') + print(F"[!] Create tmp out dir: {tmp_out_dir}") + os.mkdir(tmp_out_dir) + + print(f'[*] Create pics dir: {pics_dir}') os.mkdir(pics_dir) + + print(f'[*] Create audio dir: {audio_dir}') + os.mkdir(audio_dir) + + print(f'[*] Create video dir: {video_dir}\n') + os.mkdir(video_dir) + print(f'[*] Copy images to {pics_dir}') - shutil.copy(both_image, pics_dir + "both.png") - shutil.copy(none_image, pics_dir + "none.png") - shutil.copy(left_image, pics_dir + "left.png") - shutil.copy(right_image, pics_dir + "right.png") + shutil.copy(image_files['both'], pics_dir + "/both.png") + shutil.copy(image_files['none'], pics_dir + "/none.png") + shutil.copy(image_files['left'], pics_dir + "/left.png") + shutil.copy(image_files['right'], pics_dir + "/right.png") + + print(f'[*] Copy audio to {audio_dir}\n') + if not self.check_box_connected_channels.isChecked(): + audio_files[0]['tmp'] = audio_dir + "/left_channel" + "." + audio_files[0]['ext'] + audio_files[1]['tmp'] = audio_dir + "/right_channel" + "." + audio_files[1]['ext'] + + shutil.copy(audio_files[0]['file'], audio_files[0]['tmp']) + shutil.copy(audio_files[1]['file'], audio_files[1]['tmp']) + + else: + audio_files[0]['tmp'] = audio_dir + "/both_channel" + "." + audio_files[0]['ext'] + shutil.copy(audio_files[0]['file'], audio_files[0]['tmp']) print(f'[*] Images in {pics_dir}: {os.listdir(pics_dir)}') - echo_temp_dir_name = subprocess.check_output(["echo", tmp_dir_name]).decode('utf-8') + print(f'[*] Audo files in {audio_dir}: {os.listdir(audio_dir)}') - ech = subprocess.check_output(["bash", "./generate.sh", - tmp_dir_name, - audio_1, audio_2, - both_image, none_image, - left_image, right_image, - str(self.aresample)]).decode('utf-8') + subprocess.check_output(["echo", tmp_dir_name]).decode('utf-8') - print(echo_temp_dir_name) - print(ech) + if connected_channels: + # Split channels + print("[-] Split channels - start:") + subprocess.check_output(["bash", + "bash_commands/split_channels_to_two_ways.sh", + audio_dir, + audio_files[0]['tmp']]) + print("[+] Split channels - done") + print("[-] Create raw files - start:") + subprocess.check_output(['bash', + 'bash_commands/create_raw_files.sh', + audio_dir, self.aresample]) + print("[+] Create raw files - done") + + print("[-] Create demuxer - start:") + generate_demuxer.run(tmp_dir=tmp_dir_name) + print("[+] Create demuxer - done") + + print('[-] Create video - start:') + subprocess.check_output( + ['bash', 'bash_commands/generate_video_by_demuxer.sh', tmp_dir_name, video_dir]) + print('[+] Create video - done') + + # while True: pass + print('[-] Create final podcast - start:') + subprocess.check_output( + ['bash', 'bash_commands/connect_sound.sh', tmp_dir_name, audio_files[0]['file']]) + print('[+] Create final podcast - done')