aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--core.py135
-rw-r--r--main.py196
-rw-r--r--main.ui312
-rw-r--r--preview_thread.py58
5 files changed, 703 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..84a5e2a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__
+settings.ini \ No newline at end of file
diff --git a/core.py b/core.py
new file mode 100644
index 0000000..ac957e7
--- /dev/null
+++ b/core.py
@@ -0,0 +1,135 @@
+import sys, io
+from PyQt4 import QtCore, QtGui, uic
+from PyQt4.QtGui import QPainter, QColor
+from os.path import expanduser
+import subprocess as sp
+import numpy
+from PIL import Image, ImageDraw, ImageFont
+from PIL.ImageQt import ImageQt
+
+class Core():
+
+ def __init__(self):
+ self.lastBackgroundImage = ""
+ self._image = None
+
+ if sys.platform == "win32":
+ self.FFMPEG_BIN = "ffmpeg.exe"
+ else:
+ self.FFMPEG_BIN = "ffmpeg" # on Linux and Mac OS
+
+ def drawBaseImage(self, backgroundImage, titleText, titleFont):
+
+ if self._image == None or not self.lastBackgroundImage == backgroundImage:
+ self.lastBackgroundImage = backgroundImage
+
+ if backgroundImage == "":
+ im = Image.new("RGB", (1280, 720), "black")
+ else:
+ im = Image.open(backgroundImage)
+
+ # resize if necessary
+ if not im.size == (1280, 720):
+ im = im.resize((1280, 720), Image.ANTIALIAS)
+
+ self._image = ImageQt(im)
+
+ self._image1 = QtGui.QImage(self._image)
+ painter = QPainter(self._image1)
+ font = titleFont
+ font.setPointSizeF(35)
+ painter.setFont(font)
+ painter.setPen(QColor(255, 255, 255))
+
+ painter.drawText(70, 375, titleText)
+ painter.end()
+
+ buffer = QtCore.QBuffer()
+ buffer.open(QtCore.QIODevice.ReadWrite)
+ self._image1.save(buffer, "PNG")
+
+ strio = io.BytesIO()
+ strio.write(buffer.data())
+ buffer.close()
+ strio.seek(0)
+ return Image.open(strio)
+
+ def drawBars(self, spectrum, image):
+
+ imTop = Image.new("RGBA", (1280, 360))
+ draw = ImageDraw.Draw(imTop)
+ for j in range(0, 63):
+ draw.rectangle((10 + j * 20, 325, 10 + j * 20 + 20, 325 - spectrum[j * 4] * 1 - 10), fill=(255, 255, 255, 50))
+ draw.rectangle((15 + j * 20, 320, 15 + j * 20 + 10, 320 - spectrum[j * 4] * 1), fill="white")
+
+
+ imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
+
+ im = Image.new("RGB", (1280, 720), "black")
+
+ im.paste(image, (0, 0))
+ im.paste(imTop, (0, 0), mask=imTop)
+ im.paste(imBottom, (0, 360), mask=imBottom)
+
+ return im
+
+ def readAudioFile(self, filename):
+ command = [ self.FFMPEG_BIN,
+ '-i', filename,
+ '-f', 's16le',
+ '-acodec', 'pcm_s16le',
+ '-ar', '44100', # ouput will have 44100 Hz
+ '-ac', '1', # mono (set to '2' for stereo)
+ '-']
+ in_pipe = sp.Popen(command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
+
+ completeAudioArray = numpy.empty(0, dtype="int16")
+
+ while True:
+ # read 2 seconds of audio
+ raw_audio = in_pipe.stdout.read(88200*4)
+ if len(raw_audio) == 0:
+ break
+ audio_array = numpy.fromstring(raw_audio, dtype="int16")
+ completeAudioArray = numpy.append(completeAudioArray, audio_array)
+ # print(audio_array)
+
+ in_pipe.kill()
+ in_pipe.wait()
+
+ # add 0s the end
+ completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16")
+ completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
+ completeAudioArray = completeAudioArrayCopy
+
+ return completeAudioArray
+
+ def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum):
+ if len(completeAudioArray) < (i + sampleSize):
+ sampleSize = len(completeAudioArray) - i
+
+ window = numpy.hanning(sampleSize)
+ data = completeAudioArray[i:i+sampleSize][::1] * window
+ paddedSampleSize = 2048
+ paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), 'constant')
+ spectrum = numpy.fft.fft(paddedData)
+ sample_rate = 44100
+ frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
+
+ y = abs(spectrum[0:paddedSampleSize/2 - 1])
+
+ # filter the noise away
+ # y[y<80] = 0
+
+ y = 20 * numpy.log10(y)
+ y[numpy.isinf(y)] = 0
+
+ if lastSpectrum is not None:
+ lastSpectrum[y < lastSpectrum] = y[y < lastSpectrum] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
+ lastSpectrum[y >= lastSpectrum] = y[y >= lastSpectrum] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
+ else:
+ lastSpectrum = y
+
+ x = frequencies[0:paddedSampleSize/2 - 1]
+
+ return lastSpectrum \ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..df18222
--- /dev/null
+++ b/main.py
@@ -0,0 +1,196 @@
+import sys, io, os
+from PyQt4 import QtCore, QtGui, uic
+from PyQt4.QtGui import QPainter, QColor, QFont
+from os.path import expanduser
+import subprocess as sp
+import numpy
+from PIL import Image, ImageDraw, ImageFont
+from PIL.ImageQt import ImageQt
+import atexit
+from queue import Queue
+from PyQt4.QtCore import QSettings
+
+import preview_thread, core
+
+class Main(QtCore.QObject):
+
+ newTask = QtCore.pyqtSignal(str, str, QFont)
+ processTask = QtCore.pyqtSignal()
+
+ def __init__(self, window):
+
+ QtCore.QObject.__init__(self)
+
+ # print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
+ self.window = window
+ self.core = core.Core()
+
+ self.settings = QSettings('settings.ini', QSettings.IniFormat)
+
+ self.previewQueue = Queue()
+
+ self.previewThread = QtCore.QThread(self)
+ self.previewWorker = preview_thread.Worker(self, self.previewQueue)
+
+ self.previewWorker.moveToThread(self.previewThread)
+ self.previewWorker.imageCreated.connect(self.showPreviewImage)
+
+ self.previewThread.start()
+
+ self.timer = QtCore.QTimer(self)
+ self.timer.timeout.connect(self.processTask.emit)
+ self.timer.start(500)
+
+ window.pushButton_selectInput.clicked.connect(self.openInputFileDialog)
+ window.pushButton_selectOutput.clicked.connect(self.openOutputFileDialog)
+ window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation)
+ window.pushButton_selectBackground.clicked.connect(self.openBackgroundFileDialog)
+
+ window.fontComboBox.currentFontChanged.connect(self.drawPreview)
+ window.lineEdit_title.textChanged.connect(self.drawPreview)
+
+ window.progressBar_create.setValue(0)
+ window.setWindowTitle("Audio Visualizer")
+ window.pushButton_selectInput.setText("Select Input Music File")
+ window.pushButton_selectOutput.setText("Select Output Video File")
+ window.pushButton_selectBackground.setText("Select Background Image")
+ window.label_font.setText("Title Font")
+ window.label_title.setText("Title Text")
+ window.pushButton_createVideo.setText("Create Video")
+ window.groupBox_create.setTitle("Create")
+ window.groupBox_settings.setTitle("Settings")
+ window.groupBox_preview.setTitle("Preview")
+
+ titleFont = self.settings.value("titleFont")
+ if not titleFont == None:
+ window.fontComboBox.setCurrentFont(QFont(titleFont))
+
+ self.drawPreview()
+
+ window.show()
+
+ def cleanUp(self):
+ self.timer.stop()
+ self.previewThread.quit()
+ self.previewThread.wait()
+
+ self.settings.setValue("titleFont", self.window.fontComboBox.currentFont().toString())
+
+ def openInputFileDialog(self):
+ inputDir = self.settings.value("inputDir", expanduser("~"))
+
+ fileName = QtGui.QFileDialog.getOpenFileName(self.window,
+ "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.flac)");
+
+ if not fileName == "":
+ self.settings.setValue("inputDir", os.path.dirname(fileName))
+ self.window.label_input.setText(fileName)
+
+ def openOutputFileDialog(self):
+ outputDir = self.settings.value("outputDir", expanduser("~"))
+
+ fileName = QtGui.QFileDialog.getSaveFileName(self.window,
+ "Set Output Video File", outputDir, "Video Files (*.mp4)");
+
+ if not fileName == "":
+ self.settings.setValue("outputDir", os.path.dirname(fileName))
+ self.window.label_output.setText(fileName)
+
+ def openBackgroundFileDialog(self):
+ backgroundDir = self.settings.value("backgroundDir", expanduser("~"))
+
+ fileName = QtGui.QFileDialog.getOpenFileName(self.window,
+ "Open Background Image", backgroundDir, "Image Files (*.jpg *.png)");
+
+ if not fileName == "":
+ self.settings.setValue("backgroundDir", os.path.dirname(fileName))
+ self.window.label_background.setText(fileName)
+ self.drawPreview()
+
+ def createAudioVisualisation(self):
+
+ imBackground = self.core.drawBaseImage(
+ self.window.label_background.text(),
+ self.window.lineEdit_title.text(),
+ self.window.fontComboBox.currentFont())
+
+ self.window.progressBar_create.setValue(0)
+
+ completeAudioArray = self.core.readAudioFile(self.window.label_input.text())
+
+ out_pipe = sp.Popen([ self.core.FFMPEG_BIN,
+ '-y', # (optional) means overwrite the output file if it already exists.
+ '-f', 'rawvideo',
+ '-vcodec', 'rawvideo',
+ '-s', '1280x720', # size of one frame
+ '-pix_fmt', 'rgb24',
+ '-r', '30', # frames per second
+ '-i', '-', # The input comes from a pipe
+ '-an',
+ '-i', self.window.label_input.text(),
+ '-acodec', "libmp3lame", # output audio codec
+ self.window.label_output.text()],
+ stdin=sp.PIPE,stdout=sp.DEVNULL, stderr=sp.DEVNULL)
+
+ smoothConstantDown = 0.08
+ smoothConstantUp = 0.8
+ lastSpectrum = None
+ progressBarValue = 0
+ sampleSize = 1470
+
+ numpy.seterr(divide='ignore')
+
+ for i in range(0, len(completeAudioArray), sampleSize):
+ # create video for output
+ lastSpectrum = self.core.transformData(
+ i,
+ completeAudioArray,
+ sampleSize,
+ smoothConstantDown,
+ smoothConstantUp,
+ lastSpectrum)
+ im = self.core.drawBars(lastSpectrum, imBackground)
+
+ # write to out_pipe
+ try:
+ out_pipe.stdin.write(im.tostring())
+ finally:
+ True
+
+ # increase progress bar value
+ if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100:
+ progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100)
+ self.window.progressBar_create.setValue(progressBarValue)
+
+ numpy.seterr(all='print')
+
+ out_pipe.stdin.close()
+ if out_pipe.stderr is not None:
+ print(out_pipe.stderr.read())
+ out_pipe.stderr.close()
+ out_pipe.terminate()
+ out_pipe.wait()
+ print("Video file created")
+ self.window.progressBar_create.setValue(100)
+
+ def drawPreview(self):
+ self.newTask.emit(self.window.label_background.text(),
+ self.window.lineEdit_title.text(),
+ self.window.fontComboBox.currentFont())
+ # self.processTask.emit()
+
+ def showPreviewImage(self, image):
+ self._scaledPreviewImage = image
+ self._previewPixmap = QtGui.QPixmap.fromImage(self._scaledPreviewImage)
+
+ self.window.label_preview.setPixmap(self._previewPixmap)
+
+if __name__ == "__main__":
+ app = QtGui.QApplication(sys.argv)
+ window = uic.loadUi("main.ui")
+
+ main = Main(window)
+
+ atexit.register(main.cleanUp)
+
+ sys.exit(app.exec_())
diff --git a/main.ui b/main.ui
new file mode 100644
index 0000000..f9e79c3
--- /dev/null
+++ b/main.ui
@@ -0,0 +1,312 @@
+<?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>635</width>
+ <height>610</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox_settings">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>200</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>200</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>GroupBox</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="pushButton_selectInput">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>PushButton</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_input">
+ <property name="frameShape">
+ <enum>QFrame::Box</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="pushButton_selectOutput">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>PushButton</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_output">
+ <property name="frameShape">
+ <enum>QFrame::Box</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QPushButton" name="pushButton_selectBackground">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>PushButton</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_background">
+ <property name="frameShape">
+ <enum>QFrame::Box</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLabel" name="label_font">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFontComboBox" name="fontComboBox"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLabel" name="label_title">
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="lineEdit_title"/>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ <zorder>verticalLayoutWidget_2</zorder>
+ <zorder>layoutWidget_2</zorder>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_preview">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>220</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>220</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>GroupBox</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <widget class="QLabel" name="label_preview">
+ <property name="minimumSize">
+ <size>
+ <width>320</width>
+ <height>180</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>320</width>
+ <height>180</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::Box</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_create">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>GroupBox</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QProgressBar" name="progressBar_create">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_createVideo">
+ <property name="text">
+ <string>PushButton</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/preview_thread.py b/preview_thread.py
new file mode 100644
index 0000000..740f6c6
--- /dev/null
+++ b/preview_thread.py
@@ -0,0 +1,58 @@
+from PyQt4 import QtCore, QtGui, uic
+from PyQt4.QtCore import pyqtSignal, pyqtSlot
+from PIL import Image, ImageDraw, ImageFont
+from PIL.ImageQt import ImageQt
+import core
+import time
+from queue import Queue, Empty
+import numpy
+
+class Worker(QtCore.QObject):
+
+ imageCreated = pyqtSignal(['QImage'])
+
+ def __init__(self, parent=None, queue=None):
+ QtCore.QObject.__init__(self)
+ parent.newTask.connect(self.createPreviewImage)
+ parent.processTask.connect(self.process)
+ self.core = core.Core()
+ self.queue = queue
+
+
+ @pyqtSlot(str, str, QtGui.QFont)
+ def createPreviewImage(self, backgroundImage, titleText, titleFont):
+ # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
+ dic = {
+ "backgroundImage": backgroundImage,
+ "titleText": titleText,
+ "titleFont": titleFont
+ }
+ self.queue.put(dic)
+
+ @pyqtSlot()
+ def process(self):
+ try:
+ nextPreviewInformation = self.queue.get(block=False)
+ while self.queue.qsize() >= 2:
+ try:
+ self.queue.get(block=False)
+ except Empty:
+ continue
+
+ im = self.core.drawBaseImage(
+ nextPreviewInformation["backgroundImage"],
+ nextPreviewInformation["titleText"],
+ nextPreviewInformation["titleFont"])
+
+ spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16")
+
+ im = self.core.drawBars(spectrum, im)
+
+ self._image = ImageQt(im)
+ self._previewImage = QtGui.QImage(self._image)
+
+ self._scaledPreviewImage = self._previewImage.scaled(320, 180, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
+
+ self.imageCreated.emit(self._scaledPreviewImage)
+ except Empty:
+ True