├── docs ├── rt60_1k.png ├── ir_delta.png ├── ir_power.png ├── rt60_raw.png ├── ir_delta_pwr.png ├── rt60_drawn.png ├── HawleyBathroom.m4a ├── ir_spectrogram.png ├── rt60_hawleyb_125.png ├── rt60_hawleyb_4k.png ├── rt60_hawleyb_oops.png ├── rt60_octaveselect.png ├── rt60_hawleyb_4k_box.png ├── rt60_hawleyb_4k_zoom.png ├── ir_sweep_and_inv_waveform.png ├── rt60_hawleyb_4k_zoom_line.png ├── ir.md └── rt60.md ├── images ├── modes.png ├── power.png ├── rt60.png ├── sabine.png ├── spectro.png ├── waterfall.png ├── shaart_logo.jpg ├── mandrill_echo.png ├── mandrill_reverb.png ├── mandrill_wahwah.png ├── wavelet_vs_fft.png ├── mandrill_leveler.png ├── mandrill_spectro.png ├── mandrill_mp3_to_wav.png └── ir_sweep_and_inv_waveform.png ├── audio ├── mandrill.wav ├── sample_data.wav ├── HawleyBathroom.m4a └── HawleyBathroom.mp3 ├── source ├── SHAART.icns ├── shaart_logo.png ├── shaart_logo_icon.ico ├── extra-hooks │ └── hook-librosa.py ├── setup.py ├── SHAART.spec ├── spectrowidget.py ├── waterwidget.py ├── waveformwidget.py ├── pwrspecwidget.py ├── modegraphwidget.py ├── rcgraphwidget.py ├── rt60widget.py ├── SHAART.py └── ui_shaart.py ├── README.md └── LICENSE.md /docs/rt60_1k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_1k.png -------------------------------------------------------------------------------- /images/modes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/modes.png -------------------------------------------------------------------------------- /images/power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/power.png -------------------------------------------------------------------------------- /images/rt60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/rt60.png -------------------------------------------------------------------------------- /audio/mandrill.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/audio/mandrill.wav -------------------------------------------------------------------------------- /docs/ir_delta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/ir_delta.png -------------------------------------------------------------------------------- /docs/ir_power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/ir_power.png -------------------------------------------------------------------------------- /docs/rt60_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_raw.png -------------------------------------------------------------------------------- /images/sabine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/sabine.png -------------------------------------------------------------------------------- /images/spectro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/spectro.png -------------------------------------------------------------------------------- /source/SHAART.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/source/SHAART.icns -------------------------------------------------------------------------------- /audio/sample_data.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/audio/sample_data.wav -------------------------------------------------------------------------------- /docs/ir_delta_pwr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/ir_delta_pwr.png -------------------------------------------------------------------------------- /docs/rt60_drawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_drawn.png -------------------------------------------------------------------------------- /images/waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/waterfall.png -------------------------------------------------------------------------------- /docs/HawleyBathroom.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/HawleyBathroom.m4a -------------------------------------------------------------------------------- /docs/ir_spectrogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/ir_spectrogram.png -------------------------------------------------------------------------------- /images/shaart_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/shaart_logo.jpg -------------------------------------------------------------------------------- /source/shaart_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/source/shaart_logo.png -------------------------------------------------------------------------------- /audio/HawleyBathroom.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/audio/HawleyBathroom.m4a -------------------------------------------------------------------------------- /audio/HawleyBathroom.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/audio/HawleyBathroom.mp3 -------------------------------------------------------------------------------- /docs/rt60_hawleyb_125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_125.png -------------------------------------------------------------------------------- /docs/rt60_hawleyb_4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_4k.png -------------------------------------------------------------------------------- /docs/rt60_hawleyb_oops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_oops.png -------------------------------------------------------------------------------- /docs/rt60_octaveselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_octaveselect.png -------------------------------------------------------------------------------- /images/mandrill_echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_echo.png -------------------------------------------------------------------------------- /images/mandrill_reverb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_reverb.png -------------------------------------------------------------------------------- /images/mandrill_wahwah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_wahwah.png -------------------------------------------------------------------------------- /images/wavelet_vs_fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/wavelet_vs_fft.png -------------------------------------------------------------------------------- /docs/rt60_hawleyb_4k_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_4k_box.png -------------------------------------------------------------------------------- /images/mandrill_leveler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_leveler.png -------------------------------------------------------------------------------- /images/mandrill_spectro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_spectro.png -------------------------------------------------------------------------------- /source/shaart_logo_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/source/shaart_logo_icon.ico -------------------------------------------------------------------------------- /docs/rt60_hawleyb_4k_zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_4k_zoom.png -------------------------------------------------------------------------------- /images/mandrill_mp3_to_wav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/mandrill_mp3_to_wav.png -------------------------------------------------------------------------------- /docs/ir_sweep_and_inv_waveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/ir_sweep_and_inv_waveform.png -------------------------------------------------------------------------------- /docs/rt60_hawleyb_4k_zoom_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/docs/rt60_hawleyb_4k_zoom_line.png -------------------------------------------------------------------------------- /images/ir_sweep_and_inv_waveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drscotthawley/SHAART/HEAD/images/ir_sweep_and_inv_waveform.png -------------------------------------------------------------------------------- /source/extra-hooks/hook-librosa.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | datas = collect_data_files('librosa') 3 | 4 | -------------------------------------------------------------------------------- /source/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is used in creating a standalone application via py2app 3 | 4 | To use this, run: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | APP = ['SHAART.py'] 11 | DATA_FILES = [] 12 | PKGS = ['scikits.audiolab'] 13 | OPTIONS = { 14 | 'argv_emulation': True, 15 | 'optimize': True, 16 | 'packages' : PKGS, 17 | 'iconfile':'SHAART.icns' 18 | } 19 | 20 | setup( 21 | app=APP, 22 | data_files=DATA_FILES, 23 | options={'py2app': OPTIONS}, 24 | setup_requires=['py2app'], 25 | ) 26 | -------------------------------------------------------------------------------- /source/SHAART.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['SHAART.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=['PyQt6', 'librosa'], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.datas, 24 | [], 25 | name='SHAART', 26 | debug=False, 27 | bootloader_ignore_signals=False, 28 | strip=False, 29 | upx=True, 30 | upx_exclude=[], 31 | runtime_tmpdir=None, 32 | console=False, 33 | disable_windowed_traceback=False, 34 | argv_emulation=True, 35 | target_arch=None, 36 | codesign_identity=None, 37 | entitlements_file=None, 38 | icon=['SHAART.icns'], 39 | ) 40 | app = BUNDLE( 41 | exe, 42 | name='SHAART.app', 43 | icon='SHAART.icns', 44 | bundle_identifier="edu.belmont.SHAART", 45 | ) 46 | -------------------------------------------------------------------------------- /source/spectrowidget.py: -------------------------------------------------------------------------------- 1 | # Python Qt5 bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | # import the Qt5Agg FigureCanvas object, that binds Figure to 5 | # Qt5Agg backend. It also inherits from QWidget 6 | from matplotlib.backends.backend_qt5agg \ 7 | import FigureCanvasQTAgg as FigureCanvas 8 | 9 | # Matplotlib Figure object 10 | from matplotlib.figure import Figure 11 | 12 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 13 | from matplotlib import cm 14 | 15 | 16 | import numpy as np 17 | 18 | class SpectroCanvas(FigureCanvas): 19 | """Class to represent the FigureCanvas widget""" 20 | def __init__(self): 21 | # setup Matplotlib Figure and Axis 22 | self.fig = Figure() 23 | 24 | # initialization of the canvas 25 | FigureCanvas.__init__(self, self.fig) 26 | 27 | self.ax = self.fig.clear() 28 | self.ax = self.fig.add_subplot(111) 29 | self.fig.subplots_adjust(left=0.12,right=0.98,bottom=0.1, top=.97) 30 | 31 | # we define the widget as expandable 32 | FigureCanvas.setSizePolicy(self, 33 | QtWidgets.QSizePolicy.Policy.Expanding, 34 | QtWidgets.QSizePolicy.Policy.Expanding) 35 | # notify the system of updated policy 36 | FigureCanvas.updateGeometry(self) 37 | 38 | class SpectroWidget(QtWidgets.QWidget): 39 | """Widget defined in Qt Designer""" 40 | def __init__(self, parent = None): 41 | # initialization of Qt MainWindow widget 42 | QtWidgets.QWidget.__init__(self, parent) 43 | 44 | # set the canvas to the Matplotlib widget 45 | self.canvas = SpectroCanvas() 46 | 47 | # create a vertical box layout 48 | self.vbl = QtWidgets.QVBoxLayout() 49 | 50 | # add spectro widget to vertical box 51 | self.vbl.addWidget(self.canvas) 52 | 53 | # add interactive navigation 54 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 55 | self.vbl.addWidget(self.navi_toolbar) 56 | 57 | # set the layout to th vertical box 58 | self.setLayout(self.vbl) 59 | 60 | def update_graph(self,amp,sample_rate,colormap_choice=0): 61 | """Updates the graph with new data/annotations""" 62 | 63 | self.canvas.ax.clear() 64 | 65 | nsamples = len(amp) 66 | 67 | NFFT = 1024 # the length of the windowing segments 68 | 69 | if (0 == colormap_choice): 70 | colormap = cm.gist_heat 71 | elif (1 == colormap_choice): 72 | colormap = cm.gist_rainbow 73 | elif (2 == colormap_choice): 74 | colormap = cm.rainbow 75 | elif (3 == colormap_choice): 76 | colormap = cm.gray 77 | elif (4 == colormap_choice): 78 | colormap = cm.Blues 79 | elif (5 == colormap_choice): 80 | colormap = cm.gist_ncar 81 | 82 | 83 | Pxx, freqs, bins, im = self.canvas.ax.specgram(amp, NFFT=NFFT, Fs=1.0*sample_rate, noverlap=900 84 | #)# ,cmap=cm.gray) 85 | # ,cmap=cm.Blues) # color map 86 | ,cmap=colormap) # color map 87 | 88 | # Annotation 89 | self.canvas.ax.set_xlabel('Time (s)') 90 | self.canvas.ax.set_ylabel('Frequency (Hz) ') 91 | self.canvas.ax.axis([0,bins[-1],0,freqs[-1]]) 92 | 93 | # Actually draw everything 94 | self.canvas.draw() 95 | -------------------------------------------------------------------------------- /docs/ir.md: -------------------------------------------------------------------------------- 1 | # Creating Impulse Responses with SHAART 2 | 3 | Download SHAART. 4 | 5 | Sure, you could press a button and have Room Eq Wizard or FuzzMeasure or Logic do it for you, but what if you want to "see under the hood" a bit, to learn what's really going on? Here we follow the now-ubiquitous method (and ISO standard) of Farina [1]. 6 | 7 | 1. Create an exponential sine sweep of uniform amplitude. 8 | 9 | - You may create the sweep in SHAART by clicking on the "Equation" tab and leaving the default equation text in place and selecting "Go". (The default is for a 10 second sweep from 20 Hz to 20000 Hz.) 10 | 11 | ```blah 12 | 0.8 * sin( 20 *2*PI*TMAX/ln(20000.0/20) * (exp(t/TMAX*ln(20000.0/20))-1) ) 13 | ``` 14 | 15 | - Alternatively you may generate a sweep in Audacity by selecting Generate > Chirp and then "Logarithmic". Be sure to set the starting and ending amplitudes to be the same. 16 | 17 | 2. Play the sweep from your speaker while recording the response. (SHAART does not currently have recording functions). Save the response to a WAV file. 18 | 19 | 3. In SHAART, choose "Equation" and this time copy & paste in the "Inverse Exponential Sine Sweep" [2] equation text... 20 | 21 | ``` 22 | exp(ln(20000.0/20)*(-t)/TMAX) * sin( 20 *2*PI*TMAX/ln(20000.0/20) * (exp((TMAX-t)/TMAX*ln(20000.0/20))-1) ) 23 | ``` 24 | 25 | ...and press Go. This will go into "File A" in SHAART. 26 | 27 | 4. Load the response WAV file into "File B" in SHAART. A comparison of the two waveforms now in memory will look like this: 28 | ![ir_sweep_and_inv](ir_sweep_and_inv_waveform.png) 29 | 30 | 5. Go to the "Convolve" tab and simply press "Go". (No other instructions or actions are necessary. Do not time-reverse file A). 31 | 32 | 6. File A now contains your Impulse Response! 33 | **TODO:** show screenshot(s) of constructed IR. 34 | 35 | 36 | 37 | ## Check: IR of a Dry Signal 38 | 39 | If we use the "original" (constant-amplitude, forward) sine sweep and convolve it with its "inverse" (exponential-amplitude, backward) sweep, in theory we should get a Dirac delta function (i.e. a "spike") in time and a flat power spectrum. Let's check: 40 | 41 | 1. As in Step 1 above, use the Equation feature to generate the forward sweep as File A and save it to a file: `sweep.wav`. 42 | 43 | 2. As in Step 3 above, use the Equation feature to generate the 'Inverse filter' as File A (i.e. overwrite what's there), and keep it there. 44 | 45 | 3. Load back the original sweep file as File B. (Take a look in the Waveform display. You should see something similar to the screenshot shown in Step 4 above.) 46 | 47 | 4. Go the Convolve tab and press the big "GO!" button. 48 | 49 | 5. Go back to the waveform display to see this 'spike': ![ir_delta](ir_delta.png)...which is not quite perfect but pretty 'impulsive'! If we look at it on a dB scale (go to the RT60 tab), we see... 50 | 51 | ![ir_delta_pwr](ir_delta_pwr.png) 52 | 53 | ...Defects include the "blip" on the left, and an asymmetry that shows up about 50dB lower than the maximum. I'll look into those. The spectrogram looks like this: 54 | 55 | ![ir_spectrogram](ir_spectrogram.png)**TODO:** Feature Request: add a color bar to the side of the plot so we know the scale of the colors. 56 | 57 | 6. Check the power spectrum: Is it flat? Press the Power tab to see this: ![ir_power](ir_power.png) ...pretty flat, eh? ;-) *(And if you change the equations to run from 10 Hz to 22 kHz instead of 20 to 20k it'll look even flatter, but there's not really a point to that because you won't be measuring RT60 in those extra frequency ranges.)* 58 | 59 | ## References: 60 | 61 | [1] Farina's Method: http://aurora-plugins.forumfree.it/?t=53443032 62 | 63 | [2] Inverse Exponential Sweep, see Peter Pabon: http://kc.koncon.nl/staff/pabon/IRM/IRMeasurementInstruction/assignment_IR_ExpSweepTheory.htm 64 | 65 |
66 | Author: Scott Hawley 67 | 68 | -------------------------------------------------------------------------------- /source/waterwidget.py: -------------------------------------------------------------------------------- 1 | # Python Qt5 bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | # import the Qt5Agg FigureCanvas object, that binds Figure to 5 | # Qt5Agg backend. It also inherits from QWidget 6 | from matplotlib.backends.backend_qt5agg \ 7 | import FigureCanvasQTAgg as FigureCanvas 8 | 9 | # Matplotlib Figure object 10 | from matplotlib.figure import Figure 11 | 12 | 13 | import numpy as np 14 | from mpl_toolkits.mplot3d import axes3d 15 | 16 | #from scipy.misc import imresize 17 | from scipy import ndimage 18 | 19 | 20 | class WaterCanvas(FigureCanvas): 21 | """Class to represent the FigureCanvas widget""" 22 | def __init__(self): 23 | # setup Matplotlib Figure and Axis 24 | self.fig = Figure() 25 | 26 | # initialization of the canvas 27 | FigureCanvas.__init__(self, self.fig) 28 | 29 | self.ax = self.fig.add_subplot(111, projection='3d') 30 | self.fig.subplots_adjust(left=0,right=1.0,bottom=0, top=1.0) 31 | 32 | # we define the widget as expandable 33 | FigureCanvas.setSizePolicy(self, 34 | QtWidgets.QSizePolicy.Policy.Expanding, 35 | QtWidgets.QSizePolicy.Policy.Expanding) 36 | # notify the system of updated policy 37 | FigureCanvas.updateGeometry(self) 38 | 39 | class WaterWidget(QtWidgets.QWidget): 40 | """Widget defined in Qt Designer""" 41 | def __init__(self, parent = None): 42 | # initialization of Qt MainWindow widget 43 | QtWidgets.QWidget.__init__(self, parent) 44 | 45 | # set the canvas to the Matplotlib widget 46 | self.canvas = WaterCanvas() 47 | 48 | # create a vertical box layout 49 | self.vbl = QtWidgets.QVBoxLayout() 50 | 51 | # add water widget to vertical box 52 | self.vbl.addWidget(self.canvas) 53 | 54 | # set the layout to th vertical box 55 | self.setLayout(self.vbl) 56 | 57 | 58 | def update_graph(self,amp,sample_rate): 59 | """Updates the graph with new data/annotations""" 60 | 61 | from matplotlib import cm # lazy loading 62 | import matplotlib.mlab as mlab 63 | 64 | nsamples = len(amp) 65 | NFFT = 1024 # the length of the windowing segments 66 | """ The next line creates a spectrogram but doesn't draw it.""" 67 | Pxx, f, t = mlab.specgram(amp,NFFT=NFFT, Fs=1.0*sample_rate, noverlap=900) 68 | #Pxx, f, t = mlab.specgram(amp, Fs=1.0*sample_rate) 69 | image = np.row_stack(Pxx) 70 | image = ndimage.gaussian_filter(image, 3) 71 | 72 | """convert the spectrogram into the type of 2D-array(s) that can be plotted""" 73 | dt = t[1]-t[0] 74 | df = f[1]-f[0] 75 | x = [ a*dt for a in range(image.shape[1])] # times 76 | y = [ a*df for a in range(image.shape[0])] # frequencies 77 | X,Y = np.meshgrid(x,y) 78 | Z = 10.0*np.log10(image) # log scale for intensity 79 | maxval = np.max(Z) 80 | Z = np.array([ x - maxval for x in Z]) # normalize to zero dB 81 | #print "X.shape = ", X.shape, ", Y.shape = ", Y.shape #, ", Z.shape = ", Z.shape 82 | 83 | """A form of downsampling: set the 'stride' when plotting, for cris-crossy lines""" 84 | cstride = 10 # stride in time indices 85 | rstride = X.shape[0] # stride in frequency indices 86 | maxn = 1000 # say we can reasonably handle a maxn values, beyond that, we reduce 87 | if X.shape[1] > maxn: cstride = 10* X.shape[1] // maxn 88 | # self.canvas.ax.plot_surface(X,Y,Z,rstride=rstride, cstride=cstride, alpha=1.0, cmap=cm.Blues) 89 | self.canvas.ax.plot_surface(X,Y,Z,rstride=rstride, cstride=cstride, cmap=cm.Blues) 90 | 91 | self.canvas.ax.set_xlabel('Time (s)') 92 | self.canvas.ax.set_ylabel('Freq (Hz)') 93 | self.canvas.ax.set_zlabel('Power (dB)') 94 | 95 | """Actually draw everything""" 96 | self.canvas.draw() 97 | -------------------------------------------------------------------------------- /docs/rt60.md: -------------------------------------------------------------------------------- 1 | # Measuring Reverb Times with SHAART 2 | 3 | Download SHAART. 4 | 5 | ## I. Get a Recording of a Decay 6 | 7 | * You can start with a premade recording, such as [sample_data.wav](../audio/sample_data.wav) or [HawleyBathroom.mp3](../audio/HawleyBathroom.mp3) (or [HawleyBathroom.m4a](../audio/HawleyBathroom.m4a)) that I made by clapping and banging a book in my bathroom. 8 | 9 | **...Or...** 10 | 11 | * You can record one yourself: 12 | * The easist way is to provide some impulse sound such as clapping, popping a balloon or banging a book. 13 | * Or, the "proper" way is to digitally contruct an impulse response from sine sweeps; if you want to do that, see the tutorial ["Creating Impulse Responses with SHAART"](ir.md). 14 | 15 | ## II. Measure the Reverb Time (Single Decay) 16 | Load the audio file for your recorded decay as File A. You can also load a second one as File B. We will use the simple case of one decay given by [sample_data.wav](../audio/sample_data.wav) for what follows. For multiple decays in one recording, see Part III, below. 17 | 18 | 1. Initially you see the decay itself, shown without any octave-band filtering: 19 | 20 | ![rt60_raw](rt60_raw.png) 21 | 22 | 2. Using the drop-down menu in the upper right, select the octave band you want to filter in, such as 1000 Hz, and release: ![rt60_raw](rt60_octaveselect.png) 23 | 24 | 25 | 26 | 3. With your mouse, click near the top of the graph (but ignore the initial peak) and while holding the button down, drag the resulting red line such that it is *parallel* to the overall shape of the decay (it doesn't have to be on top of the decay, just parallel to it). Also, it doesn't matter how *long* you draw the line, all we care about is the *slope*. 27 | 28 | ![rt60_raw](rt60_drawn.png) 29 | 30 | 4. In the top center of the graph, the text "RT60 = 1.24 s" appears. **That is the reverb time you measured.** 31 | 5. **Re-measuring:** To re-draw the line, simply click and drag again. To switch to different octaves, simply change the octave drop-down. 32 | 33 | ## III. Measuring Reverb Time (Multiple Decays) 34 | 35 | In this case we will measure multiple days, contained in [HawleyBathroom.m4a](../audio/HawleyBathroom.m4a). In this case, we will use the "Zoom" feature provided by the magnifying class icon in the bottom left. 36 | 37 | 1. First, we need to load the file and choose and octave; let's do 4 kHz: 38 | 39 | ![hawleybath_oops](rt60_hawleyb_oops.png) 40 | 41 | Ooops! Notice there's still a red line from the previous measurement, and **even though the Octave filter *says* "1000 Hz", it's really not: THIS IS A BUG.** It's actually showing broad-band (unfiltered) signal. 42 | 43 | 2. But when we select the 4000 Hz octave, the display becomes correct: 44 | 45 | ![hawleybath_oops](rt60_hawleyb_4k.png) In this example there are 6 decays: the first 3 impulses are from hand claps, and the last 3 are from backing a book on the wall. 46 | 47 | 3. Now click the magnifying glass icon near the bottom left of the window, decide on one of those 6 decays too zoom in on, and click-and-drag a box around it. We'll choose the last decay: ![hawleybath_oops](rt60_hawleyb_4k_box.png) When you release the mouse, they display will be zoomed in on that decay: 48 | 49 | 4. ![4k_zoom](rt60_hawleyb_4k_zoom.png) If you want to zoom out and try again, click the "Home" icon in the bottom left. If you want zoom in even more, click the magnifying class again and draw a new box. 50 | 51 | 5. At this point, simply & click and draw a red line along the decay, as in the "Single Decay" example above: 52 | 53 | ![4k_zoom_like](rt60_hawleyb_4k_zoom_line.png) 54 | 55 | Note that this small bathroom room has a **double-valued reverb time:** This can be seen a bit more obviously in the zoomed-out view, showing a "kink" in the middle of each decay. This is probably due to the bathroom door having a large opening under it, so that the bathroom is acoustically coupled to the hallway and kitchen outside. You may notice similar effects for your rooms. 56 | 57 | 6. **Bug/Feature:** When you select a new octave band, **SHAART will zoom back out** to the full display. For example, if we choose 125 Hz octave next, we get this: ![hawleyb_125](rt60_hawleyb_125.png) 58 | 59 | ...and *every time* we change the octave, it will zoom out. (Note that the 3 hand claps on the left have much less energy at low frequencies than the 3 book-bangs on the right.) 60 | 61 | This un-zooming behaviour is a "feature" of clearing the display each time we draw a plot; in order to keep the zoom window the same when filtering, it would take a fair amount more coding work -- perhaps a future version of SHAART will be able to do that. 62 | 63 |
64 | Author: Scott Hawley 65 | 66 | 67 | -------------------------------------------------------------------------------- /source/waveformwidget.py: -------------------------------------------------------------------------------- 1 | # Python Qt bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | # import the Qt5Agg FigureCanvas object, that binds Figure to 5 | # Qt5Agg backend. It also inherits from QWidget 6 | from matplotlib.backends.backend_qt5agg \ 7 | import FigureCanvasQTAgg as FigureCanvas 8 | 9 | # Matplotlib Figure object 10 | from matplotlib.figure import Figure 11 | 12 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 13 | 14 | import numpy as np 15 | import scipy.signal as signal 16 | from os import path 17 | import re 18 | import scipy.signal.signaltools as sigtool 19 | 20 | 21 | 22 | def my_resample(x,y,newnum): 23 | num = len(x) 24 | stride = int(num / newnum) 25 | x2 = np.zeros(newnum) 26 | y2 = np.zeros(newnum) 27 | i = 0 28 | for i2 in range(0,newnum): 29 | x2[i2] = x[i] 30 | y2[i2] = y[i] 31 | i = i+stride 32 | return x2, y2 33 | 34 | 35 | 36 | 37 | class WaveformCanvas(FigureCanvas): 38 | """Class to represent the FigureCanvas widget""" 39 | def __init__(self): 40 | # setup Matplotlib Figure and Axis 41 | self.fig = Figure() 42 | 43 | # initialization of the canvas 44 | FigureCanvas.__init__(self, self.fig) 45 | 46 | self.ax = self.fig.clear() 47 | self.ax = self.fig.add_subplot(111) 48 | self.fig.subplots_adjust(left=0.09,right=0.98,bottom=0.13, top=.97) 49 | 50 | 51 | # we define the widget as expandable 52 | FigureCanvas.setSizePolicy(self, 53 | QtWidgets.QSizePolicy.Policy.Expanding, 54 | QtWidgets.QSizePolicy.Policy.Expanding) 55 | # notify the system of updated policy 56 | FigureCanvas.updateGeometry(self) 57 | 58 | class WaveformWidget(QtWidgets.QWidget): 59 | """Widget defined in Qt Designer""" 60 | def __init__(self, parent = None): 61 | # initialization of Qt MainWindow widget 62 | QtWidgets.QWidget.__init__(self, parent) 63 | 64 | # set the canvas to the Matplotlib widget 65 | self.canvas = WaveformCanvas() 66 | 67 | # create a vertical box layout 68 | self.vbl = QtWidgets.QVBoxLayout() 69 | 70 | # add waveform widget to vertical box 71 | self.vbl.addWidget(self.canvas) 72 | 73 | # add interactive navigation 74 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 75 | self.vbl.addWidget(self.navi_toolbar) 76 | 77 | # set the layout to th vertical box 78 | self.setLayout(self.vbl) 79 | 80 | def legend_string(self,instr): 81 | instr = "%s" % instr # just to make sure it's of the right 'type' 82 | outstr = path.basename(instr) 83 | match = re.match(r"(.*)\.wav",outstr) # take out the .wav if possible 84 | if (match is not None): 85 | outstr = match.group(1) 86 | return outstr 87 | 88 | def draw_graph(self,amp,t,color="red",abs_checked=0, env_checked=0): 89 | """Updates the graph with new data/annotations""" 90 | 91 | nsamples = len(amp) 92 | if (nsamples > 100000): 93 | plotsamples = 4096 94 | elif (nsamples > 50000): 95 | plotsamples = 2048 96 | else: 97 | plotsamples = 1024 98 | 99 | ds_t,ds_amp = my_resample(t,amp,plotsamples) 100 | 101 | maxval = np.max(ds_amp) 102 | minval = np.min(ds_amp) 103 | 104 | if abs_checked: 105 | ds_amp = np.abs(ds_amp) 106 | 107 | if env_checked: 108 | env = np.abs(sigtool.hilbert(ds_amp)) 109 | ds_amp = env 110 | #ds_t = np.arange(0,len(env),t[1]) 111 | 112 | 113 | # Set up the plot 114 | p = self.canvas.ax.plot(ds_t, ds_amp,color=color,lw=1) 115 | 116 | self.canvas.ax.grid(True) 117 | 118 | 119 | # Annotation 120 | self.canvas.ax.set_xlabel('Time (s)') 121 | self.canvas.ax.set_ylabel('Displacement') 122 | 123 | return p 124 | 125 | 126 | def update_graph(self,amp,t,filenameA,ampB,tB,filenameB,abs_checked=0, env_checked=0): 127 | self.canvas.ax.clear() 128 | p1, = self.draw_graph(amp,t,"blue",abs_checked,env_checked) 129 | leg_fA = self.legend_string(filenameA) 130 | 131 | if (ampB is not None) & (len(ampB) > 1): 132 | p2, = self.draw_graph(ampB,tB,"purple",abs_checked,env_checked) 133 | leg_fB = self.legend_string(filenameB) 134 | l1 = self.canvas.ax.legend([p1,p2], [leg_fA,leg_fB], loc=3) 135 | else: 136 | l1 = self.canvas.ax.legend([p1], [leg_fA], loc=3) 137 | 138 | l1.draw_frame(False) # no box around the legend 139 | 140 | # Actually draw everything 141 | self.canvas.draw() 142 | -------------------------------------------------------------------------------- /source/pwrspecwidget.py: -------------------------------------------------------------------------------- 1 | # Python Qt5 bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | # import the Qt5Agg FigureCanvas object, that binds Figure to 5 | # Qt5Agg backend. It also inherits from QWidget 6 | from matplotlib.backends.backend_qt5agg \ 7 | import FigureCanvasQTAgg as FigureCanvas 8 | 9 | # Matplotlib Figure object 10 | from matplotlib.figure import Figure 11 | 12 | #from matplotlib.backends.backend_qt5agg import NavigationToolbar2QTAgg as NavigationToolbar 13 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 14 | 15 | import numpy as np 16 | import scipy.signal as signal 17 | import scipy 18 | from os import path 19 | import re 20 | 21 | 22 | def my_resample(x,y,newnum): 23 | num = len(x) 24 | stride = int(num / newnum) 25 | x2 = np.zeros(newnum) 26 | y2 = np.zeros(newnum) 27 | i = 0 28 | for i2 in range(0,newnum): 29 | x2[i2] = x[i] 30 | y2[i2] = y[i] 31 | i = i+stride 32 | return x2, y2 33 | 34 | 35 | 36 | 37 | class PwrSpecCanvas(FigureCanvas): 38 | """Class to represent the FigureCanvas widget""" 39 | def __init__(self): 40 | # setup Matplotlib Figure and Axis 41 | self.fig = Figure() 42 | 43 | # initialization of the canvas 44 | FigureCanvas.__init__(self, self.fig) 45 | 46 | self.ax = self.fig.clear() 47 | self.ax = self.fig.add_subplot(111) 48 | self.fig.subplots_adjust(left=0.09,right=0.98,bottom=0.13, top=.97) 49 | 50 | 51 | # we define the widget as expandable 52 | FigureCanvas.setSizePolicy(self, 53 | QtWidgets.QSizePolicy.Policy.Expanding, 54 | QtWidgets.QSizePolicy.Policy.Expanding) 55 | # notify the system of updated policy 56 | FigureCanvas.updateGeometry(self) 57 | 58 | class PwrSpecWidget(QtWidgets.QWidget): 59 | """Widget defined in Qt Designer""" 60 | def __init__(self, parent = None): 61 | # initialization of Qt MainWindow widget 62 | QtWidgets.QWidget.__init__(self, parent) 63 | 64 | # set the canvas to the Matplotlib widget 65 | self.canvas = PwrSpecCanvas() 66 | 67 | # create a vertical box layout 68 | self.vbl = QtWidgets.QVBoxLayout() 69 | 70 | # add pwrspec widget to vertical box 71 | self.vbl.addWidget(self.canvas) 72 | 73 | # add interactive navigation 74 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 75 | self.vbl.addWidget(self.navi_toolbar) 76 | 77 | # set the layout to th vertical box 78 | self.setLayout(self.vbl) 79 | 80 | def legend_string(self,instr): 81 | instr = "%s" % instr # just to make sure it's of the right 'type' 82 | outstr = path.basename(instr) 83 | match = re.match(r"(.*)\.wav",outstr) # take out the .wav if possible 84 | if (match is not None): 85 | outstr = match.group(1) 86 | return outstr 87 | 88 | def draw_graph(self,amp,samplerate,color="red"): 89 | """Updates the graph with new data/annotations""" 90 | 91 | print("pwrspec: Computing dB, amplength = ",len(amp)) 92 | #fftlength = 16384 93 | #dB = 20.0*np.log10(np.abs(np.fft.rfft(amp,n=fftlength))) 94 | # dB = 20 * scipy.log10(scipy.absolute(scipy.fft(amp))) 95 | dB = 20.0*np.log10(np.abs(np.fft.rfft(amp))) # this works the best 96 | print("pwrspec: finished Computing dB") 97 | graphsamples = len(dB) 98 | print("pwrspec computing f, graphsamples = ",graphsamples) 99 | f = np.linspace(0, samplerate/2.0, graphsamples) 100 | print("pwrspec finished computing f") 101 | 102 | if (graphsamples > 100000): 103 | graphsamples = 4096 104 | elif (graphsamples > 50000): 105 | graphsamples = 2048 106 | else: 107 | graphsamples = 1024 108 | graphsamples = len(dB) #todo remove this 109 | # ds_dB,ds_f = signal.resample(dB,graphsamples,f) 110 | ds_f,ds_dB = my_resample(f,dB,graphsamples) 111 | 112 | maxval = np.max(ds_dB) 113 | ds_dB = [ x - maxval for x in ds_dB] 114 | minval = np.min(ds_dB) 115 | 116 | # Set up the plot 117 | p = self.canvas.ax.plot(ds_f, ds_dB,color=color,lw=1) 118 | 119 | self.canvas.ax.grid(True) 120 | self.canvas.ax.axis([10,ds_f[-1],minval,0]) 121 | self.canvas.ax.set_xscale("log", nonpositive='clip') 122 | 123 | 124 | # Annotation 125 | self.canvas.ax.set_xlabel('Frequency (Hz)') 126 | self.canvas.ax.set_ylabel('Power (dB) ') 127 | 128 | print("leaving draw_graph") 129 | 130 | return p 131 | 132 | 133 | def update_graph(self,amp,samplerate,filenameA,ampB,samplerateB,filenameB): 134 | print("starting update_graph") 135 | self.canvas.ax.clear() 136 | p1, = self.draw_graph(amp,samplerate,"blue") 137 | leg_fA = self.legend_string(filenameA) 138 | 139 | if (ampB is not None) & (len(ampB) > 1): 140 | p2, = self.draw_graph(ampB,samplerateB,"purple") 141 | leg_fB = self.legend_string(filenameB) 142 | l1 = self.canvas.ax.legend([p1,p2], [leg_fA,leg_fB], loc=3) 143 | else: 144 | l1 = self.canvas.ax.legend([p1], [leg_fA], loc=3) 145 | 146 | l1.draw_frame(False) # no box around the legend 147 | 148 | # Actually draw everything 149 | self.canvas.draw() 150 | print("leaving update_graph") 151 | -------------------------------------------------------------------------------- /source/modegraphwidget.py: -------------------------------------------------------------------------------- 1 | # Python Qt5 bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | # import the Qt5Agg FigureCanvas object, that binds Figure to 5 | # Qt5Agg backend. It also inherits from QWidget 6 | from matplotlib.backends.backend_qt5agg \ 7 | import FigureCanvasQTAgg as FigureCanvas 8 | 9 | # Matplotlib Figure object 10 | from matplotlib.figure import Figure 11 | 12 | from matplotlib import cm 13 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 14 | 15 | 16 | import numpy as np 17 | from mpl_toolkits.mplot3d import axes3d 18 | 19 | #from scipy.misc import imresize 20 | from scipy import ndimage 21 | 22 | 23 | global color_axial, color_tangential, color_oblique 24 | color_axial = 'r' 25 | color_tangential = 'b' 26 | color_oblique = 'purple' 27 | 28 | 29 | 30 | 31 | class ModeGraphCanvas(FigureCanvas): 32 | """Class to represent the FigureCanvas widget""" 33 | def __init__(self): 34 | # setup Matplotlib Figure and Axis 35 | self.fig = Figure() 36 | 37 | # initialization of the canvas 38 | FigureCanvas.__init__(self, self.fig) 39 | 40 | self.ax = self.fig.clear() 41 | self.ax = self.fig.add_subplot(111) 42 | self.fig.subplots_adjust(left=0.12,right=0.98,bottom=0.105, top=.92) 43 | 44 | 45 | # we define the widget as expandable 46 | FigureCanvas.setSizePolicy(self, 47 | QtWidgets.QSizePolicy.Policy.Expanding, 48 | QtWidgets.QSizePolicy.Policy.Expanding) 49 | # notify the system of updated policy 50 | FigureCanvas.updateGeometry(self) 51 | 52 | 53 | class ModeGraphWidget(QtWidgets.QWidget): 54 | """Widget defined in Qt Designer""" 55 | def __init__(self, parent = None): 56 | # initialization of Qt MainWindow widget 57 | QtWidgets.QWidget.__init__(self, parent) 58 | 59 | # set the canvas to the Matplotlib widget 60 | self.canvas = ModeGraphCanvas() 61 | 62 | # create a vertical box layout 63 | self.vbl = QtWidgets.QVBoxLayout() 64 | 65 | # add mode graph widget to vertical box 66 | self.vbl.addWidget(self.canvas) 67 | 68 | # add interactive navigation 69 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 70 | self.vbl.addWidget(self.navi_toolbar) 71 | 72 | 73 | # set the layout to th vertical box 74 | self.setLayout(self.vbl) 75 | 76 | 77 | def plot_mode(self, f0, deltaf, modetype, sums): 78 | n = 200 79 | fplothalfwidth = 2.5*deltaf 80 | fstart = f0 - fplothalfwidth 81 | fend = f0 + fplothalfwidth 82 | df = 2*fplothalfwidth / n 83 | f = np.arange( fstart, fend+df, df) 84 | # modeshape = np.exp(-((f-f0)/deltaf)**2) # gaussian 85 | dB = -10*((f-f0)/deltaf)**2 # log of a gaussian is a parabola 86 | if (2 == modetype): # axial modes 87 | colorstr = color_axial 88 | dB = [x + 1 for x in dB] 89 | elif (1 == modetype): # tangential modes 90 | colorstr = color_tangential 91 | else: # oblique modes 92 | colorstr = color_oblique 93 | dB = [x - 1 for x in dB] 94 | 95 | modeshape = [10.0**x for x in dB] 96 | 97 | #line, = ax.plot(f, np.ma.log10(modeshape), colorstr) # plot the mode shape itself 98 | # make a tiny tick mark for the mode 99 | 100 | ticklen = 2 101 | tickfs = [f0,f0] 102 | if0 = len(f)/2 103 | tickstart = 7 - 2*modetype 104 | tickps = [tickstart, tickstart + ticklen] 105 | line2, = self.canvas.ax.plot(tickfs, tickps, colorstr) # draw a tick at the mode frequency 106 | 107 | if sums is not None: 108 | #ifsave = 0 # this was used to prevent double-counting #TODO: <--- This is wrong! Double counting is good! 109 | for i in range(len(f)): 110 | ifreq = int(f[i]) 111 | if (ifreq > 0) & (ifreq < len(sums)): 112 | #if (ifreq != ifsave): 113 | sums[ifreq] = sums[ifreq] + modeshape[i] 114 | #ifsave = ifreq 115 | 116 | 117 | 118 | 119 | def update_graph(self,modes,X,Y,Z): 120 | """Updates the graph with new data/annotations""" 121 | nmodes = len(modes) 122 | self.canvas.ax.clear() 123 | 124 | 125 | sum_fmax =250 126 | dB_min = -30 127 | dB_max = 10 128 | sums = np.zeros(sum_fmax) 129 | 130 | for m in modes: 131 | numzeros = m.count(0) 132 | f0 = m[0] 133 | rt60 = 0.4 134 | deltaf = 2.2/rt60 135 | self.plot_mode(f0, deltaf, numzeros, sums) 136 | 137 | 138 | line2, = self.canvas.ax.plot(range(len(sums)), np.ma.log10(sums), 'k') 139 | 140 | pl = self.canvas.ax 141 | 142 | # Global Plot characteristics 143 | pl.axis([0,sum_fmax,dB_min,dB_max]) 144 | self.canvas.ax.set_xlabel('Frequency (Hz)') 145 | self.canvas.ax.set_ylabel('Relative sound-pressure level (dB)') 146 | title = "Theoretical Steady-State Room Response" 147 | self.canvas.ax.set_title(title) 148 | pl.grid(True) 149 | 150 | # cheap hack to make a legend 151 | xs = [sum_fmax] 152 | ys = [dB_min] 153 | pA, = self.canvas.ax.plot(xs,ys,color=color_axial) 154 | pB, = self.canvas.ax.plot(xs,ys,color=color_tangential) 155 | pC, = self.canvas.ax.plot(xs,ys,color=color_oblique) 156 | l1 = self.canvas.ax.legend([pA,pB,pC], ['axial modes','tangential modes','oblique modes'], loc=4) 157 | l1.draw_frame(False) # no box around the legend 158 | 159 | 160 | 161 | """Actually draw everything""" 162 | self.canvas.draw() 163 | -------------------------------------------------------------------------------- /source/rcgraphwidget.py: -------------------------------------------------------------------------------- 1 | # Sabine Equation / Room simulation tab 2 | 3 | # Python Qt5 bindings for GUI objects 4 | from PyQt6 import QtGui, QtWidgets 5 | 6 | # import the Qt5Agg FigureCanvas object, that binds Figure to 7 | # Qt5Agg backend. It also inherits from QWidget 8 | from matplotlib.backends.backend_qt5agg \ 9 | import FigureCanvasQTAgg as FigureCanvas 10 | 11 | # Matplotlib Figure object 12 | from matplotlib.figure import Figure 13 | 14 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 15 | 16 | import numpy as np 17 | import scipy.signal as signal 18 | 19 | 20 | class RcGraphCanvas(FigureCanvas): 21 | """Class to represent the FigureCanvas widget""" 22 | def __init__(self): 23 | # setup Matplotlib Figure and Axis 24 | self.fig = Figure() 25 | 26 | # initialization of the canvas 27 | FigureCanvas.__init__(self, self.fig) 28 | 29 | self.ax = self.fig.clear() 30 | self.ax = self.fig.add_subplot(111) 31 | self.fig.subplots_adjust(left=0.1,right=0.95,bottom=0.13, top=.97) 32 | 33 | 34 | # we define the widget as expandable 35 | FigureCanvas.setSizePolicy(self, 36 | QtWidgets.QSizePolicy.Policy.Expanding, 37 | QtWidgets.QSizePolicy.Policy.Expanding) 38 | # notify the system of updated policy 39 | FigureCanvas.updateGeometry(self) 40 | 41 | class RcGraphWidget(QtWidgets.QWidget): 42 | """Widget defined in Qt Designer""" 43 | def __init__(self, parent = None): 44 | # initialization of Qt MainWindow widget 45 | QtWidgets.QWidget.__init__(self, parent) 46 | 47 | # set the canvas to the Matplotlib widget 48 | self.canvas = RcGraphCanvas() 49 | 50 | # create a vertical box layout 51 | self.vbl = QtWidgets.QVBoxLayout() 52 | 53 | # # add rt60 widget to vertical box 54 | self.vbl.addWidget(self.canvas) 55 | 56 | # add interactive navigation 57 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 58 | self.vbl.addWidget(self.navi_toolbar) 59 | 60 | # set the layout to th vertical box 61 | self.setLayout(self.vbl) 62 | 63 | 64 | #Absorption coefficients, in Sabines/ft^2 65 | self.abs_freqs = np.array([125.0, 500.0, 1000.0, 2000.0]) # in Hz 66 | abs_concrete = np.array([0.01, 0.02, 0.02, 0.02]) 67 | abs_glass = np.array([0.19, 0.06, 0.04, 0.03]) 68 | abs_plaster = np.array([0.20, 0.10, 0.08, 0.04]) 69 | abs_plywood = np.array([0.45, 0.13, 0.11, 0.10]) 70 | abs_carpet = np.array([0.10, 0.30, 0.35, 0.50]) 71 | abs_curtains = np.array([0.05, 0.25, 0.35, 0.40]) 72 | abs_acousbrd = np.array([0.25, 0.80, 0.90, 0.90]) 73 | self.sab_adult = np.array([3.0, 4.5, 5.0, 5.2]) # Just Sabines 74 | 75 | # Note this is 2d-array 76 | self.abs_coeffs = np.array([ abs_concrete, abs_glass, abs_plaster, abs_plywood, \ 77 | abs_carpet, abs_curtains, abs_acousbrd ] ) 78 | 79 | self.surfacenames = ["floor", "ceiling", "fwall", "bwall", "lwall", "rwall"] 80 | self.areas = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) 81 | 82 | def update(self): 83 | """Updates the graph with new data/annotations""" 84 | self.canvas.ax.clear() 85 | 86 | # Get information from the various inputs 87 | main = self.parent().parent().parent().parent().parent() #TODO: this will break if a single thing is changed 88 | ss_text = str(main.sabinesslineEdit.text()) 89 | x_text = str(main.sabinexlineEdit.text()) 90 | y_text = str(main.sabineylineEdit.text()) 91 | z_text = str(main.sabinezlineEdit.text()) 92 | adults_text = str(main.adultslineEdit.text()) 93 | floor_mi = main.floorcomboBox.currentIndex() # mi = "material index" 94 | ceiling_mi = main.ceilingcomboBox.currentIndex() 95 | fwall_mi = main.fwallcomboBox.currentIndex() 96 | bwall_mi = main.bwallcomboBox.currentIndex() 97 | lwall_mi = main.lwallcomboBox.currentIndex() 98 | rwall_mi = main.rwallcomboBox.currentIndex() 99 | 100 | 101 | 102 | # do nothing for bad input 103 | if (""==ss_text) | (""==x_text) | (""==y_text) | (""==z_text) | (""==adults_text): return 104 | 105 | # Parse the text info 106 | vs = float(ss_text) 107 | x = float(x_text) 108 | y = float(y_text) 109 | z = float(z_text) 110 | adults = float(adults_text) 111 | volume = x * y * z 112 | self.areas = [ x*y, x*y, x*z, x*z, y*z, y*z ] 113 | surf_materials = [ floor_mi, ceiling_mi, fwall_mi, bwall_mi, lwall_mi, rwall_mi] 114 | 115 | #print "vs, x, y, z = ", vs, x, y, z 116 | #print "areas = ",self.areas 117 | 118 | freqs = self.abs_freqs 119 | rtimes = 0.0*freqs 120 | # Calculate reverb times 121 | for f_ind in range(len(self.abs_freqs)): 122 | sabines = adults*self.sab_adult[f_ind] 123 | for surf in range(len(self.surfacenames)): 124 | #TODO: concrete only for now 125 | surf_coeffs = self.abs_coeffs[surf_materials[surf]] 126 | sabines = sabines + self.areas[surf]* surf_coeffs[f_ind] 127 | 128 | rtimes[f_ind] = 0.050 * 1140.0/vs * volume / sabines 129 | 130 | #print "freqs = ",self.abs_freqs 131 | #print "rtimes = ",rtimes 132 | 133 | # Set up the plot 134 | self.canvas.ax.plot(freqs, rtimes, 'bo-') 135 | self.canvas.ax.grid(True) 136 | self.canvas.ax.axis([0.0,freqs[-1]*1.05,0.0, np.max(rtimes)*1.05 ]) 137 | 138 | # Annotation 139 | self.canvas.ax.set_xlabel('Frequency (Hz)') 140 | self.canvas.ax.set_ylabel('Reverberation Time (s) ') 141 | 142 | # Actually draw everything 143 | self.canvas.draw() 144 | -------------------------------------------------------------------------------- /source/rt60widget.py: -------------------------------------------------------------------------------- 1 | # Python Qt5 bindings for GUI objects 2 | from PyQt6 import QtGui, QtWidgets 3 | 4 | from matplotlib.backends.backend_qt5agg \ 5 | import FigureCanvasQTAgg as FigureCanvas 6 | 7 | # Matplotlib Figure object 8 | from matplotlib.figure import Figure 9 | 10 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 11 | 12 | import numpy as np 13 | import scipy.signal as signal 14 | import re 15 | from os import path 16 | import math 17 | 18 | 19 | def my_resample(x,newnum,y): 20 | 21 | method = 0 22 | 23 | if (0==method): 24 | if (len(y) != len(x)): 25 | print("my_resample: Error: lengths of x and y are not equal!") 26 | # pad signal such that its length is a power of 2 = much faster 27 | orig_len = len(x) 28 | p2_len = int(math.pow(2, math.ceil(math.log(orig_len)/math.log(2)))) 29 | x3 = np.zeros(p2_len) 30 | y3 = np.zeros(p2_len) 31 | x3[0:orig_len-1] = x[0:orig_len-1] 32 | y3[0:orig_len-1] = y[0:orig_len-1] 33 | x2, y2 = signal.resample(x3,newnum*p2_len//orig_len,y3) 34 | x2 = x2[0:newnum-1] 35 | y2 = y2[0:newnum-1] 36 | else: 37 | newnum = int(newnum) 38 | num = len(x) 39 | stride = int(num / newnum) 40 | x2 = np.zeros(newnum) 41 | y2 = np.zeros(newnum) 42 | i = 0 43 | for i2 in range(0,newnum): 44 | i = i2*stride 45 | x2[i2] = x[i] 46 | y2[i2] = y[i] 47 | return x2, y2 48 | 49 | 50 | 51 | class Rt60Canvas(FigureCanvas): 52 | """Class to represent the FigureCanvas widget""" 53 | def __init__(self): 54 | # setup Matplotlib Figure and Axis 55 | self.fig = Figure() 56 | 57 | # initialization of the canvas 58 | FigureCanvas.__init__(self, self.fig) 59 | 60 | self.ax = self.fig.clear() 61 | self.ax = self.fig.add_subplot(111) 62 | self.fig.subplots_adjust(left=0.08,right=0.98,bottom=0.105, top=.92) 63 | 64 | 65 | # we define the widget as expandable 66 | FigureCanvas.setSizePolicy(self, 67 | QtWidgets.QSizePolicy.Policy.Expanding, 68 | QtWidgets.QSizePolicy.Policy.Expanding) 69 | # notify the system of updated policy 70 | FigureCanvas.updateGeometry(self) 71 | 72 | class Rt60Widget(QtWidgets.QWidget): 73 | """Widget defined in Qt Designer""" 74 | def __init__(self, parent = None): 75 | # initialization of Qt MainWindow widget 76 | QtWidgets.QWidget.__init__(self, parent) 77 | 78 | # set the canvas to the Matplotlib widget 79 | self.canvas = Rt60Canvas() 80 | 81 | # create a vertical box layout 82 | self.vbl = QtWidgets.QVBoxLayout() 83 | 84 | # add rt60 widget to vertical box 85 | self.vbl.addWidget(self.canvas) 86 | 87 | # add interactive navigation 88 | self.navi_toolbar = NavigationToolbar(self.canvas, self) 89 | self.vbl.addWidget(self.navi_toolbar) 90 | 91 | # bind events related to drawing lines 92 | self.canvas.mpl_connect('button_press_event', self.on_press) 93 | self.canvas.mpl_connect('button_release_event', self.on_release) 94 | self.canvas.mpl_connect('motion_notify_event', self.on_motion) 95 | 96 | # set the layout to th vertical box 97 | self.setLayout(self.vbl) 98 | 99 | # no buttons pressed 100 | self.press = None 101 | 102 | # the line that users get to draw 103 | self.linex = [0] 104 | self.liney = [0] 105 | self.line, = self.canvas.ax.plot(self.linex, self.liney) 106 | self.line.set_color("red") 107 | self.line.set_linewidth(1.5) 108 | self.line.set_linestyle('-') 109 | 110 | # initialize annotation 111 | self.status_text = self.canvas.ax.text(0.45, 0.9, '', transform=self.canvas.ax.transAxes,size=14,ha='center') 112 | 113 | def legend_string(self,instr): 114 | instr = "%s" % instr # just to make sure it's of the right 'type' 115 | outstr = path.basename(instr) 116 | match = re.match(r"(.*)\.wav",outstr) # take out the .wav if possible 117 | if (match is not None): 118 | outstr = match.group(1) 119 | return outstr 120 | 121 | def refresh(self): # some code taken from init 122 | self.canvas.ax.clear() 123 | 124 | # the line that users get to draw 125 | self.line, = self.canvas.ax.plot(self.linex, self.liney) 126 | self.line.set_color("red") 127 | self.line.set_linewidth(2.0) 128 | self.line.set_linestyle('-') 129 | 130 | ## initialize annotation 131 | self.status_text = self.canvas.ax.text(0.45, 0.9, '', transform=self.canvas.ax.transAxes,size=14,ha='center') 132 | 133 | 134 | def on_press(self, event): 135 | 'on button press we will start drawing a line' 136 | #unless we're in pan or zoom interactive mode 137 | mode = self.canvas.ax.get_navigate_mode() 138 | if ((mode != 'ZOOM') & (mode != 'PAN')): 139 | x0, y0 = event.xdata, event.ydata 140 | self.press = x0, y0 141 | 142 | def on_motion(self, event): 143 | 'on motion we will draw the line ' 144 | if self.press is None: return 145 | x0, y0 = self.press 146 | x = x0, event.xdata 147 | y = y0, event.ydata 148 | 149 | self.line.set_data(x, y) # this sets up the line to be drawn 150 | self.linex = x 151 | self.liney = y 152 | 153 | if (y[1] != y[0]): 154 | rt60 = -60.0*(x[1]-x[0])/(1.0*y[1]-y[0]) 155 | status_template = 'RT60 = %.2f s' 156 | self.status_text.set_text(status_template%(rt60)) 157 | self.line.figure.canvas.draw() # update the canvas 158 | 159 | def on_release(self, event): 160 | '''on release we reset the press data''' 161 | self.press = None 162 | mode = self.canvas.ax.get_navigate_mode() 163 | # undo any zoom effects 164 | if ('ZOOM' == mode): 165 | self.navi_toolbar.zoom() 166 | return 167 | 168 | def disconnect(self): 169 | '''disconnect all the stored connection ids''' 170 | self.line.figure.canvas.mpl_disconnect(self.cidpress) 171 | self.line.figure.canvas.mpl_disconnect(self.cidrelease) 172 | self.line.figure.canvas.mpl_disconnect(self.cidmotion) 173 | 174 | def update_graph(self,amp,t,filenameA,ampB,tB, filenameB): 175 | """Updates the graph with new data/annotations""" 176 | 177 | if len(amp) <= 1: return # do nothing when there's nothing to graph 178 | 179 | epsilon = 1.0e-8 # added to avoid log(0) errors 180 | 181 | # Compute the quantity to be plotted 182 | power = (amp + epsilon) **2 183 | maxval = 1.0*np.max(power) 184 | power = power / maxval 185 | dB = 10*np.ma.log10(np.abs(power)) # "ma"=masked array, throws out -Inf values 186 | 187 | 188 | # Downsample for plotting purposes. (otherwise the plotting takes forever) 189 | nsamples = len(dB) 190 | if (nsamples > 100000): 191 | plotsamples = 2048 192 | elif (nsamples > 50000): 193 | plotsamples = 2048 194 | else: 195 | plotsamples = 1024 196 | ds_dB, ds_t = my_resample(dB,plotsamples,t) 197 | 198 | # Set up the plot 199 | self.refresh() 200 | p1, = self.canvas.ax.plot(ds_t, ds_dB,color="blue",lw=1) 201 | leg_fA = self.legend_string(filenameA) 202 | 203 | # second file 204 | if (filenameB != "") and (ampB is not None) and (len(ampB) > 1): 205 | powerB = (ampB+epsilon)**2 206 | powerB = powerB / maxval # use same max value as for file A 207 | dB_B = 10*np.ma.log10(np.abs(powerB)) # "ma"=masked array, throws out -Inf values 208 | # Downsample for plotting purposes. (otherwise the plotting takes forever) 209 | nsamplesB = len(dB_B) 210 | if (nsamplesB > 100000): 211 | plotsamplesB = 4096 212 | elif (nsamplesB > 50000): 213 | plotsamplesB = 2048 214 | else: 215 | plotsamplesB = 1024 216 | ds_dB_B,ds_t_B = signal.resample(dB_B,plotsamplesB,tB) 217 | p2, = self.canvas.ax.plot(ds_t_B, ds_dB_B,color="purple",lw=1) 218 | leg_fB = self.legend_string(filenameB) 219 | l1 = self.canvas.ax.legend([p1,p2], [leg_fA,leg_fB], loc=1) 220 | else: 221 | l1 = self.canvas.ax.legend([p1], [leg_fA], loc=1) 222 | 223 | l1.draw_frame(False) # no box around the legend 224 | self.canvas.ax.grid(True) 225 | 226 | #draw the line again, on top 227 | self.line, = self.canvas.ax.plot(self.linex, self.liney) 228 | self.line.set_data(self.linex, self.liney) 229 | self.line.set_color("red") 230 | self.line.set_linewidth(2.0) 231 | self.line.set_linestyle('-') 232 | self.line.figure.canvas.draw() 233 | 234 | # Annotation 235 | self.canvas.ax.set_xlabel('Time (s)') 236 | self.canvas.ax.set_ylabel('Power (dB) ') 237 | 238 | # Actually draw everything 239 | self.canvas.draw() 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | # SHAART Acoustic Tools 16 | 17 | 18 |

19 | SHAART Acoustic Tools, v 0.81
20 | (March 28, 2025)
21 |
22 | (yes, the name is a joke)
23 | About    24 | Features    25 | Downloads    26 | License    27 | Tutorials    28 | Screenshots   
29 | Running From Source    30 | Building an Executable    31 | FAQ    32 | Release Notes    33 |

34 |

35 | 36 | 37 | 38 | 39 | 40 | 41 | ## About 42 | 43 | This lightweight audio analysis suite was initially written for educational purposes only 44 | over a period of 4 days. (And then improved in bits.) It's amazing how much you can accomplish with minimal knowledge of Python programming! 45 | 46 | The name "SHAART" uses the author's initials (S.H.) in homage to the famous "SMAART" set of acoustics analysis tools by Rational Acoustics, Inc. ...That and "SHAART" is just hilarious to say, for other reasons. *(Note: "homage" = parody, derivative work = fair use = please don't sue.)* 47 | 48 | 49 | 50 | 51 | 52 | ## Features 53 | 54 | Most of these features are illustrated in the Screenshots section further down this page. 55 | 56 | * **Reverberation Time (RT60) Measurement:** The reason SHAART was written in the first place. Load an audio file, filter in different octave bands, draw a "best fit" line on the graph by hand, read off the reverb time. (Designed to mimic functionality of SMAART Acoustics Tools(tm).) Can show two files ("File A" and "File B") at once. See Tutorials for more on this feature. 57 | * **Waveform Display:** Linear scale only. Displays two waveforms ("File A" and "File B") at once. 58 | * **Power Spectrum:** Pretty standard display Doesn't do a log scale yet, though I'd like to add that. Displays two spectra ("File A" and "File B" at once.) 59 | * **Spectrogram:** Shows magnitude as color, vs frequency and time. as with Power Spectrum. Offers a few colormaps. No log freq scale yet. Only one file ("File A") shown. 60 | * **Waterfall Plot:** Alternative to Spectrogram, shows magnitude surface a function of time & frequency. No log freq scale yet. Only one file ("File A") shown. 61 | * **"Inverse Spectrogram" (Image-To-Audio):** Import an image, output audio for which the spectrogram will resemble that image. Sounds a little "phasey," could be cleaner. Useful for demonstrating audio effects. See "Screenshots," below. 62 | * **Room Mode Calculator:** Uses the Rayleigh equation for standing waves of a 3D box, and also plots a "Fake Room Response" by assigning relative amplitudes to axial, tangential, and oblique modes. Useful for demonstrating mode distributions for different room shapes. 63 | * **Sabine Equation Calculator:** Assumes a box-shaped room, lets you apply absorption to different surfaces. Based on Chapter 8 of Berg & Stork textbook, including their table for absorption coefficients. 64 | * **Equation-to-Audio:** Specify a time-dependent function, and it'll generate audio from that. Useful for sine sweeps, e.g. for building impulse responses using Convolution (below) 65 | * **Convolution:** Convolve File A with File B. Useful for making impulse responses from sine sweeps, or creating convolution reverb effects, or just for screwing around (e.g., convolving Led Zeppelin's "The Ocean" with the sound of a dog bark.) 66 | * **Play(/Record):** Very rudimentary. Will play the audio out the speaker, with no controls. Record doesn't work yet. 67 | 68 | 69 | 70 | ## Downloads 71 | 72 | * [Mac Binary application](https://hedges.belmont.edu/~shawley/SHAART/SHAART.app.tar.gz) (93 MB) 73 | 74 | * [Windows executable](https://hedges.belmont.edu/~shawley/SHAART/SHAART.exe) (361 MB). Note that the Windows EXE takes *a while* to come up when you first run it. 75 | * [Linux executable](https://hedges.belmont.edu/~shawley/SHAART/SHAART_Linux) (132 MB, Pop!\_OS / Ubuntu). You can also run from source (below) 76 | * [Source code (GitHub)](http://github.com/drscotthawley/SHAART) (in Python) See Running From Source below for further instructions. 77 | * [Sample WAV file](audio/sample_data.wav) 78 | 79 | 80 | 81 | 82 | 83 | ## License 84 | 85 | 2013 - 2015: This software is both "Open Source" and "Free," released under the Jesus license: "Freely you have received, freely give" (Matthew 10:8). Do as you like. Modify, redistribute, etc. 86 | 87 | 2015+: GPL 2. See [LICENSE.md](LICENSE.md) file. 88 | 89 | 90 | 91 | 92 | 93 | ## Tutorials 94 | 95 | * **General Instructions:** Go up to the "File" tab and select an audio file to analyze. 96 | 97 | **TODO:** Add more here 98 | 99 | * [Measuring Reverb Times with SHAART](https://github.com/drscotthawley/SHAART/blob/master/docs/rt60.md) 100 | 101 | * [Creating Impulse Responses with SHAART](https://github.com/drscotthawley/SHAART/blob/master/docs/ir.md) 102 | 103 | 104 | 105 | 106 | 107 | ## Screenshots 108 | 109 | ![rt60](images/rt60.png) 110 | 111 | ![power](images/power.png) 112 | 113 | 114 | 115 | The spectrogram and waterfall plot (below) only show File A: 116 | 117 | ![spectro](images/spectro.png) 118 | 119 | ![waterfall](images/waterfall.png) 120 | 121 | 122 | 123 | Note: The following 'Theoretical Steady-Steate Room Response' is pretty fake; it's just a bunch of superimposed parabolae, but without it this pane had a bunch of empty space:
124 | ![modes](images/modes.png) 125 | 126 | 127 | 128 | The values of absorption coefficients for the Sabine calculator come from the table in Chapter 8 of the Berg & Stork textbook: 129 | 130 | ![sabine](images/sabine.png) 131 | 132 | 133 | 134 | The "invSpectro" feature created the file [mandrill.wav](audio/mandrill.wav) which has a spectrogram shown below: 135 | 136 | ![mandrill_spectro](images/mandrill_spectro.png) 137 |
138 | And interestingly, if the audio is encoded as an MP3, then re-read and re-written as a WAV, one can see the "lossyness" of the MP3:
139 | ![mandrill_mp3](images/mandrill_mp3_to_wav.png) 140 |
141 | One can also apply various audio effects to the sound and see the effect on the image, e.g. echo:
142 | ![echo](images/mandrill_echo.png) 143 | 144 | Wah-wah:![wah](images/mandrill_wahwah.png) 145 | 146 | Reverb:![reverb](images/mandrill_reverb.png) 147 | 148 | And here's an interesting one: a "leveler" effect:
149 | ![leveler](images/mandrill_leveler.png) 150 |
151 | 152 | 153 | 154 | ## Running from Source 155 | Running SHAART.py from source:
156 | Create a new Python environment and install dependencies: 157 | ```bash 158 | pip install librosa pyqt6 pillow pyaudio numpy matplotlib 159 | ``` 160 | 161 | Then run... 162 | 163 | ```bash 164 | cd SHAART/source 165 | ./SHAART.py 166 | ``` 167 | 168 | 169 | 170 | 171 | 172 | ## Building an Executable 173 | 174 | First follow the instructions above for running from source. Then we will proceed by using `pyinstaller`, that will create a new directory called `SHAART/source/dist/`, **in which a successful build will result in the presence of working binary executable.** 175 | 176 | ```bash 177 | pip install pyinstaller 178 | ``` 179 | 180 | ### Mac 181 | 182 | In order to run from source, you'd already need to have XCode, the command-line tools, and HomeBrew installed. Then in we install `python.app` and [an older versions of a few things](https://github.com/pyinstaller/pyinstaller/issues/4067) to build the app: 183 | 184 | ```bash 185 | pyinstaller SHAART.spec 186 | ``` 187 | 188 | ...and you'll find `SHAART.app` in `source/dist/`. 189 | 190 | ### Linux (Pop!\_OS / Ubuntu) 191 | 192 | We *could* re-use the .spec file from the Mac build, but it would give us a whole directory instead of one executable. Instead, run this line: 193 | 194 | ```bash 195 | pyinstaller SHAART.spec 196 | ``` 197 | 198 | ...And then you can just run the `dist/SHAART` executable from the command line. (Note: I can't seem to get it to be a "clickable icon" in Nautilus/Gnome. Not sure how to do that.) 199 | 200 | ### Windows 201 | 202 | Here are the steps taken to build the Window EXE: 203 | 204 | 1. Download & Install Windows SDK: https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk/ 205 | 206 | 2. Download & Install Anaconda (64 bit): https://www.anaconda.com/ 207 | 208 | 3. Run Anaconda Powershell prompt and install requirements: 209 | 210 | ```bash 211 | conda install -c conda-forge ffmpeg 212 | conda install -c anaconda pyqt pywin32 pypiwin32 213 | conda install pyaudio librosa pywintypes 214 | ``` 215 | 216 | 5. Downgrade `setuptools` to avoid conflicts `pyinstaller` as per https://github.com/pypa/setuptools/issues/1963: 217 | `pip install --upgrade 'setuptools<45.0.0'` 218 | 219 | 6. Run `pyinstaller` with these arguments: 220 | 221 | ```bash 222 | pyinstaller -w --icon=shaart_logo_icon.ico --hidden-import="pypiwin32" --hidden-import="pywintypes" --hidden-import="sklearn.utils._cython_blas" --hidden-import="sklearn.neighbors._typedefs" --hidden-import="sklearn.neighbors.quad_tree" --hidden-import="sklearn.tree._utils" --onefile SHAART.py 223 | ``` 224 | 225 | 226 | 227 | ## Changing the GUI 228 | 229 | Run QT's `designer` or `Designer` app ([good luck finding this on your hard drive](https://stackoverflow.com/questions/37419138/is-qt-designer-bundled-with-anaconda), btw; instead you might want to just [download QT from the main site](https://www.qt.io/)). Open `ui_shaart.ui` as an input file. Change the GUI as you like, save it, and then to generate the .py file, run 230 | 231 | ```bash 232 | pyuic6 -x ui_shaart.ui -o ui_shaart.py 233 | ``` 234 | 235 | 236 | 237 | 238 | 239 | ## FAQ 240 | 241 | * You do realize what the name "SHAART" sounds like...? See "About" above. thatsthejoke.jpg 242 | * Can it only read WAV files? No. Despite saying WAV file everywhere, SHAART can read anything [librosa](https://librosa.github.io/librosa/) can read, which is...pretty much anything, e.g. WAV, AIFF, M4A,...? 243 | * Can I get a logarithmic frequency scale for the spectrogram? Not yet, but soon. 244 | * For waterfall plots, it doesn't clear the window if you change the input data, resulting in multiple plots on the same page. Bug or feature? 245 | * Does the "Record" feature work? Not yet. Use Audacity or....any other utility to record. ;-) 246 | * How do I contribute to SHAART? Submit a Pull Request! 247 | 248 | 249 | 250 | ## Release Notes / Issues 251 | * v0.81: 252 | * Fixed problem with checkboxes in PyQt6 253 | * Rebuilt Mac app: new `pyinstaller` file `SHAART.spec` uses "onefile" bundling which is *very* slow to launch app (~ 1 minute!) but is robust to Qt6/MacOS changes that broke some things. 254 | * v0.8: 255 | * Upgrades for execution on M1 Macs: 256 | * Upgraded from PyQt5 to PyQt6 257 | 258 | * v0.7: 259 | 260 | * Updated code from Python 2.7 to Python 3.7 261 | * Updated GUI from Qt4 to Qt5 262 | * Switched executable build from py2app to PyInstaller, added capability for Windows & Linux executable builds 263 | * Re-ordered feature panes 264 | 265 | * v0.6: Minor improvements to speed and reliability 266 | 267 | * v0.5: 268 | 269 | * Got its own App icon! 270 | 271 | * "Power": Improved power spectrum calculation and display. 272 | * "Equation": Added equation for inverse exp. sine sweep (with "depinking"). 273 | * Added IR creation tutorial (documentation) 274 | 275 | 276 | 277 | ## More docs & info 278 | 279 | Purpose: 280 | SHAART is intended as an in-house solution for teaching PHY2010 ("Physics for 281 | Audio Engineering Technology") at Belmont University. Perhaps others will find 282 | it useful as well. ...That and the PHY4410 ("Survey of Advanced Physics") 283 | students and I have been learing Python this semester to implement our 284 | simulations and analysis, so writing this also serves as an instructive 285 | exercise in Python programming. 286 | 287 | Nomenclature: 288 | The name is an acronym using the author's initials (S.H.), along with words 289 | like "Acoustic," "Analysis," "Reverberation Time" or "Research Tools" -- as well 290 | as, it is hoped, a lighthearted and not-legally-problematic play on words with 291 | the name of the industry-standard SMAART audio analysis software made by 292 | Rational Acoustics, Inc. (SHAART is in no way affiliated with SMAART or 293 | Rational Acoustics, fyi.) ...I mean, "SHAART" is just hilarious to say. 294 | 295 | Author: Dr. Scott H. Hawley, Professor of Physics, 296 | Belmont University, Nashville TN USA. 297 | 298 | Contact: Improvements, bug reports, inquiries, donations, etc.: scott.hawley@(belmont) 299 | 300 |
301 | Author: Scott Hawley 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /source/SHAART.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # used to parse files more easily 3 | from __future__ import with_statement 4 | from __future__ import print_function 5 | 6 | import numpy as np 7 | import os 8 | import sys 9 | 10 | if sys.platform == 'darwin': 11 | os.environ['QT_MAC_WANTS_LAYER'] = '1' 12 | # Force the dock icon to appear 13 | from PyQt6.QtGui import QIcon 14 | from PyQt6.QtCore import Qt 15 | 16 | # GUI bindings 17 | from PyQt6 import QtGui, QtCore, QtWidgets 18 | from PyQt6.QtWidgets import QMainWindow 19 | from PyQt6.QtCore import * 20 | 21 | # import the MainWindow widget from the converted .ui files 22 | from ui_shaart import Ui_TheMainWindow 23 | 24 | from scipy.fft import ifft 25 | import scipy.io.wavfile as wavfile 26 | import scipy.signal as signal 27 | 28 | #from scikits.audiolab import Sndfile 29 | #import librosa 30 | 31 | from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar 32 | import re 33 | from PIL import Image 34 | 35 | import pyaudio 36 | import time 37 | 38 | #----------------- for filtering signals ----------------- 39 | # from post by Warren Weckesser, http://tinyurl.com/d4cjs7m 40 | def butter_bandpass(lowcut, highcut, fs, order=5): 41 | nyq = 0.5 * fs 42 | low = lowcut / nyq 43 | high = highcut / nyq if highcut < nyq else 0.999 # avoid exceeding bounds 44 | b, a = signal.butter(order, [low, high], btype='band') 45 | return b, a 46 | 47 | def butter_bandpass_filter(data, lowcut, highcut, fs, order=5): 48 | if len(data) <= 1: return data # do nothing for no data 49 | b, a = butter_bandpass(lowcut, highcut, fs, order=order) 50 | y = signal.lfilter(b, a, data) 51 | return y 52 | #--------------------------------------------------------- 53 | 54 | # Just for debugging: 55 | def funcname(): 56 | import traceback 57 | return traceback.extract_stack(None, 2)[0][2] 58 | 59 | 60 | # Global variables (it's a GUI-based code, so...) 61 | amp = [1.0] 62 | orig_amp = amp 63 | t= [1.0] 64 | dB = [1.0] 65 | sample_rate = 44100 66 | nofile = 1 67 | nofileB = 1 68 | filenameA = "" 69 | ampB = amp 70 | orig_ampB = amp 71 | tB = t 72 | dB_B = dB 73 | sample_rateB = sample_rate 74 | filenameB = filenameA 75 | 76 | class DesignerMainWindow(QMainWindow, Ui_TheMainWindow): 77 | """Customization for Qt Designer created window""" 78 | def __init__(self, parent = None): 79 | # initialization of the superclass 80 | super(DesignerMainWindow, self).__init__(parent) 81 | if sys.platform == 'darwin': 82 | self.setWindowIcon(QtGui.QIcon('SHAART.icns')) 83 | # Ensure window flags are set correctly for macOS 84 | self.setWindowFlags(self.windowFlags() | Qt.WindowType.Window) 85 | # setup the GUI --> function generated by pyuic4 86 | self.setupUi(self) 87 | # connect the signals with the slots 88 | #QtCore.QObject.connect(self.actionChangeFeedbackController, QtCore.SIGNAL("triggered()"), self.changeFeedbackController) 89 | #self.actionChangeFeedbackController.triggered.connect(self.changeFeedbackController) 90 | self.theactionOpen.triggered.connect(self.menuselect_read_fileA) 91 | self.theactionOpenB.triggered.connect(self.menuselect_read_fileB) 92 | #self.theactionQuit.triggered.connect(QtGui.qApp, QtCore.SLOT("quit()")) 93 | self.theactionQuit.triggered.connect(self.close) 94 | 95 | self.theactionAbout.triggered.connect(self.about_message ) 96 | self.theactionSave.triggered.connect(self.write_wav_file) 97 | 98 | #QtCore.QObject.connect(self.waveform_abs_checkBox, QtCore.SIGNAL('stateChanged(int)'), self.update_tab) 99 | self.waveform_abs_checkBox.stateChanged.connect(self.update_tab) 100 | self.waveform_env_checkBox.stateChanged.connect(self.update_tab) 101 | 102 | self.inwavfileselectButton.clicked.connect(self.menuselect_read_fileA ) 103 | # QtCore.QObject.connect(self.rt60lineEdit, QtCore.SIGNAL('editingFinished()'), self.changedtext_read_fileA ) 104 | self.inwavfileBselectButton.clicked.connect(self.menuselect_read_fileB ) 105 | # QtCore.QObject.connect(self.fileBlineEdit, QtCore.SIGNAL('editingFinished()'), self.changedtext_read_fileB ) 106 | 107 | self.spectrocmcomboBox.currentIndexChanged.connect(self.update_tab) 108 | 109 | 110 | self.rt60comboBox.currentIndexChanged.connect(self.filter_signal) 111 | self.tabWidget.currentChanged.connect(self.update_tab ) 112 | self.speedlineEdit.editingFinished.connect(self.calc_modes ) 113 | self.lengthlineEdit.editingFinished.connect(self.calc_modes ) 114 | self.widthlineEdit.editingFinished.connect(self.calc_modes ) 115 | self.heightlineEdit.editingFinished.connect(self.calc_modes ) 116 | self.maxmodelineEdit.editingFinished.connect(self.calc_modes ) 117 | 118 | self.is_img_pushButton.clicked.connect(self.select_read_img_file) 119 | self.is_wav_pushButton.clicked.connect(self.select_write_wav_file) 120 | self.is_go_pushButton.clicked.connect(self.img_to_wav) 121 | 122 | self.eq_go_pushButton.clicked.connect(self.equation_go) 123 | self.convolve_go_pushButton.clicked.connect(self.convo_go) 124 | 125 | self.pushButton_playrec_go.clicked.connect(self.playrec_go) 126 | 127 | 128 | # writes a PCM 16 bit WAV file 129 | def write_wav_file(self): 130 | global sample_rate, orig_amp 131 | filename = QtWidgets.QFileDialog.getSaveFileName(self,"Filename to Save to","",'WAV File Name (*.wav)') 132 | if filename != "": 133 | filename = filename[0] # Qt5 dialog returns a tuple 134 | # write to file 135 | #-------------- 136 | wavfile.write(filename, sample_rate, amp) 137 | return 138 | 139 | # generic reader routine for audio files. 140 | def read_audio_file(self, file_name): 141 | from librosa import load 142 | if file_name=='': return 143 | 144 | y, samplerate = load(file_name, sr=None, dtype=np.float32) 145 | nsamples = len(y) 146 | if (len(y.shape) > 1): # take left channel of stereo track 147 | y = y[:,0] 148 | 149 | x = np.arange(nsamples)*1.0/samplerate # time values 150 | return y, x, samplerate 151 | 152 | def changedtext_read_fileA(self): 153 | global amp, filenameA, t, sample_rate, orig_amp 154 | filenameA = self.rt60lineEdit.text() 155 | if filenameA=='': return 156 | amp, t, sample_rate = self.read_audio_file(filenameA) 157 | orig_amp = amp 158 | global nofile 159 | nofile = 0 160 | self.update_tab() 161 | 162 | def menuselect_read_fileA(self): 163 | """opens a file select dialog""" 164 | # open the dialog and get the selected file 165 | file = QtWidgets.QFileDialog.getOpenFileName() 166 | # if a file is selected 167 | if file: 168 | file = file[0] # newer qt5 also returns list of file types, which we don't want 169 | # update the lineEdit text with the selected filename 170 | self.rt60lineEdit.setText(file) 171 | self.changedtext_read_fileA() 172 | filenameA = "%s" % file 173 | 174 | def changedtext_read_fileB(self): 175 | global ampB, filenameB, tB, sample_rateB, orig_ampB 176 | filenameB = self.fileBlineEdit.text() 177 | ampB, tB, sample_rateB = self.read_audio_file(filenameB) 178 | orig_ampB = ampB 179 | global nofileB 180 | nofileB = 0 181 | self.update_tab() 182 | 183 | def menuselect_read_fileB(self): 184 | """opens a file select dialog""" 185 | # open the dialog and get the selected file 186 | fileB = QtWidgets.QFileDialog.getOpenFileName() 187 | # if a file is selected 188 | if fileB: 189 | fileB = fileB[0] # newer qt5 also returns list of file types, which we don't want 190 | # update the lineEdit text with the selected filename 191 | self.fileBlineEdit.setText(fileB) 192 | self.changedtext_read_fileB() 193 | filenameB = "%s" % fileB 194 | 195 | 196 | def select_read_img_file(self): 197 | # open the dialog and get the selected file 198 | file = QtWidgets.QFileDialog.getOpenFileName() 199 | if file: self.is_imname_lineEdit.setText(file[0]) # update the lineEdit text with the selected filename 200 | 201 | def select_write_wav_file(self): 202 | # open the dialog and get the selected file 203 | file = QtWidgets.QFileDialog.getSaveFileName() 204 | if file: self.is_wavname_lineEdit.setText(file[0]) # update the lineEdit text with the selected filename 205 | 206 | #----------------------------------------------------- 207 | # In response to "Octave" comboBox trigger 208 | # Filters signal for use with rt60 measurements 209 | #----------------------------------------------------- 210 | def filter_signal(self): 211 | global amp, orig_amp, filenameA 212 | global ampB, orig_ampB, filenameB 213 | octave_text = str(self.rt60comboBox.currentText()) 214 | if (octave_text != 'All'): 215 | octave_center_freq = (float)(re.sub(r' Hz','',octave_text)) 216 | lowcut = 1.0*(int)(0.71 * octave_center_freq) 217 | highcut = 1.0*(int)(1.42 * octave_center_freq) 218 | amp = butter_bandpass_filter(orig_amp, lowcut, highcut, 1.0*sample_rate, order=3) 219 | if (len(orig_ampB) > 1): 220 | ampB = butter_bandpass_filter(orig_ampB, lowcut, highcut, 1.0*sample_rate, order=3) 221 | else: 222 | amp = orig_amp 223 | if (len(orig_ampB) > 1): 224 | ampB = orig_ampB 225 | self.rt60.linex = [0] # erase the old line 226 | self.rt60.liney = [0] # erase the old line 227 | self.rt60.update_graph(amp,t,filenameA,ampB,tB,filenameB) 228 | return 229 | 230 | 231 | 232 | def calc_modes(self): 233 | vs_text = str(self.speedlineEdit.text()) 234 | x_text = str(self.lengthlineEdit.text()) 235 | y_text = str(self.widthlineEdit.text()) 236 | z_text = str(self.heightlineEdit.text()) 237 | mm_text = str(self.maxmodelineEdit.text()) 238 | if (vs_text=="") | (x_text=="") | (y_text=="") or (z_text==""): return 239 | vs = float(vs_text) 240 | x = float(x_text) 241 | y = float(y_text) 242 | z = float(z_text) 243 | maxmodenum = int(mm_text) 244 | modes = [] 245 | #maxmodenum = 15 246 | outstring = "Freq (Hz) Nx Ny Nz\n--------- -- -- -- \n" 247 | # The following convoluted loops & if statement are simply to enforce a 248 | # particular ordering I am fond of for displaying mode numbers 249 | for modesum in np.arange(1,maxmodenum+1): 250 | for mm in np.arange(1,modesum+1): 251 | for i in np.arange(mm,-1,-1): 252 | for j in np.arange(mm,-1,-1): 253 | for k in np.arange(0,mm+1): 254 | if (i+j+k == modesum) & (i<= mm) & (j<=mm) & (k<=mm) & \ 255 | ((i==mm)| (j==mm) | (k==mm)): 256 | f = vs/2.0*np.sqrt( (1.0*i/x)**2 + (1.0*j/y)**2 + (1.0*k/z)**2) 257 | mode = [f, i, j, k] 258 | modes.append(mode) 259 | # or we can destroy that ordering by sorting by frequency 260 | modes.sort() 261 | for m in modes: 262 | thisline = '%9.1f %2d %2d %2d\n' % (m[0], m[1], m[2], m[3]) 263 | outstring = outstring + thisline 264 | self.modesTextEdit.setPlainText(outstring) 265 | self.modegraph.update_graph(modes,x,y,z) 266 | 267 | def tojas_isft(self, X, fs, T, hop): 268 | x = np.zeros(T*fs) 269 | framesamp = X.shape[1] 270 | hopsamp = int(hop*fs) 271 | for n,i in enumerate(range(0, len(x)-framesamp, hopsamp)): 272 | x[i:i+framesamp] += scipy.real(scipy.ifft(X[n])) 273 | return x 274 | 275 | 276 | def my_istft(self, X, fs, T): 277 | # Inverse Short-Time Fourier Transform, i.e. "Inverse Spectrogram" 278 | # Props to Steve Tjoa, cf. http://stackoverflow.com/questions/2459295/stft-and-istft-in-python 279 | # Added the overlapping frames / buffers to improve image quality - Scott Hawley 280 | # inputs: 281 | # X = image/ stft 282 | # fs = sample rate, in samples/sec 283 | # T = duration, in secs 284 | # 285 | # Output array will have this structure: 286 | # pixel pixel 287 | # ||----------|--------------------o------------------|-----------------------o-----------------|-----etc 288 | # <------------------hop----------------><----------------------hop----------------> 289 | # <--------------------hop------------------> 290 | # <---buf---> <---buf---> 291 | # <---------------------------- frame--------------------------> 292 | # <--buf---> <--buf---> 293 | # <---------------------------- frame--------------------------> 294 | #...and then the starting and ending buffers will be removed 295 | 296 | nhops = X.shape[1] # each pixel is a "hop" 297 | hop_duration = T / nhops # in secs 298 | samples_per_hop = int(hop_duration * fs) 299 | 300 | ibuf = 128 * fs / 44100 # buffer size, calibrated for 44.1 kHz 301 | tbuf = 1.0 * ibuf / fs # may seem redundant, but readable 302 | x = np.zeros((T+2*tbuf)*fs) 303 | 304 | # around each hop, we put a frame, centered on the hop, but wider by buf on each side 305 | samples_per_frame = samples_per_hop + 2*ibuf 306 | n = samples_per_frame 307 | 308 | for ihop in range(nhops): 309 | b = np.array(ifft(X[ihop], n = n)) # b is a 'vertical' set of pixels 310 | framedata = np.imag(b) # I actually find that imag() gives better image results than real() 311 | ibgn = ibuf + samples_per_hop/2 + ihop*samples_per_hop - samples_per_frame/2 312 | iend = ibgn + n-1 313 | x[ibgn:iend] += framedata[0:n-1] 314 | 315 | y = x[ibuf:-ibuf] # chop off the buffers 316 | return y 317 | 318 | def ekman_istft(self, X, fs, T, minfreq, maxfreq): 319 | # This is based on Coagula by Rasmus Ekman: https://www.abc.se/~re/Coagula/Coagula.html 320 | # In this method, we don't actually take an inverse STFT. 321 | # Ekman: "How: Coagula uses one sinewave (beep) per image line, one short blip per point (pixel) on the line. " 322 | # X is the image, i.e. the STFT of the time series data to be produced (called 'x', below) 323 | # fs = sample rate, in samples/sec 324 | # T = duration, in secs 325 | # minfreq, maxfreq = min & max frequency (in Hz) in which to map image ("vertically") 326 | 327 | nhops = X.shape[0] # each pixel is a "hop" 328 | hop_duration = T / nhops # in secs 329 | samples_per_hop = int(hop_duration * fs) 330 | nfreq = X.shape[1] 331 | dfreq = (maxfreq - minfreq) / nfreq 332 | 333 | print('ekman: nhops, nfreq, dfreq = ',nhops, nfreq, dfreq) 334 | 335 | ibuf = 0 # no buffer 336 | tbuf = 1.0 * ibuf / fs # may seem redundant, but readable 337 | x = np.zeros( int( (T+2*tbuf)*fs) ) # x is the time series data 338 | for ihop in range(nhops): 339 | for ifreq in range(nfreq): #scan vertically upwards 340 | freq = minfreq + ifreq * dfreq 341 | intensity = X[ihop,ifreq] 342 | phase = 0.0 343 | framedata = intensity * np.sin( 2*3.14159*freq * np.arange(0,hop_duration,hop_duration/samples_per_hop) + phase ) 344 | 345 | ibgn = ibuf + ihop*samples_per_hop 346 | iend = ibgn + samples_per_hop 347 | x[ibgn:iend] += framedata[0:iend-ibgn] 348 | 349 | 350 | y = x 351 | return y 352 | 353 | 354 | def img_to_wav(self): 355 | im_filename = str( self.is_imname_lineEdit.text() ) 356 | wav_filename = str( self.is_wavname_lineEdit.text() ) 357 | dur_str = str( self.is_duration_lineEdit.text() ) 358 | rate_str = str( self.is_rate_lineEdit.text() ) 359 | minf_str = str( self.is_minf_lineEdit.text() ) 360 | maxf_str = str( self.is_maxf_lineEdit.text() ) 361 | 362 | if (im_filename=="") | (wav_filename=="") | (dur_str=="") | (rate_str=="") |(minf_str=="") | (maxf_str=="") : return 363 | 364 | rate = int( rate_str ) 365 | image_duration = float( dur_str ) 366 | minfreq = float(minf_str) 367 | maxfreq = float(maxf_str) 368 | 369 | pic = Image.open(im_filename).convert("LA") 370 | # pic = pic.resize((512, 512), Image.ANTIALIAS) # my_istft works 'best' with 512x512... 371 | image = np.array(pic.getdata()) 372 | image = np.array(image[:,0]).reshape(pic.size[1], pic.size[0]) 373 | 374 | 375 | # Construct the signal. Put the image into X, transpose & flip, take its inverse stft 376 | #--------------------- 377 | X = 1.0*np.array(image) # floating point values 378 | 379 | GL, mel_scale = False, False # use Griffin-Lim and mel scale? 380 | if (GL): # use Griffin-Lim 381 | import soundfile as sf # for librosa 382 | from librosa.feature.inverse import mel_to_stft 383 | from librosa import griffinlim 384 | 385 | n_fft = 2048 386 | X = X[::-1,:] # is upside down for librosa 387 | if mel_scale: 388 | self.is_status_label.setText("Converting Mel Scale to STFT (slow)...") 389 | app.processEvents() 390 | X = mel_to_stft(X, sr=rate, n_fft=n_fft) 391 | self.is_status_label.setText('Starting Griffin-Lim iteration...') 392 | app.processEvents() 393 | data = griffinlim(X) 394 | sf.write(wav_filename,data,rate) 395 | else: # "my" orig method glitchy but cleaner spectra 396 | contrast_power = 1.5 # higher = more contrast, but also introduces more distortion 397 | X = (X)**contrast_power 398 | X = X.T # transpose 399 | X = np.fliplr(X) # flip 400 | self.is_status_label.setText("Starting ISTFT calculation...") 401 | app.processEvents() 402 | mysignal = self.ekman_istft(X, rate, image_duration,minfreq,maxfreq) # conversion routine 403 | 404 | # normalize & convert the signal to int 405 | #------------------------------------------- 406 | maxval = np.max(mysignal) 407 | mysignal = np.array([1.0*x / maxval for x in mysignal]) 408 | 409 | iscale = 32767 # This sets the overall volume, 32727 = full scale 410 | 411 | # In the following line, the dtype='i2' selects 16-bit; without it you get 64 bits & no audio 412 | data = np.array([int(iscale * x) for x in mysignal], dtype='i2') 413 | 414 | # write to file 415 | #-------------- 416 | wavfile.write(wav_filename, rate, data) 417 | 418 | 419 | self.is_status_label.setText("Finished! Try opening the WAV file in the Spectrogram!") 420 | 421 | return 422 | 423 | 424 | 425 | #--------------------------------------------------- 426 | # Code for generating sound based on equation 427 | #--------------------------------------------------- 428 | def equation_go(self): 429 | global amp, orig_amp, nofile, filenameA, t, sample_rate 430 | dur_str = str( self.eqnduration_lineEdit.text() ) 431 | rate_str = str( self.eqnsr_lineEdit.text() ) 432 | eq_str = str( self.equation_lineEdit.text() ) 433 | 434 | # basic definitions 435 | TMAX = float( dur_str ) 436 | SR = int( rate_str ) 437 | SRm1 = 1.0/SR 438 | sample_rate = SR 439 | NS = int( TMAX * sample_rate ) 440 | print('TMAX, SR, NS, SRm1 = ',TMAX, SR, NS, SRm1) 441 | 442 | #allocate storage 443 | amp = np.zeros(NS,dtype=np.float64) 444 | 445 | #'parse' the equation string 446 | eq_str = re.sub('sin','np.sin',eq_str) 447 | eq_str = re.sub('cos','np.cos',eq_str) 448 | eq_str = re.sub('tan','np.tan',eq_str) 449 | eq_str = re.sub('arcnp.','np.arc',eq_str) 450 | eq_str = re.sub('sqrt','np.sqrt',eq_str) 451 | eq_str = re.sub('ln','np.log',eq_str) 452 | eq_str = re.sub('log10','np.log10',eq_str) 453 | eq_str = re.sub('abs','np.abs',eq_str) 454 | eq_str = re.sub('exp','np.exp',eq_str) 455 | eq_str = re.sub('PI','3.14159265358979323846',eq_str) 456 | eq_str = re.sub('np.np.','np.',eq_str) 457 | 458 | # now create the sounds 459 | t = np.arange(0,TMAX, SRm1) 460 | newstr = "amp[0:t.shape[0]] = " + eq_str 461 | print('Executing newstr = [', newstr, ']') 462 | exec(newstr) 463 | 464 | print('Finished loop') 465 | 466 | # Global settings for commucating with the rest of the program 467 | orig_amp = amp 468 | t = np.arange(0.0,TMAX,SRm1) 469 | nofile = 0 470 | filenameA = eq_str 471 | return 472 | 473 | # My own little autocorrelation routine 474 | def my_autocorr(self,x): 475 | meanx = np.mean(x) 476 | x -= meanx 477 | result = np.correlate(x, x, mode='full') 478 | maxval = np.max(result); 479 | result = result / maxval; 480 | return result 481 | 482 | #--------------------------------------------------- 483 | # Convolution 484 | #--------------------------------------------------- 485 | def convo_go(self): 486 | global orig_amp, sample_rate, amp, t, filenameA 487 | global orig_ampB, sample_rateB, ampB, tB, filenameB 488 | global nofile, nofileB 489 | 490 | if ((1==nofileB) and (False == self.checkBox_autocorr.isChecked())): 491 | print("convo_go: Unable to perform convolution") 492 | return 493 | 494 | if self.checkBox_timerev.isChecked(): 495 | amp3 = amp[::-1] 496 | amp = amp3 497 | 498 | if self.checkBox_autocorr.isChecked(): # autocorrelation 499 | print("Debug: autocorrelation is checked") 500 | amp3 = self.my_autocorr(amp) 501 | elif (0 == nofileB): # normal convolution 502 | print("Debug: autocorrelation is NOT checked") 503 | amp3 = signal.fftconvolve( amp, ampB, mode="same") 504 | 505 | maxval_3 = np.max(amp3) 506 | amp3 *= 0.9999999 / maxval_3 507 | 508 | if self.checkBox_removefirsthalf.isChecked(): 509 | print("Debug: remove first half is checked") 510 | amp = amp3[amp3.size/2:] # cut off first half of array 511 | else: 512 | print("Debug: remove first half is NOT checked") 513 | amp = amp3 514 | 515 | orig_amp = amp 516 | t = np.arange(0.0,amp.size,1.0)/sample_rate; 517 | amp3 = 0 518 | 519 | nofileB = 1 # wipe out fileB 520 | ampB = [1.0] 521 | tB = [1.0] 522 | filenameB = "" 523 | return 524 | 525 | 526 | #--------------------------------------------------------------------- 527 | # Tab for playing & recording audio 528 | #--------------------------------------------------------------------- 529 | def playrec_go(self): 530 | global amp, sample_rate 531 | global current_index, maxi 532 | import pyaudio # lazy loading 533 | 534 | current_index = 0 535 | maxi = len(amp)-1 536 | 537 | 538 | # pull a frame_count samples from array "amp" 539 | def get_chunk(frame_count): 540 | global current_index 541 | 542 | if ( current_index < maxi): 543 | iend = np.min( [current_index + frame_count-1, maxi ]) 544 | chunk = amp[current_index: maxi] 545 | out_data = chunk.astype(np.float32).tostring() 546 | current_index += frame_count 547 | else: 548 | out_data = [] 549 | return out_data 550 | 551 | 552 | # define callback 553 | def callback(in_data, frame_count, time_info, status): 554 | out_data = get_chunk(frame_count) 555 | return ( out_data , pyaudio.paContinue) 556 | 557 | 558 | 559 | if nofile: return 560 | 561 | # instantiate PyAudio 562 | p = pyaudio.PyAudio() 563 | 564 | # open stream using callback 565 | stream = p.open( 566 | format=pyaudio.paFloat32, 567 | channels=1, 568 | rate=sample_rate, 569 | output=True, 570 | stream_callback=callback) 571 | 572 | #start the stream 573 | stream.start_stream() 574 | 575 | # wait for the stream to finish 576 | while stream.is_active() and (current_index < len(amp)-1) : 577 | time.sleep(0.1) 578 | 579 | #stop stream 580 | stream.stop_stream() 581 | stream.close() 582 | 583 | #close PyAudio 584 | p.terminate() 585 | print("leaving playrec_go") 586 | return 587 | 588 | 589 | 590 | #---------------------------------------------------------------- 591 | # Generic routine for updating whichever tab is currently showing 592 | #---------------------------------------------------------------- 593 | def update_tab(self): 594 | global orig_amp, sample_rate, amp, t, filenameA 595 | global orig_ampB, sample_rateB, ampB, tB, filenameB 596 | tabnum = self.tabWidget.currentIndex() 597 | if (0 == tabnum): # rt60 598 | if nofile: return 599 | self.rt60.update_graph(amp,t,filenameA,ampB,tB,filenameB) 600 | elif (1 == tabnum): # waveform 601 | self.waveform.update_graph(amp,t,filenameA,ampB,tB,filenameB, 602 | self.waveform_abs_checkBox.isChecked(),self.waveform_env_checkBox.isChecked()) 603 | if nofile: return 604 | elif (2 == tabnum): # power spectrum 605 | if nofile: return 606 | self.pwrspec.update_graph(amp,sample_rate,filenameA,ampB,sample_rateB,filenameB) 607 | elif (3 == tabnum): # spectrogram 608 | if nofile: return 609 | self.spectro.update_graph(amp,sample_rate,self.spectrocmcomboBox.currentIndex()) 610 | elif (4 == tabnum): # wateverfall 611 | if nofile: return 612 | self.water.update_graph(amp,sample_rate) 613 | elif (5 == tabnum): # invSpectro 614 | self.img_to_wav() 615 | return 616 | elif (6 == tabnum): # Room Modes 617 | self.calc_modes() 618 | elif (7 == tabnum): # Sabine Eq 619 | return 620 | elif (8 == tabnum): #equation 621 | # self.equation_go() only run equation_go when the go button is pushed 622 | return 623 | elif (9 == tabnum): # convolve 624 | if nofile: return 625 | # self.convo_make(); only run convo_go when the go button is pushed 626 | elif (10 == tabnum): # play/rec 627 | if nofile: return 628 | else: 629 | print("ERROR: tabnum =",tabnum,"is unsupported.") 630 | 631 | 632 | def about_message(self): 633 | msg = """ 634 | SHAART v.0.81 635 | http://hedges.belmont.edu/~shawley/SHAART 636 | 637 | A simple audio analysis suite intended for 638 | educational purposes, student projects, etc. 639 | 640 | Scott H. Hawley, Ph.D. 641 | Belmont University 642 | scott.hawley@belmont.edu 643 | """ 644 | QtWidgets.QMessageBox.about(self, "About SHAART", msg.strip()) 645 | 646 | return 647 | 648 | if __name__ == '__main__': 649 | # create the GUI application 650 | app = QtWidgets.QApplication(sys.argv) 651 | app.setApplicationName("SHAART") 652 | app.setWindowIcon(QtGui.QIcon('SHAART.icns')) 653 | 654 | # Force application to register with macOS 655 | if sys.platform == 'darwin': 656 | app.setAttribute(QtCore.Qt.ApplicationAttribute.AA_DontUseNativeMenuBar, False) 657 | 658 | # instantiate the main window 659 | dmw = DesignerMainWindow() 660 | # show it 661 | dmw.show() 662 | # start the Qt main loop execution, exiting from this script 663 | # with the same return code of Qt application 664 | sys.exit(app.exec()) 665 | 666 | -------------------------------------------------------------------------------- /source/ui_shaart.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'ui_shaart.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.4.2 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_TheMainWindow(object): 13 | def setupUi(self, TheMainWindow): 14 | TheMainWindow.setObjectName("TheMainWindow") 15 | TheMainWindow.resize(1105, 724) 16 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 17 | sizePolicy.setHorizontalStretch(0) 18 | sizePolicy.setVerticalStretch(0) 19 | sizePolicy.setHeightForWidth(TheMainWindow.sizePolicy().hasHeightForWidth()) 20 | TheMainWindow.setSizePolicy(sizePolicy) 21 | icon = QtGui.QIcon() 22 | icon.addPixmap(QtGui.QPixmap("../../../.designer/backup/shaart_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) 23 | TheMainWindow.setWindowIcon(icon) 24 | self.thecentralwidget = QtWidgets.QWidget(parent=TheMainWindow) 25 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 26 | sizePolicy.setHorizontalStretch(0) 27 | sizePolicy.setVerticalStretch(0) 28 | sizePolicy.setHeightForWidth(self.thecentralwidget.sizePolicy().hasHeightForWidth()) 29 | self.thecentralwidget.setSizePolicy(sizePolicy) 30 | self.thecentralwidget.setObjectName("thecentralwidget") 31 | self.verticalLayout = QtWidgets.QVBoxLayout(self.thecentralwidget) 32 | self.verticalLayout.setObjectName("verticalLayout") 33 | self.horizontalLayout = QtWidgets.QHBoxLayout() 34 | self.horizontalLayout.setObjectName("horizontalLayout") 35 | self.tabWidget = QtWidgets.QTabWidget(parent=self.thecentralwidget) 36 | self.tabWidget.setEnabled(True) 37 | font = QtGui.QFont() 38 | font.setItalic(False) 39 | self.tabWidget.setFont(font) 40 | self.tabWidget.setTabShape(QtWidgets.QTabWidget.TabShape.Rounded) 41 | self.tabWidget.setObjectName("tabWidget") 42 | self.tabrt60 = QtWidgets.QWidget() 43 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 44 | sizePolicy.setHorizontalStretch(0) 45 | sizePolicy.setVerticalStretch(0) 46 | sizePolicy.setHeightForWidth(self.tabrt60.sizePolicy().hasHeightForWidth()) 47 | self.tabrt60.setSizePolicy(sizePolicy) 48 | self.tabrt60.setObjectName("tabrt60") 49 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tabrt60) 50 | self.verticalLayout_2.setObjectName("verticalLayout_2") 51 | self.rt60horizontalLayout = QtWidgets.QHBoxLayout() 52 | self.rt60horizontalLayout.setObjectName("rt60horizontalLayout") 53 | self.inwavfileselectButton = QtWidgets.QPushButton(parent=self.tabrt60) 54 | self.inwavfileselectButton.setObjectName("inwavfileselectButton") 55 | self.rt60horizontalLayout.addWidget(self.inwavfileselectButton) 56 | self.rt60lineEdit = QtWidgets.QLineEdit(parent=self.tabrt60) 57 | self.rt60lineEdit.setEnabled(False) 58 | self.rt60lineEdit.setReadOnly(True) 59 | self.rt60lineEdit.setObjectName("rt60lineEdit") 60 | self.rt60horizontalLayout.addWidget(self.rt60lineEdit) 61 | self.inwavfileBselectButton = QtWidgets.QPushButton(parent=self.tabrt60) 62 | self.inwavfileBselectButton.setObjectName("inwavfileBselectButton") 63 | self.rt60horizontalLayout.addWidget(self.inwavfileBselectButton) 64 | self.fileBlineEdit = QtWidgets.QLineEdit(parent=self.tabrt60) 65 | self.fileBlineEdit.setReadOnly(True) 66 | self.fileBlineEdit.setObjectName("fileBlineEdit") 67 | self.rt60horizontalLayout.addWidget(self.fileBlineEdit) 68 | self.rt60octavelabel = QtWidgets.QLabel(parent=self.tabrt60) 69 | self.rt60octavelabel.setObjectName("rt60octavelabel") 70 | self.rt60horizontalLayout.addWidget(self.rt60octavelabel) 71 | self.rt60comboBox = QtWidgets.QComboBox(parent=self.tabrt60) 72 | self.rt60comboBox.setObjectName("rt60comboBox") 73 | self.rt60comboBox.addItem("") 74 | self.rt60comboBox.addItem("") 75 | self.rt60comboBox.addItem("") 76 | self.rt60comboBox.addItem("") 77 | self.rt60comboBox.addItem("") 78 | self.rt60comboBox.addItem("") 79 | self.rt60comboBox.addItem("") 80 | self.rt60comboBox.addItem("") 81 | self.rt60comboBox.addItem("") 82 | self.rt60horizontalLayout.addWidget(self.rt60comboBox) 83 | self.verticalLayout_2.addLayout(self.rt60horizontalLayout) 84 | self.rt60 = Rt60Widget(parent=self.tabrt60) 85 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 86 | sizePolicy.setHorizontalStretch(0) 87 | sizePolicy.setVerticalStretch(0) 88 | sizePolicy.setHeightForWidth(self.rt60.sizePolicy().hasHeightForWidth()) 89 | self.rt60.setSizePolicy(sizePolicy) 90 | self.rt60.setObjectName("rt60") 91 | self.verticalLayout_2.addWidget(self.rt60) 92 | self.tabWidget.addTab(self.tabrt60, "") 93 | self.tabWaveform = QtWidgets.QWidget() 94 | self.tabWaveform.setObjectName("tabWaveform") 95 | self.verticalLayout_38 = QtWidgets.QVBoxLayout(self.tabWaveform) 96 | self.verticalLayout_38.setObjectName("verticalLayout_38") 97 | self.horizontalLayout_83 = QtWidgets.QHBoxLayout() 98 | self.horizontalLayout_83.setObjectName("horizontalLayout_83") 99 | self.waveform_abs_checkBox = QtWidgets.QCheckBox(parent=self.tabWaveform) 100 | self.waveform_abs_checkBox.setObjectName("waveform_abs_checkBox") 101 | self.horizontalLayout_83.addWidget(self.waveform_abs_checkBox) 102 | self.waveform_env_checkBox = QtWidgets.QCheckBox(parent=self.tabWaveform) 103 | self.waveform_env_checkBox.setObjectName("waveform_env_checkBox") 104 | self.horizontalLayout_83.addWidget(self.waveform_env_checkBox) 105 | self.verticalLayout_38.addLayout(self.horizontalLayout_83) 106 | self.waveform = WaveformWidget(parent=self.tabWaveform) 107 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 108 | sizePolicy.setHorizontalStretch(0) 109 | sizePolicy.setVerticalStretch(0) 110 | sizePolicy.setHeightForWidth(self.waveform.sizePolicy().hasHeightForWidth()) 111 | self.waveform.setSizePolicy(sizePolicy) 112 | self.waveform.setObjectName("waveform") 113 | self.verticalLayout_38.addWidget(self.waveform) 114 | self.tabWidget.addTab(self.tabWaveform, "") 115 | self.tabpower = QtWidgets.QWidget() 116 | self.tabpower.setObjectName("tabpower") 117 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tabpower) 118 | self.verticalLayout_3.setObjectName("verticalLayout_3") 119 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 120 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 121 | self.pwrspec = PwrSpecWidget(parent=self.tabpower) 122 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 123 | sizePolicy.setHorizontalStretch(0) 124 | sizePolicy.setVerticalStretch(0) 125 | sizePolicy.setHeightForWidth(self.pwrspec.sizePolicy().hasHeightForWidth()) 126 | self.pwrspec.setSizePolicy(sizePolicy) 127 | self.pwrspec.setObjectName("pwrspec") 128 | self.horizontalLayout_2.addWidget(self.pwrspec) 129 | self.verticalLayout_3.addLayout(self.horizontalLayout_2) 130 | self.tabWidget.addTab(self.tabpower, "") 131 | self.tabspectro = QtWidgets.QWidget() 132 | self.tabspectro.setObjectName("tabspectro") 133 | self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tabspectro) 134 | self.verticalLayout_4.setObjectName("verticalLayout_4") 135 | self.horizontalLayout_21 = QtWidgets.QHBoxLayout() 136 | self.horizontalLayout_21.setObjectName("horizontalLayout_21") 137 | self.label_5 = QtWidgets.QLabel(parent=self.tabspectro) 138 | self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 139 | self.label_5.setObjectName("label_5") 140 | self.horizontalLayout_21.addWidget(self.label_5) 141 | self.spectrocmcomboBox = QtWidgets.QComboBox(parent=self.tabspectro) 142 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) 143 | sizePolicy.setHorizontalStretch(0) 144 | sizePolicy.setVerticalStretch(0) 145 | sizePolicy.setHeightForWidth(self.spectrocmcomboBox.sizePolicy().hasHeightForWidth()) 146 | self.spectrocmcomboBox.setSizePolicy(sizePolicy) 147 | self.spectrocmcomboBox.setObjectName("spectrocmcomboBox") 148 | self.spectrocmcomboBox.addItem("") 149 | self.spectrocmcomboBox.addItem("") 150 | self.spectrocmcomboBox.addItem("") 151 | self.spectrocmcomboBox.addItem("") 152 | self.spectrocmcomboBox.addItem("") 153 | self.spectrocmcomboBox.addItem("") 154 | self.horizontalLayout_21.addWidget(self.spectrocmcomboBox) 155 | self.verticalLayout_4.addLayout(self.horizontalLayout_21) 156 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout() 157 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 158 | self.spectro = SpectroWidget(parent=self.tabspectro) 159 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding) 160 | sizePolicy.setHorizontalStretch(0) 161 | sizePolicy.setVerticalStretch(0) 162 | sizePolicy.setHeightForWidth(self.spectro.sizePolicy().hasHeightForWidth()) 163 | self.spectro.setSizePolicy(sizePolicy) 164 | self.spectro.setObjectName("spectro") 165 | self.horizontalLayout_3.addWidget(self.spectro) 166 | self.verticalLayout_4.addLayout(self.horizontalLayout_3) 167 | self.tabWidget.addTab(self.tabspectro, "") 168 | self.tabwaterfall = QtWidgets.QWidget() 169 | self.tabwaterfall.setObjectName("tabwaterfall") 170 | self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.tabwaterfall) 171 | self.verticalLayout_6.setObjectName("verticalLayout_6") 172 | self.verticalLayout_10 = QtWidgets.QVBoxLayout() 173 | self.verticalLayout_10.setObjectName("verticalLayout_10") 174 | self.label_4 = QtWidgets.QLabel(parent=self.tabwaterfall) 175 | self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 176 | self.label_4.setObjectName("label_4") 177 | self.verticalLayout_10.addWidget(self.label_4) 178 | self.water = WaterWidget(parent=self.tabwaterfall) 179 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 180 | sizePolicy.setHorizontalStretch(0) 181 | sizePolicy.setVerticalStretch(0) 182 | sizePolicy.setHeightForWidth(self.water.sizePolicy().hasHeightForWidth()) 183 | self.water.setSizePolicy(sizePolicy) 184 | self.water.setObjectName("water") 185 | self.verticalLayout_10.addWidget(self.water) 186 | self.verticalLayout_6.addLayout(self.verticalLayout_10) 187 | self.tabWidget.addTab(self.tabwaterfall, "") 188 | self.tabinvspectro = QtWidgets.QWidget() 189 | self.tabinvspectro.setObjectName("tabinvspectro") 190 | self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.tabinvspectro) 191 | self.verticalLayout_7.setObjectName("verticalLayout_7") 192 | self.label_21 = QtWidgets.QLabel(parent=self.tabinvspectro) 193 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Minimum) 194 | sizePolicy.setHorizontalStretch(0) 195 | sizePolicy.setVerticalStretch(0) 196 | sizePolicy.setHeightForWidth(self.label_21.sizePolicy().hasHeightForWidth()) 197 | self.label_21.setSizePolicy(sizePolicy) 198 | self.label_21.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 199 | self.label_21.setObjectName("label_21") 200 | self.verticalLayout_7.addWidget(self.label_21) 201 | self.horizontalLayout_6 = QtWidgets.QHBoxLayout() 202 | self.horizontalLayout_6.setObjectName("horizontalLayout_6") 203 | self.is_img_pushButton = QtWidgets.QPushButton(parent=self.tabinvspectro) 204 | self.is_img_pushButton.setObjectName("is_img_pushButton") 205 | self.horizontalLayout_6.addWidget(self.is_img_pushButton) 206 | self.is_imname_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 207 | self.is_imname_lineEdit.setObjectName("is_imname_lineEdit") 208 | self.horizontalLayout_6.addWidget(self.is_imname_lineEdit) 209 | self.verticalLayout_7.addLayout(self.horizontalLayout_6) 210 | self.horizontalLayout_18 = QtWidgets.QHBoxLayout() 211 | self.horizontalLayout_18.setObjectName("horizontalLayout_18") 212 | self.label_22 = QtWidgets.QLabel(parent=self.tabinvspectro) 213 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Preferred) 214 | sizePolicy.setHorizontalStretch(0) 215 | sizePolicy.setVerticalStretch(0) 216 | sizePolicy.setHeightForWidth(self.label_22.sizePolicy().hasHeightForWidth()) 217 | self.label_22.setSizePolicy(sizePolicy) 218 | self.label_22.setObjectName("label_22") 219 | self.horizontalLayout_18.addWidget(self.label_22) 220 | self.is_duration_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 221 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) 222 | sizePolicy.setHorizontalStretch(0) 223 | sizePolicy.setVerticalStretch(0) 224 | sizePolicy.setHeightForWidth(self.is_duration_lineEdit.sizePolicy().hasHeightForWidth()) 225 | self.is_duration_lineEdit.setSizePolicy(sizePolicy) 226 | self.is_duration_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 227 | self.is_duration_lineEdit.setObjectName("is_duration_lineEdit") 228 | self.horizontalLayout_18.addWidget(self.is_duration_lineEdit) 229 | self.label_23 = QtWidgets.QLabel(parent=self.tabinvspectro) 230 | self.label_23.setObjectName("label_23") 231 | self.horizontalLayout_18.addWidget(self.label_23) 232 | self.is_rate_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 233 | self.is_rate_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 234 | self.is_rate_lineEdit.setObjectName("is_rate_lineEdit") 235 | self.horizontalLayout_18.addWidget(self.is_rate_lineEdit) 236 | self.label_9 = QtWidgets.QLabel(parent=self.tabinvspectro) 237 | self.label_9.setObjectName("label_9") 238 | self.horizontalLayout_18.addWidget(self.label_9) 239 | self.is_minf_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 240 | self.is_minf_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 241 | self.is_minf_lineEdit.setObjectName("is_minf_lineEdit") 242 | self.horizontalLayout_18.addWidget(self.is_minf_lineEdit) 243 | self.label_10 = QtWidgets.QLabel(parent=self.tabinvspectro) 244 | self.label_10.setObjectName("label_10") 245 | self.horizontalLayout_18.addWidget(self.label_10) 246 | self.is_maxf_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 247 | self.is_maxf_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 248 | self.is_maxf_lineEdit.setObjectName("is_maxf_lineEdit") 249 | self.horizontalLayout_18.addWidget(self.is_maxf_lineEdit) 250 | self.verticalLayout_7.addLayout(self.horizontalLayout_18) 251 | self.horizontalLayout_10 = QtWidgets.QHBoxLayout() 252 | self.horizontalLayout_10.setObjectName("horizontalLayout_10") 253 | self.is_wav_pushButton = QtWidgets.QPushButton(parent=self.tabinvspectro) 254 | self.is_wav_pushButton.setObjectName("is_wav_pushButton") 255 | self.horizontalLayout_10.addWidget(self.is_wav_pushButton) 256 | self.is_wavname_lineEdit = QtWidgets.QLineEdit(parent=self.tabinvspectro) 257 | self.is_wavname_lineEdit.setObjectName("is_wavname_lineEdit") 258 | self.horizontalLayout_10.addWidget(self.is_wavname_lineEdit) 259 | self.verticalLayout_7.addLayout(self.horizontalLayout_10) 260 | self.horizontalLayout_19 = QtWidgets.QHBoxLayout() 261 | self.horizontalLayout_19.setObjectName("horizontalLayout_19") 262 | self.is_go_pushButton = QtWidgets.QPushButton(parent=self.tabinvspectro) 263 | self.is_go_pushButton.setObjectName("is_go_pushButton") 264 | self.horizontalLayout_19.addWidget(self.is_go_pushButton) 265 | self.verticalLayout_7.addLayout(self.horizontalLayout_19) 266 | self.horizontalLayout_20 = QtWidgets.QHBoxLayout() 267 | self.horizontalLayout_20.setObjectName("horizontalLayout_20") 268 | self.is_status_label = QtWidgets.QLabel(parent=self.tabinvspectro) 269 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding) 270 | sizePolicy.setHorizontalStretch(0) 271 | sizePolicy.setVerticalStretch(0) 272 | sizePolicy.setHeightForWidth(self.is_status_label.sizePolicy().hasHeightForWidth()) 273 | self.is_status_label.setSizePolicy(sizePolicy) 274 | self.is_status_label.setText("") 275 | self.is_status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 276 | self.is_status_label.setObjectName("is_status_label") 277 | self.horizontalLayout_20.addWidget(self.is_status_label) 278 | self.verticalLayout_7.addLayout(self.horizontalLayout_20) 279 | self.tabWidget.addTab(self.tabinvspectro, "") 280 | self.tabmodecalc = QtWidgets.QWidget() 281 | self.tabmodecalc.setObjectName("tabmodecalc") 282 | self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.tabmodecalc) 283 | self.verticalLayout_8.setObjectName("verticalLayout_8") 284 | self.horizontalLayout_7 = QtWidgets.QHBoxLayout() 285 | self.horizontalLayout_7.setObjectName("horizontalLayout_7") 286 | self.label = QtWidgets.QLabel(parent=self.tabmodecalc) 287 | self.label.setObjectName("label") 288 | self.horizontalLayout_7.addWidget(self.label) 289 | self.speedlineEdit = QtWidgets.QLineEdit(parent=self.tabmodecalc) 290 | self.speedlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 291 | self.speedlineEdit.setObjectName("speedlineEdit") 292 | self.horizontalLayout_7.addWidget(self.speedlineEdit) 293 | self.label_2 = QtWidgets.QLabel(parent=self.tabmodecalc) 294 | self.label_2.setObjectName("label_2") 295 | self.horizontalLayout_7.addWidget(self.label_2) 296 | self.lengthlineEdit = QtWidgets.QLineEdit(parent=self.tabmodecalc) 297 | self.lengthlineEdit.setText("") 298 | self.lengthlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 299 | self.lengthlineEdit.setObjectName("lengthlineEdit") 300 | self.horizontalLayout_7.addWidget(self.lengthlineEdit) 301 | self.label_7 = QtWidgets.QLabel(parent=self.tabmodecalc) 302 | self.label_7.setObjectName("label_7") 303 | self.horizontalLayout_7.addWidget(self.label_7) 304 | self.widthlineEdit = QtWidgets.QLineEdit(parent=self.tabmodecalc) 305 | self.widthlineEdit.setText("") 306 | self.widthlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 307 | self.widthlineEdit.setObjectName("widthlineEdit") 308 | self.horizontalLayout_7.addWidget(self.widthlineEdit) 309 | self.label_8 = QtWidgets.QLabel(parent=self.tabmodecalc) 310 | self.label_8.setObjectName("label_8") 311 | self.horizontalLayout_7.addWidget(self.label_8) 312 | self.heightlineEdit = QtWidgets.QLineEdit(parent=self.tabmodecalc) 313 | self.heightlineEdit.setText("") 314 | self.heightlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 315 | self.heightlineEdit.setObjectName("heightlineEdit") 316 | self.horizontalLayout_7.addWidget(self.heightlineEdit) 317 | self.label_6 = QtWidgets.QLabel(parent=self.tabmodecalc) 318 | self.label_6.setObjectName("label_6") 319 | self.horizontalLayout_7.addWidget(self.label_6) 320 | self.maxmodelineEdit = QtWidgets.QLineEdit(parent=self.tabmodecalc) 321 | self.maxmodelineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 322 | self.maxmodelineEdit.setObjectName("maxmodelineEdit") 323 | self.horizontalLayout_7.addWidget(self.maxmodelineEdit) 324 | self.verticalLayout_8.addLayout(self.horizontalLayout_7) 325 | self.horizontalLayout_5 = QtWidgets.QHBoxLayout() 326 | self.horizontalLayout_5.setObjectName("horizontalLayout_5") 327 | self.modesTextEdit = QtWidgets.QPlainTextEdit(parent=self.tabmodecalc) 328 | self.modesTextEdit.setEnabled(True) 329 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) 330 | sizePolicy.setHorizontalStretch(0) 331 | sizePolicy.setVerticalStretch(0) 332 | sizePolicy.setHeightForWidth(self.modesTextEdit.sizePolicy().hasHeightForWidth()) 333 | self.modesTextEdit.setSizePolicy(sizePolicy) 334 | font = QtGui.QFont() 335 | font.setFamily("Courier") 336 | font.setPointSize(16) 337 | font.setItalic(False) 338 | self.modesTextEdit.setFont(font) 339 | self.modesTextEdit.setUndoRedoEnabled(False) 340 | self.modesTextEdit.setReadOnly(True) 341 | self.modesTextEdit.setOverwriteMode(False) 342 | self.modesTextEdit.setObjectName("modesTextEdit") 343 | self.horizontalLayout_5.addWidget(self.modesTextEdit) 344 | self.modegraph = ModeGraphWidget(parent=self.tabmodecalc) 345 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 346 | sizePolicy.setHorizontalStretch(0) 347 | sizePolicy.setVerticalStretch(0) 348 | sizePolicy.setHeightForWidth(self.modegraph.sizePolicy().hasHeightForWidth()) 349 | self.modegraph.setSizePolicy(sizePolicy) 350 | self.modegraph.setObjectName("modegraph") 351 | self.horizontalLayout_5.addWidget(self.modegraph) 352 | self.verticalLayout_8.addLayout(self.horizontalLayout_5) 353 | self.tabWidget.addTab(self.tabmodecalc, "") 354 | self.tabsabine = QtWidgets.QWidget() 355 | self.tabsabine.setObjectName("tabsabine") 356 | self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tabsabine) 357 | self.verticalLayout_5.setObjectName("verticalLayout_5") 358 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout() 359 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 360 | self.label_3 = QtWidgets.QLabel(parent=self.tabsabine) 361 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 362 | sizePolicy.setHorizontalStretch(0) 363 | sizePolicy.setVerticalStretch(0) 364 | sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) 365 | self.label_3.setSizePolicy(sizePolicy) 366 | self.label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 367 | self.label_3.setObjectName("label_3") 368 | self.horizontalLayout_4.addWidget(self.label_3) 369 | self.sabinesslineEdit = QtWidgets.QLineEdit(parent=self.tabsabine) 370 | self.sabinesslineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 371 | self.sabinesslineEdit.setObjectName("sabinesslineEdit") 372 | self.horizontalLayout_4.addWidget(self.sabinesslineEdit) 373 | self.label_12 = QtWidgets.QLabel(parent=self.tabsabine) 374 | self.label_12.setObjectName("label_12") 375 | self.horizontalLayout_4.addWidget(self.label_12) 376 | self.sabinexlineEdit = QtWidgets.QLineEdit(parent=self.tabsabine) 377 | self.sabinexlineEdit.setText("") 378 | self.sabinexlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 379 | self.sabinexlineEdit.setObjectName("sabinexlineEdit") 380 | self.horizontalLayout_4.addWidget(self.sabinexlineEdit) 381 | self.label_13 = QtWidgets.QLabel(parent=self.tabsabine) 382 | self.label_13.setObjectName("label_13") 383 | self.horizontalLayout_4.addWidget(self.label_13) 384 | self.sabineylineEdit = QtWidgets.QLineEdit(parent=self.tabsabine) 385 | self.sabineylineEdit.setText("") 386 | self.sabineylineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 387 | self.sabineylineEdit.setObjectName("sabineylineEdit") 388 | self.horizontalLayout_4.addWidget(self.sabineylineEdit) 389 | self.label_14 = QtWidgets.QLabel(parent=self.tabsabine) 390 | self.label_14.setObjectName("label_14") 391 | self.horizontalLayout_4.addWidget(self.label_14) 392 | self.sabinezlineEdit = QtWidgets.QLineEdit(parent=self.tabsabine) 393 | self.sabinezlineEdit.setText("") 394 | self.sabinezlineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 395 | self.sabinezlineEdit.setObjectName("sabinezlineEdit") 396 | self.horizontalLayout_4.addWidget(self.sabinezlineEdit) 397 | self.verticalLayout_5.addLayout(self.horizontalLayout_4) 398 | self.horizontalLayout_8 = QtWidgets.QHBoxLayout() 399 | self.horizontalLayout_8.setObjectName("horizontalLayout_8") 400 | self.verticalLayout_9 = QtWidgets.QVBoxLayout() 401 | self.verticalLayout_9.setObjectName("verticalLayout_9") 402 | self.horizontalLayout_9 = QtWidgets.QHBoxLayout() 403 | self.horizontalLayout_9.setObjectName("horizontalLayout_9") 404 | self.label_15 = QtWidgets.QLabel(parent=self.tabsabine) 405 | self.label_15.setObjectName("label_15") 406 | self.horizontalLayout_9.addWidget(self.label_15) 407 | self.floorcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 408 | self.floorcomboBox.setObjectName("floorcomboBox") 409 | self.floorcomboBox.addItem("") 410 | self.floorcomboBox.addItem("") 411 | self.floorcomboBox.addItem("") 412 | self.floorcomboBox.addItem("") 413 | self.floorcomboBox.addItem("") 414 | self.floorcomboBox.addItem("") 415 | self.floorcomboBox.addItem("") 416 | self.horizontalLayout_9.addWidget(self.floorcomboBox) 417 | self.verticalLayout_9.addLayout(self.horizontalLayout_9) 418 | self.horizontalLayout_11 = QtWidgets.QHBoxLayout() 419 | self.horizontalLayout_11.setObjectName("horizontalLayout_11") 420 | self.label_17 = QtWidgets.QLabel(parent=self.tabsabine) 421 | self.label_17.setObjectName("label_17") 422 | self.horizontalLayout_11.addWidget(self.label_17) 423 | self.ceilingcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 424 | self.ceilingcomboBox.setObjectName("ceilingcomboBox") 425 | self.ceilingcomboBox.addItem("") 426 | self.ceilingcomboBox.addItem("") 427 | self.ceilingcomboBox.addItem("") 428 | self.ceilingcomboBox.addItem("") 429 | self.ceilingcomboBox.addItem("") 430 | self.ceilingcomboBox.addItem("") 431 | self.ceilingcomboBox.addItem("") 432 | self.horizontalLayout_11.addWidget(self.ceilingcomboBox) 433 | self.verticalLayout_9.addLayout(self.horizontalLayout_11) 434 | self.horizontalLayout_12 = QtWidgets.QHBoxLayout() 435 | self.horizontalLayout_12.setObjectName("horizontalLayout_12") 436 | self.label_18 = QtWidgets.QLabel(parent=self.tabsabine) 437 | self.label_18.setObjectName("label_18") 438 | self.horizontalLayout_12.addWidget(self.label_18) 439 | self.fwallcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 440 | self.fwallcomboBox.setObjectName("fwallcomboBox") 441 | self.fwallcomboBox.addItem("") 442 | self.fwallcomboBox.addItem("") 443 | self.fwallcomboBox.addItem("") 444 | self.fwallcomboBox.addItem("") 445 | self.fwallcomboBox.addItem("") 446 | self.fwallcomboBox.addItem("") 447 | self.fwallcomboBox.addItem("") 448 | self.horizontalLayout_12.addWidget(self.fwallcomboBox) 449 | self.verticalLayout_9.addLayout(self.horizontalLayout_12) 450 | self.horizontalLayout_13 = QtWidgets.QHBoxLayout() 451 | self.horizontalLayout_13.setObjectName("horizontalLayout_13") 452 | self.label_19 = QtWidgets.QLabel(parent=self.tabsabine) 453 | self.label_19.setObjectName("label_19") 454 | self.horizontalLayout_13.addWidget(self.label_19) 455 | self.bwallcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 456 | self.bwallcomboBox.setObjectName("bwallcomboBox") 457 | self.bwallcomboBox.addItem("") 458 | self.bwallcomboBox.addItem("") 459 | self.bwallcomboBox.addItem("") 460 | self.bwallcomboBox.addItem("") 461 | self.bwallcomboBox.addItem("") 462 | self.bwallcomboBox.addItem("") 463 | self.bwallcomboBox.addItem("") 464 | self.horizontalLayout_13.addWidget(self.bwallcomboBox) 465 | self.verticalLayout_9.addLayout(self.horizontalLayout_13) 466 | self.horizontalLayout_14 = QtWidgets.QHBoxLayout() 467 | self.horizontalLayout_14.setObjectName("horizontalLayout_14") 468 | self.label_20 = QtWidgets.QLabel(parent=self.tabsabine) 469 | self.label_20.setObjectName("label_20") 470 | self.horizontalLayout_14.addWidget(self.label_20) 471 | self.lwallcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 472 | self.lwallcomboBox.setObjectName("lwallcomboBox") 473 | self.lwallcomboBox.addItem("") 474 | self.lwallcomboBox.addItem("") 475 | self.lwallcomboBox.addItem("") 476 | self.lwallcomboBox.addItem("") 477 | self.lwallcomboBox.addItem("") 478 | self.lwallcomboBox.addItem("") 479 | self.lwallcomboBox.addItem("") 480 | self.horizontalLayout_14.addWidget(self.lwallcomboBox) 481 | self.verticalLayout_9.addLayout(self.horizontalLayout_14) 482 | self.horizontalLayout_15 = QtWidgets.QHBoxLayout() 483 | self.horizontalLayout_15.setObjectName("horizontalLayout_15") 484 | self.label_16 = QtWidgets.QLabel(parent=self.tabsabine) 485 | self.label_16.setObjectName("label_16") 486 | self.horizontalLayout_15.addWidget(self.label_16) 487 | self.rwallcomboBox = QtWidgets.QComboBox(parent=self.tabsabine) 488 | self.rwallcomboBox.setObjectName("rwallcomboBox") 489 | self.rwallcomboBox.addItem("") 490 | self.rwallcomboBox.addItem("") 491 | self.rwallcomboBox.addItem("") 492 | self.rwallcomboBox.addItem("") 493 | self.rwallcomboBox.addItem("") 494 | self.rwallcomboBox.addItem("") 495 | self.rwallcomboBox.addItem("") 496 | self.horizontalLayout_15.addWidget(self.rwallcomboBox) 497 | self.verticalLayout_9.addLayout(self.horizontalLayout_15) 498 | self.horizontalLayout_16 = QtWidgets.QHBoxLayout() 499 | self.horizontalLayout_16.setObjectName("horizontalLayout_16") 500 | self.label_11 = QtWidgets.QLabel(parent=self.tabsabine) 501 | self.label_11.setObjectName("label_11") 502 | self.horizontalLayout_16.addWidget(self.label_11) 503 | self.adultslineEdit = QtWidgets.QLineEdit(parent=self.tabsabine) 504 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) 505 | sizePolicy.setHorizontalStretch(0) 506 | sizePolicy.setVerticalStretch(0) 507 | sizePolicy.setHeightForWidth(self.adultslineEdit.sizePolicy().hasHeightForWidth()) 508 | self.adultslineEdit.setSizePolicy(sizePolicy) 509 | self.adultslineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 510 | self.adultslineEdit.setObjectName("adultslineEdit") 511 | self.horizontalLayout_16.addWidget(self.adultslineEdit) 512 | self.verticalLayout_9.addLayout(self.horizontalLayout_16) 513 | self.horizontalLayout_17 = QtWidgets.QHBoxLayout() 514 | self.horizontalLayout_17.setObjectName("horizontalLayout_17") 515 | self.plainTextEdit = QtWidgets.QPlainTextEdit(parent=self.tabsabine) 516 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding) 517 | sizePolicy.setHorizontalStretch(0) 518 | sizePolicy.setVerticalStretch(0) 519 | sizePolicy.setHeightForWidth(self.plainTextEdit.sizePolicy().hasHeightForWidth()) 520 | self.plainTextEdit.setSizePolicy(sizePolicy) 521 | font = QtGui.QFont() 522 | font.setFamily("Monaco") 523 | font.setPointSize(11) 524 | font.setBold(False) 525 | font.setItalic(False) 526 | font.setWeight(50) 527 | self.plainTextEdit.setFont(font) 528 | self.plainTextEdit.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 529 | self.plainTextEdit.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) 530 | self.plainTextEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.NoWrap) 531 | self.plainTextEdit.setObjectName("plainTextEdit") 532 | self.horizontalLayout_17.addWidget(self.plainTextEdit) 533 | self.verticalLayout_9.addLayout(self.horizontalLayout_17) 534 | self.horizontalLayout_8.addLayout(self.verticalLayout_9) 535 | self.verticalLayout_11 = QtWidgets.QVBoxLayout() 536 | self.verticalLayout_11.setObjectName("verticalLayout_11") 537 | self.sabinercgraph = RcGraphWidget(parent=self.tabsabine) 538 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 539 | sizePolicy.setHorizontalStretch(0) 540 | sizePolicy.setVerticalStretch(0) 541 | sizePolicy.setHeightForWidth(self.sabinercgraph.sizePolicy().hasHeightForWidth()) 542 | self.sabinercgraph.setSizePolicy(sizePolicy) 543 | self.sabinercgraph.setObjectName("sabinercgraph") 544 | self.verticalLayout_11.addWidget(self.sabinercgraph) 545 | self.horizontalLayout_8.addLayout(self.verticalLayout_11) 546 | self.verticalLayout_5.addLayout(self.horizontalLayout_8) 547 | self.tabWidget.addTab(self.tabsabine, "") 548 | self.tabEquation = QtWidgets.QWidget() 549 | self.tabEquation.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) 550 | self.tabEquation.setAutoFillBackground(False) 551 | self.tabEquation.setObjectName("tabEquation") 552 | self.verticalLayout_14 = QtWidgets.QVBoxLayout(self.tabEquation) 553 | self.verticalLayout_14.setObjectName("verticalLayout_14") 554 | self.horizontalLayout_28 = QtWidgets.QHBoxLayout() 555 | self.horizontalLayout_28.setObjectName("horizontalLayout_28") 556 | self.label_26 = QtWidgets.QLabel(parent=self.tabEquation) 557 | self.label_26.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 558 | self.label_26.setObjectName("label_26") 559 | self.horizontalLayout_28.addWidget(self.label_26) 560 | self.verticalLayout_14.addLayout(self.horizontalLayout_28) 561 | self.horizontalLayout_34 = QtWidgets.QHBoxLayout() 562 | self.horizontalLayout_34.setObjectName("horizontalLayout_34") 563 | self.label_27 = QtWidgets.QLabel(parent=self.tabEquation) 564 | self.label_27.setObjectName("label_27") 565 | self.horizontalLayout_34.addWidget(self.label_27) 566 | self.eqnduration_lineEdit = QtWidgets.QLineEdit(parent=self.tabEquation) 567 | self.eqnduration_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 568 | self.eqnduration_lineEdit.setObjectName("eqnduration_lineEdit") 569 | self.horizontalLayout_34.addWidget(self.eqnduration_lineEdit) 570 | self.label_28 = QtWidgets.QLabel(parent=self.tabEquation) 571 | self.label_28.setObjectName("label_28") 572 | self.horizontalLayout_34.addWidget(self.label_28) 573 | self.eqnsr_lineEdit = QtWidgets.QLineEdit(parent=self.tabEquation) 574 | self.eqnsr_lineEdit.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) 575 | self.eqnsr_lineEdit.setObjectName("eqnsr_lineEdit") 576 | self.horizontalLayout_34.addWidget(self.eqnsr_lineEdit) 577 | self.verticalLayout_14.addLayout(self.horizontalLayout_34) 578 | self.horizontalLayout_30 = QtWidgets.QHBoxLayout() 579 | self.horizontalLayout_30.setObjectName("horizontalLayout_30") 580 | self.textBrowser = QtWidgets.QTextBrowser(parent=self.tabEquation) 581 | self.textBrowser.setObjectName("textBrowser") 582 | self.horizontalLayout_30.addWidget(self.textBrowser) 583 | self.verticalLayout_14.addLayout(self.horizontalLayout_30) 584 | self.horizontalLayout_31 = QtWidgets.QHBoxLayout() 585 | self.horizontalLayout_31.setObjectName("horizontalLayout_31") 586 | self.label_25 = QtWidgets.QLabel(parent=self.tabEquation) 587 | self.label_25.setObjectName("label_25") 588 | self.horizontalLayout_31.addWidget(self.label_25) 589 | self.equation_lineEdit = QtWidgets.QLineEdit(parent=self.tabEquation) 590 | self.equation_lineEdit.setObjectName("equation_lineEdit") 591 | self.horizontalLayout_31.addWidget(self.equation_lineEdit) 592 | self.verticalLayout_14.addLayout(self.horizontalLayout_31) 593 | self.horizontalLayout_32 = QtWidgets.QHBoxLayout() 594 | self.horizontalLayout_32.setObjectName("horizontalLayout_32") 595 | self.eq_go_pushButton = QtWidgets.QPushButton(parent=self.tabEquation) 596 | self.eq_go_pushButton.setObjectName("eq_go_pushButton") 597 | self.horizontalLayout_32.addWidget(self.eq_go_pushButton) 598 | self.verticalLayout_14.addLayout(self.horizontalLayout_32) 599 | self.tabWidget.addTab(self.tabEquation, "") 600 | self.tabConvolve = QtWidgets.QWidget() 601 | self.tabConvolve.setEnabled(True) 602 | self.tabConvolve.setObjectName("tabConvolve") 603 | self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.tabConvolve) 604 | self.verticalLayout_12.setObjectName("verticalLayout_12") 605 | self.textBrowser_2 = QtWidgets.QTextBrowser(parent=self.tabConvolve) 606 | self.textBrowser_2.setObjectName("textBrowser_2") 607 | self.verticalLayout_12.addWidget(self.textBrowser_2) 608 | self.horizontalLayout_23 = QtWidgets.QHBoxLayout() 609 | self.horizontalLayout_23.setObjectName("horizontalLayout_23") 610 | self.checkBox_autocorr = QtWidgets.QCheckBox(parent=self.tabConvolve) 611 | self.checkBox_autocorr.setObjectName("checkBox_autocorr") 612 | self.horizontalLayout_23.addWidget(self.checkBox_autocorr) 613 | self.checkBox_timerev = QtWidgets.QCheckBox(parent=self.tabConvolve) 614 | self.checkBox_timerev.setObjectName("checkBox_timerev") 615 | self.horizontalLayout_23.addWidget(self.checkBox_timerev) 616 | self.checkBox_removefirsthalf = QtWidgets.QCheckBox(parent=self.tabConvolve) 617 | self.checkBox_removefirsthalf.setObjectName("checkBox_removefirsthalf") 618 | self.horizontalLayout_23.addWidget(self.checkBox_removefirsthalf) 619 | self.verticalLayout_12.addLayout(self.horizontalLayout_23) 620 | self.horizontalLayout_22 = QtWidgets.QHBoxLayout() 621 | self.horizontalLayout_22.setObjectName("horizontalLayout_22") 622 | self.convolve_go_pushButton = QtWidgets.QPushButton(parent=self.tabConvolve) 623 | self.convolve_go_pushButton.setObjectName("convolve_go_pushButton") 624 | self.horizontalLayout_22.addWidget(self.convolve_go_pushButton) 625 | self.verticalLayout_12.addLayout(self.horizontalLayout_22) 626 | self.tabWidget.addTab(self.tabConvolve, "") 627 | self.tabPlayRec = QtWidgets.QWidget() 628 | self.tabPlayRec.setObjectName("tabPlayRec") 629 | self.verticalLayout_13 = QtWidgets.QVBoxLayout(self.tabPlayRec) 630 | self.verticalLayout_13.setObjectName("verticalLayout_13") 631 | self.horizontalLayout_29 = QtWidgets.QHBoxLayout() 632 | self.horizontalLayout_29.setObjectName("horizontalLayout_29") 633 | self.label_30 = QtWidgets.QLabel(parent=self.tabPlayRec) 634 | self.label_30.setObjectName("label_30") 635 | self.horizontalLayout_29.addWidget(self.label_30) 636 | self.verticalLayout_13.addLayout(self.horizontalLayout_29) 637 | self.horizontalLayout_24 = QtWidgets.QHBoxLayout() 638 | self.horizontalLayout_24.setObjectName("horizontalLayout_24") 639 | self.groupBox_2 = QtWidgets.QGroupBox(parent=self.tabPlayRec) 640 | font = QtGui.QFont() 641 | font.setPointSize(16) 642 | self.groupBox_2.setFont(font) 643 | self.groupBox_2.setObjectName("groupBox_2") 644 | self.horizontalLayoutWidget_2 = QtWidgets.QWidget(parent=self.groupBox_2) 645 | self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(0, 19, 861, 71)) 646 | self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2") 647 | self.horizontalLayout_35 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2) 648 | self.horizontalLayout_35.setContentsMargins(0, 0, 0, 0) 649 | self.horizontalLayout_35.setObjectName("horizontalLayout_35") 650 | self.radioButton_3 = QtWidgets.QRadioButton(parent=self.horizontalLayoutWidget_2) 651 | font = QtGui.QFont() 652 | font.setPointSize(13) 653 | self.radioButton_3.setFont(font) 654 | self.radioButton_3.setChecked(True) 655 | self.radioButton_3.setObjectName("radioButton_3") 656 | self.horizontalLayout_35.addWidget(self.radioButton_3) 657 | self.radioButton_4 = QtWidgets.QRadioButton(parent=self.horizontalLayoutWidget_2) 658 | font = QtGui.QFont() 659 | font.setPointSize(13) 660 | self.radioButton_4.setFont(font) 661 | self.radioButton_4.setObjectName("radioButton_4") 662 | self.horizontalLayout_35.addWidget(self.radioButton_4) 663 | self.horizontalLayout_24.addWidget(self.groupBox_2) 664 | self.verticalLayout_13.addLayout(self.horizontalLayout_24) 665 | self.horizontalLayout_25 = QtWidgets.QHBoxLayout() 666 | self.horizontalLayout_25.setObjectName("horizontalLayout_25") 667 | self.groupBox = QtWidgets.QGroupBox(parent=self.tabPlayRec) 668 | font = QtGui.QFont() 669 | font.setPointSize(16) 670 | self.groupBox.setFont(font) 671 | self.groupBox.setFlat(False) 672 | self.groupBox.setCheckable(False) 673 | self.groupBox.setObjectName("groupBox") 674 | self.horizontalLayoutWidget = QtWidgets.QWidget(parent=self.groupBox) 675 | self.horizontalLayoutWidget.setGeometry(QtCore.QRect(0, 20, 861, 80)) 676 | self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") 677 | self.horizontalLayout_33 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) 678 | self.horizontalLayout_33.setContentsMargins(0, 0, 0, 0) 679 | self.horizontalLayout_33.setObjectName("horizontalLayout_33") 680 | self.radioButton = QtWidgets.QRadioButton(parent=self.horizontalLayoutWidget) 681 | font = QtGui.QFont() 682 | font.setPointSize(13) 683 | self.radioButton.setFont(font) 684 | self.radioButton.setChecked(True) 685 | self.radioButton.setObjectName("radioButton") 686 | self.horizontalLayout_33.addWidget(self.radioButton) 687 | self.radioButton_2 = QtWidgets.QRadioButton(parent=self.horizontalLayoutWidget) 688 | font = QtGui.QFont() 689 | font.setPointSize(13) 690 | self.radioButton_2.setFont(font) 691 | self.radioButton_2.setObjectName("radioButton_2") 692 | self.horizontalLayout_33.addWidget(self.radioButton_2) 693 | self.horizontalLayout_25.addWidget(self.groupBox) 694 | self.verticalLayout_13.addLayout(self.horizontalLayout_25) 695 | self.horizontalLayout_27 = QtWidgets.QHBoxLayout() 696 | self.horizontalLayout_27.setObjectName("horizontalLayout_27") 697 | self.label_24 = QtWidgets.QLabel(parent=self.tabPlayRec) 698 | self.label_24.setObjectName("label_24") 699 | self.horizontalLayout_27.addWidget(self.label_24) 700 | self.lineEdit_playrec_rate = QtWidgets.QLineEdit(parent=self.tabPlayRec) 701 | self.lineEdit_playrec_rate.setObjectName("lineEdit_playrec_rate") 702 | self.horizontalLayout_27.addWidget(self.lineEdit_playrec_rate) 703 | self.label_29 = QtWidgets.QLabel(parent=self.tabPlayRec) 704 | self.label_29.setObjectName("label_29") 705 | self.horizontalLayout_27.addWidget(self.label_29) 706 | self.verticalLayout_13.addLayout(self.horizontalLayout_27) 707 | self.horizontalLayout_26 = QtWidgets.QHBoxLayout() 708 | self.horizontalLayout_26.setObjectName("horizontalLayout_26") 709 | self.pushButton_playrec_go = QtWidgets.QPushButton(parent=self.tabPlayRec) 710 | self.pushButton_playrec_go.setObjectName("pushButton_playrec_go") 711 | self.horizontalLayout_26.addWidget(self.pushButton_playrec_go) 712 | self.pushButton_playrec_stop = QtWidgets.QPushButton(parent=self.tabPlayRec) 713 | self.pushButton_playrec_stop.setObjectName("pushButton_playrec_stop") 714 | self.horizontalLayout_26.addWidget(self.pushButton_playrec_stop) 715 | self.verticalLayout_13.addLayout(self.horizontalLayout_26) 716 | self.tabWidget.addTab(self.tabPlayRec, "") 717 | self.horizontalLayout.addWidget(self.tabWidget) 718 | self.verticalLayout.addLayout(self.horizontalLayout) 719 | TheMainWindow.setCentralWidget(self.thecentralwidget) 720 | self.themenubar = QtWidgets.QMenuBar(parent=TheMainWindow) 721 | self.themenubar.setGeometry(QtCore.QRect(0, 0, 1105, 22)) 722 | self.themenubar.setObjectName("themenubar") 723 | self.menuFile = QtWidgets.QMenu(parent=self.themenubar) 724 | self.menuFile.setObjectName("menuFile") 725 | TheMainWindow.setMenuBar(self.themenubar) 726 | self.theactionOpen = QtGui.QAction(parent=TheMainWindow) 727 | self.theactionOpen.setObjectName("theactionOpen") 728 | self.theactionQuit = QtGui.QAction(parent=TheMainWindow) 729 | self.theactionQuit.setObjectName("theactionQuit") 730 | self.theactionOpenB = QtGui.QAction(parent=TheMainWindow) 731 | self.theactionOpenB.setObjectName("theactionOpenB") 732 | self.theactionAbout = QtGui.QAction(parent=TheMainWindow) 733 | self.theactionAbout.setObjectName("theactionAbout") 734 | self.theactionSave = QtGui.QAction(parent=TheMainWindow) 735 | self.theactionSave.setObjectName("theactionSave") 736 | self.menuFile.addAction(self.theactionAbout) 737 | self.menuFile.addAction(self.theactionOpen) 738 | self.menuFile.addAction(self.theactionOpenB) 739 | self.menuFile.addAction(self.theactionSave) 740 | self.menuFile.addSeparator() 741 | self.menuFile.addAction(self.theactionQuit) 742 | self.themenubar.addAction(self.menuFile.menuAction()) 743 | 744 | self.retranslateUi(TheMainWindow) 745 | self.tabWidget.setCurrentIndex(0) 746 | self.sabinesslineEdit.editingFinished.connect(self.sabinercgraph.update) # type: ignore 747 | self.sabinexlineEdit.editingFinished.connect(self.sabinercgraph.update) # type: ignore 748 | self.sabineylineEdit.editingFinished.connect(self.sabinercgraph.update) # type: ignore 749 | self.sabinezlineEdit.editingFinished.connect(self.sabinercgraph.update) # type: ignore 750 | self.floorcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 751 | self.ceilingcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 752 | self.fwallcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 753 | self.bwallcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 754 | self.lwallcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 755 | self.rwallcomboBox.currentIndexChanged['int'].connect(self.sabinercgraph.update) # type: ignore 756 | self.adultslineEdit.editingFinished.connect(self.sabinercgraph.update) # type: ignore 757 | QtCore.QMetaObject.connectSlotsByName(TheMainWindow) 758 | 759 | def retranslateUi(self, TheMainWindow): 760 | _translate = QtCore.QCoreApplication.translate 761 | TheMainWindow.setWindowTitle(_translate("TheMainWindow", "SHAART")) 762 | self.inwavfileselectButton.setText(_translate("TheMainWindow", "File A:")) 763 | self.inwavfileBselectButton.setText(_translate("TheMainWindow", "File B:")) 764 | self.rt60octavelabel.setText(_translate("TheMainWindow", "Octave:")) 765 | self.rt60comboBox.setItemText(0, _translate("TheMainWindow", "All")) 766 | self.rt60comboBox.setItemText(1, _translate("TheMainWindow", "125 Hz")) 767 | self.rt60comboBox.setItemText(2, _translate("TheMainWindow", "250 Hz")) 768 | self.rt60comboBox.setItemText(3, _translate("TheMainWindow", "500 Hz")) 769 | self.rt60comboBox.setItemText(4, _translate("TheMainWindow", "1000 Hz")) 770 | self.rt60comboBox.setItemText(5, _translate("TheMainWindow", "2000 Hz")) 771 | self.rt60comboBox.setItemText(6, _translate("TheMainWindow", "4000 Hz")) 772 | self.rt60comboBox.setItemText(7, _translate("TheMainWindow", "8000 Hz")) 773 | self.rt60comboBox.setItemText(8, _translate("TheMainWindow", "16000 Hz")) 774 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabrt60), _translate("TheMainWindow", "RT60")) 775 | self.waveform_abs_checkBox.setText(_translate("TheMainWindow", "Absolute Value")) 776 | self.waveform_env_checkBox.setText(_translate("TheMainWindow", "Envelope Only")) 777 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabWaveform), _translate("TheMainWindow", "Waveform")) 778 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabpower), _translate("TheMainWindow", "Power")) 779 | self.label_5.setText(_translate("TheMainWindow", "Color Map:")) 780 | self.spectrocmcomboBox.setItemText(0, _translate("TheMainWindow", "Heat")) 781 | self.spectrocmcomboBox.setItemText(1, _translate("TheMainWindow", "Rainbow")) 782 | self.spectrocmcomboBox.setItemText(2, _translate("TheMainWindow", "Reverse Rainbow")) 783 | self.spectrocmcomboBox.setItemText(3, _translate("TheMainWindow", "Grays")) 784 | self.spectrocmcomboBox.setItemText(4, _translate("TheMainWindow", "Blues")) 785 | self.spectrocmcomboBox.setItemText(5, _translate("TheMainWindow", "NCAR")) 786 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabspectro), _translate("TheMainWindow", "Spectrogram")) 787 | self.label_4.setText(_translate("TheMainWindow", "Warning: This can be very slow for large data sets.")) 788 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabwaterfall), _translate("TheMainWindow", "Waterfall")) 789 | self.label_21.setText(_translate("TheMainWindow", "Encode an image as a WAV file")) 790 | self.is_img_pushButton.setText(_translate("TheMainWindow", "Input Image File:")) 791 | self.label_22.setText(_translate("TheMainWindow", "Image Duration (s): ")) 792 | self.is_duration_lineEdit.setText(_translate("TheMainWindow", "8")) 793 | self.label_23.setText(_translate("TheMainWindow", "Sample Rate (Hz):")) 794 | self.is_rate_lineEdit.setText(_translate("TheMainWindow", "48000")) 795 | self.label_9.setText(_translate("TheMainWindow", "Min Freq (Hz): ")) 796 | self.is_minf_lineEdit.setText(_translate("TheMainWindow", "20")) 797 | self.label_10.setText(_translate("TheMainWindow", "Max Freq (Hz):")) 798 | self.is_maxf_lineEdit.setText(_translate("TheMainWindow", "20000")) 799 | self.is_wav_pushButton.setText(_translate("TheMainWindow", "Output WAV File:")) 800 | self.is_go_pushButton.setText(_translate("TheMainWindow", "GO!")) 801 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabinvspectro), _translate("TheMainWindow", "invSpectro")) 802 | self.label.setText(_translate("TheMainWindow", "Sound Speed:")) 803 | self.speedlineEdit.setText(_translate("TheMainWindow", "1140")) 804 | self.label_2.setText(_translate("TheMainWindow", "Room Dimensions: X:")) 805 | self.label_7.setText(_translate("TheMainWindow", "Y:")) 806 | self.label_8.setText(_translate("TheMainWindow", "Z:")) 807 | self.label_6.setText(_translate("TheMainWindow", "Max Mode #:")) 808 | self.maxmodelineEdit.setText(_translate("TheMainWindow", "10")) 809 | self.modesTextEdit.setPlainText(_translate("TheMainWindow", "Freq (Hz) Nx Ny Nz\n" 810 | "--------- -- -- --\n" 811 | "")) 812 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabmodecalc), _translate("TheMainWindow", "Modes")) 813 | self.label_3.setText(_translate("TheMainWindow", "Sound Speed (ft/s)")) 814 | self.sabinesslineEdit.setText(_translate("TheMainWindow", "1140")) 815 | self.label_12.setText(_translate("TheMainWindow", "Room Dimensions (ft): X:")) 816 | self.label_13.setText(_translate("TheMainWindow", "Y:")) 817 | self.label_14.setText(_translate("TheMainWindow", "Z:")) 818 | self.label_15.setText(_translate("TheMainWindow", "Floor (XY):")) 819 | self.floorcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 820 | self.floorcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 821 | self.floorcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 822 | self.floorcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 823 | self.floorcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 824 | self.floorcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 825 | self.floorcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 826 | self.label_17.setText(_translate("TheMainWindow", "Ceiling (XY):")) 827 | self.ceilingcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 828 | self.ceilingcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 829 | self.ceilingcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 830 | self.ceilingcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 831 | self.ceilingcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 832 | self.ceilingcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 833 | self.ceilingcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 834 | self.label_18.setText(_translate("TheMainWindow", "Front wall (XZ):")) 835 | self.fwallcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 836 | self.fwallcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 837 | self.fwallcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 838 | self.fwallcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 839 | self.fwallcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 840 | self.fwallcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 841 | self.fwallcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 842 | self.label_19.setText(_translate("TheMainWindow", "Back wall (XZ):")) 843 | self.bwallcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 844 | self.bwallcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 845 | self.bwallcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 846 | self.bwallcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 847 | self.bwallcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 848 | self.bwallcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 849 | self.bwallcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 850 | self.label_20.setText(_translate("TheMainWindow", "Left wall (YZ):")) 851 | self.lwallcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 852 | self.lwallcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 853 | self.lwallcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 854 | self.lwallcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 855 | self.lwallcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 856 | self.lwallcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 857 | self.lwallcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 858 | self.label_16.setText(_translate("TheMainWindow", "Right wall (YZ):")) 859 | self.rwallcomboBox.setItemText(0, _translate("TheMainWindow", "Concrete")) 860 | self.rwallcomboBox.setItemText(1, _translate("TheMainWindow", "Glass")) 861 | self.rwallcomboBox.setItemText(2, _translate("TheMainWindow", "Plasterboard")) 862 | self.rwallcomboBox.setItemText(3, _translate("TheMainWindow", "Plywood")) 863 | self.rwallcomboBox.setItemText(4, _translate("TheMainWindow", "Carpet")) 864 | self.rwallcomboBox.setItemText(5, _translate("TheMainWindow", "Curtains")) 865 | self.rwallcomboBox.setItemText(6, _translate("TheMainWindow", "Acoustical Board")) 866 | self.label_11.setText(_translate("TheMainWindow", "Adults in seats:")) 867 | self.adultslineEdit.setText(_translate("TheMainWindow", "0")) 868 | self.plainTextEdit.setPlainText(_translate("TheMainWindow", "Absorption Coefficients (Sab/ft^2)\n" 869 | "-----------------------------------\n" 870 | "Freq (Hz): 125 500 1000 2000 \n" 871 | "-----------------------------------\n" 872 | "Concrete 0.01 0.02 0.02 0.02 \n" 873 | "Glass 0.19 0.06 0.04 0.03 \n" 874 | "Plasterboard 0.20 0.10 0.08 0.04 \n" 875 | "Plywood 0.45 0.13 0.11 0.10 \n" 876 | "Carpet 0.10 0.30 0.35 0.50 \n" 877 | "Curtains 0.05 0.25 0.35 0.40 \n" 878 | "Acous Board 0.25 0.80 0.90 0.90 \n" 879 | "\n" 880 | "Adult (Sab): 3.0 4.5 5.0 5.2\n" 881 | "")) 882 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabsabine), _translate("TheMainWindow", "Sabine")) 883 | self.label_26.setText(_translate("TheMainWindow", "Create/replace contents of File A based on an equation")) 884 | self.label_27.setText(_translate("TheMainWindow", "Duration (s):")) 885 | self.eqnduration_lineEdit.setText(_translate("TheMainWindow", "10")) 886 | self.label_28.setText(_translate("TheMainWindow", " Sample Rate (Hz):")) 887 | self.eqnsr_lineEdit.setText(_translate("TheMainWindow", "44100")) 888 | self.textBrowser.setHtml(_translate("TheMainWindow", "\n" 889 | "\n" 892 | "

Predefined variables for use in equation:

\n" 893 | "

SR = sample rate

\n" 894 | "

NS = number of samples

\n" 895 | "

t = time (in seconds)

\n" 896 | "

TMAX = Duration ( = NS/SR )

\n" 897 | "

PI = 3.14159...

\n" 898 | "

ln() = Natural Log

\n" 899 | "

log10() = log base ten

\n" 900 | "

Other standard math functions apply, e.g. exp() = e^()

\n" 901 | "

Also other Python functions (be careful here...)

\n" 902 | "


\n" 903 | "

Examples:

\n" 904 | "

Exponential Sine Sweep between 20 Hz and 20,000 Hz, Amplitude 0.8 =

\n" 905 | "

0.8 * sin( 20 *2*PI*TMAX/ln(20000.0/20) * (exp(t/TMAX*ln(20000.0/20))-1) )

\n" 906 | "


\n" 907 | "

"Inverse Filter" for use with Exp. Sine Sweep Data (no need to "Time Reverse File A", it\'s built-in):

\n" 908 | "

exp(ln(20000.0/20)*(-t)/TMAX) * sin( 20 *2*PI*TMAX/ln(20000.0/20) * (exp((TMAX-t)/TMAX*ln(20000.0/20))-1) )

\n" 909 | "


\n" 910 | "

White Noise, Amplitude 0.5 = 0.5 * np.random.uniform(-1,1,NS)

\n" 911 | "


\n" 912 | "

NOTE: As of SHAART 0.6, Equation is a (super-fast) array operation. "t" is an array.

")) 913 | self.label_25.setText(_translate("TheMainWindow", "EQUATION: Sound(t) =")) 914 | self.equation_lineEdit.setText(_translate("TheMainWindow", "0.8 * sin( 20 *2*PI*TMAX/ln(20000.0/20) * (exp(t/TMAX*ln(20000.0/20))-1) )")) 915 | self.eq_go_pushButton.setText(_translate("TheMainWindow", "GO!")) 916 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabEquation), _translate("TheMainWindow", "Equation")) 917 | self.textBrowser_2.setHtml(_translate("TheMainWindow", "\n" 918 | "\n" 921 | "


\n" 922 | "

Convolves File A (e.g. music) with File B (e.g., impulse response)

\n" 923 | "

Places result in File A

\n" 924 | "


\n" 925 | "

To construct an impulse response from a sine sweep:

\n" 926 | "

Let File A = "inverse" of test sweep*, File B = recorded room response

\n" 927 | "


\n" 928 | "

*see Equation tab for inverse sweep of exp.sine sweep.

\n" 929 | "


\n" 930 | "


\n" 931 | "

"Remove First Half of Result" is typically used both for:

\n" 932 | "

- eliminating pre-ringing from constructed impulse responses

\n" 933 | "

- eliminating redundant (t<0) info from autocorrelation

")) 934 | self.checkBox_autocorr.setText(_translate("TheMainWindow", "Autocorrelation (Convolve A with A) ")) 935 | self.checkBox_timerev.setText(_translate("TheMainWindow", "Time-Reverse File A ")) 936 | self.checkBox_removefirsthalf.setText(_translate("TheMainWindow", "Remove First Half of Result")) 937 | self.convolve_go_pushButton.setText(_translate("TheMainWindow", "GO!")) 938 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabConvolve), _translate("TheMainWindow", "Convolve")) 939 | self.label_30.setText(_translate("TheMainWindow", " NOTE: CURRENTLY THE PROGRAM ONLY \"PLAYS\" FILE A. EVERYTHING ELSE IS JUST A PLACEHOLDER FOR FUTURE EXPANSION.")) 940 | self.groupBox_2.setTitle(_translate("TheMainWindow", "FileA:")) 941 | self.radioButton_3.setText(_translate("TheMainWindow", "Play")) 942 | self.radioButton_4.setText(_translate("TheMainWindow", "Record")) 943 | self.groupBox.setTitle(_translate("TheMainWindow", "FileB:")) 944 | self.radioButton.setText(_translate("TheMainWindow", "Play")) 945 | self.radioButton_2.setText(_translate("TheMainWindow", "Record")) 946 | self.label_24.setText(_translate("TheMainWindow", "Sample Rate for Recording:")) 947 | self.lineEdit_playrec_rate.setText(_translate("TheMainWindow", "44100")) 948 | self.label_29.setText(_translate("TheMainWindow", "Hz")) 949 | self.pushButton_playrec_go.setText(_translate("TheMainWindow", "GO")) 950 | self.pushButton_playrec_stop.setText(_translate("TheMainWindow", "STOP")) 951 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabPlayRec), _translate("TheMainWindow", "Play/Rec")) 952 | self.menuFile.setTitle(_translate("TheMainWindow", "File")) 953 | self.theactionOpen.setText(_translate("TheMainWindow", "Open File A")) 954 | self.theactionOpen.setToolTip(_translate("TheMainWindow", "Open File A")) 955 | self.theactionOpen.setShortcut(_translate("TheMainWindow", "Ctrl+O")) 956 | self.theactionQuit.setText(_translate("TheMainWindow", "Quit")) 957 | self.theactionOpenB.setText(_translate("TheMainWindow", "Open File B")) 958 | self.theactionOpenB.setToolTip(_translate("TheMainWindow", "Open File B")) 959 | self.theactionOpenB.setShortcut(_translate("TheMainWindow", "Ctrl+B")) 960 | self.theactionAbout.setText(_translate("TheMainWindow", "About")) 961 | self.theactionSave.setText(_translate("TheMainWindow", "Save File A")) 962 | self.theactionSave.setShortcut(_translate("TheMainWindow", "Ctrl+S")) 963 | from modegraphwidget import ModeGraphWidget 964 | from pwrspecwidget import PwrSpecWidget 965 | from rcgraphwidget import RcGraphWidget 966 | from rt60widget import Rt60Widget 967 | from spectrowidget import SpectroWidget 968 | from waterwidget import WaterWidget 969 | from waveformwidget import WaveformWidget 970 | --------------------------------------------------------------------------------