├── README.md ├── config.json ├── detectors ├── meteor_echo.py ├── meteor_echo_avg.py └── noise_level.py ├── pysdr-recviewer ├── pysdr-waterfall ├── pysdr ├── __init__.py ├── commands.py ├── console.py ├── events.py ├── ext.c ├── graph.py ├── input.py ├── overlay.py ├── persistence.py ├── recviewer.py ├── setup.py └── waterfall.py ├── scipy ├── __init__.py └── io │ ├── __init__.py │ └── wavfile.py ├── setup.py ├── tools ├── 3dwf.py ├── README.md ├── README.txt └── midi_cmd.c └── whistle ├── Makefile ├── wav.sh ├── whistle.c └── whistle.h /README.md: -------------------------------------------------------------------------------- 1 | # PySDR 2 | 3 | PySDR displays spectral waterfall, a visualization of signal's frequency spectrum over time. It is developed for SDR-related applications, but can be fed any equidistantly-sampled complex-valued signal for which it makes sense. 4 | 5 | ![stretching](https://cloud.githubusercontent.com/assets/382160/24999343/6fe18d4e-203d-11e7-9c5e-1949dc2f508b.gif) 6 | 7 | A live waterfall is launched by `pysdr-waterfall`. It connects to the JACK audio system and takes its input from there, or, if the flag `-r` is passed, it expects its input on the standard input in the form of an endless stream of 32-bit interleaved floats. 8 | 9 | $ pysdr-waterfall -h 10 | usage: pysdr-waterfall [-h] [-b BINS] [-H HEIGHT] [-o OVERLAP] [-j NAME] 11 | [-r RATE] [-d ARGS] [-p FILENAME] 12 | 13 | Plot live spectral waterfall of a quadrature signal. 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | -b BINS, --bins BINS number of FFT bins (default: 4096) 18 | -H HEIGHT, --height HEIGHT 19 | minimal height of the waterfall in seconds 20 | (default corresponds to 1024 windows) 21 | -o OVERLAP, --overlap OVERLAP 22 | overlap between consecutive windows as a 23 | proportion of the number of bins (default: 0.75) 24 | -j NAME, --jack NAME feed signal from JACK and use the given client 25 | name (by default, with name 'pysdr') 26 | -r RATE, --raw RATE feed signal from the standard input, expects 2 27 | channel interleaved floats with the given sample- 28 | rate 29 | -d ARGS, --detector ARGS 30 | attach the given detector script, expects to be 31 | given the script filename followed by arguments 32 | for the script, all joined by spaces and passed on 33 | the command-line as one quoted argument 34 | -p FILENAME, --persfn FILENAME 35 | a file in which to preserve the visualization 36 | parameters that come from interactive 37 | manipulation, i.e. the visible area of the 38 | waterfall and the selected magnitude range (save 39 | triggered by pressing 'p') 40 | 41 | 42 | ### Example usage with sox (cross-platform) 43 | 44 | $ sox -d -e floating-point -b 32 -r 48000 -t raw --buffer 1024 - | ./pysdr-waterfall -r 48000 45 | 46 | ### Example usage with ALSA 47 | 48 | $ arecord -f FLOAT_LE -c 2 -r 44100 --buffer-size 1024 | pysdr-waterfall -r 44100 49 | 50 | ## Record Viewer 51 | 52 | There's also `pysdr-reciewer`, which displays spectral waterfall of short recordings. The recordings are expected to be either WAV files, or FITS files in the format produced by [Radio Observer](https://github.com/MLAB-project/radio-observer). The number of frequency bins reflects the aspect ratio of the waterfall, and so is interactive. 53 | 54 | ### Usage 55 | 56 | $ pysdr-recviewer path/to/recording 57 | 58 | ## Dependencies 59 | 60 | ### Ubuntu 61 | 62 | $ sudo apt-get install python3-numpy python3-opengl python3-dev libjack-jackd2-dev python3-pil 63 | 64 | ## In-place build 65 | 66 | The package has a binary component which has to be built on the target machine. (It is not needed for `pysdr-waterfall` at the moment.) 67 | 68 | For usage without installation, do an in-place build first. 69 | 70 | $ python setup.py build_ext --inplace 71 | 72 | ## Installation 73 | 74 | $ python setup.py install 75 | 76 | ## Supported designs 77 | 78 | ### Radio Meteor Detection Station 79 | 80 | PySDR is developed to be used, among others, with the RMDS designs by the MLAB project. 81 | 82 | [Technical description](http://wiki.mlab.cz/doku.php?id=en:rmds) 83 | 84 | [Purchase from UST](https://www.tindie.com/products/ust/radio-meteor-detection-station-rmds/) 85 | 86 | ## License 87 | 88 | Everything in this repository is GNU GPL v3 licensed. 89 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mag_range": [ 3 | -20.444444444444443, 4 | 4.0 5 | ], 6 | "view.origin_y": 11.222386639253955, 7 | "view.origin_x": -6400.031190094295, 8 | "view.scale_x": 15148.75133882347, 9 | "view.scale_y": 1043.0578416363528 10 | } 11 | -------------------------------------------------------------------------------- /detectors/meteor_echo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | avg_spread_bin = freq2bin(100) - freq2bin(0) 4 | timeout_nrows = int(1.6 / row_duration) 5 | 6 | ongoing_event = False 7 | last_detect_row = None 8 | meteor_treshold = 0.7 9 | 10 | def lin_spectrum_pass(row, spectrum): 11 | global ongoing_event, last_detect_row 12 | 13 | (peak_pow, peak_bin) = peak(freq2bin(10500), freq2bin(10700), spectrum) 14 | avg_pow = np.average(spectrum[peak_bin - avg_spread_bin:peak_bin + avg_spread_bin]) 15 | noise_pow = noise(spectrum[freq2bin(11000):freq2bin(11500)]) 16 | 17 | sn = np.log(avg_pow / noise_pow) 18 | plot("sn", sn) 19 | 20 | if sn > meteor_treshold: 21 | last_detect_row = row 22 | if not ongoing_event: 23 | ongoing_event = (row - int(0.5 / row_duration), row + timeout_nrows, 24 | peak_bin - avg_spread_bin, peak_bin + avg_spread_bin, 25 | "@ %.3f kHz" % (bin2freq(peak_bin) / 1000)) 26 | ongoing_event = (ongoing_event[0], row + timeout_nrows) + ongoing_event[2:5] 27 | 28 | emit_event("mlab.aabb_event.meteor_echo", ongoing_event) 29 | elif ongoing_event and (row - last_detect_row) > timeout_nrows: 30 | ongoing_event = None 31 | -------------------------------------------------------------------------------- /detectors/meteor_echo_avg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | avg_spread_bin = freq2bin(100) - freq2bin(0) 4 | timeout_nrows = int(1.6 / row_duration) 5 | 6 | ongoing_event = False 7 | last_detect_row = None 8 | meteor_treshold = 0.7 9 | 10 | def run(row, spectrum): 11 | global ongoing_event, last_detect_row 12 | 13 | (peak_pow, peak_bin) = peak(freq2bin(26400), freq2bin(26600), spectrum) 14 | avg_pow = np.average(spectrum[freq2bin(26450):freq2bin(26550)]) 15 | noise_pow = np.average(spectrum[freq2bin(25000):freq2bin(26000)]) 16 | 17 | sn = np.log(avg_pow / (noise_pow)) 18 | plot("sn", sn) 19 | 20 | if sn > meteor_treshold: 21 | last_detect_row = row 22 | if not ongoing_event: 23 | ongoing_event = (row - int(0.5 / row_duration), row + timeout_nrows, 24 | peak_bin - avg_spread_bin, peak_bin + avg_spread_bin, 25 | "@ %.3f kHz" % (bin2freq(peak_bin) / 1000)) 26 | ongoing_event = (ongoing_event[0], row + timeout_nrows) + ongoing_event[2:5] 27 | 28 | emit_event("mlab.aabb_event.meteor_echo", ongoing_event) 29 | elif ongoing_event and (row - last_detect_row) > timeout_nrows: 30 | ongoing_event = None 31 | -------------------------------------------------------------------------------- /detectors/noise_level.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import math 4 | from pymlab import config 5 | 6 | def reduce(a, div=4, start=1, stop=3): 7 | return np.sum(np.sort(a, axis=0)[len(a)*start/div:len(a)*stop/div], axis=0) / (len(a) / 2) 8 | 9 | def coroutine(): 10 | def sleep(time): 11 | for i in xrange(int(math.ceil(float(time) / row_duration))): 12 | yield 13 | 14 | if len(args) != 5: 15 | raise Exception("usage: pysdr-waterfall [OTHER_PYSDR_ARGS] -d 'detectors/noise_level.py I2C_CONFIG_FILENAME OUTPUT_FILENAME START_HZ STOP_HZ STEP_HZ'") 16 | 17 | cfg = config.Config() 18 | cfg.load_python(args[0]) 19 | cfg.initialize() 20 | fgen = cfg.get_device('clkgen') 21 | fgen.reset() 22 | 23 | for i in sleep(3.0): 24 | yield 25 | 26 | fgen = cfg.get_device('clkgen') 27 | fgen.recall_nvm() 28 | 29 | for i in sleep(2.0): 30 | yield 31 | 32 | freqs = xrange(int(args[2]), int(args[3]), int(args[4])) 33 | 34 | nmeas_rows = int(math.ceil(float(1.0) / row_duration)) 35 | arr = np.zeros(nmeas_rows, dtype=np.float32) 36 | 37 | with file(args[1], 'w') as outfile: 38 | for freq in freqs: 39 | fgen.recall_nvm() 40 | print "resetting" 41 | for i in sleep(0.2): 42 | yield 43 | 44 | freq_mhz = float(freq) / 1000000 45 | fgen.set_freq(10., freq_mhz * 2) 46 | print "setting freq %f" % freq_mhz 47 | for i in sleep(1.0): 48 | yield 49 | 50 | row, _s, _n = yield 51 | 52 | emit_event("mlab.aabb_event.measurement_area", (row, row + nmeas_rows, 0, 4096, "%f MHz" % (freq_mhz,))) 53 | 54 | for i in xrange(nmeas_rows): 55 | _r, _s, noise_lvl = yield 56 | arr[i] = noise_lvl 57 | 58 | noise_lvl_sum = reduce(arr) 59 | 60 | print "for freq %f, noise level is %f" % (freq_mhz, noise_lvl_sum) 61 | outfile.write("\t%f\t%f\n" % (freq_mhz, noise_lvl_sum)) 62 | outfile.flush() 63 | 64 | coroutine_inst = None 65 | 66 | def log_spectrum_pass(row, spectrum): 67 | global coroutine_inst 68 | 69 | if coroutine_inst is None: 70 | coroutine_inst = coroutine() 71 | coroutine_inst.send(None) 72 | 73 | noise_lvl = reduce(spectrum) 74 | plot("noise", noise_lvl / 5.0) 75 | try: 76 | coroutine_inst.send((row, spectrum, noise_lvl)) 77 | except StopIteration: 78 | if __name__ == "__main__": 79 | sys.exit(0) 80 | 81 | def process(sig_input, nbins, overlap): 82 | window = 0.5 * (1.0 - np.cos((2 * math.pi * np.arange(nbins)) / nbins)) 83 | 84 | process_row = 0 85 | ringbuf = np.zeros(nbins * 4, dtype=np.complex64) 86 | ringbuf_edge = nbins 87 | readsize = nbins - overlap 88 | 89 | while True: 90 | if (ringbuf_edge + readsize > len(ringbuf)): 91 | ringbuf[0:overlap] = ringbuf[ringbuf_edge - overlap:ringbuf_edge] 92 | ringbuf_edge = overlap 93 | 94 | ringbuf[ringbuf_edge:ringbuf_edge + readsize] = sig_input.read(readsize) 95 | ringbuf_edge += readsize 96 | 97 | signal = ringbuf[ringbuf_edge - nbins:ringbuf_edge] 98 | 99 | spectrum = np.absolute(np.fft.fft(np.multiply(signal, window))) 100 | spectrum = np.concatenate((spectrum[nbins/2:nbins], spectrum[0:nbins/2])) 101 | 102 | log_spectrum_pass(process_row, np.log10(spectrum) * 20) 103 | process_row = process_row + 1 104 | 105 | class RawSigInput: 106 | def __init__(self, sample_rate, no_channels, dtype, file): 107 | self.sample_rate = sample_rate 108 | self.no_channels = no_channels 109 | self.dtype = dtype 110 | self.file = file 111 | 112 | def read(self, frames): 113 | read_len = frames * self.dtype.itemsize * self.no_channels 114 | string = "" 115 | 116 | while len(string) < read_len: 117 | string += self.file.read(read_len - len(string)) 118 | 119 | if self.no_channels == 1: 120 | return np.fromstring(string, dtype=self.dtype).astype(np.float32) 121 | elif self.no_channels == 2 and self.dtype == np.dtype(np.float32): 122 | return np.fromstring(string, dtype=np.complex64) 123 | else: 124 | raise NotImplementedError("unimplemented no of channels and type combination") 125 | 126 | def start(self): 127 | pass 128 | 129 | def __str__(self): 130 | return "raw input from '%s'" % self.file.name 131 | 132 | if __name__ == "__main__": 133 | global row_duration, plot, emit_event, args 134 | import sys 135 | 136 | args = sys.argv[1:] 137 | plot = lambda a, b: None 138 | emit_event = lambda a, b: None 139 | nbins = 4096 140 | overlap = 3072 141 | sig_input = RawSigInput(48000, 2, np.dtype(np.float32), sys.stdin) 142 | row_duration = float(nbins - overlap) / sig_input.sample_rate 143 | process(sig_input, 4096, 3072) 144 | -------------------------------------------------------------------------------- /pysdr-recviewer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == "__main__": 4 | from pysdr.recviewer import main 5 | main() 6 | -------------------------------------------------------------------------------- /pysdr-waterfall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == "__main__": 4 | from pysdr.waterfall import main 5 | main() 6 | -------------------------------------------------------------------------------- /pysdr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLAB-project/pysdr/b6b3ba203eddeeb3088c4df1b115c492b14cb030/pysdr/__init__.py -------------------------------------------------------------------------------- /pysdr/commands.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | import time 4 | 5 | from OpenGL.GL import * 6 | from OpenGL.GL.ARB.framebuffer_object import * 7 | from OpenGL.GL.EXT.framebuffer_object import * 8 | 9 | from pysdr.overlay import View 10 | from pysdr.persistence import pers_save 11 | 12 | class KeyTriggers: 13 | def __init__(self, cmds): 14 | self.cmds = cmds 15 | 16 | def on_key_press(self, key): 17 | try: 18 | cmd = self.cmds[key] 19 | except KeyError: 20 | return False 21 | 22 | cmd[0](*(cmd[1])) 23 | return True 24 | 25 | def screenshot(viewer): 26 | try: 27 | from PIL import Image 28 | 29 | resolution = viewer.screen_size 30 | 31 | image = np.zeros((resolution[1], resolution[0], 3), dtype=np.uint8) 32 | glReadPixels(0, 0, resolution[0], resolution[1], GL_RGB, GL_UNSIGNED_BYTE, image) 33 | 34 | Image.fromarray(image[::-1,:,:].copy()).save(time.strftime("screenshot_%Y%m%d%H%M%S.png", 35 | time.gmtime())) 36 | except Exception as e: 37 | print e 38 | 39 | def textureshot(viewer): 40 | prev_view = viewer.view 41 | prev_screen_size = viewer.screen_size 42 | 43 | try: 44 | from PIL import Image 45 | 46 | resolution = (viewer.multitexture.get_width(), viewer.multitexture.get_height()) 47 | tile_size = (viewer.multitexture.unit_width, viewer.multitexture.unit_height) 48 | 49 | rbo = glGenRenderbuffers(1) 50 | glBindRenderbuffer(GL_RENDERBUFFER, rbo) 51 | glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, tile_size[0], tile_size[1]) 52 | 53 | fbo = glGenFramebuffers(1) 54 | glBindFramebuffer(GL_FRAMEBUFFER, fbo) 55 | glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo) 56 | glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo) 57 | 58 | viewer.cb_reshape(tile_size[0], tile_size[1]) 59 | viewer.screen_size = resolution 60 | 61 | image = np.zeros((resolution[1], resolution[0], 3), dtype=np.uint8) 62 | tile_image = np.zeros((tile_size[1], tile_size[0], 3), dtype=np.uint8) 63 | 64 | viewer.view = view = View() 65 | view.scale_x, viewer.view.scale_y = float(resolution[0]) / 2, float(resolution[1]) 66 | view.origin_x = resolution[0] / 2 67 | 68 | for x in xrange(viewer.multitexture.units_x): 69 | for y in xrange(viewer.multitexture.units_y): 70 | glClear(GL_COLOR_BUFFER_BIT) 71 | glClearColor(0, 0, 0, 1) 72 | glLoadIdentity() 73 | 74 | glMatrixMode(GL_PROJECTION) 75 | glPushMatrix() 76 | view.screen_offset = (-tile_size[0] * x, -tile_size[1] * y) 77 | glTranslatef(view.screen_offset[0], view.screen_offset[1], 0) 78 | glMatrixMode(GL_MODELVIEW) 79 | 80 | glPushMatrix() 81 | view.setup() 82 | viewer.call_layers('draw_content') 83 | glPopMatrix() 84 | 85 | glMatrixMode(GL_PROJECTION) 86 | glPopMatrix() 87 | glMatrixMode(GL_MODELVIEW) 88 | 89 | glReadPixels(0, 0, tile_size[0], tile_size[1], GL_RGB, 90 | GL_UNSIGNED_BYTE, tile_image) 91 | image[tile_size[1] * y:tile_size[1] * (y + 1), 92 | tile_size[0] * x:tile_size[0] * (x + 1)] = tile_image 93 | 94 | glDeleteFramebuffers(1, [fbo]) 95 | glDeleteRenderbuffers(1, [rbo]) 96 | 97 | Image.fromarray(image[::-1,:,:].copy()).save(time.strftime("textureshot_%Y%m%d%H%M%S.bmp", 98 | time.gmtime())) 99 | 100 | except Exception as e: 101 | print e 102 | 103 | viewer.view = prev_view 104 | viewer.cb_reshape(prev_screen_size[0], prev_screen_size[1]) 105 | 106 | def mag_range_calibration(viewer): 107 | class Hook: 108 | def on_log_spectrum(self, spectrum): 109 | a = int(((-viewer.view.origin_x) / viewer.view.scale_x + 1) 110 | / 2 * viewer.bins) 111 | b = int(((-viewer.view.origin_x + viewer.screen_size[0]) / viewer.view.scale_x + 1) 112 | / 2 * viewer.bins) 113 | 114 | a = max(0, min(viewer.bins - 1, a)) 115 | b = max(0, min(viewer.bins - 1, b)) 116 | 117 | if a != b: 118 | area = spectrum[a:b] 119 | mag_range = (np.min(area) + 0.5, np.max(area) + 1.0) 120 | 121 | if [math.isnan(a) or math.isinf(a) for a in mag_range] == [False, False]: 122 | viewer.mag_range = mag_range 123 | 124 | viewer.layers.remove(self) 125 | 126 | viewer.layers.append(Hook()) 127 | 128 | def align_pixels(viewer): 129 | viewer.view.set_scale(viewer.multitexture.get_width(), viewer.multitexture.get_height(), 130 | viewer.screen_size[0] / 2, viewer.screen_size[1] / 2) 131 | 132 | def make_commands_layer(viewer): 133 | return KeyTriggers({ 134 | 's': (screenshot, (viewer,)), 135 | 't': (textureshot, (viewer,)), 136 | 'c': (mag_range_calibration, (viewer,)), 137 | 'm': (align_pixels, (viewer,)), 138 | 'p': (lambda v: pers_save(v, v.persfn), (viewer,)) 139 | }) 140 | -------------------------------------------------------------------------------- /pysdr/console.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from code import InteractiveConsole 4 | 5 | from OpenGL.GL import * 6 | from OpenGL.GLUT import glutBitmapCharacter, GLUT_BITMAP_8_BY_13 7 | 8 | class Console(InteractiveConsole): 9 | """Quake-like console""" 10 | 11 | CHAR_WIDTH = 8 12 | CHAR_HEIGHT = 13 13 | 14 | @staticmethod 15 | def draw_string(x, y, string): 16 | glWindowPos2i(x, y) 17 | for c in string: 18 | glutBitmapCharacter(GLUT_BITMAP_8_BY_13, ord(c)) 19 | 20 | @staticmethod 21 | def draw_cursor(x, y, w, h): 22 | glColor4f(1.0, 1.0, 1.0, 1.0) 23 | 24 | glBegin(GL_QUADS) 25 | glVertex2i(x, y) 26 | glVertex2i(x + w, y) 27 | glVertex2i(x + w, y + h) 28 | glVertex2i(x, y + h) 29 | glEnd() 30 | 31 | def __init__(self, viewer, locals): 32 | self.viewer = viewer 33 | self.lines = [] 34 | self.prompt = ">>> " 35 | self.last_line = "" 36 | self.stdout = self.stderr = self 37 | self.active = False 38 | InteractiveConsole.__init__(self, locals=locals) 39 | 40 | self.on_resize(*getattr(viewer, 'screen_size', (1024, 1024))) 41 | 42 | self.prev_stdout = sys.stdout 43 | sys.stdout = self 44 | 45 | def on_resize(self, w, h): 46 | self.lines_cutoff = max((h - 20) / self.CHAR_HEIGHT, 1) 47 | self.columns_cutoff = max((w - 20) / self.CHAR_WIDTH, 1) 48 | 49 | def draw_screen(self): 50 | if not self.active: 51 | return 52 | 53 | glColor4f(0.0, 0.0, 0.0, 0.75) 54 | 55 | w, h = self.viewer.screen_size 56 | 57 | glBegin(GL_QUADS) 58 | glVertex2i(0, 0) 59 | glVertex2i(0, h) 60 | glVertex2i(w, h) 61 | glVertex2i(w, 0) 62 | glEnd() 63 | 64 | glColor4f(1.0, 1.0, 1.0, 1.0) 65 | 66 | y = 10 + self.CHAR_HEIGHT 67 | for line in reversed(self.lines): 68 | self.draw_string(10, y, line) 69 | y += self.CHAR_HEIGHT 70 | 71 | self.draw_string(10, 10, self.prompt + self.last_line) 72 | self.draw_cursor(10 + len(self.prompt + self.last_line) * self.CHAR_WIDTH, 73 | 8, self.CHAR_WIDTH, self.CHAR_HEIGHT) 74 | 75 | def write(self, msg): 76 | self.prev_stdout.write(msg) 77 | 78 | split = msg.split("\n") 79 | 80 | if len(self.lines) > 0: 81 | split[0] = self.lines.pop() + split[0] 82 | 83 | self.lines += [s[x:x + self.columns_cutoff] for s in split for x \ 84 | in (range(0, len(s), self.columns_cutoff) if len(s) else [0])] 85 | 86 | while len(self.lines) > self.lines_cutoff: 87 | self.lines.pop(0) 88 | 89 | def on_key_press(self, key): 90 | if key == '`': 91 | self.active = not self.active 92 | return True 93 | 94 | if not self.active: 95 | return False 96 | 97 | if key == '\r': 98 | print self.prompt + self.last_line 99 | if self.push(self.last_line): 100 | self.prompt = "... " 101 | else: 102 | self.prompt = ">>> " 103 | self.last_line = " " * (len(self.last_line) - len(self.last_line.lstrip())) 104 | else: 105 | if key == '\b': 106 | if len(self.last_line.lstrip()) == 0: 107 | self.last_line = self.last_line[:len(self.last_line) - ((len(self.last_line) - 1) % 4 + 1)] 108 | else: 109 | self.last_line = self.last_line[:-1] 110 | elif key == '\t': 111 | self.last_line += " " * (4 - (len(self.last_line) % 4)) 112 | else: 113 | self.last_line += key 114 | 115 | return True 116 | -------------------------------------------------------------------------------- /pysdr/events.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import sys 3 | 4 | from OpenGL.GL import * 5 | 6 | from pysdr.graph import * 7 | 8 | class EventMarker: 9 | def __init__(self, viewer, mark_color=None): 10 | self.viewer = viewer 11 | self.mark_color = mark_color or (0.0, 1.0, 0.0, 1.0) 12 | self.marks = [] 13 | 14 | def on_event(self, event_id, payload): 15 | if event_id.startswith('mlab.aabb_event.'): 16 | event_mark = (event_id, (payload[0], payload[1]), (payload[2], payload[3]), payload[4]) 17 | 18 | for i in xrange(len(self.marks)): 19 | mark = self.marks[i] 20 | if (mark[0] == event_mark[0] and mark[1][0] == event_mark[1][0] 21 | and event_mark[2] == event_mark[2]): 22 | self.marks[i] = event_mark 23 | return 24 | self.marks.append(event_mark) 25 | 26 | def draw_content(self): 27 | for (event_id, row_range, freq_range, desc) in self.marks: 28 | xa, xb = [self.viewer.bin_to_x(x) for x in freq_range] 29 | ya, yb = [self.viewer.row_to_y(x) for x in row_range] 30 | 31 | glLineWidth(2) 32 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 33 | glColor4f(*self.mark_color) 34 | glBegin(GL_QUADS) 35 | glVertex2f(xa, ya) 36 | glVertex2f(xa, yb) 37 | glVertex2f(xb, yb) 38 | glVertex2f(xb, ya) 39 | glEnd() 40 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 41 | glLineWidth(1) 42 | 43 | glColor4f(1.0, 1.0, 1.0, 1.0) 44 | self.viewer.overlay.draw_text(xb, ya, desc) 45 | 46 | def on_texture_insert(self): 47 | self.marks = [(a, b, c, d) for (a, b, c, d) in self.marks \ 48 | if b[1] > self.viewer.texture_row - self.viewer.multitexture.get_height()] 49 | 50 | class TemporalPlot(PlotLine): 51 | def __init__(self, viewer, x_offset, title=None): 52 | PlotLine.__init__(self, 2 * viewer.multitexture.get_height()) 53 | self.viewer = viewer 54 | self.x_offset = x_offset 55 | self.title = title 56 | self.data[:] = np.zeros(self.points) 57 | 58 | def draw_screen(self): 59 | view = self.viewer.view 60 | 61 | glPushMatrix() 62 | glTranslated(self.x_offset, view.origin_y, 0) 63 | glScalef(-10.0, view.scale_y, 1.0) 64 | 65 | glColor4f(0.0, 0.0, 0.0, 0.7) 66 | glBegin(GL_QUADS) 67 | glVertex2f(-5.0, 0.0) 68 | glVertex2f(5.0, 0.0) 69 | glVertex2f(5.0, 1.0) 70 | glVertex2f(-5.0, 1.0) 71 | glEnd() 72 | glColor4f(1.0, 1.0, 1.0, 1.0) 73 | glRotatef(90, 0, 0, 1) 74 | glScalef(2, 1, 1) 75 | glTranslatef(0.5 / self.points, 0.0, 0.0) 76 | PlotLine.draw_section(self, self.viewer.texture_row - self.viewer.multitexture.get_height(), 77 | self.viewer.texture_row - 1) 78 | glPopMatrix() 79 | 80 | if self.title: 81 | self.viewer.overlay.draw_text_ss(self.x_offset - 45, view.origin_y + view.scale_y + 5, 82 | self.title) 83 | 84 | def set(self, row, value): 85 | self.data[row % self.points] = value 86 | 87 | class TemporalFreqPlot(TemporalPlot): 88 | def __init__(self, viewer, title): 89 | TemporalPlot.__init__(self, viewer, 0, title) 90 | 91 | def draw_screen(self): 92 | pass 93 | 94 | def draw_content(self): 95 | glColor4f(1.0, 1.0, 1.0, 1.0) 96 | 97 | glPushMatrix() 98 | glScalef(-1, 2, 1) 99 | glRotatef(90, 0, 0, 1) 100 | glTranslatef(0.5 / self.points, 0.0, 0.0) 101 | PlotLine.draw_section(self, self.viewer.texture_row - self.viewer.multitexture.get_height(), 102 | self.viewer.texture_row - 1) 103 | glPopMatrix() 104 | 105 | SCRIPT_API_METHODS = dict() 106 | 107 | class DetectorScript: 108 | def script_api(func): 109 | SCRIPT_API_METHODS[func.func_name] = func 110 | 111 | @script_api 112 | def peak(self, a, b, s): 113 | bin = np.argmax(s[a:b]) 114 | return (s[bin], bin + a) 115 | 116 | @script_api 117 | def noise(self, a): 118 | return np.sort(a)[len(a) / 4] * 2 119 | 120 | @script_api 121 | def plot(self, name, value): 122 | if not name in self.plots: 123 | self.plots[name] = TemporalPlot(self.viewer, self.plot_x_offset, name) 124 | self.plot_x_offset = self.plot_x_offset + 120 125 | 126 | self.plots[name].set(self.viewer.process_row, value) 127 | 128 | @script_api 129 | def plot_bin(self, name, value): 130 | if not name in self.plots: 131 | self.plots[name] = TemporalFreqPlot(self.viewer, name) 132 | 133 | self.plots[name].set(self.viewer.process_row, (float(value) / self.viewer.bins) * 2 - 1) 134 | 135 | @script_api 136 | def plot_freq(self, name, value): 137 | self.namespace['plot_bin'](name, self.viewer.freq_to_bin(value)) 138 | 139 | @script_api 140 | def emit_event(self, event_id, payload): 141 | [l.on_event(event_id, payload) for l in self.listeners] 142 | 143 | def __init__(self, viewer, listeners, args): 144 | self.viewer = viewer 145 | self.filename = args[0] 146 | self.plots = dict() 147 | self.plot_x_offset = 100 148 | self.disabled = False 149 | 150 | self.listeners = listeners 151 | 152 | self.namespace = { 153 | 'args': args[1:], 154 | 'freq2bin': self.viewer.freq_to_bin, 155 | 'bin2freq': self.viewer.bin_to_freq, 156 | 'row_duration': self.viewer.row_duration 157 | } 158 | 159 | for name, func in SCRIPT_API_METHODS.items(): 160 | self.namespace[name] = func.__get__(self, DetectorScript) 161 | 162 | execfile(self.filename, self.namespace) 163 | 164 | def draw_screen(self): 165 | for plot in self.plots.values(): 166 | if hasattr(plot.__class__, 'draw_screen'): 167 | plot.draw_screen() 168 | 169 | def draw_content(self): 170 | for plot in self.plots.values(): 171 | if hasattr(plot.__class__, 'draw_content'): 172 | plot.draw_content() 173 | 174 | def on_lin_spectrum(self, spectrum): 175 | self.call_script_pass('lin_spectrum_pass', spectrum) 176 | 177 | def on_log_spectrum(self, spectrum): 178 | self.call_script_pass('log_spectrum_pass', spectrum) 179 | 180 | def call_script_pass(self, name, spectrum): 181 | if name not in self.namespace or self.disabled: 182 | return 183 | 184 | try: 185 | self.namespace[name](self.viewer.process_row, spectrum) 186 | except Exception: 187 | print "exception in %s, disabling:" % self.filename 188 | traceback.print_exc(file=sys.stdout) 189 | self.disabled = True 190 | 191 | class MIDIEventGatherer: 192 | def __init__(self, viewer, listeners): 193 | self.viewer = viewer 194 | self.listeners = listeners 195 | 196 | def on_log_spectrum(self, spectrum): 197 | for frame, message in self.viewer.sig_input.get_midi_events(): 198 | if len(message) > 3 and message[0:2] == "\xf0\x7d" and message[-1] == "\xf7": 199 | try: 200 | event_id, payload = message[2:-1].split(':', 1) 201 | payload = tuple(payload.split(',')) 202 | 203 | if event_id.startswith('mlab.aabb_event.'): 204 | rel_frame_a, rel_frame_b, freq_a, freq_b, desc = payload 205 | 206 | row_range = tuple([(frame + int(x)) / (self.viewer.bins - self.viewer.overlap) 207 | for x in (rel_frame_a, rel_frame_b)]) 208 | bin_range = tuple([self.viewer.freq_to_bin(float(x)) for x in (freq_a, freq_b)]) 209 | 210 | payload = row_range + bin_range + (desc,) 211 | 212 | [l.on_event(event_id, payload) for l in self.listeners] 213 | except (ValueError, TypeError) as e: 214 | print "failed to parse MIDI message '%s': %s" % (message[2:-1], e) 215 | else: 216 | print "unknown MIDI message at frame %d: %s" % (frame, message.encode("hex")) 217 | -------------------------------------------------------------------------------- /pysdr/ext.c: -------------------------------------------------------------------------------- 1 | #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION 2 | 3 | #include "Python.h" 4 | #include "math.h" 5 | #include "numpy/ndarraytypes.h" 6 | #include "numpy/ufuncobject.h" 7 | #include "numpy/halffloat.h" 8 | #include 9 | #include 10 | #include 11 | 12 | typedef struct { 13 | jack_client_t *client; 14 | jack_port_t *port_i, *port_q; 15 | jack_port_t *port_events; 16 | jack_ringbuffer_t *ringbuffer; 17 | jack_ringbuffer_t *midi_ringbuffer; 18 | uint64_t nframes; 19 | int overrun; 20 | } jack_handle_t; 21 | 22 | void pysdr_jack_handle_destructor(PyObject *object) 23 | { 24 | jack_handle_t *handle = (jack_handle_t *) PyCapsule_GetPointer(object, NULL); 25 | 26 | jack_client_close(handle->client); 27 | free(handle); 28 | } 29 | 30 | int pysdr_jack_process(jack_nframes_t nframes, void *arg) 31 | { 32 | jack_handle_t *handle = (jack_handle_t *) arg; 33 | 34 | float* i = (float *) jack_port_get_buffer(handle->port_i, nframes); 35 | float* q = (float *) jack_port_get_buffer(handle->port_q, nframes); 36 | 37 | jack_ringbuffer_data_t vec[2]; 38 | jack_ringbuffer_get_write_vector(handle->ringbuffer, vec); 39 | 40 | if (vec[0].len + vec[1].len < nframes * sizeof(float) * 2) { 41 | handle->overrun++; 42 | nframes = (vec[0].len + vec[1].len) / (sizeof(float) * 2); 43 | } 44 | 45 | jack_ringbuffer_write_advance(handle->ringbuffer, nframes * 2 * sizeof(float)); 46 | 47 | char middle_of_frame = 0; 48 | int x = 0; 49 | int m; 50 | for (m = 0; m < 2; m++) { 51 | float *cpos = (float *) vec[m].buf; 52 | float *epos = ((float *) (vec[m].buf + vec[m].len)) - 1; 53 | 54 | if (middle_of_frame) { 55 | *cpos++ = q[x]; 56 | x++; 57 | middle_of_frame = 0; 58 | } 59 | 60 | while (cpos < epos && x < nframes) { 61 | *cpos++ = i[x]; 62 | *cpos++ = q[x]; 63 | x++; 64 | } 65 | 66 | if (x >= nframes) 67 | break; 68 | 69 | if (cpos == epos) { 70 | *cpos++ = i[x]; 71 | middle_of_frame = 1; 72 | } 73 | 74 | } 75 | 76 | void* midi_buf = jack_port_get_buffer(handle->port_events, nframes); 77 | 78 | jack_nframes_t events_count = jack_midi_get_event_count(midi_buf); 79 | jack_midi_event_t in_event; 80 | for (x = 0; x < events_count; x++) { 81 | jack_midi_event_get(&in_event, midi_buf, x); 82 | 83 | if (jack_ringbuffer_write_space(handle->midi_ringbuffer) 84 | < sizeof(jack_nframes_t) + sizeof(jack_nframes_t)) 85 | continue; 86 | 87 | uint64_t time = handle->nframes + in_event.time; 88 | 89 | jack_ringbuffer_write(handle->midi_ringbuffer, (char *) &time, sizeof(uint64_t)); 90 | jack_ringbuffer_write(handle->midi_ringbuffer, (char *) &(in_event.size), sizeof(size_t)); 91 | jack_ringbuffer_write(handle->midi_ringbuffer, (char *) in_event.buffer, in_event.size); 92 | } 93 | 94 | handle->nframes += nframes; 95 | 96 | return 0; 97 | } 98 | 99 | static PyObject *pysdr_jack_init(PyObject *self, PyObject *args) 100 | { 101 | const char *name; 102 | 103 | if (!PyArg_ParseTuple(args, "s", &name)) 104 | return NULL; 105 | 106 | jack_handle_t *handle = (jack_handle_t *) malloc(sizeof(jack_handle_t)); 107 | 108 | if ((handle->client = jack_client_open(name, JackNoStartServer, 0)) == 0) { 109 | free(handle); 110 | PyErr_SetString(PyExc_RuntimeError, "cannot create jack client"); 111 | return NULL; 112 | } 113 | 114 | jack_set_process_callback(handle->client, pysdr_jack_process, handle); 115 | 116 | handle->port_i = jack_port_register(handle->client, "input_i", JACK_DEFAULT_AUDIO_TYPE, 117 | JackPortIsInput, 0); 118 | handle->port_q = jack_port_register(handle->client, "input_q", JACK_DEFAULT_AUDIO_TYPE, 119 | JackPortIsInput, 0); 120 | 121 | handle->port_events = jack_port_register(handle->client, "input_events", JACK_DEFAULT_MIDI_TYPE, 122 | JackPortIsInput, 0); 123 | 124 | int sample_rate = jack_get_sample_rate(handle->client); 125 | handle->ringbuffer = jack_ringbuffer_create(4 * sample_rate * 2 * sizeof(float)); 126 | handle->midi_ringbuffer = jack_ringbuffer_create(1024); 127 | handle->overrun = 0; 128 | handle->nframes = 0; 129 | 130 | return PyCapsule_New((void *) handle, NULL, pysdr_jack_handle_destructor); 131 | } 132 | 133 | static PyObject *pysdr_jack_get_sample_rate(PyObject *self, PyObject *args) 134 | { 135 | PyObject *handle_obj; 136 | 137 | if (!PyArg_ParseTuple(args, "O", &handle_obj)) 138 | return NULL; 139 | 140 | jack_handle_t *handle; 141 | if ((handle = PyCapsule_GetPointer(handle_obj, NULL)) == 0) 142 | return NULL; 143 | 144 | return Py_BuildValue("i", jack_get_sample_rate(handle->client)); 145 | } 146 | 147 | static PyObject *pysdr_jack_activate(PyObject *self, PyObject *args) 148 | { 149 | PyObject *handle_obj; 150 | 151 | if (!PyArg_ParseTuple(args, "O", &handle_obj)) 152 | return NULL; 153 | 154 | jack_handle_t *handle; 155 | if ((handle = PyCapsule_GetPointer(handle_obj, NULL)) == 0) 156 | return NULL; 157 | 158 | if (jack_activate(handle->client)) { 159 | PyErr_SetString(PyExc_RuntimeError, "cannot activate jack client"); 160 | return NULL; 161 | } 162 | 163 | Py_INCREF(Py_None); 164 | return Py_None; 165 | } 166 | 167 | static PyObject *pysdr_jack_gather_samples(PyObject *self, PyObject *args) 168 | { 169 | PyObject *handle_obj; 170 | unsigned int frames_no; 171 | 172 | if (!PyArg_ParseTuple(args, "OI", &handle_obj, &frames_no)) 173 | return NULL; 174 | 175 | jack_handle_t *handle; 176 | if ((handle = PyCapsule_GetPointer(handle_obj, NULL)) == 0) 177 | return NULL; 178 | 179 | if (jack_ringbuffer_read_space(handle->ringbuffer) < frames_no * 2 * sizeof(float)) { 180 | Py_INCREF(Py_None); 181 | return Py_None; 182 | } 183 | 184 | float *samples = (float *) PyDataMem_NEW(sizeof(float) * 2 * frames_no); 185 | 186 | if (!samples) { 187 | PyErr_NoMemory(); 188 | return NULL; 189 | } 190 | 191 | int read = 0; 192 | while (read < frames_no) 193 | read += jack_ringbuffer_read(handle->ringbuffer, (char *) &(samples[2 * read]), 194 | 2 * sizeof(float) * (frames_no - read)); 195 | 196 | npy_intp dims[1] = { frames_no }; 197 | PyObject *array = PyArray_SimpleNewFromData(1, dims, NPY_COMPLEX64, samples); 198 | PyArray_ENABLEFLAGS((PyArrayObject *) array, NPY_ARRAY_OWNDATA); 199 | 200 | return array; 201 | } 202 | 203 | static PyObject *pysdr_jack_gather_midi_event(PyObject *self, PyObject *args) 204 | { 205 | PyObject *handle_obj; 206 | 207 | if (!PyArg_ParseTuple(args, "O", &handle_obj)) 208 | return NULL; 209 | 210 | jack_handle_t *handle; 211 | if ((handle = PyCapsule_GetPointer(handle_obj, NULL)) == 0) 212 | return NULL; 213 | 214 | if (jack_ringbuffer_read_space(handle->midi_ringbuffer) 215 | < sizeof(jack_nframes_t) + sizeof(size_t)) { 216 | Py_INCREF(Py_None); 217 | return Py_None; 218 | } 219 | 220 | uint64_t time; 221 | jack_ringbuffer_read(handle->midi_ringbuffer, (char *) &time, sizeof(uint64_t)); 222 | size_t size; 223 | jack_ringbuffer_read(handle->midi_ringbuffer, (char *) &size, sizeof(size_t)); 224 | 225 | PyObject *string; 226 | 227 | if ((string = PyString_FromStringAndSize(NULL, size)) == 0) { 228 | jack_ringbuffer_read_advance(handle->midi_ringbuffer, size); 229 | return NULL; 230 | } 231 | 232 | jack_ringbuffer_read(handle->midi_ringbuffer, PyString_AsString(string), size); 233 | 234 | return Py_BuildValue("(KN)", time, string); 235 | } 236 | 237 | float interpolate(float val, float x0, float x1, float y0, float y1) 238 | { 239 | return (val - x0) * (y1 - y0) / (x1 - x0) + y0; 240 | } 241 | 242 | float mag2col_base(float val) 243 | { 244 | if (val <= -1) 245 | return 0; 246 | 247 | if (val <= -0.5) 248 | return interpolate(val, -1, -0.5, 0.0, 1.0); 249 | 250 | if (val <= 0.5) 251 | return 1.0; 252 | 253 | if (val <= 1) 254 | return interpolate(val, 0.5, 1.0, 1.0, 0.0); 255 | 256 | return 0.0; 257 | } 258 | 259 | float mag2col_base2(float val) 260 | { 261 | if (val <= 0) 262 | return 0; 263 | if (val >= 1) 264 | return 1; 265 | 266 | return val; 267 | } 268 | 269 | float mag2col_base2_blue(float val) 270 | { 271 | if (val <= -2.75) 272 | return 0; 273 | 274 | if (val <= -1.75) 275 | return val + 2.75; 276 | 277 | if (val <= -0.75) 278 | return -(val + 0.75); 279 | 280 | if (val <= 0) 281 | return 0; 282 | 283 | if (val >= 1) 284 | return 1; 285 | 286 | return val; 287 | } 288 | 289 | static void mag2col(char **args, npy_intp *dimensions, 290 | npy_intp* steps, void* data) 291 | { 292 | npy_intp i; 293 | npy_intp n = dimensions[0]; 294 | char *in = args[0], *out = args[1]; 295 | npy_intp in_step = steps[0], out_step = steps[1]; 296 | 297 | for (i = 0; i < n; i++) { 298 | float mag = *((float *) in); 299 | 300 | *((unsigned int *) out) = (((unsigned int) (mag2col_base2(mag + 1.0) * 255)) << 24) 301 | | (((unsigned int) (mag2col_base2(mag) * 255)) << 16) 302 | | (((unsigned int) (mag2col_base2_blue(mag - 1.0) * 255)) << 8) 303 | | 0xff; 304 | 305 | in += in_step; 306 | out += out_step; 307 | } 308 | } 309 | 310 | PyUFuncGenericFunction funcs[1] = {&mag2col}; 311 | static char types[2] = {NPY_FLOAT, NPY_UINT}; 312 | static void *data[1] = {NULL}; 313 | 314 | static PyMethodDef pysdrextMethods[] = { 315 | {"jack_init", pysdr_jack_init, METH_VARARGS, NULL}, 316 | {"jack_get_sample_rate", pysdr_jack_get_sample_rate, METH_VARARGS, NULL}, 317 | {"jack_activate", pysdr_jack_activate, METH_VARARGS, NULL}, 318 | {"jack_gather_samples", pysdr_jack_gather_samples, METH_VARARGS, NULL}, 319 | {"jack_gather_midi_event", pysdr_jack_gather_midi_event, METH_VARARGS, NULL}, 320 | {NULL, NULL, 0, NULL} 321 | }; 322 | 323 | #if PY_VERSION_HEX >= 0x03000000 324 | static struct PyModuleDef moduledef = { 325 | PyModuleDef_HEAD_INIT, 326 | "ext", 327 | NULL, 328 | -1, 329 | pysdrextMethods, 330 | NULL, 331 | NULL, 332 | NULL, 333 | NULL 334 | }; 335 | 336 | PyMODINIT_FUNC PyInit_ext(void) 337 | { 338 | PyObject *m, *mag2col, *d; 339 | 340 | m = PyModule_Create(&moduledef); 341 | 342 | if (!m) 343 | return; 344 | 345 | import_array(); 346 | import_umath(); 347 | 348 | mag2col = PyUFunc_FromFuncAndData(funcs, data, types, 1, 1, 1, 349 | PyUFunc_None, "mag2col", 350 | "", 0); 351 | 352 | d = PyModule_GetDict(m); 353 | 354 | PyDict_SetItemString(d, "mag2col", mag2col); 355 | Py_DECREF(mag2col); 356 | 357 | return m; 358 | } 359 | #else 360 | PyMODINIT_FUNC initext(void) 361 | { 362 | PyObject *m, *mag2col, *d; 363 | 364 | m = Py_InitModule("ext", pysdrextMethods); 365 | 366 | if (!m) 367 | return; 368 | 369 | import_array(); 370 | import_umath(); 371 | 372 | mag2col = PyUFunc_FromFuncAndData(funcs, data, types, 1, 1, 1, 373 | PyUFunc_None, "mag2col", 374 | "l", 0); 375 | 376 | d = PyModule_GetDict(m); 377 | 378 | PyDict_SetItemString(d, "mag2col", mag2col); 379 | Py_DECREF(mag2col); 380 | } 381 | #endif 382 | -------------------------------------------------------------------------------- /pysdr/graph.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | 4 | from OpenGL.GL import * 5 | 6 | class PlotLine: 7 | def __init__(self, points): 8 | self.points = points 9 | self.array = np.zeros((self.points, 2), 'f') 10 | self.data = self.array[:,1] 11 | self.array[:,0] = np.arange(self.points, dtype=np.float) / self.points 12 | 13 | def draw(self): 14 | glEnableClientState(GL_VERTEX_ARRAY) 15 | glVertexPointer(2, GL_FLOAT, 0, self.array) 16 | glDrawArrays(GL_LINE_STRIP, 0, self.points) 17 | glDisableClientState(GL_VERTEX_ARRAY) 18 | 19 | def draw_section(self, start, end): 20 | glEnableClientState(GL_VERTEX_ARRAY) 21 | glVertexPointer(2, GL_FLOAT, 0, self.array) 22 | 23 | glPushMatrix() 24 | 25 | pos = start 26 | while pos < end: 27 | offset = pos % self.points 28 | 29 | if offset == self.points - 1: 30 | glBegin(GL_LINES) 31 | glVertex2f(0.0, self.data[-1]) 32 | glVertex2f(1.0 / self.points, self.data[0]) 33 | glEnd() 34 | glTranslatef(1.0 / self.points, 0, 0) 35 | pos += 1 36 | continue 37 | 38 | advance = min(self.points - offset - 1, end - pos) 39 | 40 | glPushMatrix() 41 | glTranslatef(-float(offset) / self.points, 0, 0) 42 | glDrawArrays(GL_LINE_STRIP, offset, advance + 1) 43 | glPopMatrix() 44 | 45 | glTranslatef(float(advance) / self.points, 0, 0) 46 | pos += advance 47 | 48 | glPopMatrix() 49 | 50 | glDisableClientState(GL_VERTEX_ARRAY) 51 | 52 | class MultiTexture(): 53 | """Abstracting grid of textures""" 54 | def __init__(self, unit_width, unit_height, units_x, units_y, 55 | format=GL_RGB, type=GL_BYTE): 56 | self.unit_width = unit_width 57 | self.unit_height = unit_height 58 | 59 | self.units_x = units_x 60 | self.units_y = units_y 61 | 62 | self.textures = glGenTextures(units_x * units_y) 63 | 64 | self.format = format 65 | self.type = type 66 | 67 | if not isinstance(self.textures, np.ndarray): 68 | self.textures = [self.textures] 69 | 70 | init_image = np.zeros(self.unit_width * self.unit_height * 16, dtype=np.uint8) 71 | 72 | for i in xrange(units_x * units_y): 73 | glEnable(GL_TEXTURE_2D) 74 | glBindTexture(GL_TEXTURE_2D, self.textures[i]) 75 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 76 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 77 | glTexImage2D(GL_TEXTURE_2D, 0, format, self.unit_width, 78 | self.unit_height, 0, format, type, init_image) 79 | 80 | def get_width(self): 81 | return self.units_x * self.unit_width 82 | 83 | def get_height(self): 84 | return self.units_y * self.unit_height 85 | 86 | def __del__(self): 87 | glDeleteTextures(self.textures) 88 | 89 | def insert(self, y, line, format=GL_RGBA, type=GL_UNSIGNED_INT_8_8_8_8): 90 | if y > self.units_y * self.unit_height: 91 | raise Error("out of bounds") 92 | 93 | base = math.trunc(y / self.unit_height) * self.units_x 94 | offset_y = y - math.trunc(y / self.unit_height) * self.unit_height 95 | 96 | for x in xrange(self.units_x): 97 | glBindTexture(GL_TEXTURE_2D, self.textures[base + x]) 98 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, offset_y, self.unit_width, 1, format, type, 99 | line[x * self.unit_width:(x + 1) * self.unit_width]) 100 | 101 | def draw(self): 102 | glEnable(GL_TEXTURE_2D) 103 | glColor3f(1.0, 1.0, 1.0) 104 | 105 | for y in xrange(self.units_y): 106 | for x in xrange(self.units_x): 107 | glBindTexture(GL_TEXTURE_2D, self.textures[self.units_x * y + x]) 108 | xa, xb = (float(x) / self.units_x), (float(x + 1) / self.units_x) 109 | ya, yb = (float(y) / self.units_y), (float(y + 1) / self.units_y) 110 | 111 | glBegin(GL_QUADS) 112 | glTexCoord2f(0, 0) 113 | glVertex2f(xa, ya) 114 | glTexCoord2f(0, 1) 115 | glVertex2f(xa, yb) 116 | glTexCoord2f(1, 1) 117 | glVertex2f(xb, yb) 118 | glTexCoord2f(1, 0) 119 | glVertex2f(xb, ya) 120 | glEnd() 121 | 122 | glDisable(GL_TEXTURE_2D) 123 | 124 | def draw_row(self, edge, row): 125 | y = row - (edge / self.unit_height) 126 | 127 | if y < 0: 128 | y = y + self.units_y 129 | 130 | row_shift = float(edge % self.unit_height) / (self.units_y * self.unit_height) 131 | 132 | if y == 0: 133 | ya, yb = 1.0 - row_shift, 1.0 134 | tya, tyb = 0, row_shift * self.units_y 135 | for x in xrange(self.units_x): 136 | glBindTexture(GL_TEXTURE_2D, self.textures[self.units_x * row + x]) 137 | xa, xb = (float(x) / self.units_x), (float(x + 1) / self.units_x) 138 | 139 | glBegin(GL_QUADS) 140 | glTexCoord2f(0, tya) 141 | glVertex2f(xa, ya) 142 | glTexCoord2f(0, tyb) 143 | glVertex2f(xa, yb) 144 | glTexCoord2f(1, tyb) 145 | glVertex2f(xb, yb) 146 | glTexCoord2f(1, tya) 147 | glVertex2f(xb, ya) 148 | glEnd() 149 | 150 | ya, yb = 0.0, (1.0 / float(self.units_y)) - row_shift 151 | tya, tyb = row_shift * self.units_y, 1.0 152 | for x in xrange(self.units_x): 153 | glBindTexture(GL_TEXTURE_2D, self.textures[self.units_x * row + x]) 154 | xa, xb = (float(x) / self.units_x), (float(x + 1) / self.units_x) 155 | 156 | glBegin(GL_QUADS) 157 | glTexCoord2f(0, tya) 158 | glVertex2f(xa, ya) 159 | glTexCoord2f(0, tyb) 160 | glVertex2f(xa, yb) 161 | glTexCoord2f(1, tyb) 162 | glVertex2f(xb, yb) 163 | glTexCoord2f(1, tya) 164 | glVertex2f(xb, ya) 165 | glEnd() 166 | 167 | else: 168 | ya, yb = (float(y) / self.units_y) - row_shift, (float(y + 1) / self.units_y) - row_shift 169 | 170 | for x in xrange(self.units_x): 171 | glBindTexture(GL_TEXTURE_2D, self.textures[self.units_x * row + x]) 172 | xa, xb = (float(x) / self.units_x), (float(x + 1) / self.units_x) 173 | 174 | glBegin(GL_QUADS) 175 | glTexCoord2f(0, 0) 176 | glVertex2f(xa, ya) 177 | glTexCoord2f(0, 1) 178 | glVertex2f(xa, yb) 179 | glTexCoord2f(1, 1) 180 | glVertex2f(xb, yb) 181 | glTexCoord2f(1, 0) 182 | glVertex2f(xb, ya) 183 | glEnd() 184 | 185 | 186 | def draw_scroll(self, edge): 187 | glEnable(GL_TEXTURE_2D) 188 | glColor3f(1.0, 1.0, 1.0) 189 | 190 | for y in xrange(self.units_y): 191 | self.draw_row(edge, y) 192 | 193 | glDisable(GL_TEXTURE_2D) 194 | 195 | class WaterfallFlat(): 196 | """Simple waterfall implementation""" 197 | def __init__(self, width, height): 198 | self.width = width 199 | self.height = height 200 | self.top_y = 0 201 | self.texture = glGenTextures(1) 202 | 203 | glEnable(GL_TEXTURE_2D) 204 | 205 | glBindTexture(GL_TEXTURE_2D, self.texture) 206 | 207 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 208 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 209 | 210 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, self.width, self.height, 0, GL_RGB, GL_BYTE, 0) 211 | 212 | def __del__(self): 213 | glDeleteTextures(self.texture) 214 | 215 | def insert(self, line): 216 | glBindTexture(GL_TEXTURE_2D, self.texture) 217 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self.top_y, self.width, 1, 218 | GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, line) 219 | 220 | self.top_y += 1 221 | 222 | if self.top_y >= self.height: 223 | self.top_y = 0 224 | 225 | def draw(self): 226 | glEnable(GL_TEXTURE_2D) 227 | glBindTexture(GL_TEXTURE_2D, self.texture) 228 | glColor3f(1.0, 1.0, 1.0) 229 | 230 | glBegin(GL_QUADS) 231 | 232 | if self.top_y != 0: 233 | glTexCoord2f(0, float(self.top_y) / self.height) 234 | glVertex2f(0, 0) 235 | glTexCoord2f(1.0, float(self.top_y) / self.height) 236 | glVertex2f(1.0, 0) 237 | glTexCoord2f(1.0, 0) 238 | glVertex2f(1.0, float(self.top_y) / self.height) 239 | glTexCoord2f(0, 0) 240 | glVertex2f(0, float(self.top_y) / self.height) 241 | 242 | if self.top_y != self.height - 1: 243 | glTexCoord2f(0.0, 1.0) 244 | glVertex2f(0, float(self.top_y) / self.height) 245 | glTexCoord2f(1.0, 1.0) 246 | glVertex2f(1.0, float(self.top_y) / self.height) 247 | glTexCoord2f(1.0, float(self.top_y) / self.height) 248 | glVertex2f(1.0, 1.0) 249 | glTexCoord2f(0.0, float(self.top_y) / self.height) 250 | glVertex2f(0, 1.0) 251 | 252 | glEnd() 253 | 254 | glDisable(GL_TEXTURE_2D) 255 | -------------------------------------------------------------------------------- /pysdr/input.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import sys 4 | 5 | import pysdr.ext as ext 6 | 7 | class SigInput: 8 | def __init__(self): 9 | self.sample_rate = 0 10 | self.no_channels = 0 11 | 12 | def read(self, frames): 13 | raise NotImplementedError("read() method must be overrided") 14 | 15 | def start(self): 16 | raise NotImplementedError("start() method must be overrided") 17 | 18 | class RawSigInput(SigInput): 19 | def __init__(self, sample_rate, no_channels, dtype, file): 20 | self.sample_rate = sample_rate 21 | self.no_channels = no_channels 22 | self.dtype = dtype 23 | self.file = file 24 | 25 | def read(self, frames): 26 | read_len = frames * self.dtype.itemsize * self.no_channels 27 | string = "" 28 | 29 | while len(string) < read_len: 30 | string += self.file.read(read_len - len(string)) 31 | 32 | if self.no_channels == 1: 33 | return np.fromstring(string, dtype=self.dtype).astype(np.float32) 34 | elif self.no_channels == 2 and self.dtype == np.dtype(np.float32): 35 | return np.fromstring(string, dtype=np.complex64) 36 | else: 37 | raise NotImplementedError("unimplemented no of channels and type combination") 38 | 39 | def start(self): 40 | pass 41 | 42 | def __str__(self): 43 | return "raw input from '%s'" % self.file.name 44 | 45 | class JackInput(SigInput): 46 | def __init__(self, name): 47 | self.name = name 48 | self.handle = ext.jack_init(name) 49 | self.sample_rate = ext.jack_get_sample_rate(self.handle) 50 | 51 | def read(self, frames): 52 | while True: 53 | r = ext.jack_gather_samples(self.handle, frames) 54 | 55 | if r != None: 56 | return r 57 | 58 | time.sleep(float(frames) / self.sample_rate / 10) 59 | 60 | def get_midi_events(self): 61 | events = [] 62 | 63 | while True: 64 | event = ext.jack_gather_midi_event(self.handle) 65 | 66 | if event == None: 67 | break 68 | 69 | events.append(event) 70 | 71 | return events 72 | 73 | def start(self): 74 | ext.jack_activate(self.handle) 75 | 76 | def __str__(self): 77 | return "JACK port '%s'" % self.name 78 | -------------------------------------------------------------------------------- /pysdr/overlay.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from input import * 4 | 5 | from OpenGL.GL import * 6 | from OpenGL.GLUT import * 7 | 8 | class View: 9 | def __init__(self): 10 | self.scale_x = 640.0 11 | self.scale_y = 480.0 12 | self.origin_x = 0.0 13 | self.origin_y = 0.0 14 | self.screen_offset = (0, 0) 15 | 16 | def on_mouse_button(self, button, state, x, y): 17 | if state != GLUT_DOWN: 18 | return 19 | 20 | self.button_right = button == GLUT_RIGHT_BUTTON 21 | self.start_x = x 22 | self.start_y = y 23 | self.drag_x = x 24 | self.drag_y = y 25 | 26 | def set_scale(self, sx, sy, px, py): 27 | sx, sy = float(sx), float(sy) 28 | self.origin_x = (self.origin_x - px) * (sx / self.scale_x) + px 29 | self.origin_y = (self.origin_y - py) * (sy / self.scale_y) + py 30 | self.scale_x = sx 31 | self.scale_y = sy 32 | 33 | def on_drag(self, x, y): 34 | if self.button_right: 35 | self.set_scale(self.scale_x * math.pow(1.01, x - self.drag_x), 36 | self.scale_y * math.pow(1.01, y - self.drag_y), 37 | self.start_x, self.start_y) 38 | else: 39 | self.origin_x += x - self.drag_x 40 | self.origin_y += y - self.drag_y 41 | self.drag_x = x 42 | self.drag_y = y 43 | 44 | def from_screen(self, x, y): 45 | return ((x - self.origin_x) / self.scale_x, (y - self.origin_y) / self.scale_y) 46 | 47 | def to_screen(self, x, y): 48 | return (x * self.scale_x + self.origin_x, y * self.scale_y + self.origin_y) 49 | 50 | def setup(self): 51 | glTranslated(self.origin_x, self.origin_y, 0) 52 | glScaled(self.scale_x, self.scale_y, 1.0) 53 | 54 | UNIT_HZ = ((1e6, "MHz"), (1e3, "kHz"), (1e0, "Hz")) 55 | UNIT_SEC = ((60, "min"), (1, "sec")) 56 | UNIT_ONE = ((1.0, ""),) 57 | 58 | def _unit_format(unit, base): 59 | if unit is None: 60 | return lambda x: "" 61 | 62 | for unit_base, postfix in unit: 63 | if unit_base < base * 10 or unit_base == 1: 64 | fmt_str = "%%.%df %s" % (max(0, math.ceil(math.log10(unit_base / base))), postfix) 65 | return lambda x: fmt_str % (x / unit_base,) 66 | 67 | def _axis(a, b, scale, offset, unit, cutoff): 68 | visible_area = abs((b - a) * scale) 69 | 70 | base = math.log(visible_area, 10) 71 | for x in [math.log10(5), math.log10(2), 0]: 72 | if math.floor(base) - x >= base - (1 + 1e-5): 73 | base = math.floor(base) - x 74 | break 75 | 76 | base = 10 ** base * (scale / abs(scale)) 77 | 78 | ticks = [((m * base - offset) / scale, m * base) for m 79 | in range(int(math.floor((a * scale + offset) / base)), 80 | int(math.ceil((b * scale + offset) / base) + 1))] 81 | ticks = [m for m in ticks if m[0] >= cutoff[0] + -1e-5 and m[0] <= cutoff[1] + 1e-5] 82 | 83 | fmt = _unit_format(unit, abs(base)) 84 | return [(m[0], fmt(m[1] + 0.0)) for m in ticks] 85 | 86 | def static_axis(unit, scale, cutoff=(0.0, 1.0), offset=0.0): 87 | return lambda a, b: _axis(a, b, scale, offset, unit, cutoff) 88 | 89 | def hms_base(a): 90 | for s in [1, 60]: 91 | for x in [1, 2, 3, 5, 10, 20, 30, 60]: 92 | if a / (s * x) < 10: 93 | return x * s 94 | 95 | return 3600 * 60 96 | 97 | def time_of_day_axis(a, b, scale, offset, unit, cutoff): 98 | visible_area = abs((b - a) * scale) 99 | 100 | if visible_area < 10: 101 | base = math.log(visible_area, 10) 102 | 103 | for x in [math.log10(5), math.log10(2), 0]: 104 | if math.floor(base) - x >= base - (1 + 1e-5): 105 | base = math.floor(base) - x 106 | break 107 | 108 | base = 10 ** base * (scale / abs(scale)) 109 | else: 110 | base = float(hms_base(visible_area)) 111 | 112 | ticks = [((m * base - offset) / scale, m * base) for m 113 | in range(int(math.floor((a * scale + offset) / base)), 114 | int(math.ceil((b * scale + offset) / base) + 1))] 115 | ticks = [m for m in ticks if m[0] >= cutoff[0] + -1e-5 and m[0] <= cutoff[1] + 1e-5] 116 | 117 | digits = max(0, math.ceil(-math.log10(base))) 118 | 119 | fmt_str = "%%02d:%%02d:%%02.%df" % digits 120 | fmt = lambda a: fmt_str % ((a / 3600) % 24, (a / 60) % 60, a % 60) 121 | 122 | return [(m[0], fmt(m[1])) for m in ticks] 123 | 124 | class PlotAxes: 125 | def __init__(self, viewer, axis_x, axis_y): 126 | self.viewer = viewer 127 | self.tickers = (axis_x, axis_y) 128 | 129 | def draw_text_ss(self, x, y, text): 130 | x, y = self.viewer.view.screen_offset[0] + x, self.viewer.view.screen_offset[1] + y 131 | glWindowPos2i(int(x), int(y)) 132 | for c in text: 133 | glutBitmapCharacter(GLUT_BITMAP_8_BY_13, ord(c)) 134 | 135 | def draw_text(self, x, y, text): 136 | glPushMatrix() 137 | glLoadIdentity() 138 | 139 | (sx, sy) = self.viewer.view.to_screen(x, y) 140 | self.draw_text_ss(sx + 5, sy + 5, text) 141 | 142 | glPopMatrix() 143 | 144 | def draw_content(self): 145 | view = self.viewer.view 146 | 147 | a, b = view.from_screen(0, 0), view.from_screen(*self.viewer.screen_size) 148 | 149 | for axis, ticker, m in zip((0, 1), self.tickers, 150 | (lambda a, b: (a, b), lambda a, b: (b, a))): 151 | if ticker == None: 152 | continue 153 | 154 | (aa, oa), (ab, ob) = m(*a), m(*b) 155 | ticks = ticker(aa, ab) 156 | 157 | glColor4f(1.0, 1.0, 1.0, 0.25) 158 | glBegin(GL_LINES) 159 | for pos, label in ticks: 160 | glVertex2f(*m(pos, oa)) 161 | glVertex2f(*m(pos, ob)) 162 | glEnd() 163 | 164 | glColor4f(1.0, 1.0, 1.0, 1.0) 165 | for pos, label in ticks: 166 | x, y = m(view.to_screen(pos, pos)[axis] + 5, 5) 167 | self.draw_text_ss(x, y, label) 168 | -------------------------------------------------------------------------------- /pysdr/persistence.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | tuple2f = lambda x: (float(x[0]), float(x[1])) 4 | 5 | fields = [ 6 | (float, 'view.scale_x'), 7 | (float, 'view.scale_y'), 8 | (float, 'view.origin_x'), 9 | (float, 'view.origin_y'), 10 | (tuple2f, 'mag_range') 11 | ] 12 | 13 | def getnestedattr(obj, name): 14 | if name == '': 15 | return obj 16 | 17 | for x in name.split('.'): 18 | obj = getattr(obj, x) 19 | 20 | return obj 21 | 22 | def setnestedattr(obj, name, val): 23 | split = name.split('.') 24 | setattr(getnestedattr(obj, '.'.join(split[0:-1])), split[-1], val) 25 | 26 | def pers_load(viewer, filename): 27 | try: 28 | obj = json.load(file(filename)) 29 | for t, n in fields: 30 | setnestedattr(viewer, n, t(obj[n])) 31 | except IOError as e: 32 | print "could not load the persistance file:", e 33 | 34 | def pers_save(viewer, filename): 35 | obj = dict([(name, getnestedattr(viewer, name)) for _, name in fields]) 36 | fp = file(filename, 'w') 37 | json.dump(obj, fp, indent=4) 38 | fp.write('\n') 39 | fp.close() 40 | -------------------------------------------------------------------------------- /pysdr/recviewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import numpy as np 4 | import Queue as queue 5 | import threading 6 | import scipy.io.wavfile 7 | import sys 8 | import os.path 9 | 10 | from OpenGL.GL import * 11 | from OpenGL.GLUT import * 12 | 13 | from pysdr.waterfall import * 14 | from pysdr.overlay import * 15 | import pysdr.ext as ext 16 | 17 | class AsyncWorker(threading.Thread): 18 | def __init__(self): 19 | threading.Thread.__init__(self) 20 | self.daemon = False 21 | self.working = False 22 | self.event = threading.Event() 23 | self.start() 24 | 25 | def set_work(self, func, args): 26 | self.work = (func, args) 27 | self.event.set() 28 | 29 | def run(self): 30 | while True: 31 | self.event.wait() 32 | self.event.clear() 33 | self.working = True 34 | (func, args) = self.work 35 | func(*args) 36 | self.working = False 37 | 38 | def waterfallize(signal, bins): 39 | window = 0.5 * (1.0 - np.cos((2 * math.pi * np.arange(bins)) / bins)) 40 | segment = bins / 2 41 | nsegments = int(len(signal) / segment) 42 | m = np.repeat(np.reshape(signal[0:segment * nsegments], (nsegments, segment)), 2, axis=0) 43 | t = np.reshape(m[1:len(m) - 1], (nsegments - 1, bins)) 44 | img = np.multiply(t, window) 45 | wf = np.log(np.abs(np.fft.fft(img))) 46 | return np.concatenate((wf[:, bins / 2:bins], wf[:, 0:bins / 2]), axis=1) 47 | 48 | class RecordViewer(Viewer): 49 | def __init__(self, signal, sample_rate=None): 50 | Viewer.__init__(self, "Record Viewer") 51 | 52 | if sample_rate is not None: 53 | # TODO: cutting off trailing frames in waterfallize 54 | # probably causes time axis to be a bit off 55 | duration = float(len(signal)) / sample_rate 56 | self.layers.append(PlotAxes(self, static_axis(UNIT_HZ, sample_rate / 2, 57 | cutoff=(-1.0, 1.0)), 58 | static_axis(UNIT_SEC, -duration, offset=duration))) 59 | 60 | glutIdleFunc(self.cb_idle) 61 | self.signal = signal 62 | self.bins = None 63 | self.texture = None 64 | self.new_data_event = threading.Event() 65 | self.new_data = None 66 | self.worker = AsyncWorker() 67 | self.update_texture() 68 | 69 | def init(self): 70 | glLineWidth(1.0) 71 | glEnable(GL_BLEND) 72 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 73 | 74 | def update_texture(self): 75 | bins = int(int(np.sqrt(len(self.signal) / self.view.scale_y * self.view.scale_x)) / 16) * 16 76 | bins = min(max(bins, 16), glGetIntegerv(GL_MAX_TEXTURE_SIZE)) 77 | 78 | if bins == self.bins: 79 | return 80 | 81 | def texture_work(self, bins): 82 | waterfall = waterfallize(self.signal, bins) 83 | waterfall[np.isneginf(waterfall)] = np.nan 84 | wmin, wmax = np.nanmin(waterfall), np.nanmax(waterfall) 85 | waterfall = ((waterfall - wmin) / (wmax - wmin)) * 5.5 - 4.5 86 | self.new_data = ext.mag2col(waterfall.astype('f')) 87 | self.new_data_event.set() 88 | 89 | self.worker.set_work(texture_work, (self, bins)) 90 | 91 | def on_mouse_button(self, button, state, x, y): 92 | if state == GLUT_UP and button == GLUT_RIGHT_BUTTON: 93 | self.update_texture() 94 | 95 | def cb_idle(self): 96 | if self.new_data_event.wait(0.01): 97 | self.new_data_event.clear() 98 | 99 | try: 100 | self.texture = Texture(self.new_data) 101 | except GLError: 102 | pass 103 | 104 | self.new_data = None 105 | glutPostRedisplay() 106 | 107 | def draw_content(self): 108 | if self.texture != None: 109 | glPushMatrix() 110 | glTranslatef(-1.0, 0, 0) 111 | glScalef(2.0, 1.0, 1.0) 112 | glColor4f(1.0, 1.0, 1.0, 1.0) 113 | self.texture.draw() 114 | glPopMatrix() 115 | 116 | def draw_screen(self): 117 | if self.worker.working: 118 | glColor4f(1.0, 0.0, 0.0, 1.0) 119 | glBegin(GL_QUADS) 120 | glVertex2i(10, 10) 121 | glVertex2i(20, 10) 122 | glVertex2i(20, 20) 123 | glVertex2i(10, 20) 124 | glEnd() 125 | 126 | def view(signal, sample_rate=None): 127 | glutInit() 128 | glutInitWindowSize(640, 480) 129 | glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA) 130 | 131 | record_viewer = RecordViewer(signal, sample_rate=sample_rate) 132 | 133 | glutMainLoop() 134 | 135 | def read_file(filename): 136 | ext = os.path.splitext(filename)[1] 137 | 138 | if ext == ".wav": 139 | import scipy.io.wavfile 140 | 141 | (sample_rate, audio) = scipy.io.wavfile.read(filename) 142 | return (sample_rate, audio[:,0] + 1j * audio[:,1]) 143 | elif ext == ".fits": 144 | import pyfits 145 | img = pyfits.open(filename)[0] 146 | 147 | if int(img.header["NAXIS"]) != 2: 148 | raise Exception("expecting a two dimensional image") 149 | 150 | size = [img.header["NAXIS%d" % (i,)] for i in [1, 2]] 151 | 152 | if size[0] % 2 != 0: 153 | raise Exception("width %d is not a multiple of 2" % (size[0],)) 154 | 155 | flat_data = np.ravel(img.data) 156 | return (48000, flat_data[0::2] + 1j * flat_data[1::2]) 157 | else: 158 | raise Exception("unknown filename extension: %s" % (ext,)) 159 | 160 | def main(): 161 | if len(sys.argv) != 2: 162 | sys.stderr.write("usage: pysdr-recviewer FILENAME\n") 163 | exit(1) 164 | 165 | sample_rate, signal = read_file(sys.argv[1]) 166 | view(signal, sample_rate=sample_rate) 167 | 168 | if __name__ == "__main__": 169 | main() 170 | -------------------------------------------------------------------------------- /pysdr/setup.py: -------------------------------------------------------------------------------- 1 | def configuration(parent_package='',top_path=None): 2 | from numpy.distutils.misc_util import Configuration 3 | config = Configuration('pysdr', parent_package, top_path) 4 | config.add_extension('ext', ['ext.c'], 5 | libraries=['jack', 'm'], include_dirs=['whistle']) 6 | return config 7 | -------------------------------------------------------------------------------- /pysdr/waterfall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import math 4 | import sys 5 | import Queue 6 | import threading 7 | import numpy as np 8 | import argparse 9 | import signal 10 | import time 11 | 12 | from OpenGL.GL import * 13 | from OpenGL.GL import shaders 14 | from OpenGL.GLUT import * 15 | from OpenGL.GLU import * 16 | 17 | from pysdr.graph import MultiTexture, PlotLine 18 | from pysdr.input import RawSigInput, JackInput 19 | from pysdr.overlay import View, PlotAxes, static_axis, UNIT_HZ, UNIT_SEC, _axis, time_of_day_axis 20 | from pysdr.console import Console 21 | from pysdr.commands import make_commands_layer 22 | from pysdr.events import EventMarker, DetectorScript, MIDIEventGatherer 23 | from pysdr.persistence import pers_load, pers_save 24 | import pysdr.ext as ext 25 | 26 | class Viewer: 27 | def __init__(self, window_name): 28 | glutCreateWindow(window_name) 29 | glutDisplayFunc(self.cb_display) 30 | glutMouseFunc(self.cb_mouse) 31 | glutMotionFunc(self.cb_motion) 32 | glutReshapeFunc(self.cb_reshape) 33 | glutKeyboardFunc(self.cb_keyboard) 34 | 35 | self.init() 36 | self.view = View() 37 | self.buttons_pressed = [] 38 | 39 | self.layers = [self, self.view] 40 | 41 | def init(self): 42 | pass 43 | 44 | def call_layers(self, method, args=()): 45 | for layer in list(self.layers): 46 | if hasattr(layer.__class__, method): 47 | getattr(layer, method)(*args) 48 | 49 | def call_layers_handler(self, method, args=()): 50 | for layer in list(reversed(self.layers)): 51 | if hasattr(layer.__class__, method): 52 | if getattr(layer, method)(*args): 53 | break 54 | 55 | def cb_display(self): 56 | glClear(GL_COLOR_BUFFER_BIT) 57 | glClearColor(0, 0, 0, 1) 58 | glLoadIdentity() 59 | 60 | glPushMatrix() 61 | self.view.setup() 62 | self.call_layers('draw_content') 63 | glPopMatrix() 64 | 65 | self.call_layers('draw_screen') 66 | 67 | glutSwapBuffers() 68 | 69 | def cb_mouse(self, button, state, x, y): 70 | if state == GLUT_DOWN: 71 | self.buttons_pressed.append(button) 72 | 73 | if state == GLUT_UP: 74 | try: 75 | self.buttons_pressed.remove(button) 76 | except ValueError: 77 | pass 78 | 79 | self.call_layers_handler('on_mouse_button', (button, state, x, 80 | self.screen_size[1] - y)) 81 | 82 | glutPostRedisplay() 83 | 84 | def cb_motion(self, x, y): 85 | if len(self.buttons_pressed) == 0: 86 | return 87 | 88 | self.call_layers_handler('on_drag', (x, self.screen_size[1] - y)) 89 | 90 | glutPostRedisplay() 91 | 92 | def cb_reshape(self, w, h): 93 | glViewport(0, 0, w, h) 94 | glMatrixMode(GL_PROJECTION) 95 | glLoadIdentity() 96 | gluOrtho2D(0.0, w, 0.0, h) 97 | glMatrixMode(GL_MODELVIEW) 98 | 99 | self.screen_size = (w, h) 100 | 101 | self.call_layers('on_resize', (w, h)) 102 | 103 | def cb_keyboard(self, key, x, y): 104 | self.call_layers_handler('on_key_press', (key)) 105 | glutPostRedisplay() 106 | 107 | def get_layer(self, type): 108 | a = [a for a in self.layers if a.__class__ == type] 109 | return a[0] if len(a) else None 110 | 111 | class Texture(): 112 | def __init__(self, image): 113 | self.texture = glGenTextures(1) 114 | 115 | glEnable(GL_TEXTURE_2D) 116 | 117 | glBindTexture(GL_TEXTURE_2D, self.texture) 118 | 119 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 120 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 121 | 122 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image.shape[1], image.shape[0], 0, 123 | GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, image) 124 | 125 | glDisable(GL_TEXTURE_2D) 126 | 127 | def __del__(self): 128 | glDeleteTextures(self.texture) 129 | 130 | def draw(self): 131 | glEnable(GL_TEXTURE_2D) 132 | glBindTexture(GL_TEXTURE_2D, self.texture) 133 | glColor3f(1.0, 1.0, 1.0) 134 | 135 | glBegin(GL_QUADS) 136 | glTexCoord2i(0, 1) 137 | glVertex2i(0, 0) 138 | glTexCoord2i(1, 1) 139 | glVertex2i(1, 0) 140 | glTexCoord2i(1, 0) 141 | glVertex2i(1, 1) 142 | glTexCoord2i(0, 0) 143 | glVertex2i(0, 1) 144 | glEnd() 145 | 146 | glDisable(GL_TEXTURE_2D) 147 | 148 | class RangeSelector(): 149 | def __init__(self, viewer): 150 | self.texture = Texture(ext.mag2col((np.arange(64, dtype=np.float32) / 64) 151 | * 3.75 - 1.75).reshape((64, 1))) 152 | self.viewer = viewer 153 | self.histogram = PlotLine(90) 154 | self.hist_range = (-60, 20) 155 | self.dragging = None 156 | 157 | def mag_to_pixel(self, mag): 158 | return int((float(mag) - self.hist_range[0]) 159 | / (self.hist_range[1] - self.hist_range[0]) * 180) 160 | 161 | def pixel_to_mag(self, pixel): 162 | return float(pixel) / 180 * (self.hist_range[1] - self.hist_range[0]) + self.hist_range[0] 163 | 164 | def draw_screen(self): 165 | w, h = self.viewer.screen_size 166 | y_a, y_b = [self.mag_to_pixel(x) for x in self.viewer.mag_range] 167 | 168 | glPushMatrix() 169 | glTranslatef(w - 100, h - 200, 0) 170 | 171 | glColor4f(0.0, 0.0, 0.0, 0.7) 172 | 173 | glBegin(GL_QUADS) 174 | glVertex2i(0, 0) 175 | glVertex2i(80, 0) 176 | glVertex2i(80, 180) 177 | glVertex2i(0, 180) 178 | glEnd() 179 | 180 | glPushMatrix() 181 | glTranslatef(0, y_b, 0) 182 | glScalef(-10, y_a - y_b, 100) 183 | self.texture.draw() 184 | glPopMatrix() 185 | 186 | glPushMatrix() 187 | glTranslatef(5, 0, 0) 188 | glRotatef(90, 0, 0, 1) 189 | glScalef(180, -70.0 / self.viewer.bins, 1) 190 | glColor4f(1.0, 1.0, 1.0, 1.0) 191 | self.histogram.draw() 192 | glPopMatrix() 193 | 194 | glColor4f(1.0, 1.0, 1.0, 0.3) 195 | glBegin(GL_QUADS) 196 | glVertex2i(0, y_a - 10) 197 | glVertex2i(0, y_a) 198 | glVertex2i(80, y_a) 199 | glVertex2i(80, y_a - 10) 200 | 201 | glVertex2i(0, y_b + 10) 202 | glVertex2i(0, y_b) 203 | glVertex2i(80, y_b) 204 | glVertex2i(80, y_b + 10) 205 | 206 | glEnd() 207 | 208 | glPopMatrix() 209 | 210 | def on_log_spectrum(self, spectrum): 211 | (self.histogram.data[:], _) = np.histogram(spectrum, bins=90, range=self.hist_range) 212 | 213 | def on_mouse_button(self, button, state, x, y): 214 | if button != GLUT_LEFT_BUTTON: 215 | return False 216 | 217 | if state == GLUT_DOWN: 218 | w, h = self.viewer.screen_size 219 | pix_a, pix_b = [self.mag_to_pixel(l) for l in self.viewer.mag_range] 220 | 221 | if w - 100 <= x <= w - 20: 222 | if y + 10 >= h - 200 + pix_a >= y: 223 | self.dragging = (pix_a - y, None) 224 | return True 225 | 226 | if y - 10 <= h - 200 + pix_b <= y: 227 | self.dragging = (None, pix_b - y) 228 | return True 229 | 230 | if w - 110 <= x <= w - 100 and pix_a <= y - h + 200 <= pix_b: 231 | self.dragging = (pix_a - y, pix_b - y) 232 | return True 233 | 234 | return False 235 | 236 | if state == GLUT_UP: 237 | if self.dragging != None: 238 | self.dragging = None 239 | return True 240 | else: 241 | return False 242 | 243 | def on_drag(self, x, y): 244 | if self.dragging != None: 245 | self.viewer.mag_range = tuple(self.pixel_to_mag(d + y) if d != None else x for (x, d) \ 246 | in zip(self.viewer.mag_range, self.dragging)) 247 | return True 248 | else: 249 | return False 250 | 251 | class ColorMappingShader: 252 | VERT_SHADER_CODE = """ 253 | #version 120 254 | void main() { 255 | gl_TexCoord[0] = gl_MultiTexCoord0; 256 | gl_Position = ftransform(); 257 | } 258 | """ 259 | 260 | FRAG_SHADER_CODE = """ 261 | #version 120 262 | uniform float scale; 263 | uniform float shift; 264 | uniform sampler2D sampler; 265 | 266 | float mag2col_base2_blue(float val) 267 | { 268 | if (val <= -2.75) 269 | return 0.0; 270 | 271 | if (val <= -1.75) 272 | return val + 2.75; 273 | 274 | if (val <= -0.75) 275 | return -(val + 0.75); 276 | 277 | if (val <= 0.0) 278 | return 0.0; 279 | 280 | if (val >= 1.0) 281 | return 1.0; 282 | 283 | return val; 284 | } 285 | 286 | vec3 mag2col(float a) { 287 | return vec3(clamp(a + 1.0, 0.0, 1.0), clamp(a, 0.0, 1.0), 288 | mag2col_base2_blue(a - 1.0)); 289 | } 290 | 291 | void main() { 292 | gl_FragColor = vec4(mag2col((texture2D(sampler, gl_TexCoord[0].xy).x + shift) * scale), 1); 293 | } 294 | """ 295 | 296 | M2C_RANGE = (-1.75, 2.) 297 | TEX_RANGE = (-100., 60.) 298 | 299 | def setup(self, mag_range): 300 | scale = (self.TEX_RANGE[1] - self.TEX_RANGE[0]) / (mag_range[1] - mag_range[0]) * (self.M2C_RANGE[1] - self.M2C_RANGE[0]) 301 | shift = (self.TEX_RANGE[0] - mag_range[0]) / (self.TEX_RANGE[1] - self.TEX_RANGE[0]) + self.M2C_RANGE[0] / scale 302 | 303 | shaders.glUseProgram(self.program) 304 | 305 | glUniform1i(glGetUniformLocation(self.program, "sampler"), 0) 306 | glUniform1f(glGetUniformLocation(self.program, "scale"), scale) 307 | glUniform1f(glGetUniformLocation(self.program, "shift"), shift) 308 | glActiveTexture(GL_TEXTURE0) 309 | 310 | def texture_insert_prep(self, line): 311 | return (line - self.TEX_RANGE[0]) / (self.TEX_RANGE[1] - self.TEX_RANGE[0]) 312 | 313 | def __init__(self): 314 | vert_shader = shaders.compileShader(self.VERT_SHADER_CODE, GL_VERTEX_SHADER) 315 | frag_shader = shaders.compileShader(self.FRAG_SHADER_CODE, GL_FRAGMENT_SHADER) 316 | self.program = shaders.compileProgram(vert_shader, frag_shader) 317 | 318 | 319 | class WaterfallWindow(Viewer): 320 | def __init__(self, sig_input, bins, overlap=0, start_time=None): 321 | if bins % 1024 != 0: 322 | raise NotImplementedError("number of bins must be a multiple of 1024") 323 | 324 | Viewer.__init__(self, "PySDR") 325 | glutIdleFunc(self.cb_idle) 326 | 327 | self.mag_range = (-45, 5) 328 | 329 | self.sig_input = sig_input 330 | self.bins = bins 331 | self.window = 0.5 * (1.0 - np.cos((2 * math.pi * np.arange(self.bins)) / self.bins)) 332 | self.overlap = overlap 333 | self.row_duration = float(bins - overlap) / sig_input.sample_rate 334 | # TODO: can we be sure the texture will be stored as floats internally? 335 | self.multitexture = MultiTexture(1024, 1024, self.bins / 1024, 1, format=GL_RED, type=GL_FLOAT) 336 | 337 | self.start_time = time.time() if start_time is None else start_time 338 | 339 | def time_axis(a, b): 340 | scale = self.row_duration * self.multitexture.get_height() 341 | shift = self.row_duration * self.texture_row - scale + self.start_time % (3600 * 24) 342 | return time_of_day_axis(a, b, scale, shift, UNIT_SEC, (0.0, 1.0)) 343 | 344 | self.overlay = PlotAxes(self, static_axis(UNIT_HZ, sig_input.sample_rate / 2, 345 | cutoff=(-1.0, 1.0)), time_axis) 346 | self.layers.append(self.overlay) 347 | 348 | self.texture_inserts = Queue.Queue() 349 | self.texture_edge = 0 350 | 351 | self.texture_row = 0 352 | self.process_row = 0 353 | 354 | self.process_thread = threading.Thread(target=self.process) 355 | self.process_thread.setDaemon(True) 356 | 357 | self.shader = ColorMappingShader() 358 | 359 | def start(self): 360 | self.sig_input.start() 361 | self.process_thread.start() 362 | 363 | def init(self): 364 | glLineWidth(1.0) 365 | glEnable(GL_BLEND) 366 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 367 | 368 | def draw_content(self): 369 | glPushMatrix() 370 | glTranslated(-1.0, 0.0, 0.0) 371 | glScalef(2.0, 1.0, 1.0) 372 | 373 | self.shader.setup(self.mag_range) 374 | self.multitexture.draw_scroll(self.texture_edge) 375 | shaders.glUseProgram(0) 376 | glPopMatrix() 377 | 378 | def freq_to_bin(self, freq): 379 | return int(freq * self.bins / self.sig_input.sample_rate + self.bins / 2) 380 | 381 | def bin_to_freq(self, bin): 382 | return float(bin - self.bins / 2) / self.bins * self.sig_input.sample_rate 383 | 384 | def bin_to_x(self, bin): 385 | return float(bin) / self.bins * 2 - 1 386 | 387 | def row_to_y(self, row): 388 | return float(row - self.texture_row) / self.multitexture.get_height() + 1.0 389 | 390 | def cb_idle(self): 391 | try: 392 | while True: 393 | rec = self.texture_inserts.get(block=True, timeout=0.01) 394 | self.multitexture.insert(self.texture_edge, rec, format=GL_RED, type=GL_FLOAT) 395 | self.texture_row = self.texture_row + 1 396 | self.texture_edge = self.texture_row % self.multitexture.get_height() 397 | 398 | self.call_layers('on_texture_insert') 399 | 400 | glutPostRedisplay() 401 | except Queue.Empty: 402 | return 403 | 404 | def process(self): 405 | ringbuf = np.zeros(self.bins * 4, dtype=np.complex64) 406 | ringbuf_edge = self.bins 407 | readsize = self.bins - self.overlap 408 | 409 | while True: 410 | if (ringbuf_edge + readsize > len(ringbuf)): 411 | ringbuf[0:self.overlap] = ringbuf[ringbuf_edge - self.overlap:ringbuf_edge] 412 | ringbuf_edge = self.overlap 413 | 414 | ringbuf[ringbuf_edge:ringbuf_edge + readsize] = self.sig_input.read(readsize) 415 | ringbuf_edge += readsize 416 | 417 | signal = ringbuf[ringbuf_edge - self.bins:ringbuf_edge] 418 | 419 | spectrum = np.absolute(np.fft.fft(np.multiply(signal, self.window))) 420 | spectrum = np.concatenate((spectrum[self.bins/2:self.bins], spectrum[0:self.bins/2])) 421 | 422 | self.call_layers('on_lin_spectrum', (spectrum,)) 423 | spectrum = np.log10(spectrum) * 10 424 | self.call_layers('on_log_spectrum', (spectrum,)) 425 | 426 | #try: 427 | # scale = 3.75 / (self.mag_range[1] - self.mag_range[0]) 428 | #except ZeroDivisionError: 429 | # scale = 3.75 / 0.00001 430 | 431 | #shift = -self.mag_range[0] * scale - 1.75 432 | 433 | #line = ext.mag2col((spectrum * scale + shift).astype('f')) 434 | line = self.shader.texture_insert_prep(spectrum).astype('f') 435 | self.process_row = self.process_row + 1 436 | self.texture_inserts.put(line) 437 | 438 | class Label: 439 | @staticmethod 440 | def draw_bg(x, y, w, h, padding=0): 441 | x, y = x - padding, y - padding 442 | w, h = w + 2 * padding, h + 2 * padding 443 | 444 | glColor4f(0.0, 0.0, 0.0, 0.5) 445 | glBegin(GL_QUADS) 446 | glVertex2i(x + w, y) 447 | glVertex2i(x + w, y + h) 448 | glVertex2i(x, y + h) 449 | glVertex2i(x, y) 450 | glEnd() 451 | 452 | def __init__(self, viewer, content): 453 | self.viewer = viewer 454 | self.content = content 455 | 456 | def draw_screen(self): 457 | w, h = self.viewer.screen_size 458 | x, y = 10, h - Console.CHAR_HEIGHT - 10 459 | 460 | self.draw_bg(x, y - 2, Console.CHAR_WIDTH * len(self.content), 461 | Console.CHAR_HEIGHT, padding=3) 462 | 463 | glColor4f(1.0, 1.0, 1.0, 0.75) 464 | Console.draw_string(x, y, self.content) 465 | 466 | class DateLabel: 467 | def __init__(self, viewer): 468 | self.viewer = viewer 469 | 470 | def draw_screen(self): 471 | d = "2015-05-22" 472 | strlen = len(d) 473 | 474 | w, h = self.viewer.screen_size 475 | x, y = w - 10 - strlen * Console.CHAR_WIDTH, 10 + Console.CHAR_HEIGHT 476 | Label.draw_bg(x, y - 2, Console.CHAR_WIDTH * strlen, Console.CHAR_HEIGHT, padding=3) 477 | 478 | glColor4f(1.0, 1.0, 1.0, 1.0) 479 | Console.draw_string(x, y, d) 480 | 481 | def main(): 482 | global viewer # so that it can be accessed from the embedded console 483 | 484 | signal.signal(signal.SIGINT, lambda a, b: sys.exit(0)) 485 | 486 | parser = argparse.ArgumentParser(description='Plot live spectral waterfall of a quadrature signal.') 487 | parser.add_argument('-b', '--bins', type=int, default=4096, 488 | help='number of FFT bins (default: %(default)s)') 489 | parser.add_argument('-H', '--height', type=float, default=0, 490 | help='minimal height of the waterfall in seconds \ 491 | (default corresponds to 1024 windows)') 492 | parser.add_argument('-o', '--overlap', type=float, default=0.75, 493 | help='overlap between consecutive windows as a proportion \ 494 | of the number of bins (default: %(default)s)') 495 | parser.add_argument('-j', '--jack', metavar='NAME', default='pysdr', 496 | help='feed signal from JACK and use the given client name \ 497 | (by default, with name \'pysdr\')') 498 | parser.add_argument('-r', '--raw', metavar='RATE', type=int, 499 | help='feed signal from the standard input, expects 2 channel \ 500 | interleaved floats with the given sample-rate') 501 | parser.add_argument('-d', '--detector', metavar='ARGS', action='append', 502 | help='attach the given detector script, \ 503 | expects to be given the script filename \ 504 | followed by arguments for the script, \ 505 | all joined by spaces and passed on the command-line \ 506 | as one quoted argument') 507 | parser.add_argument('-p', '--persfn', metavar='FILENAME', 508 | help='a file in which to preserve the visualization parameters \ 509 | that come from interactive manipulation, \ 510 | i.e. the visible area of the waterfall \ 511 | and the selected magnitude range \ 512 | (save triggered by pressing \'p\')') 513 | 514 | args = parser.parse_args() 515 | 516 | overlap_bins = int(args.bins * args.overlap) 517 | 518 | if not (overlap_bins >= 0 and overlap_bins < args.bins): 519 | raise ValueError("number of overlapping bins is out of bounds") 520 | 521 | if args.raw is not None: 522 | sig_input = RawSigInput(args.raw, 2, np.dtype(np.float32), sys.stdin) 523 | else: 524 | sig_input = JackInput(args.jack) 525 | 526 | glutInit() 527 | glutInitWindowSize(640, 480) 528 | glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA) 529 | 530 | viewer = WaterfallWindow(sig_input, args.bins, overlap=overlap_bins) 531 | 532 | if args.detector: 533 | detector_em = EventMarker(viewer) 534 | viewer.layers.append(detector_em) 535 | viewer.layers += [DetectorScript(viewer, [detector_em], a.split()) for a in args.detector] 536 | 537 | if isinstance(sig_input, JackInput): 538 | midi_em = EventMarker(viewer) 539 | viewer.layers += [midi_em, MIDIEventGatherer(viewer, [midi_em])] 540 | 541 | viewer.layers += [make_commands_layer(viewer), RangeSelector(viewer), 542 | Label(viewer, str(viewer.sig_input)), 543 | # TODO 544 | # DateLabel(viewer), 545 | Console(viewer, globals())] 546 | 547 | if args.persfn is not None: 548 | viewer.persfn = args.persfn 549 | pers_load(viewer, args.persfn) 550 | 551 | viewer.start() 552 | 553 | glutMainLoop() 554 | 555 | if __name__ == "__main__": 556 | main() 557 | -------------------------------------------------------------------------------- /scipy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLAB-project/pysdr/b6b3ba203eddeeb3088c4df1b115c492b14cb030/scipy/__init__.py -------------------------------------------------------------------------------- /scipy/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLAB-project/pysdr/b6b3ba203eddeeb3088c4df1b115c492b14cb030/scipy/io/__init__.py -------------------------------------------------------------------------------- /scipy/io/wavfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to read / write wav files using numpy arrays 3 | 4 | Functions 5 | --------- 6 | `read`: Return the sample rate (in samples/sec) and data from a WAV file. 7 | 8 | `write`: Write a numpy array as a WAV file. 9 | 10 | """ 11 | from __future__ import division, print_function, absolute_import 12 | 13 | import sys 14 | import numpy 15 | import struct 16 | import warnings 17 | 18 | 19 | class WavFileWarning(UserWarning): 20 | pass 21 | 22 | _big_endian = False 23 | 24 | WAVE_FORMAT_PCM = 0x0001 25 | WAVE_FORMAT_IEEE_FLOAT = 0x0003 26 | WAVE_FORMAT_EXTENSIBLE = 0xfffe 27 | KNOWN_WAVE_FORMATS = (WAVE_FORMAT_PCM, WAVE_FORMAT_IEEE_FLOAT) 28 | 29 | # assumes file pointer is immediately 30 | # after the 'fmt ' id 31 | 32 | 33 | def _read_fmt_chunk(fid): 34 | if _big_endian: 35 | fmt = '>' 36 | else: 37 | fmt = '<' 38 | res = struct.unpack(fmt+'iHHIIHH',fid.read(20)) 39 | size, comp, noc, rate, sbytes, ba, bits = res 40 | 41 | if comp not in KNOWN_WAVE_FORMATS or (size > 16 and comp != WAVE_FORMAT_IEEE_FLOAT): 42 | comp = WAVE_FORMAT_PCM 43 | warnings.warn("Unknown wave file format", WavFileWarning) 44 | 45 | if size > 16: 46 | fid.read(size - 16) 47 | 48 | return size, comp, noc, rate, sbytes, ba, bits 49 | 50 | 51 | # assumes file pointer is immediately 52 | # after the 'data' id 53 | def _read_data_chunk(fid, comp, noc, bits, mmap=False): 54 | if _big_endian: 55 | fmt = '>i' 56 | else: 57 | fmt = ' 1: 81 | data = data.reshape(-1,noc) 82 | return data 83 | 84 | 85 | def _skip_unknown_chunk(fid): 86 | if _big_endian: 87 | fmt = '>i' 88 | else: 89 | fmt = '' or (data.dtype.byteorder == '=' and sys.byteorder == 'big'): 242 | data = data.byteswap() 243 | _array_tofile(fid, data) 244 | 245 | # Determine file size and place it in correct 246 | # position at start of the file. 247 | size = fid.tell() 248 | fid.seek(4) 249 | fid.write(struct.pack('= 3: 259 | def _array_tofile(fid, data): 260 | # ravel gives a c-contiguous buffer 261 | fid.write(data.ravel().view('b').data) 262 | else: 263 | def _array_tofile(fid, data): 264 | fid.write(data.tostring()) 265 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | def configuration(parent_package='', top_path=None): 2 | from numpy.distutils.misc_util import Configuration 3 | 4 | config = Configuration(None, parent_package, top_path) 5 | config.add_subpackage('pysdr') 6 | 7 | return config 8 | 9 | if __name__ == "__main__": 10 | from numpy.distutils.core import setup 11 | setup(name='PySDR', 12 | author='Martin Poviser', 13 | author_email='martin.povik@gmail.com', 14 | license='GPL', 15 | version='0.1dev', 16 | url='http://www.github.com/MLAB-project/pysdr', 17 | configuration=configuration, 18 | requires=['numpy', 'pyopengl', 'scipy'], 19 | scripts=['pysdr-recviewer', 'pysdr-waterfall']) 20 | -------------------------------------------------------------------------------- /tools/3dwf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import math 8 | import gzip 9 | import ctypes 10 | import socket 11 | import numpy as np 12 | 13 | import os 14 | os.environ['PYSDL2_DLL_PATH'] = '.' 15 | 16 | import sdl2 17 | import sdl2.ext 18 | 19 | import OpenGL.GL as gl 20 | try: 21 | import queue 22 | except ImportError: 23 | import Queue as queue 24 | 25 | from OpenGL.GL import * 26 | from OpenGL.GL import shaders 27 | from OpenGL.GLU import gluOrtho2D 28 | from OpenGL.arrays import vbo, GLOBAL_REGISTRY 29 | 30 | import threading 31 | 32 | 33 | if np.float32 not in GLOBAL_REGISTRY: 34 | from OpenGL.arrays.numpymodule import NumpyHandler 35 | NumpyHandler().register(np.float32) 36 | 37 | pybuf_from_memory = ctypes.pythonapi.PyMemoryView_FromMemory 38 | pybuf_from_memory.restype = ctypes.py_object 39 | 40 | 41 | FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" 42 | 43 | font_manager = sdl2.ext.FontManager(FONT_PATH) 44 | text_cache = dict() 45 | 46 | def render_text(text): 47 | if text not in text_cache: 48 | surface = font_manager.render(text) 49 | 50 | texture = gl.glGenTextures(1) 51 | 52 | a = np.zeros((surface.h, surface.w), dtype=np.uint32) 53 | a[:,:] = sdl2.ext.pixels2d(surface).transpose() 54 | 55 | gl.glEnable(GL_TEXTURE_2D) 56 | gl.glBindTexture(gl.GL_TEXTURE_2D, texture) 57 | gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, surface.w, surface.h, 0, 58 | gl.GL_BGRA, gl.GL_UNSIGNED_INT_8_8_8_8_REV, a) 59 | gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) 60 | gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) # TODO: gl prefix? 61 | gl.glDisable(GL_TEXTURE_2D) 62 | text_cache[text] = {'surf': surface, 'text': texture} 63 | 64 | surf = text_cache[text]['surf'] 65 | texture = text_cache[text]['text'] 66 | 67 | gl.glColor4f(1.0, 1.0, 1.0, 1.0) 68 | gl.glEnable(gl.GL_TEXTURE_2D) 69 | gl.glBindTexture(gl.GL_TEXTURE_2D, texture) 70 | 71 | gl.glBegin(GL_QUADS) 72 | glTexCoord2i(0, 1) 73 | gl.glVertex2i(0, 0) 74 | glTexCoord2i(0, 0) 75 | gl.glVertex2i(0, surf.h) 76 | glTexCoord2i(1, 0) 77 | gl.glVertex2i(surf.w, surf.h) 78 | glTexCoord2i(1, 1) 79 | gl.glVertex2i(surf.w, 0) 80 | gl.glEnd() 81 | 82 | gl.glDisable(gl.GL_TEXTURE_2D) 83 | 84 | 85 | class Waterfall3D: 86 | def __init__(self, points, history=100): 87 | self.history = history 88 | self.points = points 89 | self.last_line = np.zeros(points, dtype=np.float32) 90 | self.vbo = vbo.VBO(np.zeros((points - 1) * 4 * self.history * 3, 91 | dtype=np.float32), usage='GL_STREAM_DRAW_ARB') 92 | self.vbo_edge = 0 93 | self.nrows = 0 94 | 95 | vertex_shader = shaders.compileShader(""" 96 | #version 130 97 | varying float height; 98 | void main() { 99 | height = gl_Vertex.z; 100 | gl_Position = ftransform(); 101 | } 102 | """, gl.GL_VERTEX_SHADER) 103 | 104 | frag_shader = shaders.compileShader(""" 105 | #version 130 106 | varying float height; 107 | uniform float scale; 108 | uniform float offset; 109 | 110 | float mag2col_base2(float val) 111 | { 112 | if (val <= 0.0) 113 | return 0.0; 114 | if (val >= 1.0) 115 | return 1.0; 116 | 117 | return val; 118 | } 119 | 120 | float mag2col_base2_blue(float val) 121 | { 122 | if (val <= -2.75) 123 | return 0.0; 124 | 125 | if (val <= -1.75) 126 | return val + 2.75; 127 | 128 | if (val <= -0.75) 129 | return -(val + 0.75); 130 | 131 | if (val <= 0.0) 132 | return 0.0; 133 | 134 | if (val >= 1.0) 135 | return 1.0; 136 | 137 | return val; 138 | } 139 | 140 | vec3 mag2col(float a) { 141 | return vec3(mag2col_base2(a + 1.0), mag2col_base2(a), 142 | mag2col_base2_blue(a - 1.0)); 143 | } 144 | 145 | void main() { 146 | //gl_FragColor = vec4(height, 1 - height, height, 1 ); 147 | gl_FragColor = vec4(mag2col(offset + height * scale), 1); 148 | } 149 | """, gl.GL_FRAGMENT_SHADER) 150 | self.shader = shaders.compileProgram(vertex_shader,frag_shader) 151 | 152 | def set(self, content): 153 | # TODO 154 | raise NotImplementedError 155 | 156 | def insert(self, line): 157 | vbo_edge = self.nrows % self.history 158 | 159 | strip_nvertices = (self.points - 1) * 4 160 | strip = np.zeros((strip_nvertices, 3), dtype=np.float32) 161 | strip[:,0] = np.repeat(np.arange(self.points, dtype=np.float32) / (self.points - 1), 4)[2:-2] 162 | strip[:,1] = np.tile((np.array([1.0, 0.0, 0.0, 1.0]) + vbo_edge) / self.history, self.points - 1) 163 | strip[0::4,2] = line[0:-1] 164 | strip[3::4,2] = line[1::] 165 | strip[1::4,2] = self.last_line[0:-1] 166 | strip[2::4,2] = self.last_line[1::] 167 | 168 | self.vbo[len(self.vbo) - (vbo_edge + 1) * strip_nvertices * 3 \ 169 | :len(self.vbo) - vbo_edge * strip_nvertices * 3] = strip.flatten() 170 | 171 | self.nrows = self.nrows + 1 172 | self.last_line = line 173 | 174 | def draw_scroll(self, offset, scale): 175 | glPushMatrix() 176 | 177 | shaders.glUseProgram(self.shader) 178 | 179 | glUniform1f(glGetUniformLocation(self.shader, "offset"), offset) 180 | glUniform1f(glGetUniformLocation(self.shader, "scale"), scale) 181 | 182 | self.vbo.bind() 183 | glColor4f(1.0, 0.0, 0.0, 1.0) 184 | glEnableClientState(GL_VERTEX_ARRAY) 185 | glVertexPointerf(None) 186 | 187 | vbo_edge = self.nrows % self.history 188 | strip_nvertices = (self.points - 1) * 4 189 | 190 | glPushMatrix() 191 | glTranslatef(0.0, -float(vbo_edge) / self.history, 0.0) 192 | glDrawArrays(GL_QUADS, strip_nvertices * (self.history - vbo_edge), strip_nvertices * vbo_edge) 193 | glPopMatrix() 194 | 195 | glPushMatrix() 196 | glTranslatef(0.0, -1.0 - float(vbo_edge) / self.history, 0.0) 197 | glDrawArrays(GL_QUADS, 0, strip_nvertices * (self.history - vbo_edge)) 198 | glPopMatrix() 199 | 200 | glDisableClientState(GL_VERTEX_ARRAY) 201 | self.vbo.unbind() 202 | shaders.glUseProgram(0) 203 | 204 | glPopMatrix() 205 | 206 | def draw(self): 207 | glPushMatrix() 208 | 209 | shaders.glUseProgram(self.shader) 210 | self.vbo.bind() 211 | glColor4f(1.0, 0.0, 0.0, 1.0) 212 | glEnableClientState(GL_VERTEX_ARRAY) 213 | glVertexPointerf(None) 214 | 215 | vbo_edge = self.nrows % self.history 216 | strip_nvertices = (self.points - 1) * 4 217 | 218 | glDrawArrays(GL_QUADS, 0, strip_nvertices * self.history) 219 | glDisableClientState(GL_VERTEX_ARRAY) 220 | self.vbo.unbind() 221 | shaders.glUseProgram(0) 222 | 223 | glPopMatrix() 224 | 225 | def rotate_vector(v, axis, theta_cos, theta_sin): 226 | va = axis * np.vdot(v, axis) 227 | y = v - va 228 | x = np.cross(axis, y) 229 | return va + y * theta_cos + x * theta_sin 230 | 231 | def rotation_matrix(axis, theta): 232 | theta_cos, theta_sin = math.cos(theta), math.sin(theta) 233 | 234 | m = np.eye(4) 235 | m[0:3, 0] = rotate_vector(m[0:3, 0], axis, theta_cos, theta_sin) 236 | m[0:3, 1] = rotate_vector(m[0:3, 1], axis, theta_cos, theta_sin) 237 | m[0:3, 2] = rotate_vector(m[0:3, 2], axis, theta_cos, theta_sin) 238 | 239 | return m 240 | 241 | def projection_matrix(fov, ratio, near, far): 242 | m = np.zeros((4, 4), dtype=np.float32) 243 | 244 | top = math.tan(math.radians(fov / 2)) 245 | right = top * ratio 246 | 247 | m[0,0] = 1.0 / right 248 | m[1,1] = 1.0 / top 249 | m[2,2] = (far + near) / (near - far) 250 | m[2,3] = (2.0 * far * near) / (near - far) 251 | m[3,2] = -1 252 | 253 | return m 254 | 255 | def scale_matrix(sx, sy, sz): 256 | m = np.eye(4) 257 | m[0,0] = sx; m[1,1] = sy; m[2,2] = sz; 258 | return m 259 | 260 | def translation_matrix(tx, ty, tz): 261 | m = np.eye(4) 262 | m[0:3,3] = np.array([tx, ty, tz]) 263 | return m 264 | 265 | def normv(v): 266 | return v / np.linalg.norm(v) 267 | 268 | def screen2world(m, x, y): 269 | a = np.dot(m, np.array([x, y, 1.0, 1.0])) 270 | return a[0:3] / a[3] 271 | 272 | class Slider: 273 | def __init__(self, ori_x, ori_y, cb, text): 274 | self.sliding = False 275 | self.origin_x = ori_x 276 | self.origin_y = ori_y 277 | self.text = text 278 | self.cb = cb 279 | self.start_pos = 100 280 | self.pos = self.start_pos 281 | 282 | def draw(self): 283 | gl.glPushMatrix() 284 | 285 | gl.glTranslatef(self.origin_x, self.origin_y, 0) 286 | 287 | gl.glColor4f(1.0, 1.0, 1.0, 1.0) 288 | render_text(self.text) 289 | 290 | # gl.glPushMatrix() 291 | # gl.glTranslatef(self.pos - 100, 0, 0) 292 | # gl.glBegin(GL_LINES) 293 | # gl.glVertex2i(95, 14) 294 | # gl.glVertex2i(85, 10) 295 | # gl.glVertex2i(95, 6) 296 | # gl.glVertex2i(85, 10) 297 | # 298 | # gl.glVertex2i(125, 14) 299 | # gl.glVertex2i(135, 10) 300 | # gl.glVertex2i(125, 6) 301 | # gl.glVertex2i(135, 10) 302 | # gl.glEnd() 303 | # gl.glPopMatrix() 304 | 305 | gl.glColor4f(0.7, 0.7, 0.7, 0.4) 306 | gl.glBegin(GL_QUADS) 307 | gl.glVertex2i(self.pos, 0) 308 | gl.glVertex2i(self.pos, 20) 309 | gl.glVertex2i(self.pos + 20, 20) 310 | gl.glVertex2i(self.pos + 20, 0) 311 | gl.glEnd() 312 | 313 | gl.glPopMatrix() 314 | 315 | def event(self, event): 316 | if self.sliding: 317 | if event.type == sdl2.SDL_MOUSEBUTTONUP: 318 | self.pos = self.start_pos 319 | self.sliding = False 320 | return True 321 | 322 | if event.type == sdl2.SDL_MOUSEMOTION: 323 | new_pos = self.start_pos + (event.motion.x - self.start_x) 324 | self.cb(new_pos - self.pos) 325 | self.pos = new_pos 326 | return True 327 | 328 | if event.type == sdl2.SDL_MOUSEBUTTONDOWN and (event.motion.state & sdl2.SDL_BUTTON_LEFT) \ 329 | and self.origin_x + self.pos <= event.motion.x <= self.origin_y + self.pos + 20 \ 330 | and self.origin_y <= event.motion.y <= 20 + self.origin_y: 331 | self.sliding = True 332 | self.start_x = event.motion.x 333 | return True 334 | 335 | return False 336 | 337 | class WFViewer: 338 | def __init__(self, bins): 339 | self.view_inv = self.view = self.modelview = np.eye(4) 340 | self.waterfall = Waterfall3D(bins, history=500) 341 | self.inserts = queue.Queue() 342 | 343 | self.color_offset = 1.0 344 | self.color_scale = 1.0 345 | self.volume = 100.0 346 | 347 | def offset_func(name): 348 | def func(a): 349 | self.__dict__[name] += float(a) / 100.0 350 | return func 351 | 352 | def scale_func(name, ratio=0.01): 353 | def func(a): 354 | self.__dict__[name] *= 2 ** (float(a) * ratio) 355 | return func 356 | 357 | self.sliders = [ 358 | Slider(20, 20, offset_func('color_offset'), 'offset'), 359 | Slider(20, 42, scale_func('color_scale'), 'contrast'), 360 | Slider(20, 64, scale_func('volume', ratio=0.05), 'volume'), 361 | ] 362 | 363 | def init(self, w, h): 364 | gl.glEnable(gl.GL_DEPTH_TEST) 365 | 366 | self.resize(w, h) 367 | 368 | def resize(self, w, h): 369 | self.screen_size = (w, h) 370 | 371 | gl.glViewport(0, 0, w, h) 372 | 373 | ratio = float(w) / float(h) 374 | self.projection = projection_matrix(60.0, ratio, 0.1, 100.0) 375 | self.projection_inv = np.linalg.inv(self.projection) 376 | 377 | gl.glMatrixMode(gl.GL_PROJECTION) 378 | gl.glLoadMatrixf(self.projection.transpose()) 379 | gl.glMatrixMode(gl.GL_MODELVIEW) 380 | gl.glLoadIdentity() 381 | 382 | def draw(self): 383 | gl.glClearColor(0.0, 0.0, 0.0, 0.0) 384 | gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) 385 | 386 | gl.glLoadIdentity() 387 | 388 | mat = np.dot(np.dot(np.dot(translation_matrix(0, 0, -3.0), self.modelview), 389 | scale_matrix(2.0, 1.0, 0.3)), translation_matrix(-0.5, 2.5, -0.5)) 390 | 391 | gl.glMultMatrixf(mat.transpose()) 392 | gl.glColor3f(1.0, 1.0, 1.0) 393 | 394 | gl.glTranslatef(0, float(self.shift) / self.waterfall.history, 0) 395 | gl.glScalef(1.0, 5.0, 1.0) 396 | self.waterfall.draw_scroll(self.color_offset, self.color_scale) 397 | 398 | gl.glMatrixMode(gl.GL_PROJECTION) 399 | gl.glPushMatrix() 400 | gl.glLoadIdentity() 401 | gluOrtho2D(0.0, self.screen_size[0], 0.0, self.screen_size[1]) 402 | gl.glMatrixMode(gl.GL_MODELVIEW) 403 | gl.glLoadIdentity() 404 | gl.glDisable(gl.GL_DEPTH_TEST) 405 | gl.glEnable(gl.GL_BLEND) 406 | gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) 407 | 408 | # gl.glPushMatrix() 409 | # gl.glTranslatef(100, 100, 0) 410 | # 411 | # gl.glColor4f(1.0, 1.0, 1.0, 1.0) 412 | # 413 | # gl.glBegin(gl.GL_LINES) 414 | # gl.glVertex2i(10, 8) 415 | # gl.glVertex2i(0, 4) 416 | # gl.glVertex2i(10, 0) 417 | # gl.glVertex2i(0, 4) 418 | # 419 | # gl.glVertex2i(35, 8) 420 | # gl.glVertex2i(45, 4) 421 | # gl.glVertex2i(35, 0) 422 | # gl.glVertex2i(45, 4) 423 | # gl.glEnd() 424 | # 425 | # gl.glPopMatrix() 426 | 427 | for a in self.sliders: 428 | a.draw() 429 | 430 | gl.glDisable(gl.GL_BLEND) 431 | gl.glEnable(gl.GL_DEPTH_TEST) 432 | gl.glMatrixMode(gl.GL_PROJECTION) 433 | gl.glPopMatrix() 434 | gl.glMatrixMode(gl.GL_MODELVIEW) 435 | 436 | def event2world(self, event): 437 | v = screen2world(self.projection_inv, 438 | float(event.motion.x) / self.screen_size[0] * 2.0 - 1.0, 439 | float(event.motion.y) / self.screen_size[1] * 2.0 - 1.0) 440 | return np.dot(self.view_inv, np.concatenate((normv(v), np.array([1]))))[0:3] 441 | 442 | def event(self, event): 443 | if event.type == sdl2.SDL_MOUSEBUTTONDOWN \ 444 | or event.type == sdl2.SDL_MOUSEBUTTONUP \ 445 | or event.type == sdl2.SDL_MOUSEMOTION: 446 | event.motion.y = self.screen_size[1] - event.motion.y 447 | 448 | for a in self.sliders: 449 | if a.event(event): 450 | return True 451 | 452 | if event.type == sdl2.SDL_MOUSEBUTTONDOWN and (event.motion.state & sdl2.SDL_BUTTON_LEFT): 453 | self.drag_start = self.event2world(event) 454 | 455 | if event.type == sdl2.SDL_MOUSEMOTION and (event.motion.state & sdl2.SDL_BUTTON_LEFT): 456 | v = self.event2world(event) 457 | c = np.cross(self.drag_start, v) 458 | 459 | self.modelview = np.dot(self.view, rotation_matrix(normv(c), math.atan2(np.linalg.norm(c), np.vdot(self.drag_start, v)))) 460 | 461 | if event.type == sdl2.SDL_MOUSEBUTTONUP and (event.motion.state & sdl2.SDL_BUTTON_LEFT): 462 | self.view = self.modelview 463 | self.view_inv = np.linalg.inv(self.view) 464 | 465 | if event.type == sdl2.SDL_WINDOWEVENT and event.window.event == sdl2.SDL_WINDOWEVENT_RESIZED: 466 | self.resize(event.window.data1, event.window.data2) 467 | 468 | if event.type == sdl2.SDL_USEREVENT: 469 | self.cb_idle() 470 | 471 | def cb_idle(self): 472 | try: 473 | rec = self.inserts.get(block=False) 474 | self.waterfall.insert(rec) 475 | except queue.Empty: 476 | return 477 | 478 | 479 | class interp_fir_filter: 480 | def __init__(self, taps, interp): 481 | self.interp = interp 482 | self.nhistory = (len(taps) - 1) // interp 483 | padlen = (self.nhistory + 1) * interp - len(taps) 484 | self.taps = np.concatenate((np.zeros(padlen, dtype=taps.dtype), taps)) 485 | 486 | def __call__(self, inp): 487 | interp = self.interp 488 | res = np.zeros((len(inp) - self.nhistory) * interp, dtype=inp.dtype) 489 | 490 | for i in range(interp): 491 | res[i::interp] = np.convolve(inp, self.taps[i::interp], mode='valid') 492 | 493 | return res 494 | 495 | 496 | class freq_translator: 497 | def __init__(self, phase_inc): 498 | self.phase_inc = phase_inc 499 | self.phase = 1.0 500 | 501 | def __call__(self, inp): 502 | shift = np.exp(1j * self.phase_inc * np.arange(len(inp), dtype=np.float32)) * self.phase 503 | self.phase = shift[-1] * np.exp(1j * self.phase_inc) 504 | return inp * shift 505 | 506 | 507 | def lowpass(w_c, N, start=None): 508 | a = np.arange(N) - (float(N - 1) / 2) 509 | taps = np.sin(a * w_c) / a / np.pi 510 | 511 | if N % 2 == 1: 512 | taps[N/2] = w_c / np.pi 513 | 514 | return taps 515 | 516 | 517 | class RingBuf: 518 | def __init__(self, headlen, buf): 519 | self.headlen = headlen 520 | self.buf = buf 521 | self.fill_edge = -headlen 522 | 523 | def __len__(self): 524 | return len(self.buf) - self.headlen 525 | 526 | def append(self, stuff): 527 | stufflen = len(stuff) 528 | 529 | if self.fill_edge + stufflen + self.headlen > len(self.buf): 530 | self.buf[0:self.headlen] = self.buf[self.fill_edge:self.fill_edge + self.headlen] 531 | self.fill_edge = 0 532 | 533 | self.slice(self.fill_edge, self.fill_edge + stufflen)[:] = stuff 534 | self.fill_edge += stufflen 535 | 536 | def slice(self, a, b): 537 | return self.buf[a + self.headlen: b + self.headlen] 538 | 539 | 540 | UPDATE_EVENT_TYPE = sdl2.SDL_RegisterEvents(1) 541 | UPDATE_EVENT = sdl2.SDL_Event() 542 | UPDATE_EVENT.type = UPDATE_EVENT_TYPE 543 | 544 | 545 | def input_thread(readfunc, ringbuf, nbins, overlap, viewer): 546 | window = 0.5 * (1.0 - np.cos((2 * math.pi * np.arange(nbins)) / nbins)) 547 | 548 | #ringbuf.append(np.fromfile(fil, count=ringbuf.headlen, dtype=np.complex64)) 549 | ringbuf.append(np.frombuffer(readfunc(ringbuf.headlen * 8), dtype=np.complex64)) 550 | 551 | while True: 552 | #ringbuf.append(np.fromfile(fil, count=nbins - overlap, dtype=np.complex64)) 553 | ringbuf.append(np.frombuffer(readfunc((nbins - overlap) * 8), dtype=np.complex64)) 554 | 555 | frame = ringbuf.slice(ringbuf.fill_edge - nbins, ringbuf.fill_edge) 556 | spectrum = np.absolute(np.fft.fft(np.multiply(frame, window))) 557 | spectrum = np.concatenate((spectrum[nbins//2:nbins], spectrum[0:nbins//2])) 558 | spectrum = np.log10(spectrum) * 10 559 | viewer.inserts.put(spectrum / 20.0) 560 | 561 | sdl2.SDL_PushEvent(UPDATE_EVENT) 562 | 563 | 564 | def main(): 565 | if len(sys.argv) != 2: 566 | print("usage: 3dwf.py REMOTE_ADDRESS", file=sys.stderr) 567 | sys.exit(1) 568 | 569 | nbins = 256 570 | overlap = 192 571 | rem_address = (sys.argv[1], 3731) 572 | 573 | conn = socket.create_connection(rem_address) 574 | 575 | sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_AUDIO) 576 | 577 | window = sdl2.SDL_CreateWindow(b"3D Waterfall", sdl2.SDL_WINDOWPOS_CENTERED, 578 | sdl2.SDL_WINDOWPOS_CENTERED, 800, 600, 579 | sdl2.SDL_WINDOW_RESIZABLE | sdl2.SDL_WINDOW_OPENGL) 580 | context = sdl2.SDL_GL_CreateContext(window) 581 | 582 | wf = WFViewer(nbins) 583 | wf.init(800, 600) 584 | wf.shift = 0 585 | 586 | filt = interp_fir_filter(lowpass(np.pi / 4, 512) * np.hamming(512), 4) 587 | freqx = freq_translator((0.8/8.0) * np.pi) 588 | 589 | headlen = max(filt.nhistory, overlap) 590 | ringbuf = RingBuf(headlen, np.zeros(headlen + (nbins - overlap) * 512, dtype=np.complex64)) 591 | 592 | # FIXME 593 | global audio_edge 594 | audio_edge = 0 595 | 596 | def callback(unused, buf, buflen): 597 | global audio_edge 598 | bufbuf = pybuf_from_memory(buf, buflen, 0x200) # PyBUF_WRITE 599 | array = np.frombuffer(bufbuf, np.float32) 600 | 601 | assert len(array) % filt.interp == 0 # TODO 602 | nreqframes = len(array) // filt.interp 603 | 604 | loc_ringbuf_edge = ringbuf.fill_edge 605 | if loc_ringbuf_edge < 0 or (loc_ringbuf_edge - audio_edge) % len(ringbuf) < nreqframes: 606 | print("audio underrun", file=sys.stderr) 607 | array.fill(0) 608 | return 609 | 610 | # TODO 611 | if audio_edge + nreqframes > len(ringbuf): 612 | audio_edge = 0 613 | 614 | slic = ringbuf.slice(audio_edge - filt.nhistory, audio_edge + nreqframes) 615 | array[:] = np.real(freqx(filt(slic))) * wf.volume 616 | audio_edge += nreqframes 617 | sdl2.SDL_PushEvent(UPDATE_EVENT) 618 | 619 | audio_spec = sdl2.SDL_AudioSpec(8000, 620 | sdl2.AUDIO_F32, 621 | 1, 622 | 512, 623 | sdl2.SDL_AudioCallback(callback)) 624 | audio_dev = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0) 625 | if audio_dev == 0: 626 | raise Error('could not open audio device') 627 | 628 | err_queue = queue.Queue() 629 | 630 | def readfunc(nbytes): 631 | bytes = b'' 632 | 633 | while len(bytes) < nbytes: 634 | ret = conn.recv(nbytes - len(bytes)) 635 | 636 | if not ret: 637 | raise Exception('end of stream') 638 | 639 | bytes += ret 640 | 641 | return bytes 642 | 643 | def thread_target(): 644 | try: 645 | input_thread(readfunc, ringbuf, nbins, overlap, wf) 646 | except Exception as e: 647 | err_queue.put(e) 648 | event = sdl2.SDL_Event() 649 | event.type = sdl2.SDL_QUIT 650 | sdl2.SDL_PushEvent(event) 651 | 652 | other_thread = threading.Thread(target=thread_target) 653 | other_thread.setDaemon(True) 654 | other_thread.start() 655 | 656 | sdl2.SDL_PauseAudioDevice(audio_dev, 0) 657 | 658 | running = True 659 | event = sdl2.SDL_Event() 660 | while running: 661 | sdl2.SDL_WaitEvent(ctypes.byref(event)) 662 | 663 | while True: 664 | if event.type == sdl2.SDL_QUIT: 665 | running = False 666 | break 667 | 668 | wf.event(event) 669 | 670 | if sdl2.SDL_PollEvent(ctypes.byref(event)) == 0: 671 | break 672 | 673 | # FIXME 674 | wf.shift = ((ringbuf.fill_edge - audio_edge) % len(ringbuf)) / (nbins - overlap) 675 | wf.draw() 676 | sdl2.SDL_GL_SwapWindow(window) 677 | 678 | try: 679 | for exc in iter(err_queue.get_nowait, None): 680 | sdl2.SDL_ShowSimpleMessageBox(sdl2.SDL_MESSAGEBOX_ERROR, b"Exception", str(exc).encode("ascii"), None) 681 | except queue.Empty: 682 | pass 683 | 684 | sdl2.SDL_CloseAudioDevice(audio_dev) 685 | sdl2.SDL_GL_DeleteContext(context) 686 | sdl2.SDL_DestroyWindow(window) 687 | sdl2.SDL_Quit() 688 | 689 | 690 | if __name__ == "__main__": 691 | main() 692 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Freya vizualization tool 4 | 5 | 6 | 7 | ## Install dependencies 8 | 9 | ``` 10 | sudo pip3 install PySDL2 PyOpenGL 11 | sudo apt-get install libsdl2-ttf-dev 12 | ``` 13 | 14 | 15 | ## Spouštění FREYA nástroje 16 | 17 | 18 | -------------------------------------------------------------------------------- /tools/README.txt: -------------------------------------------------------------------------------- 1 | MIDI test client 2 | 3 | 4 | 5 | Compilation: 6 | 7 | gcc midi_cmd.c -o midi_cmd -ljack 8 | 9 | 10 | Use: 11 | 12 | ./midi_cmd 13 | 14 | start_frequency,stop_frequency,sample offset start,sample_offset_stop, string_to_display 15 | 16 | Example: 17 | 10000,11000,0,48000,test 18 | 19 | -------------------------------------------------------------------------------- /tools/midi_cmd.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | jack_client_t *client; 8 | jack_port_t *port; 9 | 10 | char message_outgoing; 11 | char message[100]; 12 | 13 | int process(jack_nframes_t nframes, void *arg) 14 | { 15 | void* port_buff = jack_port_get_buffer(port, nframes); 16 | jack_midi_clear_buffer(port_buff); 17 | 18 | if (message_outgoing) { 19 | size_t msg_len = strlen(message); 20 | 21 | unsigned char* buffer = jack_midi_event_reserve(port_buff, 0, msg_len + 3); 22 | buffer[0] = 0xf0; 23 | buffer[1] = 0x7d; 24 | memcpy(buffer + 2, message, msg_len); 25 | buffer[msg_len + 2] = 0xf7; 26 | 27 | message_outgoing = 0; 28 | } 29 | 30 | return 0; 31 | } 32 | 33 | int main(int narg, char **args) 34 | { 35 | if ((client = jack_client_new("midi_cmd")) == 0) { 36 | fprintf(stderr, "jack server not running?\n"); 37 | return 1; 38 | } 39 | 40 | jack_set_process_callback(client, process, 0); 41 | port = jack_port_register(client, "out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); 42 | message_outgoing = 0; 43 | 44 | if (jack_activate(client)) { 45 | fprintf(stderr, "cannot activate client"); 46 | return 1; 47 | } 48 | 49 | while (1) { 50 | printf("(MIDI) "); 51 | fgets(message, sizeof(message), stdin); 52 | 53 | message[strlen(message) - 1] = '\0'; 54 | message_outgoing = 1; 55 | while (message_outgoing); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /whistle/Makefile: -------------------------------------------------------------------------------- 1 | ifneq ($(V),1) 2 | Q := @ 3 | endif 4 | 5 | INCDIR = -I include/ 6 | CCFLAGS = -g -O2 $(INCDIR) 7 | CXXFLAGS = $(CCFLAGS) 8 | LDFLAGS = -export-dynamic 9 | LIBS = -ljack -ldl -lm 10 | CC = gcc $(CCFLAGS) 11 | LD = gcc $(LDFLAGS) 12 | 13 | BIN = whistle 14 | OBJS = whistle.o 15 | 16 | all: $(BIN) 17 | 18 | clean: 19 | $(Q) rm -f *.o $(BINS) $(OBJS) 20 | @printf " CLEAN\n"; 21 | 22 | .c.o: 23 | @printf " CC $(subst $(shell pwd)/,,$(@))\n"; 24 | $(Q) $(CC) -c -o$@ $< 25 | 26 | $(BIN): $(OBJS) 27 | @printf " LD $(subst $(shell pwd)/,,$(@))\n"; 28 | $(Q) $(LD) -o$@ $(OBJS) $(LIBS) 29 | -------------------------------------------------------------------------------- /whistle/wav.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PIPELINE="freqx,-10000:kbfir,41,0,1000,100:freqx,1000:amplify,100" 3 | SAMP_RATE=`soxi -r $1` 4 | SOX_FORMAT="-c 2 -b 32 -r $SAMP_RATE -t raw -e floating-point" 5 | sox $1 $SOX_FORMAT - | ./whistle -r $SAMP_RATE -p $PIPELINE | sox $SOX_FORMAT - $2 6 | -------------------------------------------------------------------------------- /whistle/whistle.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include // sendfile 14 | #include // open 15 | #include // close 16 | #include // fstat 17 | #include // fstat 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | #include "whistle.h" 24 | 25 | typedef stage_t *(*stage_const_t)(float samp_rate, int nargs, char *args); 26 | 27 | stage_t *create_stage(float samp_rate, char *desc) 28 | { 29 | int nargs = 1; 30 | 31 | int i; 32 | for (i = 0; desc[i]; i++) { 33 | if (desc[i] == ',') { 34 | nargs++; 35 | desc[i] = '\0'; 36 | } 37 | } 38 | 39 | stage_const_t cons = (stage_const_t) dlsym(RTLD_DEFAULT, desc); 40 | 41 | if (!cons) { 42 | fprintf(stderr, "whistle: '%s': no such stage constructor\n", desc); 43 | return NULL; 44 | } 45 | 46 | return cons(samp_rate, nargs - 1, desc + strlen(desc) + 1); 47 | } 48 | 49 | pipeline_t *pipeline_create(float samp_rate, unsigned int buffer_size, 50 | char *desc) 51 | { 52 | int i; 53 | 54 | pipeline_t *pipeline = (pipeline_t *) malloc(sizeof(pipeline_t)); 55 | 56 | if (!pipeline) 57 | return NULL; 58 | 59 | pipeline->buffer_size = buffer_size; 60 | pipeline->nstages = 1; 61 | pipeline->input_buffers = NULL; 62 | pipeline->stages = NULL; 63 | 64 | pipeline->desc = (char *) malloc(strlen(desc) + 1); 65 | strcpy(pipeline->desc, desc); 66 | desc = pipeline->desc; 67 | 68 | for (i = 0; desc[i]; i++) { 69 | if (desc[i] == ':') { 70 | pipeline->nstages++; 71 | desc[i] = '\0'; 72 | } 73 | } 74 | 75 | pipeline->stages = (stage_t **) malloc(sizeof(stage_t *) * pipeline->nstages); 76 | 77 | if (!pipeline->stages) 78 | goto cleanup; 79 | 80 | memset(pipeline->stages, 0, sizeof(stage_t *) * pipeline->nstages); 81 | 82 | for (i = 0; i < pipeline->nstages; i++) { 83 | char *next = desc + strlen(desc) + 1; 84 | pipeline->stages[i] = create_stage(samp_rate, desc); 85 | 86 | if (!pipeline->stages[i]) 87 | goto cleanup; 88 | 89 | desc = next; 90 | } 91 | 92 | pipeline->input_buffers = (float **) malloc(sizeof(float *) * pipeline->nstages); 93 | 94 | if (!pipeline->input_buffers) 95 | goto cleanup; 96 | 97 | for (i = 0; i < pipeline->nstages; i++) { 98 | size_t bsize = sizeof(float *) * 2 * (buffer_size + pipeline->stages[i]->prelude); 99 | pipeline->input_buffers[i] = (float *) malloc(bsize); 100 | 101 | if (!pipeline->input_buffers[i]) 102 | goto cleanup; 103 | } 104 | 105 | return pipeline; 106 | 107 | cleanup: 108 | pipeline_delete(pipeline); 109 | 110 | return NULL; 111 | } 112 | 113 | void pipeline_delete(pipeline_t *pipeline) 114 | { 115 | int i; 116 | 117 | if (pipeline->desc) 118 | free(pipeline->desc); 119 | 120 | if (pipeline->input_buffers) { 121 | for (i = 0; i < pipeline->nstages; i++) 122 | if (pipeline->input_buffers[i]) 123 | free(pipeline->input_buffers[i]); 124 | 125 | free(pipeline->input_buffers); 126 | } 127 | 128 | if (pipeline->stages) { 129 | for (i = 0; i < pipeline->nstages; i++) 130 | if (pipeline->stages[i]) 131 | pipeline->stages[i]->free(pipeline->stages[i]); 132 | 133 | free(pipeline->stages); 134 | } 135 | 136 | free(pipeline); 137 | } 138 | 139 | float *pipeline_input_buffer(pipeline_t *pipeline) 140 | { 141 | return pipeline->input_buffers[0] + 2 * pipeline->stages[0]->prelude; 142 | } 143 | 144 | void pipeline_pass(pipeline_t *pipeline, float *out, unsigned int nframes) 145 | { 146 | if (nframes > pipeline->buffer_size) { 147 | fprintf(stderr, "whistle: pipeline_pass: nframes > buffer_size\n"); 148 | exit(1); 149 | } 150 | 151 | float *s_in = pipeline_input_buffer(pipeline); 152 | 153 | int i; 154 | for (i = 0; i < pipeline->nstages; i++) { 155 | float *s_out; 156 | 157 | if (i < pipeline->nstages - 1) 158 | s_out = pipeline->input_buffers[i + 1] + 2 * pipeline->stages[i + 1]->prelude; 159 | else 160 | s_out = out; 161 | 162 | pipeline->stages[i]->pass(pipeline->stages[i], s_in, s_out, nframes); 163 | 164 | int x; 165 | for (x = -pipeline->stages[i]->prelude * 2; x < 0; x++) 166 | *(s_in + x) = *(s_in + x + 2 * nframes); 167 | 168 | s_in = s_out; 169 | } 170 | } 171 | 172 | void dummy_free(stage_t *stage) 173 | { 174 | free(stage); 175 | } 176 | 177 | typedef struct { 178 | stage_t stage; 179 | unsigned int order; 180 | float *c; 181 | } fir_stage_t; 182 | 183 | void fir_free(stage_t *stage) 184 | { 185 | free(((fir_stage_t *) stage)->c); 186 | free(stage); 187 | } 188 | 189 | void fir_stride_pass(float *c, float *in, float *out, unsigned int nframes, unsigned int order) 190 | { 191 | int i, x; 192 | for (i = 0; i < nframes; i++) { 193 | float sum = 0; 194 | for (x = 0; x < order; x++) 195 | sum += c[x] * *(in + 2 * (i - x)); 196 | out[2 * i] = sum; 197 | } 198 | } 199 | 200 | void fir_pass(stage_t *stage, float *in, float *out, unsigned int nframes) 201 | { 202 | fir_stage_t *fir = (fir_stage_t *) stage; 203 | fir_stride_pass(fir->c, in, out, nframes, fir->order); 204 | fir_stride_pass(fir->c, in + 1, out + 1, nframes, fir->order); 205 | } 206 | 207 | // ported from http://www.arc.id.au/dspUtils-10.js 208 | double kb_ino(double x) 209 | { 210 | double d = 0, ds = 1, s = 1; 211 | 212 | do { 213 | d += 2; 214 | ds *= x*x / (d*d); 215 | s += ds; 216 | } while (ds > s * 0.000001); 217 | 218 | return s; 219 | } 220 | 221 | void kaiser_bessel(float fs, float fa, float fb, 222 | int m, float att, float *h) 223 | { 224 | int np = (m - 1) / 2; 225 | double *a = (double *) alloca(sizeof(double) * (np + 1)); 226 | int i; 227 | 228 | a[0] = 2 * (fb - fa) / fs; 229 | 230 | for (i = 1; i <= np; i++) 231 | a[i] = (sin(2.0f * i * M_PI * (fb / fs)) - sin(2.0f * i * M_PI * (fa / fs))) / (i * M_PI); 232 | 233 | double alpha; 234 | 235 | if (att < 21) 236 | alpha = 0; 237 | else if (att > 50) 238 | alpha = 0.1102 * (att - 8.7); 239 | else 240 | alpha = 0.5842 * pow(att - 21, 0.4) + 0.07886 * (att - 21); 241 | 242 | double inoalpha = kb_ino(alpha); 243 | 244 | for (i = 0; i <= np; i++) 245 | h[np + i] = a[i] * kb_ino(alpha * sqrt(1.0 - ((double) (i * i)) / (np * np))) / inoalpha; 246 | 247 | for (i = 0; i < np; i++) 248 | h[i] = h[m - 1 - i]; 249 | } 250 | 251 | stage_t *kbfir(float samp_rate, int nargs, char *args) 252 | { 253 | if (nargs != 4) 254 | goto usage; 255 | 256 | int ntaps = atoi(args); 257 | args += strlen(args) + 1; 258 | 259 | if (!(ntaps > 0 && ntaps % 2)) 260 | goto usage; 261 | 262 | float fa = atof(args); 263 | args += strlen(args) + 1; 264 | float fb = atof(args); 265 | args += strlen(args) + 1; 266 | float att = atof(args); 267 | 268 | fir_stage_t *fir = (fir_stage_t *) malloc(sizeof(fir_stage_t)); 269 | 270 | if (!fir) 271 | return NULL; 272 | 273 | float *c = (float *) malloc(sizeof(float) * ntaps); 274 | 275 | if (!c) { 276 | free(fir); 277 | return NULL; 278 | } 279 | 280 | kaiser_bessel(samp_rate, fa, fb, ntaps, att, c); 281 | 282 | fir->stage.pass = fir_pass; 283 | fir->stage.free = fir_free; 284 | fir->stage.prelude = ntaps - 1; 285 | fir->order = ntaps; 286 | fir->c = c; 287 | 288 | return (stage_t *) fir; 289 | 290 | usage: 291 | fprintf(stderr, "kbfir: usage: kbfir,NTAPS,FA,FB,ATT\n"); 292 | fprintf(stderr, "kbfir: NTAPS must be an positive odd number\n"); 293 | return NULL; 294 | } 295 | 296 | stage_t *customfir(float samp_rate, int nargs, char *args) 297 | { 298 | fir_stage_t *fir = (fir_stage_t *) malloc(sizeof(fir_stage_t)); 299 | 300 | if (!fir) 301 | return NULL; 302 | 303 | float *c = (float *) malloc(sizeof(float) * nargs); 304 | 305 | if (!c) { 306 | free(fir); 307 | return NULL; 308 | } 309 | 310 | int i; 311 | for (i = 0; i < nargs; i++) { 312 | c[i] = atof(args); 313 | args += strlen(args) + 1; 314 | } 315 | 316 | fir->stage.pass = fir_pass; 317 | fir->stage.free = fir_free; 318 | fir->stage.prelude = nargs - 1; 319 | fir->order = nargs; 320 | fir->c = c; 321 | 322 | return (stage_t *) fir; 323 | } 324 | 325 | void fmdemod_pass(stage_t *stage, float *in, float *out, unsigned int nframes) 326 | { 327 | int i; 328 | for (i = 0; i < nframes * 2; i += 2) { 329 | float di = in[i] - in[i - 4]; 330 | float dq = in[i + 1] - in[i - 3]; 331 | 332 | float m = in[i - 2] * in[i - 2] + in[i - 1] * in[i - 1]; 333 | out[i] = (in[i - 2] * dq - in[i - 1] * di) / m; 334 | 335 | out[i + 1] = 0; 336 | } 337 | } 338 | 339 | stage_t *fmdemod(float samp_rate, int nargs, char *args) 340 | { 341 | if (nargs != 0) { 342 | fprintf(stderr, "fmdemod: usage: fmdemod\n"); 343 | return NULL; 344 | } 345 | 346 | stage_t *stage = (stage_t *) malloc(sizeof(stage_t)); 347 | 348 | if (!stage) 349 | return NULL; 350 | 351 | stage->pass = fmdemod_pass; 352 | stage->free = dummy_free; 353 | stage->prelude = 2; 354 | 355 | return stage; 356 | } 357 | 358 | typedef struct { 359 | stage_t stage; 360 | float complex inc; 361 | float complex phase; 362 | } freqx_stage_t; 363 | 364 | void freqx_pass(freqx_stage_t *fs, float complex *in, float complex *out, unsigned int nframes) 365 | { 366 | float complex inc = fs->inc; 367 | float complex phase = fs->phase; 368 | 369 | int x; 370 | for (x = 0; x < nframes; x++) { 371 | out[x] = in[x] * phase; 372 | phase *= inc; 373 | } 374 | 375 | fs->phase = phase / cabs(phase); 376 | } 377 | 378 | stage_t *freqx(float samp_rate, int nargs, char *args) 379 | { 380 | if (nargs != 1) { 381 | fprintf(stderr, "freqx: usage: freqx,FREQ\n"); 382 | return NULL; 383 | } 384 | 385 | freqx_stage_t *fs = (freqx_stage_t *) malloc(sizeof(freqx_stage_t)); 386 | 387 | if (!fs) 388 | return NULL; 389 | 390 | fs->stage.pass = (stage_pass_cb_t) freqx_pass; 391 | fs->stage.free = dummy_free; 392 | fs->stage.prelude = 0; 393 | fs->phase = 1; 394 | fs->inc = cexp(I * (atof(args) / samp_rate) * 2 * M_PI); 395 | 396 | return (stage_t *) fs; 397 | } 398 | 399 | typedef struct { 400 | stage_t stage; 401 | float factor; 402 | } amplify_stage_t; 403 | 404 | void amplify_pass(stage_t *stage, float *in, float *out, unsigned int nframes) 405 | { 406 | float factor = ((amplify_stage_t *) stage)->factor; 407 | 408 | int i; 409 | for (i = 0; i < nframes * 2; i++) 410 | out[i] = in[i] * factor; 411 | } 412 | 413 | stage_t *amplify(float samp_rate, int nargs, char *args) 414 | { 415 | if (nargs != 1) { 416 | fprintf(stderr, "amplify: usage: amplify,FACTOR\n"); 417 | return NULL; 418 | } 419 | 420 | amplify_stage_t *amp = (amplify_stage_t *) malloc(sizeof(amplify_stage_t)); 421 | 422 | if (!amp) 423 | return NULL; 424 | 425 | amp->stage.pass = (stage_pass_cb_t) amplify_pass; 426 | amp->stage.free = dummy_free; 427 | amp->stage.prelude = 0; 428 | amp->factor = atof(args); 429 | 430 | return (stage_t *) amp; 431 | } 432 | 433 | typedef struct { 434 | stage_t stage; 435 | char *lib_path; 436 | char *lib_copy_path; 437 | void *dl_handle; 438 | int inotify_handle; 439 | stage_t *lib_stage; 440 | int nargs; 441 | char *args; 442 | float samp_rate; 443 | } dl_stage_t; 444 | 445 | char copy_file(char *source, char *dest) 446 | { 447 | int fd_source = open(source, O_RDONLY, 0); 448 | int fd_dest = open(dest, O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU); 449 | 450 | struct stat stat_source; 451 | fstat(fd_source, &stat_source); 452 | sendfile(fd_dest, fd_source, 0, stat_source.st_size); 453 | 454 | close(fd_source); 455 | close(fd_dest); 456 | 457 | return 1; 458 | } 459 | 460 | char dl_load(dl_stage_t *stage) 461 | { 462 | stage->lib_copy_path = tempnam(NULL, "whistle_dl_copy_"); 463 | 464 | if (!stage->lib_copy_path) { 465 | perror("whistle: tempnam"); 466 | return 0; 467 | } 468 | 469 | fprintf(stderr, "dl: copy of %s will be at %s\n", stage->lib_path, stage->lib_copy_path); 470 | 471 | copy_file(stage->lib_path, stage->lib_copy_path); 472 | 473 | stage->dl_handle = dlopen(stage->lib_copy_path, RTLD_NOW); 474 | 475 | if (!stage->dl_handle) { 476 | perror("whistle: dlopen"); 477 | return 0; 478 | } 479 | 480 | char *sym = stage->args + strlen(stage->args) + 1; 481 | 482 | stage_const_t cons = (stage_const_t) dlsym(stage->dl_handle, sym); 483 | 484 | if (!cons) { 485 | fprintf(stderr, "whistle: %s: '%s': no such stage constructor\n", 486 | stage->lib_path, sym); 487 | return 0; 488 | } 489 | 490 | stage->lib_stage = cons(stage->samp_rate, stage->nargs - 2, sym + strlen(sym) + 1); 491 | 492 | if (!cons) 493 | return 0; 494 | 495 | return 1; 496 | } 497 | 498 | void dl_unload(dl_stage_t *stage) 499 | { 500 | if (stage->lib_copy_path) 501 | free(stage->lib_copy_path); 502 | 503 | if (stage->lib_stage) 504 | stage->lib_stage->free(stage->lib_stage); 505 | 506 | if (stage->dl_handle) 507 | dlclose(stage->dl_handle); 508 | 509 | stage->lib_copy_path = NULL; 510 | stage->lib_stage = NULL; 511 | stage->dl_handle = NULL; 512 | } 513 | 514 | void dl_pass_proxy(dl_stage_t *stage, float *in, float *out, int nframes) 515 | { 516 | fd_set sready; 517 | struct timeval nowait; 518 | 519 | FD_ZERO(&sready); 520 | FD_SET((unsigned int) stage->inotify_handle, &sready); 521 | memset((char *) &nowait, 0, sizeof(nowait)); 522 | 523 | select(stage->inotify_handle + 1, &sready, NULL, NULL, &nowait); 524 | 525 | if (FD_ISSET(stage->inotify_handle, &sready)) { 526 | char buffer[4096]; 527 | int ret = read(stage->inotify_handle, buffer, sizeof(buffer)); 528 | 529 | int i = 0; 530 | while (i < ret) { 531 | struct inotify_event *event = (struct inotify_event *) &buffer[i]; 532 | 533 | if (!strcmp(event->name, basename(stage->lib_path))) { 534 | dl_unload(stage); 535 | if (!dl_load(stage)) { 536 | fprintf(stderr, "whistle: dl hotswap failed\n"); 537 | exit(1); 538 | } 539 | } 540 | 541 | i += sizeof(struct inotify_event) + event->len; 542 | } 543 | } 544 | 545 | stage->lib_stage->pass(stage->lib_stage, in, out, nframes); 546 | } 547 | 548 | void dl_stage_free(dl_stage_t *stage) 549 | { 550 | dl_unload(stage); 551 | 552 | if (stage->lib_path) 553 | free(stage->lib_path); 554 | 555 | if (stage->inotify_handle >= 0) 556 | close(stage->inotify_handle); 557 | 558 | free(stage); 559 | } 560 | 561 | stage_t *dl(float samp_rate, int nargs, char *args) 562 | { 563 | if (nargs < 2) { 564 | fprintf(stderr, "dl: usage: dl,LIB_PATH,CONSTRUCTOR_SYM\n"); 565 | return NULL; 566 | } 567 | 568 | dl_stage_t *dl_stage = (dl_stage_t *) malloc(sizeof(dl_stage_t)); 569 | 570 | if (!dl_stage) 571 | return NULL; 572 | 573 | dl_stage->samp_rate = samp_rate; 574 | dl_stage->lib_copy_path = NULL; 575 | dl_stage->dl_handle = NULL; 576 | dl_stage->inotify_handle = -1; 577 | dl_stage->lib_stage = NULL; 578 | dl_stage->lib_path = NULL; 579 | 580 | dl_stage->nargs = nargs; 581 | dl_stage->args = args; 582 | 583 | dl_stage->lib_path = malloc(strlen(args) + 1); 584 | 585 | if (!dl_stage->lib_path) 586 | goto cleanup; 587 | 588 | strcpy(dl_stage->lib_path, args); 589 | 590 | dl_stage->inotify_handle = inotify_init(); 591 | if (inotify_add_watch(dl_stage->inotify_handle, dirname(dl_stage->lib_path), 592 | IN_CLOSE_WRITE | IN_MOVED_TO) < 0) 593 | perror("whistle: inotify_add_watch"); 594 | 595 | strcpy(dl_stage->lib_path, args); 596 | 597 | if (!dl_load(dl_stage)) 598 | goto cleanup; 599 | 600 | dl_stage->stage.prelude = dl_stage->lib_stage->prelude; 601 | dl_stage->stage.pass = (stage_pass_cb_t) dl_pass_proxy; 602 | dl_stage->stage.free = (stage_free_cb_t) dl_stage_free; 603 | 604 | return (stage_t *) dl_stage; 605 | 606 | cleanup: 607 | if (dl_stage) 608 | dl_stage_free(dl_stage); 609 | 610 | return NULL; 611 | } 612 | 613 | typedef struct { 614 | char *pipeline_desc; 615 | pipeline_t *pipeline; 616 | jack_port_t *p_in_i, *p_in_q, *p_out_i, *p_out_q; 617 | float *output_buffer; 618 | jack_nframes_t sample_rate; 619 | jack_nframes_t buffer_size; 620 | } jack_arg_t; 621 | 622 | int jack_process(jack_nframes_t nframes, void *arg_) 623 | { 624 | jack_arg_t *arg = (jack_arg_t *) arg_; 625 | pipeline_t *pipeline = arg->pipeline; 626 | 627 | jack_default_audio_sample_t *in_i, *in_q, *out_i, *out_q; 628 | 629 | in_i = jack_port_get_buffer(arg->p_in_i, nframes); 630 | in_q = jack_port_get_buffer(arg->p_in_q, nframes); 631 | out_i = jack_port_get_buffer(arg->p_out_i, nframes); 632 | out_q = jack_port_get_buffer(arg->p_out_q, nframes); 633 | 634 | float *pl_in = pipeline_input_buffer(pipeline); 635 | float *pl_out = arg->output_buffer; 636 | 637 | int i; 638 | for (i = 0; i < nframes; i++) { 639 | pl_in[2 * i] = in_i[i]; 640 | pl_in[2 * i + 1] = in_q[i]; 641 | } 642 | 643 | pipeline_pass(pipeline, pl_out, nframes); 644 | 645 | for (i = 0; i < nframes; i++) { 646 | out_i[i] = pl_out[2 * i]; 647 | out_q[i] = pl_out[2 * i + 1]; 648 | } 649 | } 650 | 651 | void jack_shutdown(void *arg) 652 | { 653 | exit(1); 654 | } 655 | 656 | int jack_setup_pipeline(jack_arg_t *arg) 657 | { 658 | if (arg->buffer_size && arg->sample_rate) { 659 | if (arg->pipeline) 660 | pipeline_delete(arg->pipeline); 661 | 662 | if (arg->output_buffer) 663 | free(arg->output_buffer); 664 | 665 | arg->pipeline = pipeline_create(arg->sample_rate, arg->buffer_size, arg->pipeline_desc); 666 | arg->output_buffer = (float *) malloc(sizeof(float) * 2 * arg->buffer_size); 667 | 668 | return (arg->pipeline && arg->output_buffer) ? 0 : -1; 669 | } 670 | } 671 | 672 | int jack_buffer_size(jack_nframes_t nframes, void *arg_) 673 | { 674 | jack_arg_t *arg = (jack_arg_t *) arg_; 675 | 676 | if (arg->buffer_size != nframes) { 677 | arg->buffer_size = nframes; 678 | return jack_setup_pipeline(arg); 679 | } 680 | 681 | return 0; 682 | } 683 | 684 | int jack_sample_rate(jack_nframes_t nframes, void *arg_) 685 | { 686 | jack_arg_t *arg = (jack_arg_t *) arg_; 687 | 688 | if (arg->sample_rate != nframes) { 689 | arg->sample_rate = nframes; 690 | return jack_setup_pipeline(arg); 691 | } 692 | 693 | return 0; 694 | } 695 | 696 | #define BUFFER_SIZE 8192 697 | 698 | void usage(char *argv0) 699 | { 700 | fprintf(stderr, "%s: usage: %s [-r SAMPLE_RATE | -j JACK_CLIENT_NAME]" 701 | " [-p PIPELINE]\n", argv0, argv0); 702 | exit(1); 703 | } 704 | 705 | int main(int argc, char *argv[]) 706 | { 707 | float smp_rate = 0; 708 | enum { PIPE, JACK } mode = JACK; 709 | const char *client_name = "whistle"; 710 | char *pipeline_desc = "freqx,-10000:kbfir,41,0,1000,100:freqx,1000:amplify,100"; 711 | pipeline_t *pipeline; 712 | 713 | int c; 714 | while ((c = getopt(argc, argv, "hr:j:p:b")) >= 0) { 715 | switch (c) { 716 | case '?': 717 | case 'h': 718 | usage(argv[0]); 719 | 720 | case 'r': 721 | mode = PIPE; 722 | smp_rate = atof(optarg); 723 | if (smp_rate <= 0) { 724 | fprintf(stderr, "%s: %s: invalid sample rate\n", argv[0], optarg); 725 | exit(1); 726 | } 727 | break; 728 | 729 | case 'j': 730 | mode = JACK; 731 | client_name = optarg; 732 | break; 733 | 734 | case 'p': 735 | pipeline_desc = optarg; 736 | break; 737 | 738 | default: 739 | fprintf(stderr, "%d\n", c); 740 | abort(); 741 | } 742 | } 743 | 744 | if (optind < argc || (mode == PIPE && smp_rate == 0)) 745 | usage(argv[0]); 746 | 747 | if (mode == JACK) { 748 | jack_options_t options = JackNullOption; 749 | jack_status_t status; 750 | jack_client_t *client; 751 | 752 | client = jack_client_open(client_name, options, &status, NULL); 753 | 754 | if (!client) { 755 | fprintf(stderr, "whistle: jack_client_open(): failed, status = 0x%X\n", status); 756 | if (status & JackServerFailed) 757 | fprintf(stderr, "whistle: unable to connect to JACK server\n"); 758 | return 1; 759 | } 760 | 761 | jack_arg_t arg; 762 | 763 | arg.pipeline_desc = pipeline_desc; 764 | 765 | arg.buffer_size = 0; 766 | arg.sample_rate = 0; 767 | arg.pipeline = 0; 768 | arg.output_buffer = 0; 769 | 770 | jack_set_process_callback(client, jack_process, &arg); 771 | jack_on_shutdown(client, jack_shutdown, &arg); 772 | jack_set_buffer_size_callback(client, jack_buffer_size, &arg); 773 | jack_set_sample_rate_callback(client, jack_sample_rate, &arg); 774 | 775 | arg.buffer_size = jack_get_buffer_size(client); 776 | arg.sample_rate = jack_get_sample_rate(client); 777 | 778 | if (jack_setup_pipeline(&arg)) 779 | return 1; 780 | 781 | arg.p_in_i = jack_port_register(client, "input_i", JACK_DEFAULT_AUDIO_TYPE, 782 | JackPortIsInput, 0); 783 | arg.p_in_q = jack_port_register(client, "input_q", JACK_DEFAULT_AUDIO_TYPE, 784 | JackPortIsInput, 0); 785 | arg.p_out_i = jack_port_register(client, "output_i", JACK_DEFAULT_AUDIO_TYPE, 786 | JackPortIsOutput, 0); 787 | arg.p_out_q = jack_port_register(client, "output_q", JACK_DEFAULT_AUDIO_TYPE, 788 | JackPortIsOutput, 0); 789 | 790 | if (!(arg.p_in_i && arg.p_in_q && arg.p_out_i && arg.p_out_q)) { 791 | fprintf(stderr, "whistle: no more JACK ports available\n"); 792 | return 1; 793 | } 794 | 795 | if (jack_activate(client)) { 796 | fprintf(stderr, "whistle: cannot activate JACK client\n"); 797 | return 1; 798 | } 799 | 800 | sleep(-1); 801 | } else { 802 | pipeline = pipeline_create(smp_rate, BUFFER_SIZE, pipeline_desc); 803 | 804 | if (!pipeline) 805 | return 1; 806 | 807 | float buffer[BUFFER_SIZE * 2]; 808 | int ret, len; 809 | 810 | while (1) { 811 | len = fread(pipeline_input_buffer(pipeline), 812 | sizeof(float) * 2, BUFFER_SIZE, stdin); 813 | 814 | if (len == 0) { 815 | if (ret = ferror(stdin)) { 816 | fprintf(stderr, "%s: fread: %s\n", argv[0], strerror(ret)); 817 | exit(1); 818 | } else { 819 | exit(0); 820 | } 821 | } 822 | 823 | pipeline_pass(pipeline, buffer, len); 824 | 825 | ret = fwrite(buffer, sizeof(float) * 2, len, stdout); 826 | 827 | if (ret != len) { 828 | fprintf(stderr, "%s: fwrite: %s\n", argv[0], strerror(ferror(stdout))); 829 | exit(1); 830 | } 831 | } 832 | } 833 | 834 | exit(0); 835 | } 836 | -------------------------------------------------------------------------------- /whistle/whistle.h: -------------------------------------------------------------------------------- 1 | #ifndef __WHISTLE_H__ 2 | #define __WHISTLE_H__ 3 | 4 | typedef struct stage_struct { 5 | void (*pass)(struct stage_struct *stage, float *in, float *out, unsigned int nframes); 6 | void (*free)(struct stage_struct *stage); 7 | unsigned int prelude; 8 | } stage_t; 9 | 10 | typedef void (*stage_pass_cb_t)(stage_t *stage, float *in, float *out, unsigned int nframes); 11 | typedef void (*stage_free_cb_t)(stage_t *stage); 12 | 13 | typedef struct { 14 | char *desc; 15 | stage_t **stages; 16 | float **input_buffers; 17 | int nstages; 18 | unsigned int buffer_size; 19 | } pipeline_t; 20 | 21 | pipeline_t *pipeline_create(float samp_rate, unsigned int buffer_size, 22 | char *desc); 23 | void pipeline_delete(pipeline_t *pipeline); 24 | 25 | float *pipeline_input_buffer(pipeline_t *pipeline); 26 | void pipeline_pass(pipeline_t *pipeline, float *out, unsigned int nframes); 27 | 28 | #endif /* __WHISTLE_H__ */ 29 | --------------------------------------------------------------------------------