From 5e0ba19538763e77c6638175d981e4c0b38e2d1d Mon Sep 17 00:00:00 2001 From: Martin Kaistra Date: Mon, 2 Mar 2015 22:47:52 +0100 Subject: first commit --- .gitignore | 2 + core.py | 135 +++++++++++++++++++++++ main.py | 196 ++++++++++++++++++++++++++++++++++ main.ui | 312 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ preview_thread.py | 58 ++++++++++ 5 files changed, 703 insertions(+) create mode 100644 .gitignore create mode 100644 core.py create mode 100644 main.py create mode 100644 main.ui create mode 100644 preview_thread.py 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 @@ + + + MainWindow + + + + 0 + 0 + 635 + 610 + + + + MainWindow + + + + + + + + + + 0 + 200 + + + + + 16777215 + 200 + + + + GroupBox + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + PushButton + + + + + + + QFrame::Box + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + PushButton + + + + + + + QFrame::Box + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + PushButton + + + + + + + QFrame::Box + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + 200 + 0 + + + + QFrame::NoFrame + + + + + + + + + + + + + + + verticalLayoutWidget_2 + layoutWidget_2 + + + + + + + 0 + 220 + + + + + 16777215 + 220 + + + + GroupBox + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 320 + 180 + + + + + 320 + 180 + + + + QFrame::Box + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + + + + 16777215 + 100 + + + + GroupBox + + + + + + + + 24 + + + false + + + + + + + PushButton + + + + + + + + + + + + + + + + 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 -- cgit v1.2.3