Fex

Check-in [2dab5527e8]

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:2dab5527e89a9b24efd484d4089d93df68352e79249458d7b6b8012616b81505
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)