├── 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 | Audio Spectrum GUI 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 | --------------------------------------------------------------------------------