class BinauralBeat(Block): volume = Input() def __init__(self, **config): super(BinauralBeat, self).__init__(**config) self.server = pyo.Server(buffersize=1024).boot() centerFreq = pyo.Sig(256) binauralFreq = pyo.Sine(freq=0.05, add=10.5, mul=1.5) left = pyo.Sine(freq=centerFreq - binauralFreq / 2) right = pyo.Sine(freq=centerFreq + binauralFreq / 2) left.out(chnl=0) right.out(chnl=1) #left = pyo.PinkNoise().mix(2).out() import thread thread.start_new_thread(self.server.start, ()) self.left = left self.right = right def process(self): vol = float(self.volume.buffer[-1] / 3500.0) vol = min(vol, 1.0) self.left.mul = self.right.mul = vol
class BandPass(Block): input = Input() order = Range(low=2, high=8, value=5) lo = Float hi = Float nyquist = Float(125) def init(self, lo, hi, input): self.lo = lo self.hi = hi self.input = input self.output = Signal() @on_trait_change("lo,hi,order,nyquist") def _range_changed(self): b,a = iirfilter(self.order, (self.lo / self.nyquist, self.hi / self.nyquist)) self._filter_b, self._filter_a = b,a def process(self): buffer = self.input.buffer[-self.order*3:] filt = lfilter(self._filter_b, self._filter_a, buffer) self.output.append(filt[-1:]) self.output.process() @property def range(self): return self.lo, self.hi
class DominantFrequency(Block): input = Input() chunk_size = 256 def init(self, input): self.input = input self.cnt = 0 self.window = np.hanning(self.chunk_size) self.freq = Signal() def process(self): self.cnt += 1 if self.cnt > 20: self.cnt = 0 else: return C = np.fft.rfft(self.input.buffer[-self.chunk_size:] * self.window) C = abs(C) Fs = 250.0 def index_max(values): return max(xrange(len(values)),key=values.__getitem__) freq = index_max(C) freq = freq / Fs self.freq.append([freq]) self.freq.process()
class Expression(Block): args = List(Input) input = Input() def init(self, func, *args): # HACK: should not directly reference lupa here import lupa if lupa.lua_type(args) == 'table': args = args.values() print ('Expression: ', func, args) self.func = func self.args = list(args) self.output = Signal() # Workaround for list event bug self.input = self.args[0] def _input_changed(self): self.output.copy_traits(self.input, ['label', 'color']) def process(self): args = [chan.last for chan in self.args] self.output.append([self.func(*args)]) self.output.process()
class NotchDelay(Block): input = Input() frequency = Float(50.0) samplerate = Int(250) def init(self, input): self.readjust() self.input = input self.output = Signal() self.delayed = Signal() @on_trait_change("frequency,samplerate") def readjust(self): delay = self.samplerate / self.frequency / 2 delay = 3 self.delayline = [0] * int(delay) def process(self): for sample in self.input.new: self.delayline[1:] = self.delayline[:-1] self.delayline[0] = sample self.output.append([self.delayline[-1] + sample]) self.delayed.append([self.delayline[-1] + self.delayline[-2]]) self.output.process() self.delayed.process()
class DCBlock(Block): input = Input() def __init__(self, input, **config): self.dc = Signal() self.ac = Signal() super(DCBlock, self).__init__(**config) self.input = input def _input_changed(self): traits = self.input.trait_get(['label', 'color']) self.ac.trait_set(**traits) def process(self): for x in self.input.new: new_dc = self.dc.buffer[-1] * 0.95 + x * 0.05 self.dc.append([new_dc]) self.dc.process() self.ac.append([x - self.dc.buffer[-1] for x in self.input.new]) self.ac.process()
class ClockAnalyzer(Block): input = Input() alpha = 0.5 def init(self, input): self.input = input self.last_time = monotonic() self.time_diff = Signal() self.sample_rate = Signal() self.jitter = Signal() def process(self): #self.input.buffer_size = 1024 #if len(self.input.buffer) < 1024: return new_time = monotonic() diff = new_time - self.last_time self.last_time = new_time self.time_diff.append([diff]) self.time_diff.process() sample_rate = 1.0 / moving_average_exp(self.alpha, self.time_diff.buffer) self.sample_rate.append([sample_rate]) self.sample_rate.process() period = 1.0 / sample_rate jitter = abs(diff - period) * 1000 self.jitter.append([jitter]) self.jitter.process()
class JitterBuffer(Block): input = Input() buffer_size = 5 def init(self, input, sample_rate): self.input = input self.output = Signal() self.period = 1.0 / sample_rate self.buffer = [] self.started = False def process(self): newbuf = self.input.new newbuf.extend(self.buffer) self.buffer = newbuf if not self.started and len(self.buffer) > self.buffer_size: self.started = True import threading threading.Thread(target=self.run).start() def clock_sample(self): if len(self.buffer): self.output.append([self.buffer.pop()]) self.output.process() else: print('Warning: JitterBuffer ran empty') def run(self): nperiod = int(self.period * 1000000000) ncomputation = nperiod // 10 nconstraint = ncomputation * 2 print(self.period, nperiod, ncomputation, nconstraint) set_realtime(nperiod, ncomputation, nconstraint) start = monotonic() while (True): loop_begin = monotonic() #print ('diff time %f' % (loop_begin - start)) wait_time = start + self.period - monotonic() if wait_time <= 0.000001: start += self.period self.clock_sample() loop_end = monotonic() #print ('clock process took %f seconds' % (loop_end - loop_begin)) elif wait_time > self.period / 2: #print ('%f seconds left, sleeping' % wait_time) thread_yield() else: pass
class Spectrograph(Block): CHUNKSZ = Int(256) input = Input() def __init__(self, name, **config): self.img = pg.ImageItem() self.plot_widget = pg.PlotWidget(title=name) self.plot_widget.block = self self.plot_widget.addItem(self.img) #self.img_array = np.zeros((1000, self.CHUNKSZ/2+1)) self.img_array = np.zeros((1000, 48)) # bipolar colormap pos = np.array([0., 1., 0.5, 0.25, 0.75]) color = np.array([[0,255,255,255], [255,255,0,255], [0,0,0,255], (0, 0, 255, 255), (255, 0, 0, 255)], dtype=np.ubyte) cmap = pg.ColorMap(pos, color) lut = cmap.getLookupTable(0.0, 1.0, 256) self.img.setLookupTable(lut) self.img.setLevels([-2,7]) FS = 48 * 2 freq = np.arange((self.CHUNKSZ/2)+1)/(float(self.CHUNKSZ)/FS) yscale = 1.0/(self.img_array.shape[1]/freq[-1]) self.img.scale((1./FS)*self.CHUNKSZ, yscale) self.plot_widget.setLabel('left', 'Frequency', units='Hz') self.win = np.hanning(self.CHUNKSZ) #self.show() super(Spectrograph, self).__init__(**config) def process(self): # normalized, windowed frequencies in data chunk spec = np.fft.rfft(self.input.buffer*self.win) / self.CHUNKSZ spec = spec[0:48] # get magnitude psd = abs(spec) # convert to dB scale psd = np.log10(psd) #self.img.setLevels([min(psd), max(psd)]) #print (psd) # roll down one and replace leading edge with new data self.img_array = np.roll(self.img_array, -1, 0) self.img_array[-1:] = psd self.img.setImage(self.img_array, autoLevels=False) def widget(self): return self.plot_widget def updateGUI(self): pass
class Normalizer(Block): input = Input() time_factor = Float(0.9999) def __init__(self, input, **config): self.output = Signal() super(Normalizer, self).__init__(**config) self.min = 0.0 self.max = 1.0 self.input = input def process(self): min = np.min(self.input.buffer) / 2 self.min = self.min * self.time_factor + min * (1.0 - self.time_factor) max = np.max(self.input.buffer) * 2 self.max = self.max * self.time_factor + max * (1.0 - self.time_factor) out = (np.array(self.input.new) - self.min) / (self.max - self.min) * 2 self.output.append(out) self.output.process()
class NotchFilter(Block): input = Input() frequency = Float(50.0) # Find out what module_pole is and rename it module_pole = Float(0.9) nyquist = Float(125.0) @on_trait_change("frequency,module_pole") def compute_filter(self): theta = 2 * np.pi * self.frequency / self.nyquist * 2 zero = np.exp(np.array([1j, -1j]) * theta) pole = self.module_pole * zero self._filter_b = np.poly(zero) self._filter_a = np.poly(pole) def init(self, input): self.compute_filter() self.input = input self.output = Signal() def process(self): buffer = self.input.buffer assert self.input.new_samples == 1 filt = lfilter(self._filter_b, self._filter_a, buffer) self.output.append(filt[-1:]) self.output.process()
class MIDIDrum(Block): pitch = Input(default=440) velocity = Input(default=1.0) trigger = Input() def init(self, channel=0, port_name='drum'): from rtmidi2 import MidiOut self.channel = channel self.midi = MidiOut().open_virtual_port(port_name) def process(self): if self.trigger.posedge: pitch = self.pitch vel = self.velocity if not pitch: pitch = 100 if not vel: vel = 1.0 self.midi.send_noteon(self.channel, pitch, vel)
class Oscilloscope(Block): autoscale = Bool(True) #yrange = List(Float, [0.0, 1.0], minlen=2, maxlen=2) yrange = Tuple(Float, Float) channels = List(Input()) name = Str() def __init__(self, name, channels, **config): self._plot_widget = pg.PlotWidget(title=name) self._plot_widget.block = self self.plots = {} self.name = name self.scale_changed() # Workaround for lua tables if hasattr(channels, 'values'): channels = channels.values() channels = list(channels) super(Oscilloscope, self).__init__(channels=channels, **config) @on_trait_change('channels[]') def channels_changed(self, object, name, old, new): for channel in old: del self.plots[channel] for channel in new: plot = self._plot_widget.plot() plot.setPen(QtGui.QColor(channel.color)) self.plots[channel] = plot @on_trait_change('autoscale,yrange') def scale_changed(self): self._plot_widget.setYRange(*self.yrange) self._plot_widget.enableAutoRange('y', 0.95 if self.autoscale else False) def widget(self): return self._plot_widget def updateGUI(self): for channel in self.plots: plot = self.plots[channel] plot.setData(channel.buffer) def process(self): pass
class Averager(Block): input = Input() def __init__(self, input): self.output = Signal() self.average = 0.0 self.factor = 0.99 super(Averager, self).__init__(input=input) def process(self): for x in self.input.new: self.average = self.average * self.factor + x * (1.0 - self.factor) self.output.append([self.average]) self.output.process()
class PulseAnalyzer(Block): input = Input() def init(self, input): self.input = input self.output = Signal() self.bpm = Signal() self.gradient = Signal() self.pulse = Signal() self.last_beat = -1 self.timestamp = 0 def _input_changed(self): self.output.copy_traits(self.input, ['label', 'color']) def process(self): avg = np.average(self.output.buffer) max = np.max(self.output.buffer) self.timestamp += 1 self.gradient.append([self.input.buffer[-1] - self.input.buffer[-2]]) buf = np.power(self.gradient.buffer[-50:], 2) s = np.sum(np.hanning(50)[:50] * buf) self.output.append([s]) for i, sample in enumerate(self.output.new): new = 1.0 if sample > max * 0.7 else 0.0 self.pulse.append([new]) if new > self.pulse.buffer[-2]: diff = self.timestamp - self.last_beat bpm = 1.0 / (diff / 250.) * 60 self.bpm.append([bpm]) self.bpm.process() print('beat event', diff, bpm, self.timestamp, self.last_beat) self.last_beat = self.timestamp self.output.process() self.gradient.process()
class NumberBox(Block): input = Input() def init(self, name, input): self.name = name self.input = input self.label = QtGui.QLabel() self.label.setStyleSheet("QLabel { background-color : black; color : white; }") def widget(self): return self.label def updateGUI(self): self.label.setText('%.2f' % self.input.buffer[-1]) pass def process(self): pass
class HeartAnalyzer(Block): input = Input() def init(self): self.beat = Signal('Beat Event') self.bpm = Signal("Beats per Minute") def process(self): square = np.array(self.input.buffer)**2 threshold = np.percentile(square, 98) if np.max(self.input.buffer[:self.input.new_samples]) > threshold: self.beat.append([1.0]) print 'beat event' else: self.beat.append([0.0]) self.beat.process()
class RMS(Block): input = Input() avg_size = Int(42) def init(self, input): self.output = Signal() self.input = input def _input_changed(self): self.output.copy_traits(self.input, ['label', 'color']) def process(self): buf = np.array(self.input.buffer[-self.avg_size:]) rms = sum(buf ** 2) / len(buf) avg = np.sqrt(rms) self.output.append([avg]) self.output.process()
class MPlayerControl(Block): from mplayer import Player enable = Input() def init(self, path): self.player = self.Player('-input default-bindings') self.player.loadfile(path) self.player.pause() self.player.frame_drop(2) self.playing = False def process(self): enable = self.enable.last if self.playing and not enable: self.playing = False self.player.pause() if not self.playing and enable: self.playing = True self.player.pause()
class Trendline(Block): input = Input() interval = Int(25) def __init__(self, input): self.output = Signal(buffer_size=1000) self.cnt = 0 super(Trendline, self).__init__(input=input) def _input_changed(self): traits = self.input.trait_get(['label', 'color']) self.ac.trait_set(**traits) def process(self): self.cnt += self.input.new_samples if self.cnt >= self.interval: self.cnt = 0 avg = sum(self.input.buffer[-self.interval:]) / self.interval self.output.append([avg]) self.output.process()
class BEServer(Block): channels = List(Input()) def init(self, channels): global main_socket if not main_socket: main_socket = socket.socket() main_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Disable Nagle's algorithm main_socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) main_socket.bind(('', 2666)) main_socket.listen(1) self.channels = list(channels.values()) threading.Thread(target=self.socket_thread).start() # Add header and send to clients def _send_packet(self, packet): global client_socket if not client_socket: return header = b'\xce\xfa\xce\xfe' header += struct.pack('<i', 8 + len(packet)) packet = header + packet packet += b'\n' client_socket.send(packet) def _send_data(self, index, data): #print ('send_data to', index, data) packet = bytes() packet += struct.pack('<i', CMD_CHANNEL_DATA) packet += struct.pack('<i', index) packet += struct.pack('<i', len(data)) for sample in data: packet += struct.pack('<d', sample) self._send_packet(packet) def _start(self): packet = struct.pack('<i', CMD_START) self._send_packet(packet) def _stop(self): packet = struct.pack('<i', CMD_STOP) self._send_packet(packet) def _add_channel(self, channel, index): packet = bytes() packet += struct.pack('<i', CMD_ADD_CHANNEL) packet += struct.pack('<i', index) packet += struct.pack('<d', channel.sample_rate) name = 'Channel %d' % (index + 1) if hasattr(channel, 'name'): name = channel.name name = name.encode('utf-8') + b'\0' packet += struct.pack('<i', len(name)) packet += name self._send_packet(packet) print('add channel', name, index) def _remove_channel(self, index): packet = bytes() packet += struct.pack('<i', CMD_REMOVE_CHANNEL) packet += struct.pack('<i', index) self._send_packet(packet) def socket_thread(self): try: global client_socket if not client_socket: client_socket = main_socket.accept()[0] self._stop() #for i in range(32): # self._remove_channel(i) for idx, ch in enumerate(self.channels): self._add_channel(ch, idx) self._start() sent_timestamp = self.channels[0].timestamp while True: ts = self.channels[0].timestamp if sent_timestamp < ts: l = ts - sent_timestamp sent_timestamp += l #l = min(l, 2) ## Assume same clocking on all signals for idx, ch in enumerate(self.channels): newdata = ch.buffer[-l:] self._send_data(idx, newdata) time.sleep(0) #rt_thread.thread_yield() except BrokenPipeError: client_socket = None self.socket_thread() def __del__(self): print('BEServer closing socket...') global main_socket, client_socket if main_socket: main_socket.close() if client_socket: client_socket.close() def process(self): pass
class Waterfall(Block): input = Input() history_size = 100 window_size = Int(512) lo, hi = CFloat(1), CFloat(30) #align = Trait('bottom', Enum('left', 'right', 'top', 'bottom')) logarithm = Bool(False) sampling_rate = Float(250) update_rate = 20 def init(self, name): self.name = name self.autoscale_button = QtGui.QCheckBox('Autoscale') self.autoscale_button.setCheckState(True) self.welch_button = QtGui.QCheckBox('Use Welch') self.welch_button.setCheckState(False) self.plot_widget = pg.PlotWidget(title=name) self.plot_widget.block = self self.plot_widget.setLabel('bottom', 'Frequency', units='Hz') self.plot_widget.setLabel("left", "Time", units='s') #TODO: listener for this #self.plot_widget.setYRange(-self.history_size / self.sampling_rate, 0) #self.plot_widget.setLimits(xMin=0, yMax=0) #self.plot_widget.showButtons() #self.waterfallPlotWidget.setAspectLocked(True) #self.waterfallPlotWidget.setDownsampling(mode="peak") #self.waterfallPlotWidget.setClipToView(True) # Setup histogram widget (for controlling waterfall plot levels and gradients) self.histogram = pg.PlotWidget(title='Histogram') self.waterfallHistogram = pg.HistogramLUTItem() self.histogram.addItem(self.waterfallHistogram) self.waterfallHistogram.gradient.loadPreset("flame") #self.waterfallHistogram.setHistogramRange(-50, 0) #self.waterfallHistogram.setLevels(-50, 0) self.waterfallImg = pg.ImageItem() self.plot_widget.clear() self.plot_widget.addItem(self.waterfallImg) self.waterfallHistogram.setImageItem(self.waterfallImg) self.setup_range() self.update_counter = 0 @on_trait_change('window_size,input') def size_input(self): if self.input.buffer_size < self.window_size: self.input.buffer_size = self.window_size @on_trait_change('window_size,lo,hi,history_size,sampling_rate,update_rate') def setup_range(self): self.window = np.hanning(self.window_size) nyquist = self.sampling_rate / 2 bins = self.window_size / 2 + 1 freqs = np.linspace(0, nyquist, bins) freq_step = nyquist / bins # Calculate bin indices self.lo_index = int(np.floor(self.lo / freq_step)) self.hi_index = int(np.ceil(self.hi / freq_step)) # Snap values to actual bin frequencies self.lo = freq_step * self.lo_index self.hi = freq_step * self.hi_index display_bins = self.hi_index - self.lo_index self.waterfallImgArray = np.zeros((self.history_size, display_bins)) history_time = self.history_size / self.sampling_rate * self.update_rate self.waterfallImg.resetTransform() self.waterfallImg.setPos(self.lo, -history_time) self.waterfallImg.scale((self.hi - self.lo) / display_bins, history_time / self.history_size) #self.plot_widget.setYRange(-self.history_size / self.sampling_rate, 0) def process(self): self.update_counter += 1 if self.update_counter == self.update_rate: self.update_counter = 0 else: return if self.welch_button.checkState(): f, C = welch(self.input.buffer, fs=250, nperseg=self.window_size, scaling='spectrum') else: C = np.fft.rfft(self.input.buffer[-self.window_size:] * self.window) C = abs(C) #C = C*C C = C[self.lo_index: self.hi_index] if self.logarithm: C = np.log(C) # Roll down one and replace leading edge with new data self.waterfallImgArray = np.roll(self.waterfallImgArray, -1, axis=0) self.waterfallImgArray[-1] = C def updateGUI(self): self.waterfallImg.setImage(self.waterfallImgArray.T, autoLevels=self.autoscale_button.checkState(), #autoRange=False ) # TODO: This is overwritten by widget member def widget(self): config_widget = QtGui.QWidget() layout = QtGui.QHBoxLayout() layout.addWidget(self.autoscale_button) layout.addWidget(self.welch_button) # layout.addWidget(self.histogram) config_widget.setLayout(layout) layout = QtGui.QVBoxLayout() layout.addWidget(self.plot_widget) layout.addWidget(config_widget) main_widget = QtGui.QWidget() main_widget.setLayout(layout) main_widget.block = self return main_widget
class BarSpectrogram(Block): input = Input() bins = Int(256) lo, hi = CFloat(1), CFloat(30) align = Trait('bottom', Enum('left', 'right', 'top', 'bottom')) yrange = Int(65000) ratio = Bool(False) sampling_rate = Float(250) def init(self, name, input): self.plot = pg.PlotWidget(title=name) self.plot.block = self self.plot.setLabel('bottom', 'Frequency', units='Hz') self.bars = pg.BarGraphItem() self.setup_range() # TODO: Better autoranging features #self.plot.enableAutoRange('xy', False) self.plot.setYRange(0, self.yrange) self.input = input self.name = name @on_trait_change('bins,lo,hi') def setup_range(self): self.win = np.hanning(self.bins) FS = self.sampling_rate #num_bars = int(round((self.bins - 1) * (self.hi - self.lo) / FS)) num_bars = len(np.zeros(self.bins)[self.lo: self.hi]) #print 'num_bars', num_bars, self.bins * (self.hi - self.lo) / FS x = np.linspace(self.lo, self.hi, num_bars) self.bars = pg.BarGraphItem(x=x, height=range(num_bars), width=1.0) self.bars.setOpts(brushes=[pg.hsvColor(float(x) / num_bars) for x in range(num_bars)]) self.plot.addItem(self.bars) def process(self): pass def updateGUI(self): C = np.fft.rfft(self.input.buffer[-self.bins:] * self.win) C = abs(C) lo, hi = self.lo, self.hi data = C[lo : hi] if self.ratio: data = data / sum(C) self.bars.setOpts(height=data) def widget(self): return self.plot
class Threshold(Block): input = Input() average_period = Float(0.35) epoch = Float(3.0) auto_mode = Bool(True) mode = Enum('increase', 'decrease', 'range') auto_target = Float(0.90) low_target = Float(0.90) high_target = Float(0.90) def init(self, name, input): self.FS = 250 self.name = name epoch_samples = int(self.FS * self.epoch) self.signal = Signal(buffer_size=epoch_samples) self.passfail = Signal() self.ratio = Signal() self.threshold = 1.0 self.high_threshold = 0.0 self.calc_cnt = 0 self.bar = QtGui.QProgressBar(orientation=QtCore.Qt.Vertical) self.slider = QtGui.QSlider() self.slider.setRange(0, 17) self.bar.setRange(0, 17) self.pass_palette = self.bar.palette() self.input = input if isinstance(self.input.color, QtGui.QColor): self.color = self.input.color else: self.color = QtGui.QColor(self.input.color) self.bar.setStyleSheet(""" QProgressBar::chunk { background: red; } QProgressBar::chunk[pass='******'] { background: %s ; } """ % self.color.name()) self.button = QtGui.QPushButton("config") self.button.clicked.connect(self.configure_traits) self._widget = ThresholdWidget(self) def widget(self): return self._widget w = QtGui.QGroupBox() w.setTitle(self.name) l = QtGui.QGridLayout() l.addWidget(self.bar, 0, 0) l.addWidget(self.slider, 0, 1) l.addWidget(self.button, 1, 0, 1, 2) w.setLayout(l) w.block = self return w def updateGUI(self): self._widget.update() return self.bar.setValue(self.signal.last) self.bar.setProperty('pass', self.passfail.last) self.bar.style().polish(self.bar) if self.auto_mode: self.slider.setValue(self.threshold) #print type(self.threshold), self.threshold, self.nameano def process(self): assert self.input.new_samples == 1 avg_period_samples = int(self.average_period * self.FS) avg = sum(self.input.buffer[-avg_period_samples:]) / avg_period_samples self.signal.append([avg]) self.signal.process() self.calc_cnt += 1 if self.auto_mode and self.calc_cnt >= avg_period_samples: self.calc_cnt = 0 if self.mode == 'decrease': self.threshold = np.percentile(self.signal.buffer, 100 * self.auto_target) elif self.mode == 'increase': self.threshold = np.percentile(self.signal.buffer, 100 - 100 * self.auto_target) else: self.high_threshold = np.percentile(self.signal.buffer, self.high_target) self.threshold = np.percentile(self.signal.buffer, 100 - 100 * self.low_target) success = False self.ratio.append([avg / self.threshold]) self.ratio.process() if self.mode == 'decrease': if avg < self.threshold: success = True elif self.mode == 'increase': if avg > self.threshold: success = True else: if avg > self.threshold and avg < self.high_threshold: success = True self.passfail.append([float(success)]) self.passfail.process()