├── .gitignore ├── README.md ├── main.py └── visualizer.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | #Komodo files 30 | *.komodoproject 31 | *.komodotool 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyVisualizer 2 | ============ 3 | 4 | A simple music visualizer written in python and Qt. 5 | 6 | 7 | 8 | Binary Installation 9 | =================== 10 | The compiled binaries can be used without installation. 11 | 12 | Running from Source 13 | =================== 14 | Install the following dependencies: 15 | 16 | 17 | 18 | 19 | 20 |
Python 2.7http://www.python.org/download/
NumPyhttp://www.scipy.org/Download
PySidehttp://qt-project.org/wiki/PySideDownloads
PySide-QtMultimediaIncluded in the PySide binaries on Windows and OSX
21 | 22 | The program can be run with `python main.py` 23 | 24 | Usage 25 | ===== 26 | The visualizer uses sound from your microphone to generate the visuals, 27 | so make sure your microphone is working. 28 | 29 | Controls 30 | -------- 31 | 32 | 33 | 34 | 35 | 36 |
EscEnter and exit full screen mode
1-9Change number of columns displayed
Q-TChange color
I-PHide or show columns
37 | 38 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | import numpy as np 6 | from PySide import QtCore, QtGui 7 | 8 | from visualizer import * 9 | 10 | app = QtGui.QApplication(sys.argv) 11 | 12 | def record_qt_multimedia(): 13 | info = QtMultimedia.QAudioDeviceInfo.defaultInputDevice() 14 | format = info.preferredFormat() 15 | format.setChannels(CHANNEL_COUNT) 16 | format.setChannelCount(CHANNEL_COUNT) 17 | format.setSampleSize(SAMPLE_SIZE) 18 | format.setSampleRate(SAMPLE_RATE) 19 | 20 | if not info.isFormatSupported(format): 21 | print 'Format not supported, using nearest available' 22 | format = nearestFormat(format) 23 | if format.sampleSize != SAMPLE_SIZE: 24 | #this is important, since effects assume this sample size. 25 | raise RuntimeError('16-bit sample size not supported!') 26 | 27 | audio_input = QtMultimedia.QAudioInput(format, app) 28 | audio_input.setBufferSize(BUFFER_SIZE) 29 | source = audio_input.start() 30 | 31 | def read_data(): 32 | data = np.fromstring(source.readAll(), 'int16').astype(float) 33 | if len(data): 34 | return data 35 | return read_data 36 | 37 | def record_pyaudio(): 38 | p = pyaudio.PyAudio() 39 | 40 | stream = p.open(format = pyaudio.paInt16, 41 | channels = CHANNEL_COUNT, 42 | rate = SAMPLE_RATE, 43 | input = True, 44 | frames_per_buffer = BUFFER_SIZE) 45 | 46 | def read_data(): 47 | data = np.fromstring(stream.read(stream.get_read_available()), 'int16').astype(float) 48 | if len(data): 49 | return data 50 | return read_data 51 | try: 52 | from PySide import QtMultimedia 53 | read_data = record_qt_multimedia() 54 | except ImportError: 55 | print 'Using PyAudio' 56 | import pyaudio 57 | read_data = record_pyaudio() 58 | 59 | window = LineVisualizer(read_data) 60 | window.show() 61 | app.exec_() 62 | -------------------------------------------------------------------------------- /visualizer.py: -------------------------------------------------------------------------------- 1 | """Module that contains visualizer classes.""" 2 | 3 | import sys 4 | import random 5 | import time 6 | 7 | import numpy as np 8 | from PySide import QtCore, QtGui 9 | 10 | SAMPLE_MAX = 32767 11 | SAMPLE_MIN = -(SAMPLE_MAX + 1) 12 | SAMPLE_RATE = 44100 # [Hz] 13 | NYQUIST = SAMPLE_RATE / 2 14 | SAMPLE_SIZE = 16 # [bit] 15 | CHANNEL_COUNT = 1 16 | BUFFER_SIZE = 5000 17 | 18 | 19 | class Visualizer(QtGui.QLabel): 20 | """The base class for visualizers. 21 | 22 | When initializing a visualizer, you must provide a get_data function which 23 | takes no arguments and returns a NumPy array of PCM samples that will be 24 | called exactly once each time a frame is drawn. 25 | 26 | Note: Although this is an abstract class, it cannot have a metaclass of 27 | abcmeta since it is a child of QObject. 28 | """ 29 | def __init__(self, get_data, update_interval=33): 30 | super(Visualizer, self).__init__() 31 | 32 | self.get_data = get_data 33 | self.update_interval = update_interval #33ms ~= 30 fps 34 | self.sizeHint = lambda: QtCore.QSize(400, 400) 35 | self.setStyleSheet('background-color: black;'); 36 | self.setWindowTitle('PyVisualizer') 37 | 38 | def show(self): 39 | """Show the label and begin updating the visualization.""" 40 | super(Visualizer, self).show() 41 | self.refresh() 42 | 43 | 44 | def refresh(self): 45 | """Generate a frame, display it, and set queue the next frame""" 46 | data = self.get_data() 47 | interval = self.update_interval 48 | if data is not None: 49 | t1 = time.clock() 50 | self.setPixmap(QtGui.QPixmap.fromImage(self.generate(data))) 51 | #decrease the time till next frame by the processing tmie so that the framerate stays consistent 52 | interval -= 1000 * (time.clock() - t1) 53 | if self.isVisible(): 54 | QtCore.QTimer.singleShot(self.update_interval, self.refresh) 55 | 56 | def generate(self, data): 57 | """This is the abstract function that child classes will override to 58 | draw a frame of the visualization. 59 | 60 | The function takes an array of data and returns a QImage to display""" 61 | raise NotImplementedError() 62 | 63 | 64 | class LineVisualizer(Visualizer): 65 | """This visualizer will display equally sized rectangles 66 | alternating between black and another color, with the height of the 67 | rectangles determined by frequency, and the quantity of colored rectanges 68 | influnced by amplitude. 69 | """ 70 | 71 | def __init__(self, get_data, columns=1): 72 | super(LineVisualizer, self).__init__(get_data) 73 | 74 | self.columns = columns 75 | self.brushes = [QtGui.QBrush(QtGui.QColor(255, 255, 255)), #white 76 | QtGui.QBrush(QtGui.QColor(255, 0, 0)), #red 77 | QtGui.QBrush(QtGui.QColor(0, 240, 0)), #green 78 | QtGui.QBrush(QtGui.QColor(0, 0, 255)), #blue 79 | QtGui.QBrush(QtGui.QColor(255, 255, 0)), #yellow 80 | QtGui.QBrush(QtGui.QColor(0, 255, 255)), #teal 81 | ] 82 | self.brush = self.brushes[0] 83 | 84 | self.display_odds = True 85 | self.display_evens = True 86 | self.is_fullscreen = False 87 | 88 | def keyPressEvent(self, event): 89 | if event.key() == QtCore.Qt.Key_I: 90 | self.display_evens = True 91 | self.display_odds = True 92 | elif event.key() == QtCore.Qt.Key_O: 93 | self.display_evens = True 94 | self.display_odds = False 95 | elif event.key() == QtCore.Qt.Key_P: 96 | self.display_evens = False 97 | self.display_odds = True 98 | return 99 | elif event.key() == QtCore.Qt.Key_Escape: 100 | if self.is_fullscreen: 101 | self.showNormal() 102 | self.is_fullscreen = False 103 | else: 104 | self.showFullScreen() 105 | self.is_fullscreen = True 106 | else: 107 | #Qt.Key enum helpfully defines most keys as their ASCII code, 108 | # so we can use ord('Q') instead of Qt.Key.Key_Q 109 | color_bindings = dict(zip((ord(i) for i in 'QWERTYU'), self.brushes)) 110 | try: 111 | self.brush = color_bindings[event.key()] 112 | except KeyError: 113 | if QtCore.Qt.Key_0 == event.key(): 114 | self.columns = 10 115 | elif QtCore.Qt.Key_1 <= event.key() <= QtCore.Qt.Key_9: 116 | self.columns = event.key() - QtCore.Qt.Key_1 + 1 117 | 118 | def generate(self, data): 119 | fft = np.absolute(np.fft.rfft(data, n=len(data))) 120 | freq = np.fft.fftfreq(len(fft), d=1./SAMPLE_RATE) 121 | max_freq = abs(freq[fft == np.amax(fft)][0]) 122 | max_amplitude = np.amax(data) 123 | 124 | rect_width = int(self.width() / (self.columns * 2)) 125 | 126 | freq_cap = 20000. #this determines the scale of lines 127 | if max_freq >= freq_cap: 128 | rect_height = 1 129 | else: 130 | rect_height = int(self.height() * max_freq / freq_cap) 131 | if rect_height == 2: rect_height = 1 132 | 133 | 134 | img = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_RGB32) 135 | img.fill(0) #black 136 | 137 | if rect_height >= 1: 138 | painter = QtGui.QPainter(img) 139 | painter.setPen(QtCore.Qt.NoPen) 140 | painter.setBrush(self.brush) 141 | 142 | for x in xrange(0, self.width() - rect_width, rect_width * 2): 143 | for y in xrange(0, self.height(), 2 * rect_height): 144 | if random.randint(0, int(max_amplitude / float(SAMPLE_MAX) * 10)): 145 | if self.display_evens: 146 | painter.drawRect(x, y, rect_width, rect_height) 147 | if self.display_odds: 148 | painter.drawRect(x + rect_width, self.height() - y - rect_height, rect_width, rect_height) 149 | 150 | del painter # 151 | 152 | return img 153 | 154 | class Spectrogram(Visualizer): 155 | def generate(self, data): 156 | fft = np.absolute(np.fft.rfft(data, n=len(data))) 157 | freq = np.fft.fftfreq(len(fft), d=1./SAMPLE_RATE) 158 | max_freq = abs(freq[fft == np.amax(fft)][0]) / 2 159 | max_amplitude = np.amax(data) 160 | 161 | bins = np.zeros(200) 162 | #indices = (len(fft) - np.logspace(0, np.log10(len(fft)), len(bins), endpoint=False).astype(int))[::-1] 163 | #for i in xrange(len(bins) - 1): 164 | # bins[i] = np.mean(fft[indices[i]:indices[i+1]]).astype(int) 165 | #bins[-1] = np.mean(fft[indices[-1]:]).astype(int) 166 | 167 | step = int(len(fft) / len(bins)) 168 | for i in xrange(len(bins)): 169 | bins[i] = np.mean(fft[i:i+step]) 170 | 171 | img = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_RGB32) 172 | img.fill(0) 173 | painter = QtGui.QPainter(img) 174 | painter.setPen(QtCore.Qt.NoPen) 175 | painter.setBrush(QtGui.QBrush(QtGui.QColor(255, 255, 255))) #white) 176 | 177 | for i, bin in enumerate(bins): 178 | height = self.height() * bin / float(SAMPLE_MAX) / 10 179 | width = self.width() / float(len(bins)) 180 | painter.drawRect(i * width, self.height() - height, width, height) 181 | 182 | del painter 183 | 184 | return img 185 | 186 | --------------------------------------------------------------------------------