├── .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 | | Python 2.7 | http://www.python.org/download/ |
17 | | NumPy | http://www.scipy.org/Download |
18 | | PySide | http://qt-project.org/wiki/PySideDownloads |
19 | | PySide-QtMultimedia | Included in the PySide binaries on Windows and OSX |
20 |
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 | | Esc | Enter and exit full screen mode |
33 | | 1-9 | Change number of columns displayed |
34 | | Q-T | Change color |
35 | | I-P | Hide or show columns |
36 |
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 |
--------------------------------------------------------------------------------