core.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import sys, io, os
  2. from PyQt5 import QtCore, QtGui, uic
  3. from PyQt5.QtGui import QPainter, QColor
  4. from os.path import expanduser
  5. import subprocess as sp
  6. import numpy
  7. from PIL import Image, ImageDraw, ImageFont
  8. from PIL.ImageQt import ImageQt
  9. import tempfile
  10. from shutil import rmtree
  11. import atexit
  12. class Core():
  13. def __init__(self):
  14. self.lastBackgroundImage = ""
  15. self._image = None
  16. self.FFMPEG_BIN = self.findFfmpeg()
  17. self.tempDir = None
  18. atexit.register(self.deleteTempDir)
  19. def findFfmpeg(self):
  20. if sys.platform == "win32":
  21. return "ffmpeg.exe"
  22. else:
  23. try:
  24. with open(os.devnull, "w") as f:
  25. sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
  26. return "ffmpeg"
  27. except:
  28. return "avconv"
  29. def parseBaseImage(self, backgroundImage, preview=False):
  30. ''' determines if the base image is a single frame or list of frames '''
  31. if backgroundImage == "":
  32. print("background image is null")
  33. return []
  34. else:
  35. _, bgExt = os.path.splitext(backgroundImage)
  36. if not bgExt == '.mp4':
  37. return [backgroundImage]
  38. else:
  39. return self.getVideoFrames(backgroundImage, preview)
  40. def drawBaseImage(self, backgroundFile, titleText, titleFont, fontSize, alignment,\
  41. xOffset, yOffset, textColor, visColor):
  42. if backgroundFile == '' :
  43. im = Image.new("RGB", (1280, 720), "black")
  44. else:
  45. im = Image.open(backgroundFile)
  46. if self._image == None or not self.lastBackgroundImage == backgroundFile:
  47. self.lastBackgroundImage = backgroundFile
  48. # resize if necessary
  49. if not im.size == (1280, 720):
  50. im = im.resize((1280, 720), Image.ANTIALIAS)
  51. self._image = ImageQt(im)
  52. self._image1 = QtGui.QImage(self._image)
  53. painter = QPainter(self._image1)
  54. font = titleFont
  55. font.setPixelSize(fontSize)
  56. painter.setFont(font)
  57. painter.setPen(QColor(*textColor))
  58. yPosition = yOffset
  59. fm = QtGui.QFontMetrics(font)
  60. if alignment == 0: #Left
  61. xPosition = xOffset
  62. if alignment == 1: #Middle
  63. xPosition = xOffset - fm.width(titleText)/2
  64. if alignment == 2: #Right
  65. xPosition = xOffset - fm.width(titleText)
  66. painter.drawText(xPosition, yPosition, titleText)
  67. painter.end()
  68. buffer = QtCore.QBuffer()
  69. buffer.open(QtCore.QIODevice.ReadWrite)
  70. self._image1.save(buffer, "PNG")
  71. strio = io.BytesIO()
  72. strio.write(buffer.data())
  73. buffer.close()
  74. strio.seek(0)
  75. return Image.open(strio)
  76. def drawBars(self, spectrum, image, color):
  77. imTop = Image.new("RGBA", (1280, 360))
  78. draw = ImageDraw.Draw(imTop)
  79. r, g, b = color
  80. color2 = (r, g, b, 50)
  81. for j in range(0, 63):
  82. draw.rectangle((10 + j * 20, 325, 10 + j * 20 + 20, 325 - spectrum[j * 4] * 1 - 10), fill=color2)
  83. draw.rectangle((15 + j * 20, 320, 15 + j * 20 + 10, 320 - spectrum[j * 4] * 1), fill=color)
  84. imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
  85. im = Image.new("RGB", (1280, 720), "black")
  86. im.paste(image, (0, 0))
  87. im.paste(imTop, (0, 0), mask=imTop)
  88. im.paste(imBottom, (0, 360), mask=imBottom)
  89. return im
  90. def readAudioFile(self, filename):
  91. command = [ self.FFMPEG_BIN,
  92. '-i', filename,
  93. '-f', 's16le',
  94. '-acodec', 'pcm_s16le',
  95. '-ar', '44100', # ouput will have 44100 Hz
  96. '-ac', '1', # mono (set to '2' for stereo)
  97. '-']
  98. in_pipe = sp.Popen(command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
  99. completeAudioArray = numpy.empty(0, dtype="int16")
  100. while True:
  101. # read 2 seconds of audio
  102. raw_audio = in_pipe.stdout.read(88200*4)
  103. if len(raw_audio) == 0:
  104. break
  105. audio_array = numpy.fromstring(raw_audio, dtype="int16")
  106. completeAudioArray = numpy.append(completeAudioArray, audio_array)
  107. # print(audio_array)
  108. in_pipe.kill()
  109. in_pipe.wait()
  110. # add 0s the end
  111. completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16")
  112. completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
  113. completeAudioArray = completeAudioArrayCopy
  114. return completeAudioArray
  115. def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum):
  116. if len(completeAudioArray) < (i + sampleSize):
  117. sampleSize = len(completeAudioArray) - i
  118. window = numpy.hanning(sampleSize)
  119. data = completeAudioArray[i:i+sampleSize][::1] * window
  120. paddedSampleSize = 2048
  121. paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), 'constant')
  122. spectrum = numpy.fft.fft(paddedData)
  123. sample_rate = 44100
  124. frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
  125. y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
  126. # filter the noise away
  127. # y[y<80] = 0
  128. y = 20 * numpy.log10(y)
  129. y[numpy.isinf(y)] = 0
  130. if lastSpectrum is not None:
  131. lastSpectrum[y < lastSpectrum] = y[y < lastSpectrum] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
  132. lastSpectrum[y >= lastSpectrum] = y[y >= lastSpectrum] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
  133. else:
  134. lastSpectrum = y
  135. x = frequencies[0:int(paddedSampleSize/2) - 1]
  136. return lastSpectrum
  137. def deleteTempDir(self):
  138. if self.tempDir and os.path.exists(self.tempDir):
  139. rmtree(self.tempDir)
  140. def getVideoFrames(self, videoPath, firstOnly=False):
  141. self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data')
  142. # recreate the temporary directory so it is empty
  143. self.deleteTempDir()
  144. os.mkdir(self.tempDir)
  145. if firstOnly:
  146. filename = 'preview%s.jpg' % os.path.basename(videoPath).split('.', 1)[0]
  147. options = '-ss 10 -vframes 1'
  148. else:
  149. filename = '$frame%05d.jpg'
  150. options = ''
  151. sp.call( \
  152. '%s -i "%s" -y %s "%s"' % ( \
  153. self.FFMPEG_BIN,
  154. videoPath,
  155. options,
  156. os.path.join(self.tempDir, filename)
  157. ),
  158. shell=True
  159. )
  160. return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)])
  161. @staticmethod
  162. def RGBFromString(string):
  163. ''' turns an RGB string like "255, 255, 255" into a tuple '''
  164. try:
  165. tup = tuple([int(i) for i in string.split(',')])
  166. if len(tup) != 3:
  167. raise ValueError
  168. for i in tup:
  169. if i > 255 or i < 0:
  170. raise ValueError
  171. return tup
  172. except:
  173. return (255, 255, 255)