Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Initial commit of fexqt |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk | master |
Files: | files | file ages | folders |
SHA3-256: |
2dab5527e89a9b24efd484d4089d93df |
User & Date: | jmcclure 2017-08-17 22:44:04 |
Context
2017-08-18
| ||
10:50 | mainwin.py: handle divide-by-zero if/when fex line is zero length spectogram.py: initial eraser functionality added check-in: 5db78221ea user: jmcclure tags: trunk, master | |
2017-08-17
| ||
22:44 | Initial commit of fexqt check-in: 2dab5527e8 user: jmcclure tags: trunk, master | |
14:31 | Cleaning up for new python version check-in: 42a96c2c60 user: jmcclure tags: trunk, master | |
Changes
Added MANIFEST.in.
> > | 1 2 | include README.md |
Added bin/fex.
> > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #!/usr/bin/env python import os import sys from PyQt5.QtWidgets import QApplication from mainwin import MainWin def fpath(fname): for p in site.getsitepackages(): if os.path.isfile(p + '/' + fname): return p + '/' + fname return None if __name__ == '__main__': app = QApplication(sys.argv) win = MainWin() sys.exit(app.exec_()) |
Added fexqt/__init__.py.
> > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import os import sys from PyQt5.QtWidgets import QApplication from mainwin import MainWin from config import Config def fpath(fname): for p in site.getsitepackages(): if os.path.isfile(p + '/' + fname): return p + '/' + fname return None if __name__ == '__main__': app = QApplication(sys.argv) conf = Config() win = MainWin(conf) sys.exit(app.exec_()) |
Added fexqt/config.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import os import site from configparser import SafeConfigParser from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QWidget from PyQt5.QtCore import Qt class Container(object): pass _files = [p + '/default.cfg' for p in site.getsitepackages()] # TODO test for 'APPDATA' in environ on windows? # what's normally in the environment variables on win?? if 'XDG_CONFIG_HOME' in os.environ: _files.append(os.environ['XDG_CONFIG_HOME'] + '/fex/config.cfg') elif 'HOME' in os.environ: _files.append(os.environ['HOME'] + '/.config/fex/config.cfg') _files.append(os.getcwd() + '/fex.cfg') class Config(object): def __init__(self): w = QWidget() self._parser = SafeConfigParser({ 'QPalette-Window': w.palette().color(QPalette.Window).name(), 'QPalette-Background': w.palette().color(QPalette.Background).name(), 'QPalette-WindowText': w.palette().color(QPalette.WindowText).name(), 'QPalette-Foreground': w.palette().color(QPalette.Foreground).name(), 'QPalette-Base': w.palette().color(QPalette.Base).name(), 'QPalette-AlternateBase': w.palette().color(QPalette.AlternateBase).name(), 'QPalette-ToolTipBase': w.palette().color(QPalette.ToolTipBase).name(), 'QPalette-ToolTipText': w.palette().color(QPalette.ToolTipText).name(), 'QPalette-Text': w.palette().color(QPalette.Text).name(), 'QPalette-Button': w.palette().color(QPalette.Button).name(), 'QPalette-ButtonText': w.palette().color(QPalette.ButtonText).name(), 'QPalette-BrightText': w.palette().color(QPalette.BrightText).name(), 'QToolButton-IconOnly': str(Qt.ToolButtonIconOnly), 'QToolButton-TextOnly': str(Qt.ToolButtonTextOnly), 'QToolButton-TextBesideIcon': str(Qt.ToolButtonTextBesideIcon), 'QToolButton-TextUnderIcon': str(Qt.ToolButtonTextUnderIcon), 'QToolButton-FollowStyle': str(Qt.ToolButtonFollowStyle) }) f = self._parser.read(_files) self._spect = Container() self._spect.window = self._parser.getint('spectrogram','window') self._spect.overlap = self._parser.getint('spectrogram','overlap') self._spect.hipass = self._parser.getint('spectrogram','high-pass') self._spect.lopass = self._parser.getint('spectrogram','low-pass') self._spect.threshold = self._parser.getint('spectrogram','threshold') self._spect.winfunc = self._parser.get('spectrogram','window-function') self._spect.logfreq = self._parser.getboolean('spectrogram','log10-freq') self._spect.invert = self._parser.getboolean('spectrogram','invert-colors') self._spect.border = self._parser.get('spectrogram','border-color') self._spect.line = self._parser.get('spectrogram','line-color') self._spect.ratio = self._parser.getfloat('spectrogram','ratio') self._mainwin = Container() self._mainwin.menubar = self._parser.getboolean('main-window','menubar-visible') self._mainwin.macmenu = self._parser.getboolean('main-window','mac-native-menu') self._mainwin.toolbar = self._parser.getboolean('main-window','toolbar-visible') self._mainwin.toolbarposition = self._parser.get('main-window','toolbar-position') self._mainwin.toolbarstyle = self._parser.getint('main-window','toolbar-style') self._mainwin.statusbar = self._parser.getboolean('main-window','statusbar-visible') self._mainwin.theme = self._parser.get('main-window','theme-name') self._mainwin.maximized = self._parser.getboolean('main-window','start-maximized') def spectrogram(self): return self._spect def mainwin(self): return self._mainwin |
Added fexqt/default.cfg.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | # SPECTROGRAM # window (int number of samples) # window length of fft in number of samples (powers of 2 are best) # overlap (int number of samples) # overlap between adjacent windows # window-function (string) # available functions: boxcar triang blackman hamming hann bartlett # flattop parzen bohman blackmanharris nuttall barthann # high-pass / low-pass (int Hz) # signal outside this pseudo-bandpass filter are dropped from # analysis # threshold (in dB) # signals more than threshold dB below the peak of the recording are # dropped # log10-freq (bool) # whether frequency values should be log10 transformed prior to the # calculation of frequency excursion (see ...) # invert-colors (bool) # display white-on-black spectrogram # border-color, line-color (color) # colors of spectrogram window border and excursion line overlay see # "COLORS" below for options # ration (float) # ... (name to change soon) [spectrogram] window = 256 overlap = 192 window-function = hamming high-pass = 1250 low-pass = 10000 threshold = 14 log10-freq = false invert-colors = false border-color = dimgrey line-color = royalblue ratio = 8 # MAIN-WINDOW # menu-visible, toolbar-visible, statusbar-visible (bool) # whether these ui elements should be displayed # mac-native-menu (bool) # osx only: use osx-style integrated menu (not implemented yet) # toolbar-position (?) # not implemented yet # toolbar-style (int) # ... # theme-name (str) # ... [main-window] menubar-visible = true mac-native-menu = false toolbar-visible = true toolbar-position = top statusbar-visible = true toolbar-style = %(QToolButton-FollowStyle)s theme-name = oxygen start-maximized = false # COLORS # All colors are specified as a string as one of the following: # 1) hex digits: # #RGB (each of R, G, and B is a single hex digit) # #RRGGBB # #AARRGGBB # #RRRGGGBBB # #RRRRGGGGBBBB # 2) An svg color name # (see https://www.w3.org/TR/SVG/types.html#ColorKeywords) # 3) A Qt Color role variable (see VARIABLES below) # VARIABLES # This file uses pythons ConfigParser string interpolation. Any setting # can include the value of another setting as "%(NAME)s". The following # variables are set to the Qt defaults prior to this file being parsed # and can be used as desired: # QPalette-Window # QPalette-Background # QPalette-WindowText # QPalette-Foreground # QPalette-Base # QPalette-AlternateBase # QPalette-ToolTipBase # QPalette-ToolTipText # QPalette-Text # QPalette-Button # QPalette-ButtonText # QPalette-BrightText # QToolButton-IconOnly # QToolButton-TextOnly # QToolButton-TextBesideIcon # QToolButton-TextUnderIcon # QToolButton-FollowStyle |
Added fexqt/mainwin.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | #from numpy import * from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * from spectrogram import Spectrogram import time class MainWin(QMainWindow): def __init__(self, config): super().__init__() self.fnames = [ '/home/jmcclure/data/warb/batch4/4/4355.wav' ] self.conf = config.mainwin() self.spect_conf = config.spectrogram() self.init_actions() self.init_menubar() self.init_toolbar() self.init_statusbar() self.init_body(config.spectrogram()) self.setGeometry(0, 0, 800, 600) self.setWindowTitle("Fex: Frequency Excursion Calculator") self.setWindowIcon(QIcon('web.png')) # TODO if self.conf.maximized: self.setWindowState(Qt.WindowMaximized) self.show() QTimer.singleShot(20,self.next_spectrogram) # Todo handle drag & drop of sound files def update_status_fex(self, pex, tex): self.pex = pex self.tex = tex self.fex = pex / tex def update_status_text(self, x, y): self.status_text.setText("FEX: %.3f %0.3fs %0.1fKHz" %(self.fex,x,y)) def init_body(self, config): self.body = QWidget() self.body.setBackgroundRole(QPalette.AlternateBase) self.body.setAutoFillBackground(True) self.body.setContentsMargins(10,10,10,10) self.setCentralWidget(self.body) self.body.show() def next_spectrogram(self): if len(self.fnames) > 0: self.fname = self.fnames.pop(0) self.spect = Spectrogram(self.fname, self.body, self.spect_conf) self.spect.fexChanged.connect(self.update_status_fex) self.spect.mouseOver.connect(self.update_status_text) def init_actions(self): # https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html QIcon.setThemeName(self.conf.theme) # TODO icons are left out until I've identified a decent set #self.aQuit = QAction(QIcon.fromTheme('application-exit'), '&Quit', self) self.aQuit = QAction('&Quit', self) self.aQuit.setShortcut('Ctrl+Q') self.aQuit.setStatusTip('Quit Fex') self.aQuit.triggered.connect(qApp.quit) self.aNormal = QAction('&Normal', self) self.aNormal.setShortcut('Ctrl+X') self.aNormal.setStatusTip('Set Normal Mode') self.aNormal.triggered.connect(lambda: self.spect.mode_normal()) self.aCursor = QAction('Cursor', self) self.aCursor.setShortcut('Ctrl+C') self.aCursor.setStatusTip('Enable Cursor Tool') self.aCursor.triggered.connect(lambda: self.spect.mode_cursor()) self.aErase = QAction('Erase', self) self.aErase.setShortcut('Ctrl+E') self.aErase.setStatusTip('Eraser Tool To Remove Noise') self.aErase.triggered.connect(lambda: self.spect.mode_erase()) self.aZoom = QAction(QIcon.fromTheme('zoom-original'), '&Normal', self) self.aZoom.setShortcut('Ctrl+0') self.aZoom.setStatusTip('Fit Window') self.aZoom.triggered.connect(lambda: self.spect.mode_normal()) def init_menubar(self): self.menubar = self.menuBar() self.menubar.setVisible(self.conf.menubar) fileMenu = self.menubar.addMenu('&File') fileMenu.addAction(self.aQuit) fileTools = self.menubar.addMenu('&Tools') fileTools.addAction(self.aNormal) fileTools.addAction(self.aCursor) fileTools.addAction(self.aErase) def init_toolbar(self): self.toolbar = self.addToolBar('ToolBar1') self.toolbar.setVisible(self.conf.toolbar) self.toolbar.setToolButtonStyle(self.conf.toolbarstyle) self.toolbar.addAction(self.aQuit) self.toolbar.addSeparator() self.toolbar.addAction(self.aNormal) self.toolbar.addAction(self.aCursor) self.toolbar.addAction(self.aErase) self.toolbar.addSeparator() self.toolbar.addAction(self.aZoom) def init_statusbar(self): self.statusbar = self.statusBar() self.status_text = QLabel() self.statusBar().addPermanentWidget(self.status_text) # TODO add song count a/b |
Added fexqt/spectrogram.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import wave from numpy import * from scipy.signal import spectrogram from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * class Spectrogram(QWidget): fexChanged = pyqtSignal(float, float) mouseOver = pyqtSignal(float, float) def __init__(self, fname, parent, config): super().__init__(parent) self.conf = config self.fname = fname self.setMouseTracking(True) self.setGeometry(parent.contentsRect()) oldPos = QPoint() with wave.open(fname, 'rb') as song: channels, bs, samprate, nsamp, comp, compnam = song.getparams() # TODO checks data = fromstring(song.readframes(nsamp), dtype=uint16) f, t, amp = spectrogram(data, samprate, config.winfunc, config.window, config.overlap, detrend=False, mode = 'psd', return_onesided=True) # pseudo-bandpass filter bot = argmax(f > config.hipass) top = argmin(f < config.lopass) f = f[bot:top] amp = amp[bot:top,:] thresh = amp.max() / 10**(config.threshold / 10.0) thresh = 255.0 - thresh * 255.0 / amp.max() config.threshold = thresh amp = 255.0 - amp * 255.0 / amp.max() print("Avg pts per time bin = %s" %(sum(amp < thresh) / len(t))) f /= 1000.0 # create image # TODO only grab inside bandpass amp = flipud(amp) self.imgdata = array(amp, uint8) self.freq = flipud(f) self.time = t h, w = self.imgdata.shape self.image = QImage(self.imgdata.tobytes(), w, h, w, QImage.Format_Grayscale8) if self.conf.invert: self.image.invertPixels() self.fit() self.eraser = self.create_eraser(50, 150) self.cursorX = self.create_cursor(1, self.height() * 4) self.cursorY = self.create_cursor(self.width() * 4, 1) self.cursorX.hide() self.cursorY.hide() self.eraser.hide() self.show() def create_eraser(self, w, h): eraser = QWidget(self) eraser.setAttribute(Qt.WA_TransparentForMouseEvents) effect = QGraphicsOpacityEffect(eraser) effect.setOpacity(0.4) eraser.setGraphicsEffect(effect) eraser.setGeometry(0, 0, w, h) eraser.setAutoFillBackground(True) p = eraser.palette() p.setColor(eraser.backgroundRole(), Qt.yellow) eraser.setPalette(p) return eraser def mode_normal(self): self.unsetCursor() self.eraser.hide() self.cursorX.hide() self.cursorY.hide() def mode_cursor(self): self.setCursor(Qt.BlankCursor) self.eraser.hide() self.cursorX.show() self.cursorY.show() def mode_erase(self): self.setCursor(Qt.BlankCursor) self.cursorX.hide() self.cursorY.hide() self.eraser.show() def create_cursor(self, w, h): cursor = QWidget(self) cursor.setAttribute(Qt.WA_TransparentForMouseEvents) effect = QGraphicsOpacityEffect(cursor) effect.setOpacity(0.4) cursor.setGraphicsEffect(effect) cursor.setGeometry(0, 0, w, h) cursor.setAutoFillBackground(True) p = cursor.palette() p.setColor(cursor.backgroundRole(), Qt.red) cursor.setPalette(p) return cursor def paintEvent(self, e): qp = QPainter() qp.begin(self) qp.setRenderHint(QPainter.SmoothPixmapTransform) qp.drawImage(self.contentsRect(), self.image, self.image.rect()) r = QRect(0, 0, self.width() - 1, self.height() - 1) qp.setPen(QColor(self.conf.border)) qp.drawRect(r) path = QPainterPath() pfreq = 0.0 ptime = 0.0 pex = 0.0 tex = 0.0 qp.setPen(QColor(self.conf.line)) for x, y in enumerate(argmin(self.imgdata, 0)): if self.imgdata[y,x] > self.conf.threshold: continue t = self.time[x] f = self.freq[y] # TODO log10? if pfreq == 0.0: path.moveTo(x * self.sx, y * self.sy) else: path.lineTo(x * self.sx, y * self.sy) pex += sqrt((f - pfreq)**2 + (t - ptime)**2) tex += t - ptime ptime = t pfreq = f qp.drawPath(path) qp.end() self.fexChanged.emit(pex, tex) def fit(self): s = self.image.size() asp = s.width() / (s.height() * self.conf.ratio) if self.size().height() * asp > self.size().width(): self.resize(self.width(), self.width() / asp) else: self.resize(self.height() * asp, self.height()) def resizeEvent(self, e): self.sx = self.width() / self.image.width() self.sy = self.height() / self.image.height() def mousePressEvent(self, e): self.__pos = self.pos() self.__size = self.size() self.__epos = self.mapToParent(e.pos()) if e.button() == Qt.MiddleButton: self.fit() def mouseMoveEvent(self, e): x = e.pos().x() - self.eraser.width() / 2.0 y = e.pos().y() - self.eraser.height() / 2.0 self.eraser.move(x, y) x = e.pos().x() y = e.pos().y() self.cursorX.move(x, 0) self.cursorY.move(0, y) s = self.imgdata.shape x = clip(int(s[1] * e.pos().x() / self.width()), 0, self.time.size) y = clip(int(s[0] * e.pos().y() / self.height()), 0, self.freq.size) self.mouseOver.emit(self.time[x], self.time[y]) if e.buttons() and self.cursorX.isVisible(): self.mouseCursorEvent(e) elif e.buttons() and self.eraser.isVisible(): self.mouseEraseEvent(e) elif e.buttons(): self.mouseNormalEvent(e) def mouseCursorEvent(self, e): pass def mouseEraseEvent(self, e): pass def mouseNormalEvent(self, e): if e.buttons() == Qt.LeftButton: diff = self.mapToParent(e.pos()) - self.__epos + self.__pos self.move(diff) elif e.buttons() == Qt.RightButton: diff = self.mapToParent(e.pos()) - self.__epos w = self.__size.width() + diff.x() h = self.__size.height() + diff.y() self.resize(w, h) def mouseReleaseEvent(self, e): self.cursorX.resize(1, self.height() * 4) self.cursorY.resize(self.width() * 4, 1) |
Added setup.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from setuptools import setup def readme(): with open('README.md') as f: return f.read() setup(name='fexqt', version='0.1a', description='Frequency Excursion Calculator', long_description=readme(), classifiers=[ 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Science/Research', 'Programming Language :: Python :: 3', 'Topic :: Multimedia :: Sound/Audio :: Analysis', 'Topic :: Scientific/Engineering :: Visualization' ], url='http://jessemcclure.org', author='Jesse McClure', author_email='jmcclure@broadinstitute.org', license='MIT', packages=['fexqt'], install_requires=['PyQt5','scipy'], scripts=['bin/fex'], include_package_data=True, zip_safe=False) |