├── Makefile
├── requirements.txt
├── LICENSE
├── README.md
└── visuaudio.py
/Makefile:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | black visuaudio.py;\
3 | flake8 visuaudio.py
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flake8==3.8.3
2 | importlib-metadata==1.7.0
3 | mccabe==0.6.1
4 | numpy==1.22.0
5 | PyAudio==0.2.11
6 | pycodestyle==2.6.0
7 | pyflakes==2.2.0
8 | pynput==1.6.8
9 | pyobjc-core==6.2
10 | pyobjc-framework-Cocoa==6.2
11 | pyobjc-framework-Quartz==6.2
12 | PyQt5==5.14.2
13 | PyQt5-sip==12.7.2
14 | pyqtgraph==0.10.0
15 | scipy==1.10.0
16 | six==1.15.0
17 | zipp==3.19.1
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ira Horecka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # visuaudio
2 | ### A fun GUI application to visualize audio spectrum
3 |
4 | ```visuaudio.py``` uses the ```pyqtgraph``` and ```pyaudio``` libraries to view the audio spectrum of input sound.
5 |
6 | ## Running the application:
7 | 1) Clone repository
8 | 2) ```$ pip install -r requirements.txt```
9 | 3) ```$ python visuaudio.py```
10 |
11 |
12 | ## Visuaudio default bar graph display
13 |
14 |
16 |
17 |
18 | ## Color Change
19 |
20 | #### You can change color of graph by playing with the following commands:
21 | * ↑,→,←: R,G,B += 10
22 | * F1 ~ F9: White ~ Pink
23 |
24 |
25 | ## Notes:
26 |
27 | * Ensure you have a working input sound source (e.g. a working input mic).
28 | * Run the application on your native terminal (i.e. not iTerm2, etc.).
29 | * Python 2 is not supported. It is advised to run this application using Python>=3.6.
30 |
31 | MacOS
32 |
33 | * You will have to grant Terminal permission to use the input sound source.
34 |
35 | Windows
36 |
37 | * Go link: Click here!
38 | * You should download:
39 |
40 | PyAudio-0.2.11-cp{your python version}-cp{your python version}m-win_amd64.whl
41 |
42 | * Installation:
43 |
44 | $ pip install PyAudio-0.2.11-cp37-cp37m-win_amd64.whl
45 |
46 | Ubuntu Linux
47 |
48 | * Currently unknown. It is likely possible to execute the app in Linux, but it is yet untested.
49 |
50 |
--------------------------------------------------------------------------------
/visuaudio.py:
--------------------------------------------------------------------------------
1 | """python application to view audio equalizer*
2 | of input sound source"""
3 | import struct
4 | import sys
5 | import numpy as np
6 | from pyqtgraph.Qt import QtGui, QtCore
7 | import pyqtgraph as pg
8 | import pyaudio
9 | from scipy.fftpack import fft
10 | from pynput.keyboard import Listener, Key
11 |
12 |
13 | class AudioStream:
14 | """stream audio from input source (mic) and continuously
15 | plot (bar) based on audio spectrum from waveform data"""
16 |
17 | def __init__(self, num, symbol):
18 | self.traces = set()
19 | self.number = num
20 | self.symbol = symbol
21 | # pyaudio setup
22 | self.FORMAT = pyaudio.paInt16 # bytes / sample
23 | self.CHANNELS = 1 # mono sound
24 | self.RATE = 44100 # samples / sec (44.1 kHz)
25 | self.CHUNK = 1024 # how much audio processed / frame -- set smaller for higher frame rate
26 |
27 | self.p = pyaudio.PyAudio()
28 | self.stream = self.p.open(
29 | format=self.FORMAT,
30 | channels=self.CHANNELS,
31 | rate=self.RATE,
32 | input=True,
33 | output=True,
34 | frames_per_buffer=self.CHUNK,
35 | )
36 | # spectrum x points
37 | self.f = np.linspace(1, int(self.RATE / 2), 64)
38 |
39 | # pyqtgraph setup
40 | pg.setConfigOptions(antialias=True)
41 | self.app = QtGui.QApplication(sys.argv)
42 | self.win = pg.GraphicsWindow(title="Audio Spectrum")
43 | self.win.setGeometry(10, 52, 480 * 2, 200 * 2)
44 | # window content setup
45 | self.audio_plot = self.win.addPlot(row=1, col=1)
46 | self.audio_plot.setYRange(0.00, 0.25)
47 | self.audio_plot.setXRange(2000, int(self.RATE / 2))
48 | self.audio_plot.showGrid(x=True, y=True)
49 | self.audio_plot.hideAxis("bottom")
50 | self.audio_plot.hideAxis("left")
51 |
52 | self.a = {
53 | "color": "r",
54 | "colorIndex_x": 100,
55 | "colorIndex_y": 100,
56 | "colorIndex_z": 255,
57 | }
58 |
59 | # graph init
60 | self.graph = None
61 |
62 | @staticmethod
63 | def set_gradient_brush():
64 | """set color gradient, return QtGui.QBrush obj"""
65 | grad = QtGui.QLinearGradient(0, 0, 0, 1)
66 | grad.setColorAt(0.1, pg.mkColor("#FF0000"))
67 | grad.setColorAt(0.24, pg.mkColor("#FF7F00"))
68 | grad.setColorAt(0.38, pg.mkColor("#FFFF00"))
69 | grad.setColorAt(0.52, pg.mkColor("#00FF00"))
70 | grad.setColorAt(0.66, pg.mkColor("#0000FF"))
71 | grad.setColorAt(0.80, pg.mkColor("#4B0082"))
72 | grad.setColorAt(0.94, pg.mkColor("#9400D3"))
73 | grad.setCoordinateMode(QtGui.QGradient.ObjectMode)
74 | return QtGui.QBrush(grad)
75 |
76 | # Bar Graph
77 | def set_plotdata_1(self, name, data_x, data_y):
78 | """set plot with init and new data -- reference
79 | self.traces to verify init or recurring data input"""
80 | if name in self.traces:
81 | # update bar plot content
82 | self.graph.setOpts(x=data_x, height=data_y, width=350)
83 | else:
84 | self.traces.add(name)
85 | # initial setup of bar plot
86 | brush = self.set_gradient_brush()
87 | self.graph = pg.BarGraphItem(
88 | x=data_x, height=data_y, width=50, brush=brush, pen=(0, 0, 0)
89 | )
90 | self.audio_plot.addItem(self.graph)
91 |
92 | # Scatter Plot Graph
93 | def set_plotdata_2(self, name, data_x, data_y):
94 | data_y = data_y[:64]
95 | brush_default = pg.mkBrush(
96 | self.a["colorIndex_x"], self.a["colorIndex_y"], self.a["colorIndex_z"], 100
97 | )
98 | if name in self.traces:
99 | # update scatter plot content
100 | self.graph.clear()
101 | self.graph.setData(data_x, data_y, brush=brush_default)
102 | else:
103 | self.traces.add(name)
104 | # initial setup of scatter plot
105 | self.graph = pg.ScatterPlotItem(
106 | x=data_x,
107 | y=data_y,
108 | pen=None,
109 | symbol=self.symbol,
110 | size=30,
111 | brush=(100, 100, 255, 100),
112 | )
113 | self.audio_plot.addItem(self.graph)
114 |
115 | # Curve Graph
116 | def set_plotdata_3(self, name, data_x, data_y):
117 | data_y = data_y[:64]
118 | if name in self.traces:
119 | # update curve plot content
120 | self.graph.clear()
121 | pen1 = pg.mkPen(
122 | self.a["colorIndex_x"],
123 | self.a["colorIndex_y"],
124 | self.a["colorIndex_z"],
125 | bright=100,
126 | )
127 | self.graph = pg.PlotCurveItem(x=data_x, y=data_y, pen=pen1,)
128 | else:
129 | self.traces.add(name)
130 | # initial setup of curve plot
131 | self.graph = pg.PlotCurveItem(x=data_x, y=data_y, pen="r",)
132 | self.audio_plot.addItem(self.graph)
133 |
134 | # Line Graph
135 | def set_plotdata_4(self, name, data_x, data_y):
136 | data_y = data_y[:64]
137 | if name in self.traces:
138 | # update curve plot content
139 | self.graph.clear()
140 | pen1 = pg.mkPen(
141 | self.a["colorIndex_x"],
142 | self.a["colorIndex_y"],
143 | self.a["colorIndex_z"],
144 | bright=100,
145 | width=15,
146 | style=QtCore.Qt.DashLine,
147 | )
148 | self.graph.setData(data_x, data_y, pen=pen1, shadowPen="#19070B")
149 | else:
150 | pen1 = pg.mkPen(color=(250, 0, 0), width=15, style=QtCore.Qt.DashLine)
151 | self.traces.add(name)
152 | # initial setup of curve plot
153 | self.graph = pg.PlotCurveItem(x=data_x, y=data_y, pen=pen1, shadowPen="r",)
154 | self.audio_plot.addItem(self.graph)
155 |
156 | def update(self):
157 | """update plot by number which user chose"""
158 | plot_data = {
159 | 1: self.set_plotdata_1,
160 | 2: self.set_plotdata_2,
161 | 3: self.set_plotdata_3,
162 | 4: self.set_plotdata_4,
163 | }
164 |
165 | plot_data.get(self.number)(
166 | name="spectrum", data_x=self.f, data_y=self.calculate_data()
167 | )
168 |
169 | def calculate_data(self):
170 | """get sound data and manipulate for plotting using fft"""
171 | # get and unpack waveform data
172 | wf_data = self.stream.read(self.CHUNK, exception_on_overflow=False)
173 | wf_data = struct.unpack(
174 | str(2 * self.CHUNK) + "B", wf_data
175 | ) # 2 * self.CHUNK :: wf_data 2x len of CHUNK -- wf_data range(0, 255)
176 | # generate spectrum data for plotting using fft (fast fourier transform)
177 | sp_data = fft(
178 | np.array(wf_data, dtype="int8") - 128
179 | ) # - 128 :: any int less than 127 will wrap around to 256 down
180 | # np.abs (below) converts complex num in fft to real magnitude
181 | sp_data = (
182 | np.abs(sp_data[0 : int(self.CHUNK)]) # slice: slice first half of our fft
183 | * 2
184 | / (256 * self.CHUNK)
185 | ) # rescale: mult 2, div amp waveform and no. freq in your spectrum
186 | sp_data[sp_data <= 0.001] = 0
187 | return sp_data
188 |
189 | @staticmethod
190 | def start():
191 | """start application"""
192 | if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"):
193 | QtGui.QApplication.instance().exec_()
194 |
195 | def change_color(self, key):
196 | """change color in curve graph after start"""
197 | """
198 | white : 255,255,255 red : 255, 0, 0 orange : 255, 106, 0
199 | yellow : 255, 255, 0 green : 0, 255, 0 skyblue : 0, 255, 255
200 | blue : 0, 0, 255 purple: 166, 0, 255 pink : 255, 0, 255
201 | """
202 | color_index = {
203 | Key.f1: (255, 255, 255), # white
204 | Key.f2: (255, 0, 0), # red
205 | Key.f3: (255, 106, 255), # orange
206 | Key.f4: (255, 255, 0), # yellow
207 | Key.f5: (0, 255, 0), # green
208 | Key.f6: (0, 255, 255), # skyblue
209 | Key.f7: (0, 0, 255), # blue
210 | Key.f8: (166, 0, 255), # purple
211 | Key.f9: (255, 0, 255), # pink
212 | }
213 | self.color_index_control()
214 |
215 | try:
216 | if key == Key.up: # red higher
217 | AUDIO_APP.a["color_x"] = self.a["colorIndex_x"]
218 | self.a["colorIndex_x"] += 10
219 | elif key == Key.right: # green higher
220 | AUDIO_APP.a["color_y"] = self.a["colorIndex_y"]
221 | self.a["colorIndex_y"] += 10
222 | elif key == Key.left: # blue higher
223 | AUDIO_APP.a["color_z"] = self.a["colorIndex_z"]
224 | self.a["colorIndex_z"] += 10
225 | else:
226 | (
227 | self.a["colorIndex_x"],
228 | self.a["colorIndex_y"],
229 | self.a["colorIndex_z"],
230 | ) = color_index.get(key)
231 | except Exception as error:
232 | print(error)
233 |
234 | def color_index_control(self):
235 | for color_rgb in ("colorIndex_x", "colorIndex_y", "colorIndex_z"):
236 | if self.a[color_rgb] >= 255:
237 | self.a[color_rgb] = 0
238 |
239 | def animation(self):
240 | """call self.start and self.update for continuous
241 | output application"""
242 | timer = QtCore.QTimer()
243 | timer.timeout.connect(self.update)
244 | timer.start(20)
245 | self.start()
246 |
247 |
248 | def int_to_symbol(symbol):
249 | symbol_to_char = {1: "d", 2: "o", 3: "x", 4: "t", 5: "s"}
250 |
251 | return symbol_to_char.get(symbol)
252 |
253 |
254 | if __name__ == "__main__":
255 | # commonly used cout messages -------------------
256 | line_break = "-" * 20
257 | out_of_range = "Out of range! try again: "
258 | # -----------------------------------------------
259 |
260 | print("Choose and type number.")
261 | print(line_break)
262 | print("1: Bar Graph")
263 | print("2: Scatter Graph")
264 | print("3: Curve Graph")
265 | print("4: Line Graph")
266 | print("5: quit")
267 | print(line_break)
268 | symbol = "o" # default symbol to instantiate AudioStream class
269 | number_input = input("Graph Type: ")
270 | while True:
271 | try:
272 | number = int(number_input)
273 | if number >= 1 and number <= 5:
274 | break
275 | except ValueError: # i.e. not a number
276 | pass
277 | number_input = input(out_of_range)
278 |
279 | if number == 2:
280 | print("Choose symbol")
281 | print(line_break)
282 | print("1: Diamond")
283 | print("2: Circular")
284 | print("3: Cross")
285 | print("4: Triangular")
286 | print("5: Square")
287 | print(line_break)
288 | symbol = int(input("Symbol: "))
289 | while symbol < 1 or symbol > 5:
290 | symbol = int(input(out_of_range))
291 | symbol = int_to_symbol(symbol)
292 | elif number == 5:
293 | exit()
294 |
295 | AUDIO_APP = AudioStream(number, symbol)
296 | with Listener(on_press=AUDIO_APP.change_color) as listener:
297 | AUDIO_APP.animation()
298 | listener.join()
299 |
--------------------------------------------------------------------------------